浅谈python反序列化
浅谈python反序列化
pickle反序列化
pickle讲解
与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
指令处理器
从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。
stack
由 Python 的 list 实现,被用来临时存储数据、参数以及对象。
memo
由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储。
类似于我们在 PHP 中的 serialize 和 unserialize,如果 unserialize 的输入可控我们就可能可以进行恶意的攻击
pickletools
使用pickletools可以方便的将opcode转化为便于肉眼读取的形式。
下面是pickletools的三个方法
1,pickletools.dis(*pickle*, *out=None*, *memo=None*, *indentlevel=4*, *annotate=0*)
1 | 将 pickle 的符号化反汇编数据输出到文件类对象 out,默认为 sys.stdout。 pickle 可以是一个字符串或一个文件类对象。 memo 可以是一个将被用作 pickle 的备忘记录的 Python 字典;它可被用来对由同一封存器创建的多个封存对象执行反汇编。 由 MARK 操作码指明的每个连续级别将会缩进 indentlevel 个空格。 如果为 annotate 指定了一个非零值,则输出中的每个操作码将以一个简短描述来标注。 annotate 的值会被用作标注所应开始的列的提示。 |
2,pickletools.genops(pickle)
1 | 提供包含 pickle 中所有操作码的 iterator,返回一个 (opcode, arg, pos) 三元组的序列。 opcode 是 OpcodeInfo 类的一个实例;arg 是 Python 对象形式的 opcode 参数的已解码值;pos 是 opcode 所在的位置。 pickle 可以是一个字符串或一个文件类对象。 |
3,pickletools.optimize(*picklestring*)
1 | 在消除未使用的 `PUT` 操作码之后返回一个新的等效 pickle 字符串。 优化后的 pickle 将更为简短,耗费更为的传输时间,要求更少的存储空间并能更高效地解封。 |
举例:
1 | import pickle |
pickle模块常见方法及接口
1 | pickle.dump(*obj*, *file*, *protocol=None*, ***, *fix_imports=True*) |
将打包好的对象 obj 写入文件中,其中protocol为pickling的协议版本(下同)。
1 | pickle.dumps(*obj*, *protocol=None*, ***, *fix_imports=True*) |
将 obj 打包以后的对象作为bytes类型直接返回。
1 | pickle.load(*file*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*) |
从文件中读取二进制字节流,将其反序列化为一个对象并返回。
1 | pickle.loads(*data*, ***, *fix_imports=True*, *encoding="ASCII"*, *errors="strict"*) |
从data中读取二进制字节流,将其反序列化为一个对象并返回。
1 | object.__reduce__() |
__reduce__()其实是object类中的一个魔术方法,我们可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。
Python要求该方法返回一个字符串或者元组。如果返回元组(callable, ([para1,para2...])[,...]) ,那么每当该类的对象被反序列化时,该callable就会被调用,参数为para1、para2...
与PHP 反序列化漏洞不同的是(PHP 反序列化漏洞要求源代码中必须存在有问题的类,且对象参数可控可生成恶意对象),而 Python 反序列化漏洞,只需要被反序列化的字符可控即可造成 RCE(可自己创建危险方法来命令执行)
可以绕过对builtins system os的waf
1 | func = getattr(__import__('builtins'), 'eval') |
opcode
pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)
| 指令 | 描述 | 具体写法 | 栈上的变化 |
|---|---|---|---|
| c | 获取一个全局对象或import一个模块 | c[module]\n[instance]\n | 获得的对象入栈 |
| o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 |
| i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 |
| N | 实例化一个None | N | 获得的对象入栈 |
| S | 实例化一个字符串对象 | S’xxx’\n | 获得的对象入栈 |
| V | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 |
| I | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 |
| F | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 |
| R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 |
| . | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 |
| ( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 |
| t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 |
| ) | 向栈中直接压入一个空元组 | ) | 空元组入栈 |
| l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 |
| ] | 向栈中直接压入一个空列表 | ] | 空列表入栈 |
| d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 |
| } | 向栈中直接压入一个空字典 | } | 空字典入栈 |
| p | 将栈顶对象储存至memo_n | pn\n | 无 |
| g | 将memo_n的对象压栈 | gn\n | 对象被压栈 |
| 0 | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 |
| b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 |
| s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 |
| u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 |
| a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 |
| e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 |
1 | MARK = b'(' # push special markobject on stack |
利用方法
rce
reduce
1 | import pickle |
利用反序列化执行指令
eval
1 | import pickle |
变量覆盖
1 | import pickle |
将key1和key2的值进行了覆盖
ctf例题
[HFCTF 2021 Final]easyflask
5ee2df16-6fa5-4013-bf10-93942829bbf3.node5.buuoj.cn:81/file?file=/app/source
非预期直接读取环境变量
/proc/1/environ
通过文件包含获取源码
1 | #!/usr/bin/python3.6 |
/proc/self/environ读取该信息获取key进行session伪造
1 | import pickle |
根据生成的数据进行session伪造,使用伪造的session访问admin路由
python3 flask_session_cookie_manager3.py encode -s ‘glzjin22948575858jfjfjufirijidjitg3uiiuuh’ -t “{‘u’:{‘b’:’gASVKwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjBBjYXQgL2ZsYWc+L3Rlc3QylIWUUpQu’}}”
[HZNUCTF 2023 preliminary]pickle
1 | import base64 |
分析源码经典的python反序列化题目
1 |
|
关键代码,具有漏洞并且其有个waf是将os替换为空
1 | import pickle |
使用字符串拼接绕过即可
最后查看环境变量env即可获得flag
可以通过题目发现ctf中的python题目十分容易出现阅读环境变量的非预期解
pker的使用
- pker是由@eddieivan01编写的以仿照Python的形式产生pickle opcode的解析器,可以在https://github.com/eddieivan01/pker下载源码。
- 使用pker,我们可以更方便地编写pickle opcode(生成pickle版本0的opcode)。
- 再次建议,在能够手写opcode的情况下使用pker进行辅助编写,不要过分依赖pker。
- 此外,pker的实现用到了python的ast(抽象语法树)库,抽象语法树也是一个很重要东西,有兴趣的可以研究一下ast库和pker的源码,由于篇幅限制,这里不再叙述。
Pker可以做到什么
- 变量赋值:存到memo中,保存memo下标和变量名即可
- 函数调用
- 类型字面量构造
- list和dict成员修改
- 对象成员变量修改
pker工具可以用来构造opcode
pyyaml反序列化
<5.1 版本中提供了几个方法用于解析 YAML:
yaml.load:加载单个 YAML 配置yaml.load_all:加载多个 YAML 配置
以上这两种均可以通过 Loader 参数来指定加载器。一共有三个加载器,加载器后面对应了三个不同的构造器:
BaseConstructor:最最基础的构造器,不支持强制类型转换SafeConstructor:集成 BaseConstructor,强制类型转换和 YAML 规范保持一致,没有魔改Constructor:在 YAML 规范上新增了很多强制类型转换
Constructor 这个是最危险的构造器,却是默认使用的构造器。
python/object/apply
对应的函数是 construct_python_object_apply,最终在 make_python_instance 中引入了模块中的方法并执行。
python/object/apply 要求参数必须用一个列表的形式提供,所以以下 payload 都是等价的,但是写法不一样,可以用来绕过:
1 | yaml.load('exp: !!python/object/apply:os.system ["whoami"]') |
python/module
对应的函数是 construct_python_module,里面调用了 find_python_module,等价于 import。
那么在这种没有调用逻辑的情况下,是否有办法利用呢?我感觉在可以写任意文件的时候是有办法的。比如搭配任意文件上传。
首先写入执行目录,yaml 中指定同名模块,例如上传一段恶意代码,叫 exp.py,然后通过 yaml.load('!!python/module:exp') 加载。
常规利用方式
常规的利用方式和 <5.1 版本的姿势是一样的。当然前提是构造器必须用的是 UnsafeConstructor 或者 Constructor,也就是这种情况:
yaml.unsafe_load(exp)yaml.unsafe_load_all(exp)yaml.load(exp, Loader=UnsafeLoader)yaml.load(exp, Loader=Loader)yaml.load_all(exp, Loader=UnsafeLoader)yaml.load_all(exp, Loader=Loader)
通用poc
通用POC
经过上面的了解与验证,我们知道只要存在yaml.load()且参数可控,则可以利用yaml反序列化漏洞,下面为常用的Payload:
1 | !!python/object/apply:os.system ["calc.exe"] |






