HelloCTF RCE-labs · 命令执行:终端特殊字符(空格过滤)—— Writeup
| 项目 | 内容 |
|---|---|
| 靶场 | HelloCTF RCE 靶场(github.com/ProbiusOfficial/RCE-labs,作者 探姬) |
| 关卡 | 命令执行 —— 终端特殊字符(空格过滤) |
| 题型 | Web / PHP 命令执行(Command Injection / RCE) |
| 目标 | http://80-eb623a7c-d07c-4025-9dc1-71c667d03a98.challenge.ctfplus.cn/ |
| 入口参数 | GET 参数 cmd |
| 过滤情况 | 过滤空格(命令和文件名之间没法用空格分隔) |
| 工具 | Burp Suite Community v2026.8(新版 UI)→ Repeater |
| 结果 | 拿到 Geesec{802e0839-0240-4a1c-90e5-6297e0317664} |
注:上方地址是平台下发的临时实例,题目做完后会失效。复现时请替换成自己那一份。
一、题目与解题思路
页面照例用 highlight_file(__FILE__) 打印源码,出题人注释标注本关主题为"终端特殊字符"。核心逻辑与前面几关一致:GET 参数 cmd 原样拼进 system() 执行,但本关对输入里的空格做了过滤。
于是问题收敛成一个:没有空格,怎么让 cat 和文件名分开?
最终 payload:
GET /?cmd=cat${IFS}/????? HTTP/1.1(/????? 用 ? 通配符匹配 /flag,5 个 ? 对应 5 个字符;如果本关只拦空格不拦 flag,直接写 cat${IFS}/flag 同样能过。)
响应里拿到 flag:
Geesec{802e0839-0240-4a1c-90e5-6297e0317664}二、核心问题:为什么 %20 不行,${IFS} 却行
这是本关最值得挖透的地方。答案一句话:
%20只是空格的"传输马甲",到服务器就现形了;${IFS}的空格是 shell 在过滤之后才变出来的。
2.1 两条路径的完整时序
payload 从你手里到被执行,要经过四个阶段:
URL 解码 → WAF 过滤 → shell 展开 → 命令执行%20 路径(被拦):
| 阶段 | 发生了什么 |
|---|---|
| 服务端收到查询串 | cmd=cat%20flag |
| ① URL 解码 | PHP 第一件事就是解码 → cat flag,空格在进入任何过滤代码之前就现形了 |
| ② WAF 过滤 | 检查字符串里有没有空格 → 有 → 拦截 |
${IFS} 路径(放行):
| 阶段 | 发生了什么 |
|---|---|
| 服务端收到查询串 | cmd=cat${IFS}flag |
| ① URL 解码 | ${IFS} 不是编码,解码后还是这串字符,全程不含空格 |
| ② WAF 过滤 | 找不到空格 → 没东西可拦 → 放行 |
| ③ shell 展开 | PHP 把字符串原样交给 system(),bash 解析时做变量展开:IFS(Internal Field Separator,内部字段分隔符)默认值就是空格 → ${IFS} 展开成空格 |
| ④ 命令执行 | 实际执行 cat flag ✅ |
关键在于:过滤逻辑只看"解码后的字符串",而空格是在过滤之后的 shell 展开阶段才产生的——过滤器在时间线上根本够不到它。%20 是把空格"提前带进来"(解码即现形),${IFS} 是把空格"延后造出来"(执行时才展开),前者死在②,后者活在③。
2.2 为什么是 ${IFS} 而不是 $IFS
花括号不是可有可无的装饰,它决定变量名的边界:
| 写法 | bash 怎么解析 | 结果 |
|---|---|---|
cat${IFS}flag | 变量名明确是 IFS | 展开成 cat flag ✅ |
cat$IFSflag | 变量名被解析成 IFSflag(变量名可含字母数字下划线,整个吞进去) | 变量不存在 → 展开成空串 → 变成 catflag ❌ |
cat$IFS/flag | 后面是 /,不是变量名字符,边界自然断开 | 展开成 cat /flag ✅(省花括号的情况) |
规则:$IFS 后面紧跟字母/数字/下划线时必须加花括号;后面是 /、'、; 这类非变量名字符时可以省。
2.3 $IFS$9 —— 经典的"垫边界"写法
老牌写法 cat$IFS$9flag:$9 是 shell 的第 9 个位置参数,没传参时默认是空字符串。它本身不贡献内容,作用是把 $IFS 和后面的 flag 隔开,防止变量名被整体吞掉——和花括号殊途同归,只是更老练的写法。
三、空格替代品全表(同款思路,变体题直接换着试)
| Payload | 原理 | 备注 |
|---|---|---|
${IFS} | bash 变量展开,IFS 默认 = 空格 | 最通用,首选 |
$IFS | 同上,但后面的字符可能被并进变量名 | 后接 /、' 等非变量名字符时可省花括号 |
$IFS$9 | $9 是空串位置参数,用来垫开变量名边界 | 经典写法 |
%09 | URL 编码的 Tab | Tab 同样能分隔命令参数;只有 WAF 单拦空格不拦 Tab 时可用(本关解码后 preg_match 连 Tab 一起算进空白字符时就会失效,要实测) |
{cat,flag} | bash 花括号展开,, 展开为空格 | 连空格带写法一起绕,但要确认 WAF 没拦 {} 或 , |
<flag / cat<>flag | 重定向符号本身就是分隔符,cat<flag 等价读取文件 | 需目标文件对 www-data 可读 |
$IFS%0A 系变体 | 换行符在某些 shell 语境可当命令分隔符 | 更多用于绕 ; 的过滤,不是主流空格替代 |
四、这道题想教你什么
| 教学点 | 具体内容 |
|---|---|
| 1. 绕过的本质是时序错位 | 过滤发生在"解码后的字符串"上,而 shell 展开(变量、花括号、通配符)发生在过滤之后——把危险字符的产生从"输入阶段"挪到"过滤之后",就绕过了 |
| 2. 编码 ≠ 绕过 | %20 这类百分号编码是给 HTTP 传输用的"马甲",到服务器解码就现形;它解决的是"怎么传",不是"怎么绕" |
| 3. bash 变量展开是天然的 payload 生成器 | ${IFS}、$IFS$9、{a,b} 都是"输入时没有空格、执行时空格自己长出来" |
| 4. 变量名边界意识 | $IFS 和 ${IFS} 的区别在花括号划定变量名边界——这是 bash 语法基本功,不是玄学 |
| 5. 通配符配合使用 | ? / * 代替文件名(/????? ≈ /flag),和空格绕过是两个独立的维度,组合使用 |
五、汇总表格
| 项目 | 内容 |
|---|---|
| 关卡主题 | 空格过滤(终端特殊字符) |
| 漏洞点 | system($cmd),GET 参数 cmd 原样拼入 |
| 过滤规则 | 输入中含空格即拦截 |
| 最终 payload | ?cmd=cat${IFS}/?????(或 cat${IFS}/flag) |
| 绕过原理 | 输入串无空格过 WAF → bash 展开 ${IFS}(IFS 默认空格)→ 实际执行 cat flag |
| 关键时序 | URL 解码 → WAF 过滤 → shell 展开 → 命令执行(空格诞生于第③步,过滤器只盯到第②步) |
%20 失败原因 | 解码即还原成真空格,先于过滤现形 |
| 花括号作用 | 划定变量名边界,防止 $IFSflag 被整体解析成 $IFSflag 变量 |
| 同类替代 | $IFS$9 / %09(Tab) / {cat,flag} / cat<flag |
| 结果 | Geesec{802e0839-0240-4a1c-90e5-6297e0317664} |
六、一句话总结
WAF 只能拦"它看得见的字符"。
%20把空格送到它眼前,${IFS}把空格藏到它身后——所有过滤类绕过,本质上都是在和"检测与执行之间的时间差"打交道。
- 本文链接: https://quinc.cn/posts/helloctf_rce_labs_lv7_writeup/
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。

