Python PyYAML反序列化漏洞
基本概念
(引用百度)YAML是“YAML不是一種標記語言”的外語縮寫;但為了強調這種語言以資料做為中心,而不是以置標語言為重點,而用返璞詞重新命名。它是一種直觀的能夠被電腦識別的資料序列化格式,是一個可讀性高並且容易被人類閱讀,容易和指令碼語言互動,用來表達資料序列的程式語言。
PyYAML是Python中YAML語言的編輯器和直譯器。
安裝:pip install PyYAML
兩個函式:
yaml.dump():將一個Python物件序列化生成為yaml文件。
yaml.load():將一個yaml文件反序列化為一個Python物件。
簡單的用例:
可以看到,User物件經過yaml序列化之後內容為一行字串,簡單解釋一下:“!!pythonobject”為yaml標籤,yaml.load()會識別該標籤並呼叫相應的方法執行反序列化操作;冒號後面的“__main__”為py檔名,這裡為本檔案的意思;“User”為序列化的物件型別,後面緊跟的大括號即為該物件的屬性及其屬性值。
更詳細的說明可參考官方文件:https://pyyaml.org/wiki/PyYAMLDocumentation
Demo
這裡編寫簡單的Demo,一個py檔案用於將惡意類序列化為字串儲存到yaml檔案中,另一個py檔案用於反序列化yaml檔案內容為惡意類物件從而達到利用反序列化漏洞的目的。
yaml_test.py
先建立一個poc物件再呼叫yaml.dump()將其序列化為一個字串,其中第10行程式碼為將預設的“__main__”替換為該檔名“yaml_test”,目的是為了後面yaml.load()反序列化該字串的時候會根據yaml檔案中的指引去讀取yaml_ test.py中的poc這個類,否則無法正確執行:
yaml_test2.py
直接yaml.load()讀取目標yaml檔案,由!!python/object標籤解析其中的名為yaml_test的module中的poc類,最後執行了該類物件的__init__()方法從而執行了命令:
漏洞根源分析
到$PYTHON_HOME/lib/site-packages/yaml/constructor.py中檢視3個特殊Python標籤的原始碼。
!!python/object標籤:
!!python/object/new標籤:
!!python/object/apply標籤:
可以看到,!!python/object/new標籤的程式碼實現其實就是!!python/object/apply標籤的程式碼實現,只是最後newobj引數值不同而已。這3個Python標籤中都是呼叫了make_python_instance()函式,檢視該函式:
可以看到,在該函式是會根據引數來動態建立新的Python類物件或通過引用module的類建立物件,從而可以執行任意命令。
通用payload
只要存在yaml.load()且引數可控,則可以利用yaml反序列化漏洞,payload列舉如下,當然不止如下:
附上測試程式碼:
import yaml
payload = '!!python/object/apply:subprocess.check_output [[calc.exe]]'
#payload = '!!python/object/apply:subprocess.check_output ["calc.exe"]'
#payload = '!!python/object/apply:subprocess.check_output [["calc.exe"]]'
#payload = '!!python/object/apply:os.system ["calc.exe"]'
#payload = '!!python/object/new:subprocess.check_output [["calc.exe"]]'
#payload = '!!python/object/new:os.system ["calc.exe"]'
yaml.load(payload)
一個疑問點?
為什麼“!!python/object”標籤不好使,明明Demo用的是這個標籤,但通用payload中無法執行該payload?看了一些網上的文章也沒有分析原因,其實檢視官方文件就知道怎麼回事了:
可以看到,!!python/object標籤的使用格式和另外兩個根本就是兩碼事,其接收引數是使用大括號{}而非中括號[],且並沒有對引數args進行接收。也就是說,!!python/object標籤只針對於物件類進行使用。
檢測方法
全域性搜尋Python程式碼中是否包含“import yaml”,若包含則進一步排查是否呼叫yaml.load()且引數是可控的。
防禦方法
使用安全函式yaml.safe_load()替代yaml.load()即可。