Python 反序列化安全问题(二)

1fe1se

python pickle允许类定义__reduce__方法来声明如何进行序列化。其返回字符串或者tuple,前者可能代表着一个python的全局变量的名称,后者则是描述在反序列化过程中如何进行重构。安全问题也是主要出在后者,本文主要针对于该情况进行pickle模块源码分析。

一、源码分析

代码结构可以分为:基础变量、自定义异常类、操作变量、序列化以及反序列化类以及普通函数。

1.1 基础变量

代码(28-57行)最先定义了部分变量,如最高协议号还有代码中使用了struct.pack()以及marshal.loads()进行序列化和反序列化,并且解释了为何用这两个函数。

1.2 自定义异常类

代码(59-85行)中自定义了4个异常类,分别为PickleError、PicklingError、UnpicklingError以及_Stop.

  1. PickleError:PickingError和UnpicklingError的基类
  2. PicklingError:序列化过程中异常
  3. UnpicklingError:反序列化过程中异常
  4. _Stop:在反序列化过程中结尾处触发该异常

1.3 操作变量

代码(99-126行)定义了操作变量,我们可以理解为操作指令,每一个变量都对应着相关操作,这些指令在序列化的过程中写入,然后在反序列化过程中读取进行对应操作;我们主要理解如下操作指令。

  1. c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中。
  2. p:将堆栈中索引为-1的对应存储入内存。
  3. (:将一个标记对象插入到堆栈中。
  4. t:构建元组压入堆栈。
  5. S:读取字符串进行处理之后压入堆栈。
  6. R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。
  7. .:调用_Stop结束反序列化。

1.4 序列化以及反序列化类

代码定义了Pickler和Unpickler类,这两个类是pickle模块进行序列化反序列化的核心,下面看其实现过程:

1.4.1 序列化过程

  1. dumps函数接收参数后首先进行Pickler类的初始化,然后调用类中的dump函数进行序列化。
  2. dump()函数首先调用save函数,save函数可以看做字典类型调度器,key为需要进行序列化的对象的type,value为对应type的存储函数名。例如如果序列化对象为[1, 2, 3],也就是list类型,save函数判断完类型之后,在调度器内查找对应的方法save_list,然后调用结束后将结果写入内存中,最后dump函数写入结束符号完成整个序列化过程。
  3. 如果上一步查询调度器并没有查询到对应的方法,即对象的type不在NoneType/bool/builtin/classobj/dict/float/function/instance/int/list/long/str/tuple/type/unicode这些类型中的时候,首先查看是否存在__reduce_ex__,如果存在则不再查找__reduce__,不存在的话则继续查找__reduce__;进而判断该函数返回值是string还是tuple,前者进入save_global;后者进入危险开始的save_reduce函数。
  4. save_reduce会将__reduce__返回的tuple结果,调用save_tuple方法进行序列化存储

序列化流程图
测试代码

import os
class A():
    def __reduce__(self):
        a = 'whoami'
        return (os.system, (a,))
print type(A)
print dumps(A)
print type(A())
print  dumps(A())
class B(object):
    def __reduce__(self):
        a = 'whoami'
        return (os.system, (a,))
print type(B)
print  dumps(B)
print type(B())
print dumps(B())

测试结果:

<type 'classobj'>
c__main__
A
p0
.
<type 'instance'>
(i__main__
A
p0
(dp1
b.
<type 'type'>
c__main__
B
p0
.
<class '__main__.B'>
cnt
system
p0
(S'whoami'
p1
tp2
Rp3
.

上述结果可以看出如果我们要达到执行任意代码的目的,需要使用的是第四种即dumps(B())才能进入到save_reduce方法,前三种只能调用save_global方法,只是对于命名引用进行序列化,所以也只能使用于相同环境中,否则在反序列化的过程中会报错。

1.4.2 反序列化过程

反序列化和序列化的过程挺相似,按字节读取然后在调度器中查找对应的处理函数;上面我们提到过一些操作指令,反序列化的调度器中将上述的操作指令作为key,处理函数作为value,此处主要分析上述最后一个实例的反序列化过程。

序列化的结果:

cnt
system
p0
(S'whoami'
p1
tp2
Rp3
.

反序列化过程:

  1. 读取第一个字符c,查询调度器,对应的方法为load_global;
  2. 调用load_global,读取该行将该行后面nt作为模块名,下一行system作为方法名,两者作为参数进入find_class;
  3. find_class的目的就是返回nt.system方法,然后将返回结果压入堆栈中;该方法的代码如下所示:
    def find_class(self, module, name):
        __import__(module)
        mod = sys.modules[module]
        klass = getattr(mod, name)
        return klass
  1. 继续读取字节p,调用load_put,load_put从堆栈中获取最后一个对象,放入内存中,p后面的数字,为key;
  2. 继续读取字节(,调用load_mark,将object()压入堆栈;
  3. 继续读取字节S,调用load_string,将'whoami'去除" '压入堆栈;
  4. 到目前堆栈中有3个对象,分别为nt.system、object()、whoami;继续读取p,将whoami存储到内存中,key为1;
  5. 读取字节t,调用load_tuple,其首先调用marker获取object()的索引号,此处为1,然后将stack[1:]变为('whoami',),也就是说执行完这一步操作之后,堆栈中只有nt.system和('whoami',);
  6. 读取字节p,调用load_put,将('whoami',)存储如内存,key为2,;
  7. 读取字节R,调用load_reduce,运行nt.system('whoami'),得出结果之后赋值给堆栈索引为-1;
  8. 读取p,调用load_put,将结果存储到内存;
  9. 读取.,即结束符号,清空堆栈,结束反序列化。

总结

序列化以及反序列化其实是给每种能够识别出来的类型的对象一个既定的方式去进行序列化或者反序列化,如果碰到不认识的,那就去查找__reduce__,将其序列化,然后在根据它去进行反序列化过程中的重构;
从上面的分析过程中可以看出,如果我们要在反序列化的过程中去执行命令,就要满足在序列化的时候能执行save_reduce,然后在反序列化的过程中才能执行load_reduce,进而执行命令;

阅读 2.3k

网络安全路上走,一走不回头。

6 声望
0 粉丝
0 条评论
你知道吗?

网络安全路上走,一走不回头。

6 声望
0 粉丝
文章目录
宣传栏