HelloCTF RCE-labs Level 7 题解:空格过滤与 ${IFS} 绕过

HelloCTF RCE-labs Level 7 题解:空格过滤与 ${IFS} 绕过

本文字数:2244  阅读时长:5分钟

访问量 0

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:

text
GET /?cmd=cat${IFS}/????? HTTP/1.1

(/????? 用 ? 通配符匹配 /flag,5 个 ? 对应 5 个字符;如果本关只拦空格不拦 flag,直接写 cat${IFS}/flag 同样能过。)

响应里拿到 flag:

text
Geesec{802e0839-0240-4a1c-90e5-6297e0317664}

二、核心问题:为什么 %20 不行,${IFS} 却行

这是本关最值得挖透的地方。答案一句话:

%20 只是空格的"传输马甲",到服务器就现形了;${IFS} 的空格是 shell 在过滤之后才变出来的。

2.1 两条路径的完整时序

payload 从你手里到被执行,要经过四个阶段:

text
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 是空串位置参数,用来垫开变量名边界经典写法
%09URL 编码的 TabTab 同样能分隔命令参数;只有 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} 把空格藏到它身后——所有过滤类绕过,本质上都是在和"检测与执行之间的时间差"打交道。