Python Pickle反序列化漏洞
前言:
刷題的時候做了一道[CISCN2019]ikun的題目,提示考察的知識點是Python Pickle,之前接觸的都是有關PHP反序列化,這次就來好好學習一下Python Pickle反序列化漏洞。
基礎知識
0x00:Pickle/CPickle
pickle或cPickle,作用和PHP的serialize與unserialize一樣,兩者只是實現的語言不同,一個是純Python實現、另一個是C實現,函數調用基本相同,但cPickle庫的性能更好,之后就以pickle庫來進行演示。
0x01:Pickle庫及函數
pickle是python語言的一個標準模塊,實現了基本的數據序列化和反序列化。
pickle模塊是以二進制的形式序列化后保存到文件中(保存文件的后綴為.pkl),不能直接打開進行預覽。
| dumps | 對象反序列化為bytes對象 |
| dump | 對象反序列化到文件對象,存入文件 |
| loads | 從bytes對象反序列化 |
| load | 對象反序列化,從文件中讀取數據 |
先通過幾個例子來看下這幾個函數的作用:
dump/load
#序列化 pickle.dump(obj, file, protocol=None,) obj表示要進行封裝的對象(必填參數) file表示obj要寫入的文件對象 以二進制可寫模式打開即wb(必填參數) #反序列化 pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None) file文件中讀取封存后的對象 以二進制可讀模式打開即rb(必填參數)(模仿Epicccal師傅的例子)
dumps/loads
#序列化 pickle.dumps(obj, protocol=None,*,fix_imports=True) dumps()方法不需要寫入文件中,直接返回一個序列化的bytes對象。 #反序列化 pickle.loads(bytes_object, *,fix_imports=True, encoding="ASCII". errors="strict") loads()方法是直接從bytes對象中讀取序列化的信息,而非從文件中讀取。在python2中以字符串的形式進行轉換時,這些序列化的字符串是什么意思,又按照什么規則生成的,這就涉及到了PVM,因為它是Python序列化過程和反序列化過程中最根本的東西。
0x02:PVM的作用
對于Python而言,它可以直接從源代碼運行程序。Python解釋器會將源代碼編譯為字節碼,然后將編譯后的字節碼轉發到Python虛擬機中執行。總的來說,PVM的作用便是用來解釋字節碼的解釋引擎。
0x03:PVM的執行流程
當運行Python程序時,PVM會執行兩個步驟。
字節碼是Python特有的一種表現形式,不是二進制機器碼,需要進一步編譯才能被機器執行 . 如果 Python 進程在主機上有寫入權限 , 那么它會把程序字節碼保存為一個以 .pyc 為擴展名的文件 . 如果沒有寫入權限 , 則 Python 進程會在內存中生成字節碼 , 在程序執行結束后被自動丟棄 .
0x04:PVM與Pickle模塊的關系
Pickle是一門基于棧的編程語言 , 有不同的編寫方式 , 其本質就是一個輕量級的 PVM .
這個輕量級的PVM由三部分組成及其功能如下:
- 指令處理器( Instruction processor )
從數據流中讀取操作碼和參數 , 并對其進行解釋處理 . 指令處理器會循環執行這個過程 , 不斷改變 stack 和 memo 區域的值 .直到遇到 . 這個結束符號 。這時 , 最終停留在棧頂的的值將會被作為反序列化對象返回 。
- 棧區( stack )
由 Python 的列表( list )實現 , 作為流數據處理過程中的暫存區 , 在不斷的進出棧過程中完成對數據流的反序列化操作,并最終在棧頂生成反序列化的結果
- 標簽區( memo )
由 Python 的字典( dict )實現 , 可以看作是數據索引或者標記 , 為 PVM 的整個生命周期提供存儲功能 .簡單來說就是將反序列化完成的數據以 key-value 的形式儲存在memo中,以便使用。
需要重點關注一下指令處理器可讀的操作碼,列出幾個比較重要的:
這六個符號便是在Pickle序列化時最常用到的操作碼,可以結合下hachp1師傅做的動圖理解下
反序列化分析
就結合上面所舉的例子進行分析:
整個序列化的過程可以分為三個步驟
反序列化的過程就是序列化過程的逆過程。
0x06:Pickle/CPickle反序列化漏洞分析
反序列化漏洞出現在 __reduce__()魔法函數上,這一點和PHP中的__wakeup() 魔術方法類似,都是因為每當反序列化過程開始或者結束時 , 都會自動調用這類函數。而這恰好是反序列化漏洞經常出現的地方。
而且在反序列化過程中,因為編程語言需要根據反序列化字符串去解析出自己獨特的語言數據結構,所以就必須要在內部把解析出來的結構去執行一下。如果在反序列化過程中出現問題,便可能直接造成RCE漏洞.
另外pickle.loads會解決import 問題,對于未引入的module會自動嘗試import。那么也就是說整個python標準庫的代碼執行、命令執行函數都可以進行使用。
最后還是來看一下這個魔法函數
__reduce__()官方文檔如下:
當 __reduce__() 函數返回一個元組時 , 第一個元素是一個可調用對象 , 這個對象會在創建對象時被調用 . 第二個元素是可調用對象的參數 , 同樣是一個元組。這點跟我們上面提到的PVM中的R操作碼功能相似,可以對比下:
事實上 , R操作碼就是 __reduce__() 魔術函數的底層實現 . 而在反序列化過程結束的時候 , Python 進程會自動調用 __reduce__() 魔術方法 . 如果可以控制被調用函數的參數 , Python 進程就可以執行惡意代碼 .
注意:
在python2中只有內置類才有__reduce__方法,即用class A(object)聲明的類,而python3中已經默認都是內置類了
0x06:反序列化漏洞利用
漏洞可能出現的位置:
命令執行
#模仿Epicccal師傅的例子 import pickle import osclass Test2(object):def __reduce__(self):#被調用函數的參數cmd = "/usr/bin/id" return (os.system,(cmd,))if __name__ == "__main__":test = Test2()#執行序列化操作result1 = pickle.dumps(test)#執行反序列化操作result2 = pickle.loads(result1)# __reduce__()魔法方法的返回值: # return(os.system,(cmd,)) # 1.滿足返回一個元組,元組中有兩個參數 # 2.第一個參數是被調用函數 : os.system() # 3.第二個參數是一個元組:(cmd,),元組中被調用的參數 cmd # 4. 因此序列化時被解析執行的代碼是 os.system("/usr/bin/id")題目訓練
0x00:[CISCN2019 華北賽區 Day1 Web2]ikun
前面的步驟就不再詳細描述了,這里直接從Pickle反序列化漏洞入手
在settings.py文件中發現提示,unicode解碼一下
觀察源碼發現后門在Admin.py中
來觀察一下form.html頁面
說明傳入的是可以直接進行回顯的,而且可以將自定義的類進行序列化和反序列化,因此存在Pickle反序列化漏洞,那我們就可以構造一個通過pickle.dumps序列化的payload,從而被解析讀取flag或其他信息。
構造payload可以使用方法__reduce__(self),先要獲取的flag文件的位置,然后進行讀取
但需要注意幾點:
#os.system和os.popen os.system 調用系統命令,完成后退出,返回結果是命令執行狀態,一般是0 os.popen() 無法讀取程序執行的返回值這兩個函數只有以print輸出時才會回顯,如果是以return返回的就不會顯示結果。
查了資料發現
可以使用commands.getoutput()這個函數來進行代替,構造payload
發現flag.txt文件,那接下來就讀取即可
但很多時候需要一次執行多個函數或一次進行多個指令,就不能光用 __reduce__
來解決問題,reduce一次只能執行一個函數,當exec被禁用時,就不能一次執行多條指令了。
參考博客
膜Epicccal師傅,原理寫的太詳細了,膜
https://blog.csdn.net/u013008795/article/details/89790828
https://xz.aliyun.com/t/7436#toc-5
https://www.guildhab.top/?p=2178
https://www.cnblogs.com/jefree/p/4461979.html
LetheSec
總結
以上是生活随笔為你收集整理的Python Pickle反序列化漏洞的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 湖湘杯 | Misc Wp
- 下一篇: UNCTF2020 | Web Wp