1. 项目概述:这到底是个什么问题先把这个话题说透。命令执行漏洞,又称RCE(Remote Code Execution),在渗透测试和漏洞挖掘里一直是高危级别的存在。攻击者一旦拿到命令执行点,基本上等同于拿下了服务器的半条命。但现实情况是,拿到命令执行点只是开始,目标系统里的过滤规则、安全设备、WAF(Web应用防火墙)并不会让你舒服地执行命令。而通配符绕过,正是绕开过滤限制的经典手法之一,尤其当过滤机制只做了简单的字符串匹配,阻挡了“空格”“管道符”“分号”等常见分隔字符,却没有对通配符做严格限制的时候,整个防线实际上已经出现了裂缝。简单来说,这个项目的本质,就是探讨如何利用系统通配符的展开机制,在过滤规则只检查关键字而不检查特殊符号的场景下,完成命令拼接与执行。很多人第一次听说会觉得“就这?”,但真正在实战中踩过坑的都知道,通配符绕过看起来简单,真正落地的时候坑很多,比如通配符匹配不到文件、参数位置不对、命令长度受限等等。这篇文章会从原理、实操、绕过思路到对应防御手段,把这条线完整讲清楚。适合谁看?如果你是安全研发、WAF规则运维、渗透测试初学者,或者在做代码审计时遇到过命令拼接类漏洞但不知道怎么系统化绕过与防御,这篇文章会帮你把思路理清楚。我不讲空话,直接上干货。2. 通配符绕过的核心原理拆解2.1 通配符的本质是Shell的“展开”机制要理解通配符绕过,先得理解一个事实:通配符不是命令本身的功能,而是Shell在解析命令时做的事。以Linux系统为例,Bash在收到一条命令后,会先对命令字符串进行词法分析,把其中有特殊含义的字符(比如*、?、[、{)进行路径展开。展开的意思是,Shell会把*.txt这个模式去匹配当前目录下的文件名,如果匹配到a.txt b.txt,它就会把*.txt这个字符串替换成a.txt b.txt再执行。这个机制和过滤有什么关系?关系大了。很多WAF和代码层过滤规则,检查的是开发者能“猜到的”危险字符和危险命令。比如过滤/bin/sh、过滤cat、过滤flag、过滤空格。但过滤脚本很难对所有通配符组合做穷举,因为通配符展开之后,进入了命令参数或者命令本身的位置,最终被执行的会是展开后的字符串,而不是你传进去的那一串原始内容。这个时间差,就是绕过空间。举一个最朴素的例子。过滤规则拦截了cat字符串,你直接提交cat /etc/passwd会被拦。但如果你构造c\at /etc/passwd、/bin/c?t /etc/passwd或者用ca[t] /etc/passwd,就可能绕过去,因为正则规则匹配的是cat,而Shell展开后正好是cat。2.2 为什么空格是绕过的重灾区深入一点说,命令执行漏洞里最头疼的往往不是命令关键字,而是空格。空格在很多过滤场景下是第一个被处理的符号。为什么?因为绝大多数命令拼接格式都非常依赖空格,比如cat /etc/passwd中间的空格就是天然分隔符。过滤规则往往简单粗暴:不允许出现空格,出现就直接拦截。这里就要用到通配符的另一种特性了。Bash在路径展开之外,还有一些特殊的变量展开机制。比如${IFS},这个变量在Bash里默认是空格、Tab、换行的一组字符。你在命令中写${IFS},Shell在展开变量的时候会把它替换成空格字符。所以cat${IFS}/etc/passwd在命令执行时等价于cat /etc/passwd,但如果过滤规则只匹配空格和%20,那么你提交cat${IFS}/etc/passwd,过滤规则如果没处理${和}这类字符,直接就被放行了。2.3 通配符的几种典型形式通配符在绕过场景里,远比很多人想象中丰富。列一下常见的几种:*:星号,匹配任意长度的任意字符,包括空字符串。常用于模糊匹配文件名,但也经常被用来代替命令名或参数的一部分。?:问号,匹配恰好一个任意字符。比如c?t能匹配cat、cbt、cct,只要中间恰好是一个字符。[...]:字符集匹配,匹配括号内任意一个字符。比如ca[t]就是cat,[c-f]at可以匹配cat、dat、eat、fat。[!...]或[^...]:反向字符集,匹配不在括号内的任意一个字符。{...}:花括号展开,它比路径展开更早发生,用于生成多个字符串。比如{cat,echo} /etc/passwd,Shell会把它展开成两条命令依次执行。实际在绕过中,前三种用得最多,但花括号展开往往能带来惊喜,因为它的行为和路径展开略微不同,很多过滤规则完全没考虑这一层。2.4 命令名的通配符替代理解了展开机制,很自然的思路就是:不仅可以对参数做通配符处理,命令本身也能用通配符代替。假设目标系统里存在/bin/cat,而你提交的原始参数被过滤规则检查时,“cat”这个子串被拦截。此时如果构造/bin/c?t /etc/passwd,WAF的规则可能只检测命令首段是否匹配已知黑名单命令,/bin/c?t不在黑名单里,于是放行。等到Shell真正执行时,路径展开把/bin/c?t匹配成/bin/cat,命令照常执行。这里有一个前提条件,就是目标目录下不能有多个能匹配上这个模式的文件。比如你写/bin/c?t,如果恰好/bin下有一个cat但也有一个cut,那么?展开会同时匹配到/bin/cat和/bin/cut,Shell会尝试把/bin/cat /bin/cut一起执行,这往往会导致命令报错或者产生不可预期的结果。所以实际利用中,通常需要先了解目标环境的结构,越了解,构造的通配符越精确,成功率越高。3. 绕过场景建模与前置条件分析3.1 典型的过滤规则模型在深入学习具体绕过方式前,先建立一个概念框架。实际项目中遇到的过滤规则,基本上可以归为三类,这三类的绕法思路完全不同。第一类是黑名单过滤。维护一个危险命令和危险字符的列表,只要输入串匹配到黑名单就拦截。这类过滤最脆弱,因为它只认识它认识的东西,你稍微变换一下写法它就不认识了。经典的绕过方式有字符串拼接、编码绕过、大小写混合、通配符插入等。第二类是白名单过滤。只允许特定字符集和特定格式的输入,其余全部拒绝。这类过滤相较而言更安全,但也不是铜墙铁壁,问题往往出在白名单本身设计不合理,比如允许了${}却不知道它会被展开,允许了*却不知道它会被路径展开。第三类是语义分析过滤。这类已经接近WAF的更高阶形态了,它会尝试还原你输入的最终意图,甚至模拟Shell的展开结果来判断是不是恶意命令。但即便这样,绕过仍然可能,因为通配符展开依赖目标服务器的文件系统状态,你无法在WAF层完全预测Shell在所有场景下的展开结果。在真实业务中,绝大多数情况是三类混用,但底层大多还是黑名单思路。这也就解释了为什么通配符绕过在今天依然有效:黑名单再长,也长不过Shell的展开机制。3.2 绕过的前置条件清单通配符绕过不是万能的,它对运行环境有比较明确的要求。我在实际测试里总结了一份前置条件清单,供你对照判断:目标系统是类Unix环境(Linux、Unix、macOS),Windows的cmd和PowerShell对通配符的处理机制不同,绕法也完全不同。命令执行漏洞点支持传入多字符参数,而不是只能传单个数字或固定字符串。过滤规则关注的是命令和参数的“关键字”,而不是限制参数长度和特殊字符种类。目标环境中有可以被通配符匹配到的文件或目录,满足路径展开的条件。执行的Shell具备正常的展开功能(Bash、Dash、Sh均可,C Shell部分行为不同)。如果你判断当前漏洞点不满足以上条件,那通配符绕过大概率行不通,需要考虑别的路子,比如换行符分隔、十六进制编码、环境变量拼接等,但那些不在本文重点范围内。3.3 为什么说“简洁版”不等于“简单版”标题里写着“简洁版”,但实际处理过类似问题的同行都清楚,越是基础的手法,越要精细控制。很多新手一上来就写cat /etc/passwd,被拦了就换个cat${IFS}/etc/passwd,还是被拦,就不知道下一步怎么走了。原因在于,他缺少一个系统的测试思路:没有先探测过滤规则、没有构造分步测试的Payload、没有观察哪些字符能过哪些不能过、没有根据报错信息反推过滤规则的匹配逻辑。我习惯的做法是,把一次绕过拆成四个阶段来推进:第一阶段探测过滤边界,第二阶段构造基础Payload,第三阶段在基础Payload上叠加变形,第四阶段在目标环境实测并调整。每个阶段都有对应的测试方法和判断标准,而不是盲目堆Payload。下面的实操部分就按照这个思路来展开。4. 实操全记录:从探测到完整绕过4.1 第一阶段:探测过滤规则的边界拿到一个漏洞点之后,我第一个动作永远不是急着打Payload,而是先搞清楚对面的过滤器到底拦了什么、放行了什么。这一步看起来简单,其实非常考验细节。构造一组探测请求,本质上是在测试字符能否通过。比如给每个可能用到的重要符号或命令字母单独发送一次,观察响应码和回显差异。我常用的探测字符大致包括:空格、制表符、$、(、)、&、|、;、<、>、'、"、*、?、[、]、{、}、${IFS}、%0a等。对于每一种字符,分别构造请求,观察是否被拦截(拦截通常会表现为400/403/406或返回统一错误页),以及不被拦截时命令执行的结果是否出现差异。这里举一个实际探测的案例。假设目标存在一个Ping功能的参数,系统拼接命令为ping -c 4 你的输入,过滤规则未知。我先提交一个正常的IP127.0.0.1,确保命令能执行。然后依次提交127.0.0.1;id、127.0.0.1|id、127.0.0.1&id、127.0.0.1$(id)、以及带空格的127.0.0.1 id。观察哪一类请求被拦截。接着,在确认;被拦截后,再提交127.0.0.1%3bid(URL编码后的分号),看URL解码是否发生在过滤检查之前。这个探测信息量很大。它能直接告诉你过滤的位置:是在应用层做了正则匹配,还是WAF在HTTP层拦截,还是系统层做命令黑名单检测。不同层级的过滤,对应的绕过策略根本不一样。4.2 第二阶段:构造基础Payload的五种经典姿势在完成了边界探测后,就可以开始构造第一个能跑通的绕过Payload了。这里我按实战经验给你列五种最常用的姿势,每种都附带一个最简示例和适用场景。姿势一:利用?代替单字符适用场景:命令关键字被拦截,但通配符没有被处理。提交127.0.0.1; /bin/c?t /etc/passwd,目标过滤规则如果匹配的是cat,那么/bin/c?t就不中招。注意这里的命令名因为路径展开变成了/bin/cat。姿势二:利用*模糊化命令路径适用场景:命令执行环境相对简单,比如/usr/bin下文件较少,或者你能确定目标系统版本对应的命令路径。可以构造/usr/bin/c* /etc/passwd,只要当前环境里匹配到的文件只有cat,就能顺利执行。如果匹配到多个文件,命令会报错,但报错信息里往往会把匹配到的文件名带出来,恰好可以当目录列举用。姿势三:利用[...]精确匹配单字符适用场景:需要避免多文件匹配的副作用,而?太宽泛的时候,用字符集能把匹配范围收紧到“必然命中目标文件”。比如cat可以写成ca[t],ls可以写成l[s]。这个技巧的隐蔽性比?和*更强,因为正则引擎匹配“cat”字符串时,ca[t]是匹配不上的。姿势四:利用${IFS}替代空格适用场景:一切因为空格被拦而无法拼接命令的情况。注意${IFS}和$IFS不一样,$IFS在某些场景下替换的字符串末尾会带上空字符,需要留意。实测最稳定的写法是${IFS}完整带花括号。另外,$IFS$1也是一种变体,$1是Shell位置参数,默认状态下为空,利用它可以起到分隔变量的作用。姿势五:利用花括号展开{cmd,arg}适用场景:过滤规则对逗号和花括号没有做限制时,可以用花括号展开来绕过空格过滤。例如{cat,/etc/passwd}。Shell会把他展开成cat /etc/passwd,而且整个展开发生在命令解析阶段,WAF看到的只是{cat,/etc/passwd}一串,很难直观判断其危险性。4.3 第三阶段:基础Payload的叠加变形与混合利用真实场景里,单一姿势往往不够用。过滤规则常常同时拦截命令关键字和空格,甚至有更变态的会拦截“路径分割符号/”。下面讲几个常用的叠加技巧。第一个叠加思路是反斜杠绕过。很多过滤规则对反斜杠没有处理能力,因为单个\在部分语言里不是正则的活跃字符。此时构造c\at /etc/passwd,Shell中反斜杠会转义后面一个字符,最终执行时仍然是cat。把它和${IFS}叠加,就能同时绕开命令关键字和空格的检测:c\at${IFS}/etc/passwd。第二个叠加思路是利用环境变量拼接。比如你写${PATH:0:1},替换出来的结果是/,因为PATH环境变量的第一个字符通常是/。如果你同时写了${PATH:0:1}etc${PATH:0:1}passwd,展开后就是/etc/passwd。这种用法适合系统拦截了/字符的场景。但注意,这个技巧对服务器环境较敏感,如果PATH被改过就用不了,需要先验证。第三个叠加思路是变量拼接命令名。假设ls被拦截,但l和s单独不拦截,可以用l''s或l$@s来绕过。$@在Bash中展开为空或一系列参数,配合字符串拼接效果显著。这种技巧适合过滤规则简单粗暴匹配单个命令字符串的情况,叠加通配符也能用:/bin/l$@s或者l$@s${IFS}/etc。第四个思路是换行符与注释符号的组合。有的过滤是按行检测的,也就是只检测第一行内容,此时可以用%0a(换行)绕过。例如原命令是ping -c 4 127.0.0.1,你提交127.0.0.1%0acat /etc/passwd#,换行后新命令cat /etc/passwd在下一行执行,#注释掉系统本来要拼接的尾部。这种方式和通配符无关,但经常和通配符叠加使用,形成双保险。4.4 第四阶段:实战案例完整拆解上面讲了一堆原理和姿势,现在用一个虚构但完全符合真实场景的案例走一遍完整流程。场景描述:某公司有一个内部测试系统,提供了一个通过Web页面执行系统命令的功能模块,用于管理员查看服务器磁盘信息。Web后端逻辑大致是:接收参数disk,在Shell中执行df -h 你的输入,然后返回输出结果。我只知道这个系统的过滤规则会对常见命令和符号做拦截,但具体规则未知。第一步,先用普通参数/?disk=/tmp测试,命令正常执行,返回了df输出,确认命令执行点存在。第二步,做边界探测,提交/?disk=/tmp;id,返回403拦截页。再提交/?disk=/tmp|id,同样403。再试/?disk=/tmp$(id),有意思的是这个请求通过了,返回结果里没有id的输出。这说明两条线索:命令分隔符;、|被拦截;$()没有完全被拦截但可能嵌套深度不够导致命令没执行,或者被其他方式解析掉了。第三步,验证通配符是否可用。提交/?disk=/tmp*,返回结果正常,*符号没有被过滤。这是一个关键信息,意味着路径展开机制可能可以利用。第四步,构造通配符绕过Payload。既然;被拦截,但可以通过换行符%0a来分隔命令。构造/?disk=/tmp%0a/bin/c?t${IFS}/etc/passwd。这里同时用到了三个技巧:换行符做命令分隔、?代替at中的a、${IFS}代替空格。实际执行结果成功返回了/etc/passwd的内容。第五步,优化Payload减少噪音。上面的Payload已经成功,但里面带了明显的passwd字样,如果目标有第二层过滤会很难堪。继续优化为/?disk=/tmp%0a/bin/c?t${IFS}/etc/p*sswd,这样过滤规则连passwd都匹配不到了,但通配符展开后仍然是/etc/passwd。同样,如果系统还拦截了bin、etc,可以继续用/?通配符替换:/b?n/c?t${IFS}/etc/p*sswd。这个案例里最关键的一步是什么?是对*符号未被过滤的确认。没有这一步,后面所有操作都是盲猜。很多新手上来直接堆Payload,往往就是因为跳过了探测环节,导致连自己失败的原因在哪里都说不清楚。5. 难点突破:当通配符也失效时的备用方案5.1 通配符失效的四种典型原因实践中总会遇到通配符绕不过去的情况。总结下来,最常见的原因有下面几种,必须逐一排查。第一种情况,目标环境不是Bash系。部分特殊环境使用sh的POSIX模式时,通配符行为基本一致,但某些嵌入式设备或精简系统使用的Shell对花括号展开支持不全。这种时候就要优先用?和*,避免花括号方案。第二种情况,匹配到了多个文件导致命令执行失败。比如/usr/bin/c*在目标系统上同时命中了cat、cut、curl等多个文件,Shell会把所有文件名拼接成一条参数,命令大概率直接崩溃。这种情况需要改用?精确定位数量:/usr/bin/ca?只精确匹配两个字符再加一个任意字符,命中面小很多。或者用字符集:/usr/bin/c[au][rt]精确控制每一位。第三种情况,WAF做了语义还原。部分WAF在收到请求后,会对%0a做解码,并对${IFS}做变量模拟,导致换行和变量拼接双双失效。这种情况不能只靠单一工具绕了,得考虑利用时间延迟判断命令是否执行,或利用DNS查询外带数据,但这些超出了本文范围,以后再单独开篇。第四种情况,命令参数位置导致通配符无法展开。例如目标的命令拼接是ping -c 4 你的输入,而你输入的参数在最后一个位置。此时如果你提交的是*,Shell会把当前目录下所有文件名都填进来当参数,没法控制。这种情况可以考虑先在输入里把命令分隔符打通,再做二次利用。5.2 一个容易忽视的辅助技巧:利用Pinpoint说完失效原因,分享一个我常用但看到很多人不太会用的技巧:利用命令本身作为目录探测工具。当你不知道目标系统的目录结构时,可以利用ls命令配合通配符让目标自己“报名字”。比如构造/?disk=/tmp; l?st${IFS}/${IFS}b*n这样的Payload(注意这里l?st可以匹配ls或者某个名字相近的命令),如果命令成功执行,返回的结果自然把目标目录下的文件列表带出来。之后你就有了文件清单,再针对性地构造第二次、第三次Payload,命中率大幅提升。本质上,这就是“信息收集—确定目标—精准打击”的思路在命令执行漏洞利用上的具体化。很多新手失败不是Payload不够多,而是对目标环境了解太少,连当前目录下有什么文件都没搞清楚就去猜cat flag,失败是理所当然的。5.3 短命令场景的特殊处理还有一种特殊情况:命令执行点可能只是一个回显型接口,比如把参数作为外部程序的参数执行,但输出受限。这种情况下,你构造的通配符Paylaod能不能生效,往往取决于回显本身的内容。如果回显总是被截断,那就优先考虑写文件型的Payload,利用命令把结果写入到Web目录下的一个文件里,再通过另一个HTTP请求读取。类似地,如果命令行需要交互,通配符绕过是无法直接完成交互的。这种情况需要借助已经执行成功的命令来建立一个更方便的执行通道,通常思路是先下载一个反向工具或写入一个WebShell。但这类操作涉及的使用方式千差万别,而且合规风险很高,这里就不展开具体操作了。6. 防御视角:从攻到防的关键加固措施6.1 过滤规则不是安全边界在讲具体防御措施前,先说一个我踩过坑后的血泪经验:单纯依赖输入过滤来防御命令注入,本质上是在和攻击者玩猫鼠游戏。今天补了cat的过滤,明天攻击者用c\at绕过;今天补了反斜杠,明天还有变量拼接;今天全部补上,后天还有通配符。所以真正的防线必须前移到“如何避免用户输入进入命令解释器”。这一条是所有防御措施的大前提。如果你的系统架构已经允许用户的原始输入直接拼接到Shell命令里,那么任何过滤手段都只是降低风险不等于消除风险。6.2 代码层的正确写法从代码层面修复命令注入,最稳妥的是两条路。第一条是彻底的“不要用Shell执行”。比如在Python里不要用os.system或者os.popen,改成subprocess.run并传入参数列表,不让Shell介入解释。subprocess.run(["df", "-h", user_input])中的user_input会被当作df的独立参数而不是Shell命令的一部分。这条说完,很多人会问那如果业务必须用Shell呢?比如要用管道、重定向、通配符展开这些Shell能力。这种时候没有银弹,只能做白名单校验。只允许输入匹配预定义的安全格式(比如IP地址、数字、纯文件名),格式之外的一律拒绝。在PHP场景里,必须避免使用eval、system、exec、shell_exec、`等危险函数,或者在使用前进行严格的参数校验。在Java场景里,优先使用ProcessBuilder,传入字符串数组避免Shell解释。工欲善其事必先利其器,这些是代码审计和研发自测的底线要求。6.3 拦截规则如何设计才算合理如果确实需要在应用层做输出过滤或输入过滤(比如临时热修、WAF规则应急),那么设计规则时要有几个原则:第一,不要只匹配明文命令。规则要覆盖常见的编码形式和通配符形式:URL编码、Unicode编码、十六进制、反斜杠转义、变量拼接、通配符插值。一个规则里出现cat的同时,最好也出现c[a-z]at、c\at、c%61t、${IFS}等变体。但这么做也容易产生误拦,所以要有配套的放行机制。第二,要在命令到达Shell之前做内容解码后的二次检查。比如请求先经过一次URL解码,检查一次,到后端又可能被框架二次解码,再检查一次。很多绕过就是利用了“过滤时看到的和解码后执行的不是同一个字符串”这个时间差。第三,要对执行结果设置返回限制。即使命令被执行了,如果回显很小或者只有状态码,攻击者拿不到输出数据,利用难度会大幅上升。可考虑关闭命令回显、只保留成功/失败状态码。6.4 纵深防御的最后几条防线在业务无法快速迭代代码的时候,通过系统层加固来兜底,是我在应急响应中反复推荐的方案。降低Web服务进程的权限,绝对不能用root或者管理员权限运行。开启SELinux或者AppArmor,限制进程能执行的二进制文件范围。对关键系统命令做别名或白名单包装。限制Web服务可以访问的目录,禁止执行/tmp、/var/www/html等目录下的文件。开启审计日志,记录命令执行类系统调用,便于事后追溯。有条件时把命令执行功能迁移到独立的沙箱容器中,与核心业务网络隔离。这些措施不能阻止攻击者执行第一条命令,但能极大延长攻击链路,把“拿下服务器”变成“看了两眼目录就走”,这是实际对抗中非常常见的分水岭。7. 实际项目中的踩坑记录与排查技巧7.1 踩坑记录:匹配多文件导致全部命令失效先说最典型的坑。某次在测试一个老系统的命令执行点时,我用/?disk=/tmp; /usr/bin/c*${IFS}/etc/passwd来做注入,结果不仅cat没执行成功,连原本应该正常执行的df输出都丢了。排查发现原因在于/usr/bin目录下同时存在cat、calendar、catch、catman等一堆以c开头的命令,c*把所有匹配项都展开成参数,最后传给cat的命令行变成了一堆文件路径,直接报错。这个问题的解法有两个方向。方向一是缩小通配范围:换成/usr/bin/ca?,这样只匹配cat、cal(如果有)、cap(如果有)等恰好三位且以ca开头的命令,命中面小很多。方向二是用字符集精确替代:/usr/bin/c[at]匹配到的是ca和ct两个文件,不一定命中目标。所以最佳选择往往是用?加路径限制,先探测目标目录下与该模式匹配的文件到底有几个,再做调整。还有一个小技巧是,如果匹配到多个文件导致报错,报错信息往往会列出“参数过多”或者“无法打开”的文件名。这些报错信息本身就是Banner信息,可以反向利用来获取目录列表。7.2 踩坑记录:${IFS}在某些位置不生效另一个常见的问题是${IFS}在部分命令的中间位置不生效。举例来说,cat${IFS}/etc/passwd展开后是cat /etc/passwd,正常。但如果你把它放在管道符后面,比如ls${IFS}./|${IFS}cat${IFS}/etc/passwd,某些精简版Shell对${IFS}的展开会有兼容性问题。实测中,Bash完全没问题,Dash在部分场景下会有细微差异。解决方式也很简单,放弃${IFS}在这个场景的使用,改用Tab字符。Tab字符在Shell中同样是命令行的合法分隔符,而且很多过滤规则不会检查Tab(尤其是只按空格切割输入的情况)。在HTTP请求中,Tab可以表示为%09。实测效果非常稳定。如果Tab和${IFS}都被过滤,还有最后的选择:利用参数扩展拼接。构造$IFS$9,其中$9是第九个位置参数,正常Shell环境下它是空的,利用它作为变量结束边界,有时能规避过滤对$IFS的精确匹配。这个技巧对Bash有效,对Sh的兼容性稍差,需要现场验证。7.3 踩坑记录:通配符绕过成功但命令输出被截断命令执行成功不等于拿到了完整输出,很多实战场景下命令回显只返回前几行或者部分内容。出现这种情况,首先要确认是因为过滤规则截断了回显,还是Web层做了输出长度限制,还是后端仅返回部分日志。如果输出被截断,一个通用的处理思路是利用命令本身能力缩小输出范围或者把输出变成更容易回显的形式。比如cat /etc/passwd | cut -c1-30,但这里管道符可能也被过滤,那就换成head -c 30 /etc/passwd(配合通配符做命令名绕过)。或者干脆用grep定位到指定行号再输出:sed -n '3p' /etc/passwd。如果连grep、sed这样的命令也被拦,最简单的办法是利用过滤规则不拦的关键字构造通配符名称,比如s?d、gr?p,再配合${IFS}完成参数注入。最终目标是把回显数据压缩到最小可读范围,而不是贪多求全。7.4 问题排查速查表现场现象可能原因优先排查方式参考方案空格被拦且命令执行失败过滤规则拦空格换${IFS}、Tab、花括号综合使用多种替代分隔符cat被拦黑名单精确匹配换?、[...]、\转义通配符插入、变量拼接*匹配到多个文件环境文件较多换?或字符集缩小范围c[au]t精确定位命令执行无输出回显被截断用head、grep、sed精简输出换用外带方式(DNS/HTTP)命令执行不了也不报错语义过滤拦截检查是否触发二次解码叠加变量与通配符变形注入分隔符;被拦黑名单包含分隔符试%0a、$()、换行换行符分离命令全盘过滤但命令点可用过滤规则有逻辑漏洞逐字符边界探测构造不在规则语义内的Payload这个表是我每次做此类测试前的自查清单,你完全可以照抄到自己项目里用。7.5 一个值得单独说说的技巧:命令执行的响应时间探测有时候目标系统配置了命令回显关闭,所有的命令执行结果都不会返回到页面,这种情况下怎么判断命令是否执行成功?答案是:利用时间差。比如构造127.0.0.1 && sleep 5,如果页面等待了5秒再返回,说明命令成功执行了。这个技巧在探测阶段的价值非常大,尤其在无法确认过滤器到底拦截了什么的时候,时间差能帮你判断一个Payload是否真正到达了Shell。更进阶的用法是结合DNS外带。命令成功执行后,触发一次DNS解析请求,让目标服务器返回一个可控域名,通过检查DNS日志确认命令是否执行。虽然这个方法依赖外部基础设施,但在实战中面对较高级的过滤规则时,它往往是最可靠的验证手段之一。这里不展开讲具体搭建过程,但思路本身值得你在防御设计时警惕:即使页面没有任何回显,攻击者仍然可能通过时间差和网络请求确认漏洞是否可利用。8. 我的实际体会与几条扩展思考做了这么多年的安全测试和应急响应,我最大的感受是,命令执行漏洞之所以高危,不是因为Shell命令本身有多难防,而是因为它总是以各种意想不到的方式出现在业务逻辑里。很多系统的过滤规则写得很努力,但面对通配符这种“发生在Shell内部”的展开机制时,往往只能眼睁睁看着防线被拆穿。这里想用最后一点篇幅分享三个扩展思考,给正在做防护的同行参考。第一个思考是关于LLM与过滤规则的结合。现在的WAF和代码扫描器大多还是基于正则和语义规则来做命令注入检测,但我在实际测试中发现,规则再长也有边界,而通配符展开的变体几乎是无限的。如果能引入大型语言模型对输入做意图还原,在检测阶段模拟Shell展开,也许能显著提高拦截率。当然,这个方案的误报问题很严重,部署成本也高,短期内很难替代传统规则。第二个思考是关于系统命令的现代化改造。既然通配符展开是Shell的机制,那么能减少Shell使用的系统设计,就是更安全的设计。比如在很多云原生场景中,业务容器已经不再内置完整的Bash,而只提供静态编译的二进制可执行文件。这种方式下,攻击者即使拿到了命令执行点,能够利用的系统命令也大大减少,很多通配符绕过技术会失去用武之地。第三个思考是给防御方的建议:在写过滤规则前,先模拟攻击者做一轮完整的绕过测试。我见过太多的规则,写的时候觉得无懈可击,但自己动手绕过一次就发现全是漏洞。所以如果你负责安全规则建设,建议自己也搭建一个测试环境,把文章里提到的几种变形逐一验证一遍。只有亲自动手绕一次,才知道你写的规则在实战面前到底有多脆弱。最后给真正在做攻防对抗的同行留一句:通配符绕过只是命令注入绕过一个很小的切面,它背后隐藏的“Shell语义解析”问题,远比几个Payload重要。理解机制,比记住Payload更能让你走得更远。
1. 项目概述:这到底是个什么问题先把这个话题说透。命令执行漏洞,又称RCE(Remote Code Execution),在渗透测试和漏洞挖掘里一直是高危级别的存在。攻击者一旦拿到命令执行点,基本上等同于拿下了服务器的半条命。但现实情况是,拿到命令执行点只是开始,目标系统里的过滤规则、安全设备、WAF(Web应用防火墙)并不会让你舒服地执行命令。而通配符绕过,正是绕开过滤限制的经典手法之一,尤其当过滤机制只做了简单的字符串匹配,阻挡了“空格”“管道符”“分号”等常见分隔字符,却没有对通配符做严格限制的时候,整个防线实际上已经出现了裂缝。简单来说,这个项目的本质,就是探讨如何利用系统通配符的展开机制,在过滤规则只检查关键字而不检查特殊符号的场景下,完成命令拼接与执行。很多人第一次听说会觉得“就这?”,但真正在实战中踩过坑的都知道,通配符绕过看起来简单,真正落地的时候坑很多,比如通配符匹配不到文件、参数位置不对、命令长度受限等等。这篇文章会从原理、实操、绕过思路到对应防御手段,把这条线完整讲清楚。适合谁看?如果你是安全研发、WAF规则运维、渗透测试初学者,或者在做代码审计时遇到过命令拼接类漏洞但不知道怎么系统化绕过与防御,这篇文章会帮你把思路理清楚。我不讲空话,直接上干货。2. 通配符绕过的核心原理拆解2.1 通配符的本质是Shell的“展开”机制要理解通配符绕过,先得理解一个事实:通配符不是命令本身的功能,而是Shell在解析命令时做的事。以Linux系统为例,Bash在收到一条命令后,会先对命令字符串进行词法分析,把其中有特殊含义的字符(比如*、?、[、{)进行路径展开。展开的意思是,Shell会把*.txt这个模式去匹配当前目录下的文件名,如果匹配到a.txt b.txt,它就会把*.txt这个字符串替换成a.txt b.txt再执行。这个机制和过滤有什么关系?关系大了。很多WAF和代码层过滤规则,检查的是开发者能“猜到的”危险字符和危险命令。比如过滤/bin/sh、过滤cat、过滤flag、过滤空格。但过滤脚本很难对所有通配符组合做穷举,因为通配符展开之后,进入了命令参数或者命令本身的位置,最终被执行的会是展开后的字符串,而不是你传进去的那一串原始内容。这个时间差,就是绕过空间。举一个最朴素的例子。过滤规则拦截了cat字符串,你直接提交cat /etc/passwd会被拦。但如果你构造c\at /etc/passwd、/bin/c?t /etc/passwd或者用ca[t] /etc/passwd,就可能绕过去,因为正则规则匹配的是cat,而Shell展开后正好是cat。2.2 为什么空格是绕过的重灾区深入一点说,命令执行漏洞里最头疼的往往不是命令关键字,而是空格。空格在很多过滤场景下是第一个被处理的符号。为什么?因为绝大多数命令拼接格式都非常依赖空格,比如cat /etc/passwd中间的空格就是天然分隔符。过滤规则往往简单粗暴:不允许出现空格,出现就直接拦截。这里就要用到通配符的另一种特性了。Bash在路径展开之外,还有一些特殊的变量展开机制。比如${IFS},这个变量在Bash里默认是空格、Tab、换行的一组字符。你在命令中写${IFS},Shell在展开变量的时候会把它替换成空格字符。所以cat${IFS}/etc/passwd在命令执行时等价于cat /etc/passwd,但如果过滤规则只匹配空格和%20,那么你提交cat${IFS}/etc/passwd,过滤规则如果没处理${和}这类字符,直接就被放行了。2.3 通配符的几种典型形式通配符在绕过场景里,远比很多人想象中丰富。列一下常见的几种:*:星号,匹配任意长度的任意字符,包括空字符串。常用于模糊匹配文件名,但也经常被用来代替命令名或参数的一部分。?:问号,匹配恰好一个任意字符。比如c?t能匹配cat、cbt、cct,只要中间恰好是一个字符。[...]:字符集匹配,匹配括号内任意一个字符。比如ca[t]就是cat,[c-f]at可以匹配cat、dat、eat、fat。[!...]或[^...]:反向字符集,匹配不在括号内的任意一个字符。{...}:花括号展开,它比路径展开更早发生,用于生成多个字符串。比如{cat,echo} /etc/passwd,Shell会把它展开成两条命令依次执行。实际在绕过中,前三种用得最多,但花括号展开往往能带来惊喜,因为它的行为和路径展开略微不同,很多过滤规则完全没考虑这一层。2.4 命令名的通配符替代理解了展开机制,很自然的思路就是:不仅可以对参数做通配符处理,命令本身也能用通配符代替。假设目标系统里存在/bin/cat,而你提交的原始参数被过滤规则检查时,“cat”这个子串被拦截。此时如果构造/bin/c?t /etc/passwd,WAF的规则可能只检测命令首段是否匹配已知黑名单命令,/bin/c?t不在黑名单里,于是放行。等到Shell真正执行时,路径展开把/bin/c?t匹配成/bin/cat,命令照常执行。这里有一个前提条件,就是目标目录下不能有多个能匹配上这个模式的文件。比如你写/bin/c?t,如果恰好/bin下有一个cat但也有一个cut,那么?展开会同时匹配到/bin/cat和/bin/cut,Shell会尝试把/bin/cat /bin/cut一起执行,这往往会导致命令报错或者产生不可预期的结果。所以实际利用中,通常需要先了解目标环境的结构,越了解,构造的通配符越精确,成功率越高。3. 绕过场景建模与前置条件分析3.1 典型的过滤规则模型在深入学习具体绕过方式前,先建立一个概念框架。实际项目中遇到的过滤规则,基本上可以归为三类,这三类的绕法思路完全不同。第一类是黑名单过滤。维护一个危险命令和危险字符的列表,只要输入串匹配到黑名单就拦截。这类过滤最脆弱,因为它只认识它认识的东西,你稍微变换一下写法它就不认识了。经典的绕过方式有字符串拼接、编码绕过、大小写混合、通配符插入等。第二类是白名单过滤。只允许特定字符集和特定格式的输入,其余全部拒绝。这类过滤相较而言更安全,但也不是铜墙铁壁,问题往往出在白名单本身设计不合理,比如允许了${}却不知道它会被展开,允许了*却不知道它会被路径展开。第三类是语义分析过滤。这类已经接近WAF的更高阶形态了,它会尝试还原你输入的最终意图,甚至模拟Shell的展开结果来判断是不是恶意命令。但即便这样,绕过仍然可能,因为通配符展开依赖目标服务器的文件系统状态,你无法在WAF层完全预测Shell在所有场景下的展开结果。在真实业务中,绝大多数情况是三类混用,但底层大多还是黑名单思路。这也就解释了为什么通配符绕过在今天依然有效:黑名单再长,也长不过Shell的展开机制。3.2 绕过的前置条件清单通配符绕过不是万能的,它对运行环境有比较明确的要求。我在实际测试里总结了一份前置条件清单,供你对照判断:目标系统是类Unix环境(Linux、Unix、macOS),Windows的cmd和PowerShell对通配符的处理机制不同,绕法也完全不同。命令执行漏洞点支持传入多字符参数,而不是只能传单个数字或固定字符串。过滤规则关注的是命令和参数的“关键字”,而不是限制参数长度和特殊字符种类。目标环境中有可以被通配符匹配到的文件或目录,满足路径展开的条件。执行的Shell具备正常的展开功能(Bash、Dash、Sh均可,C Shell部分行为不同)。如果你判断当前漏洞点不满足以上条件,那通配符绕过大概率行不通,需要考虑别的路子,比如换行符分隔、十六进制编码、环境变量拼接等,但那些不在本文重点范围内。3.3 为什么说“简洁版”不等于“简单版”标题里写着“简洁版”,但实际处理过类似问题的同行都清楚,越是基础的手法,越要精细控制。很多新手一上来就写cat /etc/passwd,被拦了就换个cat${IFS}/etc/passwd,还是被拦,就不知道下一步怎么走了。原因在于,他缺少一个系统的测试思路:没有先探测过滤规则、没有构造分步测试的Payload、没有观察哪些字符能过哪些不能过、没有根据报错信息反推过滤规则的匹配逻辑。我习惯的做法是,把一次绕过拆成四个阶段来推进:第一阶段探测过滤边界,第二阶段构造基础Payload,第三阶段在基础Payload上叠加变形,第四阶段在目标环境实测并调整。每个阶段都有对应的测试方法和判断标准,而不是盲目堆Payload。下面的实操部分就按照这个思路来展开。4. 实操全记录:从探测到完整绕过4.1 第一阶段:探测过滤规则的边界拿到一个漏洞点之后,我第一个动作永远不是急着打Payload,而是先搞清楚对面的过滤器到底拦了什么、放行了什么。这一步看起来简单,其实非常考验细节。构造一组探测请求,本质上是在测试字符能否通过。比如给每个可能用到的重要符号或命令字母单独发送一次,观察响应码和回显差异。我常用的探测字符大致包括:空格、制表符、$、(、)、&、|、;、<、>、'、"、*、?、[、]、{、}、${IFS}、%0a等。对于每一种字符,分别构造请求,观察是否被拦截(拦截通常会表现为400/403/406或返回统一错误页),以及不被拦截时命令执行的结果是否出现差异。这里举一个实际探测的案例。假设目标存在一个Ping功能的参数,系统拼接命令为ping -c 4 你的输入,过滤规则未知。我先提交一个正常的IP127.0.0.1,确保命令能执行。然后依次提交127.0.0.1;id、127.0.0.1|id、127.0.0.1&id、127.0.0.1$(id)、以及带空格的127.0.0.1 id。观察哪一类请求被拦截。接着,在确认;被拦截后,再提交127.0.0.1%3bid(URL编码后的分号),看URL解码是否发生在过滤检查之前。这个探测信息量很大。它能直接告诉你过滤的位置:是在应用层做了正则匹配,还是WAF在HTTP层拦截,还是系统层做命令黑名单检测。不同层级的过滤,对应的绕过策略根本不一样。4.2 第二阶段:构造基础Payload的五种经典姿势在完成了边界探测后,就可以开始构造第一个能跑通的绕过Payload了。这里我按实战经验给你列五种最常用的姿势,每种都附带一个最简示例和适用场景。姿势一:利用?代替单字符适用场景:命令关键字被拦截,但通配符没有被处理。提交127.0.0.1; /bin/c?t /etc/passwd,目标过滤规则如果匹配的是cat,那么/bin/c?t就不中招。注意这里的命令名因为路径展开变成了/bin/cat。姿势二:利用*模糊化命令路径适用场景:命令执行环境相对简单,比如/usr/bin下文件较少,或者你能确定目标系统版本对应的命令路径。可以构造/usr/bin/c* /etc/passwd,只要当前环境里匹配到的文件只有cat,就能顺利执行。如果匹配到多个文件,命令会报错,但报错信息里往往会把匹配到的文件名带出来,恰好可以当目录列举用。姿势三:利用[...]精确匹配单字符适用场景:需要避免多文件匹配的副作用,而?太宽泛的时候,用字符集能把匹配范围收紧到“必然命中目标文件”。比如cat可以写成ca[t],ls可以写成l[s]。这个技巧的隐蔽性比?和*更强,因为正则引擎匹配“cat”字符串时,ca[t]是匹配不上的。姿势四:利用${IFS}替代空格适用场景:一切因为空格被拦而无法拼接命令的情况。注意${IFS}和$IFS不一样,$IFS在某些场景下替换的字符串末尾会带上空字符,需要留意。实测最稳定的写法是${IFS}完整带花括号。另外,$IFS$1也是一种变体,$1是Shell位置参数,默认状态下为空,利用它可以起到分隔变量的作用。姿势五:利用花括号展开{cmd,arg}适用场景:过滤规则对逗号和花括号没有做限制时,可以用花括号展开来绕过空格过滤。例如{cat,/etc/passwd}。Shell会把他展开成cat /etc/passwd,而且整个展开发生在命令解析阶段,WAF看到的只是{cat,/etc/passwd}一串,很难直观判断其危险性。4.3 第三阶段:基础Payload的叠加变形与混合利用真实场景里,单一姿势往往不够用。过滤规则常常同时拦截命令关键字和空格,甚至有更变态的会拦截“路径分割符号/”。下面讲几个常用的叠加技巧。第一个叠加思路是反斜杠绕过。很多过滤规则对反斜杠没有处理能力,因为单个\在部分语言里不是正则的活跃字符。此时构造c\at /etc/passwd,Shell中反斜杠会转义后面一个字符,最终执行时仍然是cat。把它和${IFS}叠加,就能同时绕开命令关键字和空格的检测:c\at${IFS}/etc/passwd。第二个叠加思路是利用环境变量拼接。比如你写${PATH:0:1},替换出来的结果是/,因为PATH环境变量的第一个字符通常是/。如果你同时写了${PATH:0:1}etc${PATH:0:1}passwd,展开后就是/etc/passwd。这种用法适合系统拦截了/字符的场景。但注意,这个技巧对服务器环境较敏感,如果PATH被改过就用不了,需要先验证。第三个叠加思路是变量拼接命令名。假设ls被拦截,但l和s单独不拦截,可以用l''s或l$@s来绕过。$@在Bash中展开为空或一系列参数,配合字符串拼接效果显著。这种技巧适合过滤规则简单粗暴匹配单个命令字符串的情况,叠加通配符也能用:/bin/l$@s或者l$@s${IFS}/etc。第四个思路是换行符与注释符号的组合。有的过滤是按行检测的,也就是只检测第一行内容,此时可以用%0a(换行)绕过。例如原命令是ping -c 4 127.0.0.1,你提交127.0.0.1%0acat /etc/passwd#,换行后新命令cat /etc/passwd在下一行执行,#注释掉系统本来要拼接的尾部。这种方式和通配符无关,但经常和通配符叠加使用,形成双保险。4.4 第四阶段:实战案例完整拆解上面讲了一堆原理和姿势,现在用一个虚构但完全符合真实场景的案例走一遍完整流程。场景描述:某公司有一个内部测试系统,提供了一个通过Web页面执行系统命令的功能模块,用于管理员查看服务器磁盘信息。Web后端逻辑大致是:接收参数disk,在Shell中执行df -h 你的输入,然后返回输出结果。我只知道这个系统的过滤规则会对常见命令和符号做拦截,但具体规则未知。第一步,先用普通参数/?disk=/tmp测试,命令正常执行,返回了df输出,确认命令执行点存在。第二步,做边界探测,提交/?disk=/tmp;id,返回403拦截页。再提交/?disk=/tmp|id,同样403。再试/?disk=/tmp$(id),有意思的是这个请求通过了,返回结果里没有id的输出。这说明两条线索:命令分隔符;、|被拦截;$()没有完全被拦截但可能嵌套深度不够导致命令没执行,或者被其他方式解析掉了。第三步,验证通配符是否可用。提交/?disk=/tmp*,返回结果正常,*符号没有被过滤。这是一个关键信息,意味着路径展开机制可能可以利用。第四步,构造通配符绕过Payload。既然;被拦截,但可以通过换行符%0a来分隔命令。构造/?disk=/tmp%0a/bin/c?t${IFS}/etc/passwd。这里同时用到了三个技巧:换行符做命令分隔、?代替at中的a、${IFS}代替空格。实际执行结果成功返回了/etc/passwd的内容。第五步,优化Payload减少噪音。上面的Payload已经成功,但里面带了明显的passwd字样,如果目标有第二层过滤会很难堪。继续优化为/?disk=/tmp%0a/bin/c?t${IFS}/etc/p*sswd,这样过滤规则连passwd都匹配不到了,但通配符展开后仍然是/etc/passwd。同样,如果系统还拦截了bin、etc,可以继续用/?通配符替换:/b?n/c?t${IFS}/etc/p*sswd。这个案例里最关键的一步是什么?是对*符号未被过滤的确认。没有这一步,后面所有操作都是盲猜。很多新手上来直接堆Payload,往往就是因为跳过了探测环节,导致连自己失败的原因在哪里都说不清楚。5. 难点突破:当通配符也失效时的备用方案5.1 通配符失效的四种典型原因实践中总会遇到通配符绕不过去的情况。总结下来,最常见的原因有下面几种,必须逐一排查。第一种情况,目标环境不是Bash系。部分特殊环境使用sh的POSIX模式时,通配符行为基本一致,但某些嵌入式设备或精简系统使用的Shell对花括号展开支持不全。这种时候就要优先用?和*,避免花括号方案。第二种情况,匹配到了多个文件导致命令执行失败。比如/usr/bin/c*在目标系统上同时命中了cat、cut、curl等多个文件,Shell会把所有文件名拼接成一条参数,命令大概率直接崩溃。这种情况需要改用?精确定位数量:/usr/bin/ca?只精确匹配两个字符再加一个任意字符,命中面小很多。或者用字符集:/usr/bin/c[au][rt]精确控制每一位。第三种情况,WAF做了语义还原。部分WAF在收到请求后,会对%0a做解码,并对${IFS}做变量模拟,导致换行和变量拼接双双失效。这种情况不能只靠单一工具绕了,得考虑利用时间延迟判断命令是否执行,或利用DNS查询外带数据,但这些超出了本文范围,以后再单独开篇。第四种情况,命令参数位置导致通配符无法展开。例如目标的命令拼接是ping -c 4 你的输入,而你输入的参数在最后一个位置。此时如果你提交的是*,Shell会把当前目录下所有文件名都填进来当参数,没法控制。这种情况可以考虑先在输入里把命令分隔符打通,再做二次利用。5.2 一个容易忽视的辅助技巧:利用Pinpoint说完失效原因,分享一个我常用但看到很多人不太会用的技巧:利用命令本身作为目录探测工具。当你不知道目标系统的目录结构时,可以利用ls命令配合通配符让目标自己“报名字”。比如构造/?disk=/tmp; l?st${IFS}/${IFS}b*n这样的Payload(注意这里l?st可以匹配ls或者某个名字相近的命令),如果命令成功执行,返回的结果自然把目标目录下的文件列表带出来。之后你就有了文件清单,再针对性地构造第二次、第三次Payload,命中率大幅提升。本质上,这就是“信息收集—确定目标—精准打击”的思路在命令执行漏洞利用上的具体化。很多新手失败不是Payload不够多,而是对目标环境了解太少,连当前目录下有什么文件都没搞清楚就去猜cat flag,失败是理所当然的。5.3 短命令场景的特殊处理还有一种特殊情况:命令执行点可能只是一个回显型接口,比如把参数作为外部程序的参数执行,但输出受限。这种情况下,你构造的通配符Paylaod能不能生效,往往取决于回显本身的内容。如果回显总是被截断,那就优先考虑写文件型的Payload,利用命令把结果写入到Web目录下的一个文件里,再通过另一个HTTP请求读取。类似地,如果命令行需要交互,通配符绕过是无法直接完成交互的。这种情况需要借助已经执行成功的命令来建立一个更方便的执行通道,通常思路是先下载一个反向工具或写入一个WebShell。但这类操作涉及的使用方式千差万别,而且合规风险很高,这里就不展开具体操作了。6. 防御视角:从攻到防的关键加固措施6.1 过滤规则不是安全边界在讲具体防御措施前,先说一个我踩过坑后的血泪经验:单纯依赖输入过滤来防御命令注入,本质上是在和攻击者玩猫鼠游戏。今天补了cat的过滤,明天攻击者用c\at绕过;今天补了反斜杠,明天还有变量拼接;今天全部补上,后天还有通配符。所以真正的防线必须前移到“如何避免用户输入进入命令解释器”。这一条是所有防御措施的大前提。如果你的系统架构已经允许用户的原始输入直接拼接到Shell命令里,那么任何过滤手段都只是降低风险不等于消除风险。6.2 代码层的正确写法从代码层面修复命令注入,最稳妥的是两条路。第一条是彻底的“不要用Shell执行”。比如在Python里不要用os.system或者os.popen,改成subprocess.run并传入参数列表,不让Shell介入解释。subprocess.run(["df", "-h", user_input])中的user_input会被当作df的独立参数而不是Shell命令的一部分。这条说完,很多人会问那如果业务必须用Shell呢?比如要用管道、重定向、通配符展开这些Shell能力。这种时候没有银弹,只能做白名单校验。只允许输入匹配预定义的安全格式(比如IP地址、数字、纯文件名),格式之外的一律拒绝。在PHP场景里,必须避免使用eval、system、exec、shell_exec、`等危险函数,或者在使用前进行严格的参数校验。在Java场景里,优先使用ProcessBuilder,传入字符串数组避免Shell解释。工欲善其事必先利其器,这些是代码审计和研发自测的底线要求。6.3 拦截规则如何设计才算合理如果确实需要在应用层做输出过滤或输入过滤(比如临时热修、WAF规则应急),那么设计规则时要有几个原则:第一,不要只匹配明文命令。规则要覆盖常见的编码形式和通配符形式:URL编码、Unicode编码、十六进制、反斜杠转义、变量拼接、通配符插值。一个规则里出现cat的同时,最好也出现c[a-z]at、c\at、c%61t、${IFS}等变体。但这么做也容易产生误拦,所以要有配套的放行机制。第二,要在命令到达Shell之前做内容解码后的二次检查。比如请求先经过一次URL解码,检查一次,到后端又可能被框架二次解码,再检查一次。很多绕过就是利用了“过滤时看到的和解码后执行的不是同一个字符串”这个时间差。第三,要对执行结果设置返回限制。即使命令被执行了,如果回显很小或者只有状态码,攻击者拿不到输出数据,利用难度会大幅上升。可考虑关闭命令回显、只保留成功/失败状态码。6.4 纵深防御的最后几条防线在业务无法快速迭代代码的时候,通过系统层加固来兜底,是我在应急响应中反复推荐的方案。降低Web服务进程的权限,绝对不能用root或者管理员权限运行。开启SELinux或者AppArmor,限制进程能执行的二进制文件范围。对关键系统命令做别名或白名单包装。限制Web服务可以访问的目录,禁止执行/tmp、/var/www/html等目录下的文件。开启审计日志,记录命令执行类系统调用,便于事后追溯。有条件时把命令执行功能迁移到独立的沙箱容器中,与核心业务网络隔离。这些措施不能阻止攻击者执行第一条命令,但能极大延长攻击链路,把“拿下服务器”变成“看了两眼目录就走”,这是实际对抗中非常常见的分水岭。7. 实际项目中的踩坑记录与排查技巧7.1 踩坑记录:匹配多文件导致全部命令失效先说最典型的坑。某次在测试一个老系统的命令执行点时,我用/?disk=/tmp; /usr/bin/c*${IFS}/etc/passwd来做注入,结果不仅cat没执行成功,连原本应该正常执行的df输出都丢了。排查发现原因在于/usr/bin目录下同时存在cat、calendar、catch、catman等一堆以c开头的命令,c*把所有匹配项都展开成参数,最后传给cat的命令行变成了一堆文件路径,直接报错。这个问题的解法有两个方向。方向一是缩小通配范围:换成/usr/bin/ca?,这样只匹配cat、cal(如果有)、cap(如果有)等恰好三位且以ca开头的命令,命中面小很多。方向二是用字符集精确替代:/usr/bin/c[at]匹配到的是ca和ct两个文件,不一定命中目标。所以最佳选择往往是用?加路径限制,先探测目标目录下与该模式匹配的文件到底有几个,再做调整。还有一个小技巧是,如果匹配到多个文件导致报错,报错信息往往会列出“参数过多”或者“无法打开”的文件名。这些报错信息本身就是Banner信息,可以反向利用来获取目录列表。7.2 踩坑记录:${IFS}在某些位置不生效另一个常见的问题是${IFS}在部分命令的中间位置不生效。举例来说,cat${IFS}/etc/passwd展开后是cat /etc/passwd,正常。但如果你把它放在管道符后面,比如ls${IFS}./|${IFS}cat${IFS}/etc/passwd,某些精简版Shell对${IFS}的展开会有兼容性问题。实测中,Bash完全没问题,Dash在部分场景下会有细微差异。解决方式也很简单,放弃${IFS}在这个场景的使用,改用Tab字符。Tab字符在Shell中同样是命令行的合法分隔符,而且很多过滤规则不会检查Tab(尤其是只按空格切割输入的情况)。在HTTP请求中,Tab可以表示为%09。实测效果非常稳定。如果Tab和${IFS}都被过滤,还有最后的选择:利用参数扩展拼接。构造$IFS$9,其中$9是第九个位置参数,正常Shell环境下它是空的,利用它作为变量结束边界,有时能规避过滤对$IFS的精确匹配。这个技巧对Bash有效,对Sh的兼容性稍差,需要现场验证。7.3 踩坑记录:通配符绕过成功但命令输出被截断命令执行成功不等于拿到了完整输出,很多实战场景下命令回显只返回前几行或者部分内容。出现这种情况,首先要确认是因为过滤规则截断了回显,还是Web层做了输出长度限制,还是后端仅返回部分日志。如果输出被截断,一个通用的处理思路是利用命令本身能力缩小输出范围或者把输出变成更容易回显的形式。比如cat /etc/passwd | cut -c1-30,但这里管道符可能也被过滤,那就换成head -c 30 /etc/passwd(配合通配符做命令名绕过)。或者干脆用grep定位到指定行号再输出:sed -n '3p' /etc/passwd。如果连grep、sed这样的命令也被拦,最简单的办法是利用过滤规则不拦的关键字构造通配符名称,比如s?d、gr?p,再配合${IFS}完成参数注入。最终目标是把回显数据压缩到最小可读范围,而不是贪多求全。7.4 问题排查速查表现场现象可能原因优先排查方式参考方案空格被拦且命令执行失败过滤规则拦空格换${IFS}、Tab、花括号综合使用多种替代分隔符cat被拦黑名单精确匹配换?、[...]、\转义通配符插入、变量拼接*匹配到多个文件环境文件较多换?或字符集缩小范围c[au]t精确定位命令执行无输出回显被截断用head、grep、sed精简输出换用外带方式(DNS/HTTP)命令执行不了也不报错语义过滤拦截检查是否触发二次解码叠加变量与通配符变形注入分隔符;被拦黑名单包含分隔符试%0a、$()、换行换行符分离命令全盘过滤但命令点可用过滤规则有逻辑漏洞逐字符边界探测构造不在规则语义内的Payload这个表是我每次做此类测试前的自查清单,你完全可以照抄到自己项目里用。7.5 一个值得单独说说的技巧:命令执行的响应时间探测有时候目标系统配置了命令回显关闭,所有的命令执行结果都不会返回到页面,这种情况下怎么判断命令是否执行成功?答案是:利用时间差。比如构造127.0.0.1 && sleep 5,如果页面等待了5秒再返回,说明命令成功执行了。这个技巧在探测阶段的价值非常大,尤其在无法确认过滤器到底拦截了什么的时候,时间差能帮你判断一个Payload是否真正到达了Shell。更进阶的用法是结合DNS外带。命令成功执行后,触发一次DNS解析请求,让目标服务器返回一个可控域名,通过检查DNS日志确认命令是否执行。虽然这个方法依赖外部基础设施,但在实战中面对较高级的过滤规则时,它往往是最可靠的验证手段之一。这里不展开讲具体搭建过程,但思路本身值得你在防御设计时警惕:即使页面没有任何回显,攻击者仍然可能通过时间差和网络请求确认漏洞是否可利用。8. 我的实际体会与几条扩展思考做了这么多年的安全测试和应急响应,我最大的感受是,命令执行漏洞之所以高危,不是因为Shell命令本身有多难防,而是因为它总是以各种意想不到的方式出现在业务逻辑里。很多系统的过滤规则写得很努力,但面对通配符这种“发生在Shell内部”的展开机制时,往往只能眼睁睁看着防线被拆穿。这里想用最后一点篇幅分享三个扩展思考,给正在做防护的同行参考。第一个思考是关于LLM与过滤规则的结合。现在的WAF和代码扫描器大多还是基于正则和语义规则来做命令注入检测,但我在实际测试中发现,规则再长也有边界,而通配符展开的变体几乎是无限的。如果能引入大型语言模型对输入做意图还原,在检测阶段模拟Shell展开,也许能显著提高拦截率。当然,这个方案的误报问题很严重,部署成本也高,短期内很难替代传统规则。第二个思考是关于系统命令的现代化改造。既然通配符展开是Shell的机制,那么能减少Shell使用的系统设计,就是更安全的设计。比如在很多云原生场景中,业务容器已经不再内置完整的Bash,而只提供静态编译的二进制可执行文件。这种方式下,攻击者即使拿到了命令执行点,能够利用的系统命令也大大减少,很多通配符绕过技术会失去用武之地。第三个思考是给防御方的建议:在写过滤规则前,先模拟攻击者做一轮完整的绕过测试。我见过太多的规则,写的时候觉得无懈可击,但自己动手绕过一次就发现全是漏洞。所以如果你负责安全规则建设,建议自己也搭建一个测试环境,把文章里提到的几种变形逐一验证一遍。只有亲自动手绕一次,才知道你写的规则在实战面前到底有多脆弱。最后给真正在做攻防对抗的同行留一句:通配符绕过只是命令注入绕过一个很小的切面,它背后隐藏的“Shell语义解析”问题,远比几个Payload重要。理解机制,比记住Payload更能让你走得更远。