一、前记

在学习利用ciscn2019华北赛区web2学习jwt的时候,碰到了python的反序列化的问题。就对python的反序列化问题进行了学习。

二、什么是序列化和反序列化、

序列化指的是类对象向字节流转化从而进行存储和传输的一个过程,目的是为了保存、传递和恢复对象的方便性

序列化结果:

反序列化指的是将字节刘转化成原始的对象的一个过程

反序列化结果:

三、序列化过程详解

序列化过程分为三个部分,第一步:从对象里面提取出所有的属性,并将属性转换成名值对;第二步:写入对象的类名;第三步:写入名值对。

(图片的例子主要是想说明类名,对象,变量的概念,并非是序列化的例子)

四、反序列化过程详解

反序列化过程分为4个步骤:第一步:获取pickle输入流;第二步:重建属性列表;第三步:根据类名创建一个新的对象;第四步:将属性复制到新的对象里面(对象,属性列表,类名缺一不可)

五、底层反序列化和序列化的过程(奇怪字符串的出现原因和含义)参考链接

出现奇怪字符串主要是和PVM有关,PVM由三个部分组成,引擎(也叫做指令分析器),栈区,Memo(也叫标签区),下面分别分析这三个部分

1、引擎

从头开始读取流中的操作码和参数,并对其进行处理,在这个过程中改变栈区和标签区,处理结束后到达栈顶,形成并返回反序列化的对象

2、栈区

作为流数据处理过程中的暂存区,在不断的进出栈过程中完成对数据流的反序列化,并最终在栈上生成反序列化的结果

3、标签区的作用

数据的一个索引或者标记

PVM操作码有不一样的含义(在这篇文章里面介绍了全部的PVM操作码:https://www.anquanke.com/post/id/188981#h3-6),在这里我们就着重的将几个我们例子里面会用得到的。

c : 读取本行的内容作为模块名( module ) , 读取下一行的内容作为对象名( object ) . 然后将 module.object 作为可调用对象压入到栈中
( : 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中 .
R : 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象  .最后将结果压入到栈中 .
. : 结束整个 Pickle 反序列化过程 .
i: 构建和推送类实例
d:在栈顶创建一个字典
b:调用__setstate__或者__dict__.update()来更新字典的内容
s:将键值对添加到字典里面

举例:

但是如果我们把字节流转换成对象之后过程如下:

1、c之后是模块名,换行后是类名,于是将__builtin__.eval放入栈中

2、(是一个标记符,我们将一个Mark放入栈中

3、S后面是字符串,我们放入栈中

4、t将Mark之前的内容取出来转化成元组,再存入栈中("open('/flag.txt','r').read()"),同时标记Mark消失

5、R将元组取出,并将callable(可调用的)取出,然后将元组作为callable的参数并执行,在这里就是__builtin__.eval"open('/flag.txt','r').read()",然后将该结果再存入栈中。

六、如何利用Python的反序列化漏洞

怎么看反序列化漏洞不太清楚,在这里我使用的是bandit,扫描出来的漏洞。(举例为2019年CISCN华北赛区web2)

在这里先讲讲如何构造反序列化的payload

1、构造反序列化的payload

构造反序列化漏洞的payload一般是使用__reduce__(Python2中这个方法一般是新式类特有的),在官方介绍的文档里面__reduce__的解释是这个样子的:

当序列化以及反序列化的过程中中碰到一无所知的扩展类型(这里指的就是新式类)的时候,可以通过类中定义的__reduce__方法来告知如何进行序列化或者反序列化

也就是说我们,只要在新式类中定义一个 __reduce__ 方法,我们就能在序列化的使用让这个类根据我们在__reduce__ 中指定的方式进行序列化,现在以2019年CISCN华北赛区web2的payload进行举例


#coding:utf-8
import pickle
import urllib

class payload(object):
    def __reduce__(self):
       return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
print a
a = urllib.quote(a)
print a

'''
c__builtin__:引入__builtin__模块
eval:引入eval对象
p0:将栈顶数据(__builtin__.eval)存储在memo中
(S"open('/flag.txt','r').read()":在栈顶创建字符串open('/flag.txt','r').read()
p1:将栈顶字符串存储到memo中
tp2:从栈中不断的弹出数据,弹出的顺序和压入栈时的顺序一样
Rp3:将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象  .最后将结果压入到栈中 .
.:结束整个反序列化过程

'''

出现的结果就是:

有关于__reduce__()方法,在序列化的时候会无参调用__reduce__()方法,而且必须返回一个字符串或者是元组。返回值为字符串的话这是一个代表全局名称的字符串,返回值为一个元组的话,必须是2到5个元组。

元组的元素定义:
1.     一个可以被调用的对象(类),重建时使用(python2中要求 被注册调用的安全构造器或者必须有__safe_for_unpickling__为真的反序列化属性。否则,将会raise一个UnpicklingError。);
2.     参数元组,供对象重建时调用(如果不接收参数是一个空元组);
3.     对象的状态,将会被传到__setstate__()方法。如果这个对象没有__setstate__()方法,这个值必须是一个字典,而且会被加入对象的__dict__(可选)
4.     一个产生列表元素的迭代器对象, 用append(item)或者extend(list_of_items)加入这个对象。列举子类是很重要,但是也可以用作其他类,只要有append()extend()的方法(可选)
5.     一个产生字典元素的迭代器对象,(key, value)会成为obj[key] = value可以用来作为字典子类,实现了__setitem__()的类也可以用(可选)

七、参考链接

https://www.guildhab.top/?p=2178

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BPython%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#3-PVM-%E6%93%8D%E4%BD%9C%E7%A0%81

https://superxiaoxiong.github.io/2017/05/18/python-pickle/

https://www.anquanke.com/post/id/188981#h3-6