HelloCTF RCE-labs Level 6 挑战关题解:仅剩一个字母的通配符

HelloCTF RCE-labs Level 6 挑战关题解:仅剩一个字母的通配符

本文字数:4512  阅读时长:10分钟

访问量 0

HelloCTF RCE-labs · 挑战关:仅剩一个字母的通配符 —— Writeup

项目内容
靶场HelloCTF RCE 靶场(github.com/ProbiusOfficial/RCE-labs,作者 探姬)
关卡RCE 挑战关(题面注释:刚才,学了什么来着!?——考的是前面所学通配符的极限运用)
题型Web / PHP 命令执行(Command Injection / RCE)
目标http://80-e36cdd6f-930e-44b3-8dae-08eb35c54cba.challenge.ctfplus.cn/
入口参数GET 参数 cmd
过滤情况`preg_match("/[b-zA-Z_@#%^&*:{}-+<>"
工具新版 HackBar(DevTools 内置版)、Burp Suite Community v2026.8(新版 UI)
结果拿到 Geesec{97487dac-ea51-4e71-bcdf-5c6592ab4471}

注:上方地址是平台下发的临时实例,题目做完后会失效。复现时请替换成自己那一份。


一、题目

打开靶机,页面照例用 highlight_file(__FILE__) 把自己打印了出来。去掉教学注释后,核心就是这几行:

php
function hello_shell($cmd){
    if(preg_match("/[b-zA-Z_@#%^&*:{}\-\+<>\"|`;\[\]]/", $cmd)){
        die("WAF!");
    }
    system($cmd);
}

isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;

highlight_file(__FILE__);

结构和上一关几乎一样,唯一的变化就是过滤从"拦一个词"升级成了"拦一大批字符"。


二、题目分析

2.1 把正则黑名单翻译成人话

[...] 是字符集合:只要命令串里出现集合内任意一个字符,就 die("WAF!")。逐段拆开:

正则片段拦掉的字符备注
b-zb c d … z小写字母只剩 a 幸存
A-Z所有大写字母大写绕过这条路直接封死
_下划线${IFS} 这种写法受影响
@ # % ^ & *六个符号* 通配符在列
: { }冒号、花括号{}, 展开被禁
\-\+减号、加号命令的 -l、-a 参数全废
< >重定向不能 cat < /flag,不能写文件
"双引号只剩单引号 ' 可用
|竖线管道没了
`反引号命令替换没了
; [ ]分号、方括号命令分隔没了,[a-z] 通配也没了

反过来,幸存字符全集就是本题的"武器库":

text
字母: a          数字: 0-9
符号: $ ( ) = . , / ? ! ' ~      以及: 空格、换行

2.2 三个关键判断

判断一:空引号插入法(c''at)这一关失效了。

上一关的招牌手法 c''at,本质是"字母还在,只是被空引号断开"。这一关 c、t 本身就在黑名单里——引号插得再花哨,字母出现就是死。凡是 b-z 区间的字母,一个都不能明文写。

判断二:通配符只剩 ? 一个。

*、[]、{} 全被禁,能用的通配符只有 ?(匹配恰好 1 个字符)。这既是限制也是提示——出题人题面那句 刚才,学了什么来着!?,两个 !? 就是明示。

判断三(本题最核心):? 展开的前提是"真有文件能匹配上"。

shell 的通配符展开(globbing)不是字符串替换,而是查文件系统:

  • ? 在当前位置找"名字长度吻合的真实文件";
  • 找到了 → 替换成文件名;找不到 → 原样保留,继续往下传。

由此推出两个结论:

  1. 读文件:flag 文件在根目录,f??? 这种写法不行(f 是被禁字母),但 ???? 可以——它匹配根目录下所有 4 字符文件;
  2. 命令本身:?a? 这种写法单独发是不行的。因为 shell 只在当前目录(webshell 场景 = /var/www/html)展开它,那里没有任何 3 字符文件叫 ?a? → 展开失败 → 整串原样传给系统 → command not found,而且这条报错走 stderr,system() 不回显,页面上什么都看不到(上一关 4.3 节"伪解"是同一个坑)。

命令本身也是文件——它躺在 /bin、/usr/bin 这些目录里。想让通配符命中命令,就得把命令当文件、连同它的目录一起通配:/bin 是 3 个字符 → 写成 /???/?a?。 这就是"cat 不到就要想到 bin"的准确含义:不是 cat 失败了才想 bin,而是命令名用通配符时,目录前缀从一开始就必须带上。

2.3 利用思路

text
把 "cat" 和 "flag" 里的每一个字母都藏进 ? 通配符里,
让 WAF 看到的只有符号,shell 展开后却还原成完整命令
  • cat → ?a?(c、t 藏起来,a 是唯一幸存字母)
  • /bin → /???(命令所在目录一起通配)
  • /flag → /????(4 字符文件名)

三、解题过程

3.1 先探底:确认哪些字符真的能过

在 Burp 内置浏览器(Proxy → Intercept 页右上角 Open browser)逐条验证黑名单是否如源码所示:

请求结果说明
?cmd=1正常回显源码,无 WAF数字能过
?cmd=a同上字母 a 能过
?cmd=lsWAF!l、s 都在 b-z
?cmd=cat%20/flagWAF!字母 + 路径全灭
?cmd=tac%20fl*WAF!t/c/f/l 踩 b-z,* 踩符号

3.2 抓包送 Repeater

Proxy → HTTP history 找到靶机请求 → Ctrl+R 送 Repeater → 切 Raw 视图。

3.3 发最终 payload

请求行改成:

http
GET /?cmd=/???/?a?%20/???? HTTP/1.1
Host: 80-e36cdd6f-930e-44b3-8dae-08eb35c54cba.challenge.ctfplus.cn
(其余头保持不动)

点 Send。响应约 43 万字节——cat 命中了一个二进制文件,拉到响应最底部(<code> 源码块之前),flag 就在二进制噪声的末尾:

text
Geesec{97487dac-ea51-4e71-bcdf-5c6592ab4471}

Burp 里二进制大响应建议切 Hex 视图尾部查看,或直接 Ctrl+F 搜 {。

3.4 flag 文件是怎么定位的(没有 ls 的枚举法)

本关 ls 用不了(字母全灭),通配符枚举只能按文件名长度逐个猜。实测记录:

payload(URL 解码后)响应体积判读
/???/?a? /????(4 字符)429,956 字节命中了二进制文件,flag 在末尾 ✓
/???/?a? /?????(5 字符)429,911 字节命中另一个二进制,无 flag
/???/?a? /??????~/?????????均为 429,911 字节同上
/???/???/??(≈/usr/bin/ls)2,279 字节(无命令输出)见 5.2 排查表

侧信道原理:无回显命令名可用时,“响应体积突变"本身就是信息——比基线(2,279 字节)大两个数量级 = cat 到了真实存在的二进制文件。逐个长度试下去,flag 自然浮出。


四、payload 推导链

一条 payload 的完整旅程(四层,每层身份不同):

text
cmd=/???/?a?%20/????          ← 你在 Repeater 里敲的原文
   ↓ URL 解码:%20 → 空格
/???/?a? /????                ← PHP $_GET['cmd'] 拿到的
   ↓ preg_match 字符黑名单检查
   无 b-z、无 A-Z、无 * 等 → 放行(这一串里只有 ? / a 和空格)
system("/???/?a? /????")      ← 交给 shell
   ↓ shell 通配符展开(globbing,查真实文件系统)
   /??? → /bin ;?a? → cat ;/???? → /flag
/bin/cat /flag                ← 文件系统上真正执行的命令

和上一关同源的信息差:WAF 检查发生在第 2 层(纯文本匹配),通配符展开发生在第 4 层(查文件系统)。中间隔了一整个 shell,? 就是钻进这条缝的工具——只不过上一关只需藏 flag 一个词,这一关要把所有字母都藏进去。


五、读不到 flag 时的排查表(本关增补版)

5.1 通用排查(沿用上一关结论)

回显现象原因处理
页面返回 WAF!命中了黑名单字符对照 2.1 的幸存字符表,逐字符排雷
无 WAF 但无输出命令跑了但失败,stderr 不回显先确认文件/命令真的存在(见下)
Burp 返回 400请求行有裸空格写 %20;或用 $IFS(本关 $ 幸存,可用)
改了没反应Repeater 不自动重发重新点 Send

5.2 本关新增的三条

回显现象原因处理
裸写 ?cmd=?a?%20/???? 无输出?a? 只在当前目录展开,/var/www/html 里没有匹配 → 原样传给 shell → command not found(stderr 不可见)命令名必须带目录前缀:/???/?a? 或 /???/???/?a?
/???/???/??(≈ls)无输出?? 这类短模式同时匹配多个二进制(ar、as、sh…),它们被依次执行,输出混乱或为空短通配符枚举不可靠;改用「按长度 cat + 响应体积」侧信道
响应 43 万字节全是乱码cat 到了二进制文件正常!拉到最底部或搜 {,flag 常被拼在二进制之后

5.3 目录前缀的两个变体

写法展开成何时用
/???/?a?/bin/cat/bin 存在(本题实测可用)
/???/???/?a?/usr/bin/cat精简镜像里命令都在 /usr/bin(4 字符)
?a?❌ 不展开当前目录没有叫 cat 的文件

不确定时两个都发一遍,反正都过 WAF。


六、原理深化:通配符展开(globbing)的三条铁律

本关所有技巧都从这三条推出来,值得单独记:

铁律一:glob 查的是文件系统,不是字符串。 preg_match 看到的是 /???/?a? 这串符号;shell 执行前会拿着它去目录里"点名”,把能对上号的真实文件名换进去。WAF 在文本层,glob 在文件系统层——这就是永远打不完的信息差。

铁律二:匹配不上就原样保留。 ?a? 在没有匹配文件的目录里不会报错、不会变空,而是原样传下去,最后由执行环节报 command not found——又因为 stderr 不回显,页面上表现为"安静的失败"。“没拦但没输出"的第一嫌疑永远是展开失败。

铁律三:一个模式匹配到几个文件,就展开成几个词。 /???? 匹配到根目录下所有 4 字符文件 → cat 的参数一下子变成好几个 → 依次全被读出来。这是双刃剑:

  • 好处:不用知道确切文件名,长度对上就中;
  • 坏处:噪声大(二进制乱码刷屏),且匹配过多时(如 /???/???/?? 匹配一堆命令)行为不可控。

七、与之前关卡的联动复习

这道"挑战关"没有引入任何新符号——它把前面学的东西全部拆掉,只留一件武器逼你用到底。对照着复习:

关卡教的手法本关还剩多少可用复习要点
Lv3 无过滤入门cat /flag 直读❌ 字母全灭认识 system() 回显位置:输出在源码块之前
SHELL 运算符; | && 拼接命令❌ 分隔符全灭本题 cmd 本身就是完整命令,本来就用不上拼接——读源码判断结构,别背手法
空字符与通配符c''at 空引号、/fla?、/???/?at /e't'c/pass?d'' 可用但没用了;? 是唯一主武器注意!那关示范里的 /???/?at 就是本关答案的雏形——目录通配 + 命令通配,本关只是把它逼成唯一解
终端特性速查表*、?、[]、{}只剩 ?? 匹配恰好 1 字符,最精确;* 贪婪但被禁;通配符优先级 /fla? > /fla[g] > /fl* > /fl''ag
上一关 4.3 伪解cat /FLAG 静默失败⚠️ 同款坑system() 只回显 stdout——“没 WAF 但没输出”= 展开失败或文件不存在

一条贯穿所有关卡的主线:从 cat /flag → c''at /fla? → /???/?at /e't'c/pass?d → /???/?a?%20/????,payload 一步步"变质”,但骨架从没变过——在 WAF 看得见的文本层和 shell 看得见的文件系统层之间制造差异。手法会随黑名单升级而失效,这条主线不会。

本关新增的两条独立知识点:

  1. 响应体积侧信道:字母被杀光、ls 不可用时,按文件名长度逐个 cat,用响应体积比基线暴涨多少来判断命中。这是"无回显枚举"的最朴素形态。
  2. 读字符集正则的方法:见到 [...] 别慌,逐段翻译成"拦了什么、剩了什么",先列幸存字符全集,再从全集出发倒推 payload——而不是从记忆里的 bypass 清单一条条试。

八、要点清单(可当速查卡)

  1. 先列幸存字符集:拿到字符级黑名单,第一件事是把"能用什么"列全,再谈 payload。
  2. 只剩 ? 时:命令、路径的所有字母都用 ? 顶替,唯一幸存字母(如 a)保留真身。
  3. 命令名通配必须带目录前缀:/???/?a?;精简镜像用 /???/???/?a?。裸写 ?a? = 静默失败。
  4. ? 匹配恰好 1 字符:文件名长度决定 ? 的个数;长度未知就按 4、5、6……逐个试。
  5. 枚举靠响应体积:比基线大两个数量级 = 命中真实文件;flag 在输出的头或尾。
  6. 短通配符(??)慎用:会同时展开成一堆命令,行为不可控。
  7. $IFS 本关可用:$ 幸存,?a?$IFS/???? 可以连空格一起省掉。
  8. system() 只回显 stdout:没 WAF 但没输出 = 展开失败 / 文件不存在 / 命令失败,按这个顺序排。
  9. WAF 在文本层、glob 在文件系统层:所有通配符绕过的总原理,背不住手法时回来想这条。
  10. 改完 payload 重新点 Send——Repeater 不会自动重发,永远的第一嫌疑。

九、汇总表

项目内容
关卡HelloCTF RCE-labs 挑战关(ctfplus / Geesec 平台)
WAF 正则[b-zA-Z_@#%^&*:{}\-\+<>\"|;[]]`
幸存字符a、0-9、$ ( ) = . , / ? ! ' 空格
我的第一发 payloadtac%20fl* → WAF!(t/c/f/l 踩 b-z,* 踩黑名单)
失败原因非a字母全被禁 + * 被禁,通配符只剩 ?
核心思路命令与路径的全部字母用 ? 藏进通配符,a 保留真身
中间踩的坑裸 ?a? 无输出(展开前提=文件真实存在);/???/???/?? 枚举失效(多匹配不可控)
最终 payloadGET /?cmd=/???/?a?%20/????
等价于cat /flag(flag 为根目录 4 字符文件)
flagGeesec{97487dac-ea51-4e71-bcdf-5c6592ab4471}
新学知识点① glob 三铁律(查文件系统/不匹配原样保留/多匹配全展开)② 响应体积侧信道枚举 ③ 命令名通配必须带目录前缀

Writeup 完