python反序列化分析
前言
本次学习是在有php反序列化基础上的,所以基础的什么是序列化和反序列化不必再说。与php反序列化类似,就是将程序运行的对象实例转换为字符串储存起来,在后续需要使用的时候就恢复原来的状态。当然,在python语言里也有类似于serialize和unserialize这样的函数,他们分别为序列化函数pickle.dumps和反序列化函数pickle.loads函数。
pickle.dumps将对象反序列化为字符串 |
python2和python3序列化出来的字符串是不一样的,python3添加了不可见字符。在这里就以python3环境为例,那就先举一个例子。
import pickle |
那么生成的序列化链为:
\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 |
看结果:
这是反汇编,解析序列化字符串,并且告诉你每一部分代表什么意思,每一行都代表了一个指令,但是光看这全是指令的东西还是不明白。所以还需要进一步的说明。根据上面解析的结果一步步的分析。
pickle构造出的字符串,有很多个版本。在dumps或loads时,可以用Protocol参数指定协议版本,例如指定为0号版本,目前这些协议有0,2,3,4号版本,默认为3号版本。所以说上面结果第一行就是3了。0号版本是人类可读的,就类似于这样,
这是使用python2执行的结果,以后的版本就加了不可打印字符。值得一提的是,pickle是向前兼容的,0版本字符串不管在什么环境都可以反序列化成功。
\x80 PROTO 3表示使用3号版本序列化字符串,机器读取一个字节,变成\x03,操作结束 |
使用pickletools.optimize函数对序列化串进行优化,删除了BINPUT无用指令,更好分析。
说到这里应该对pickle生成序列化字符串有了初步的认识。
__reduce__方法
反序列化执行命令的重要魔术方法。我们可以在这个方法里构造命令去执行。看个例子。
import pickle |
返回值要么是字符串,要么就是元组。
reduce方法在pickle反序列化时会自动执行,就是利用了R指令,它主要做的事情是:
取当前栈的栈顶记为args,然后把它弹掉。 |
目前ctf题目大多都是用reduce方法来执行命令。构造恶意字符串,在反序列化时执行恶意命令。那么要过滤掉reduce该怎么做,reduce是否能调用成功完全取决于R指令是否存在,那么我们过滤掉R指令就会完全堵死这个思路。起初我认为python的反序列化就是调用reduce方法,但是接下来说一下比较奇特的思路。
c指令码(覆盖全局变量)
回看上面的内容,c指令是干什么用的?获取全局变量,读取module和name,看一个例子更好理解c指令是怎么用的。
import pickle |
我们不知道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,这样就达到覆盖变量的效果了。
原来的序列化串: |
用师傅的例子本地测试不成功,我也很异或呢?总之覆盖变量就是这样一个改法。
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 |
看看结果:
现在我们要手动构造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反序列化的初步学习,若文章有错误之处欢迎师傅指正。
好的技术文章是我们初学者的福音,所以推荐以下两位师傅的文章: