是的,我写的开源项目被曝出了高危漏洞。。

阅读本文大概需要 3 分钟。

大家好,我是崔庆才。

前几天看到了一个开源项目中的远程执行漏洞,开发者通过 HTTP 接口暴露了一个参数,接收参数之后,代码中将该参数拼接了一个命令,然后把这个命令使用 Python 中的 Popen 执行了,然后把对应的输出结果返回到了 HTTP Response 中。

这开发者都这么不注意的吗?这谁干的好事啊?

没错,那个开发者就是我。。。

是的,我在我的开源项目 Gerapy 里面写出来了这个漏洞,然后喜提 CVE:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-43857。

这个漏洞的安全级别设置为高,披露文章见:注意!Gerapy 操作系统命令注入漏洞

不过还好,我还加了一层防护,那就是登录验证,所以如果不登录是没法利用的,所以影响还好。所以大家如果 Gerapy 有设置好密码,密码不被爆破出来的话,那就问题不大。

现在问题已经修复,大家可以升级到 Gerapy 最新版本,目前是 0.9.9 版本。

但总之,这个过程我学到了很多东西,这里来记录下这个漏洞是怎么产生的以及怎么规避。

由于 Gerapy 里面的那个逻辑相对复杂,为了便于说明,这里我来试着用一个例子复现一下这个漏洞,并说明下这个漏洞的修正方法。

示例复现

这个漏洞主要涉及到 Python 中 Popen 的使用。

比如,这里我定义一个方法,接收一个参数 host,然后拼接了一个 dig 命令,用于查找这个 host 的解析地址,代码如下:

from subprocess import Popen, PIPE

def execute(host):
    cmd = f'dig {host}'
    p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    stdout, stderr = p.stdout.read(), p.stderr.read()
    return stdout if not stderr else stderr

result = execute('www.baidu.com')
print(result)

拼接完命令之后,我调用了 Popen 执行了这条命令,并且捕获输出结果和错误,如果没有错误就返回输出结果,如果有错就返回错误。

然后我调用了 execute 方法,并传入了 www.baidu,com,一切都看起来很自然对不对?

运行结果类似如下:

; <<>> DiG 9.10.6 <<>> www.baidu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45141
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.baidu.com.                 IN      A

;; ANSWER SECTION:
www.baidu.com.          253     IN      CNAME   www.a.shifen.com.
www.a.shifen.com.       261     IN      CNAME   www.wshifen.com.
www.wshifen.com.        300     IN      A       119.63.197.151
www.wshifen.com.        300     IN      A       119.63.197.139

;; Query time: 148 msec
;; SERVER: 2001:4898::1050:5050#53(2001:4898::1050:5050)
;; WHEN: Wed Dec 29 01:25:39 CST 2021
;; MSG SIZE  rcvd: 127

没啥问题对不对?传入了一个域名,输出了 dig 命令之后域名的解析结果。

可是,如果这时候攻击者传入的命令是非法的,比如这样调用:

result = execute('www.baidu.com; cat /etc/passwd')

结果会怎样?!它在后面额外又拼接了一条 cat /etc/passwd ,这是非常危险的命令,直接将机器的相关用户信息输出出来。

运行结果就会变成这样子:

; <<>> DiG 9.10.6 <<>> www.baidu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45141
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.baidu.com.                 IN      A

;; ANSWER SECTION:
www.baidu.com.          253     IN      CNAME   www.a.shifen.com.
www.a.shifen.com.       261     IN      CNAME   www.wshifen.com.
www.wshifen.com.        300     IN      A       119.63.197.151
www.wshifen.com.        300     IN      A       119.63.197.139

;; Query time: 148 msec
;; SERVER: 2001:4898::1050:5050#53(2001:4898::1050:5050)
;; WHEN: Wed Dec 29 01:25:39 CST 2021
;; MSG SIZE  rcvd: 127

##
# 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.
#
# See the opendirectoryd(8) man page for additional information about
# Open Directory.
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
...
_oahd:*:441:441:OAH Daemon:/var/empty:/usr/bin/false

是的,cat /etc/passwd 的结果也被输出出来了!

这样是不是很可怕?

想想看,这样的话,命令换一换,攻击者几乎可以做任何的事情,比如执行点 rm -rf 都不在话下。

我当时就是使用了 Popen 方法拼接了一个 spider_name 的名称,但是没对 spider_name 做校验,导致出现了问题。

另外,这种直接拼接命令行构造并执行的方法千万别用,会造成很大的安全隐患。

有朋友就会说了,如果我把里面所有的输入参数做个安全检查不就好了吗?比如检测到有非法字符或命令就返回不执行。

行倒是行,但问题在于你一定能想全所有的可能情况吗?万一漏掉一个那也非常危险。而且万一有效字符也被过滤了咋办?另外也非常费时费力,得不偿失。

解决方案

那咋办?那这种命令行执行的方式难道一定会造成安全性问题吗?

其实办法是有的,我们只需要修改下命令行的构造方式就可以了,上面的安全性问题可以通过这样的修改来修复:

from subprocess import Popen, PIPE

def execute(host):
    cmd = ['dig', host]
    p = Popen(cmd, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    stdout, stderr = p.stdout.read(), p.stderr.read()
    return stdout if not stderr else stderr

这里我们将命令修改成了一个列表表示的内容,每个空格间隔都是列表的一个元素,使用 Popen 执行的时候执行 shell 参数为 False 即可。

这样的话,每个列表的元素都会被看作成一个字符串,被当成一个整体来看待,而不是单纯的拼接。

比如如果这时候 host 我们再传入 www.baidu.com; cat /etc/passwd,这时候这个参数就会被看作一个整体,相当于又加了一层引号,相当于被转义了一样,这样 dig 执行的对象就是 www.baidu.com; cat /etc/passwd 这个整体了,所以就不会出现之前的安全性问题了。

同样的调用:

result = execute('www.baidu.com; cat /etc/passwd')
print(result)

运行结果如下:

; <<>> DiG 9.10.6 <<>> www.baidu.com;cat /etc/passwd
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 2626
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.baidu.com\;cat\032/etc/passwd. IN  A

;; AUTHORITY SECTION:
.                       900     IN      SOA     a.root-servers.net. nstld.verisign-grs.com. 2021122801 1800 900 604800 86400

;; Query time: 92 msec
;; SERVER: 2001:4898::1050:5050#53(2001:4898::1050:5050)
;; WHEN: Wed Dec 29 01:38:41 CST 2021
;; MSG SIZE  rcvd: 133

这样就不会输出后面的 /etc/passwd 的内容了。

所以,大家看到这里,想必也能理解为什么有时候我们会看到一些命令用列表来表示,而不是单纯的命令来表示了。

比如 Docker 构建镜像的最后一条命令,类似如下:

ENTRYPOINT ["/bin/chamber", "exec", "production", "--"]
CMD ["/bin/service", "-d"]

这也是为了安全考虑的。

所以,通过这个事件,真的安全性问题要重视起来。而且尤其我作为开源项目的作者,我也有必要好好地处理好安全性问题,不然大家用了我项目,但是出现了问题,我还是难辞其咎的。

以后我会多加注意,谢谢大家的支持。

1bb417c32701f72c910bb37d33b7359a.png

End

崔庆才的新书《Python3网络爬虫开发实战(第二版)》已经正式上市了!书中详细介绍了零基础用 Python 开发爬虫的各方面知识,同时相比第一版新增了 JavaScript 逆向、Android 逆向、异步爬虫、深度学习、Kubernetes 相关内容,‍同时本书已经获得 Python 之父 Guido 的推荐,目前本书正在七折促销中!

内容介绍:《Python3网络爬虫开发实战(第二版)》内容介绍

bade92448f71e2989731d6e81fa2574e.png

扫码购买

812d8b31ca482cfd656a24f06872d2b7.png

9408d6c0e93af810cacd85da2f5f088b.png

点个在看你最好看

outside_default.png

猜你喜欢

转载自blog.csdn.net/u010467643/article/details/122227462