内容纲要

前言

前几天看了一道 Python Flask SQL Injection 的题目 . 整理时发现这是去年强网杯的题目( QWB-2018-Python is the best language ) . 原题一共有两个考点 , 其中一个是 源码审计 + SQL注入 , 而另一个考点就是 Python Pickle/CPickle 反序列漏洞 . 恰好最近实习有个任务就是学习各类语言的反序列化漏洞 , 刚好趁机学习一波 .

本文是根据 K0rz3n 师傅写的文章来学习的 , K0rz3n 师傅的 << 一篇文章带你理解漏洞 >> 系列真的是篇篇经典 , 前辈真的是太强了 !


Python 序列化与反序列化


什么是序列化过程 / 反序列化过程

在学习反序列化漏洞前 , 我们应该先弄明白什么是序列化和反序列化 . 之前我有学习过 PHP 反序列化漏洞 , 可以参考 PHP反序列化漏洞 这篇文章 . 实际上 , Python Pickle/CPickle 反序列化漏洞PHP 反序列化漏洞 非常类似 .

Python 序列化过程 , 简单的说 , 就是将一个 Python 类对象转换为字符串格式的字节流 . 而 Python 反序列化过程 , 就是将一个序列化字符串格式的字节流还原为 Python 类对象 .

让数据在存储或者传输过程中以简单的字符串格式来表述相对复杂的类数据结构 , 能够方便应用所见即所得 , 直接进行数据交流处理 . 因此 , 反序列化过程常常出现在 数据结构网络传输 , session存储 , cache存储 , 配置文件上传 , 参数接收 等多个接口处


Pickle/CPickle 模块

在 Python 中 , 包含序列化/反序列化过程的库有 Json模块 , Pickle/CPickle模块 , Shelve模块 , Marshal模块 等 , 本章我们主要来看 Pickle/CPickle模块

Pickle模块CPickle模块 在原理上非常类似 , 它们的函数调用过程基本是相同的 , 而且共用同一个调用接口 . 只不过一个底层使用 Python 语言实现 , 一个底层使用 C语言实现 . 并且 , CPickle模块 的性能远远超过 Pickle模块( 1000倍 )

因此 , 我们这里就以 Pickle模块 为例 , 来学习下面的内容 .

Pickle模块中 , 我们最常使用的是下面这四个方法 .

  • pickle.dump() : 将 Python 类对象序列化后保存到指定文件中 .
  • pickle.dumps() : 将 Python 类对象序列化成字符串格式的字节流并保持 .
  • pickle.load() : 读取文件 , 将文件中的序列化内容反序列化为 Python 类对象 .
  • pickle.loads() : 将字符串格式的字节流反序列化为 Python 类对象 .

这里写两个测试文件来验证这些方法 .

  • pickle.dump() / pickle.load()

    可以看到 , 原始的 Python 类对象和反序列化生成的 Python 类对象是完全相同的 .

  • pickle.dumps / pickle.loads

    同样 , 原始的 Python 类对象和反序列生成的 Python 类对象是完全相同的 .

你一定非常好奇这个序列化字符串是什么意思 , 它是按照什么规则生成的 . 这就要涉即到 PVM( Python Virtual Machine ) 了 , 它是实现 Python 序列化过程与反序列化过程中最根本的东西 .


PVM( Python Virtual Machine , Python 虚拟机 )

PVM 有什么用呢 ? 它的执行流程是怎样的呢 ?

  1. 先来回答第一个问题

    在使用 C , C++ 等编译性语言编写的程序时 , 解释器需要先将源代码文件转换成计算机使用的机器语言( 也就是常说的 " 编译 " 过程 ) , 然后经过链接器链接之后形成了二进制可执行文件( 也就是常说的 " 链接 " 过程 ) . 运行该程序的时候 , 计算机会将二进制可执行文件从硬盘载入到内存中并运行 .

    但是对于 Python 而言 , 它可以直接从源代码运行程序 . Python解释器会将源代码编译为字节码 , 然后将编译后的字节码转发到 Python 虚拟机中执行 .

    所以说 PVM 的作用非常简单 , 它是一个用来解释字节码的解释引擎 .

  2. 再来回答第二个问题

    一般来说 , 当运行 Python 程序时 , PVM 会执行两个步骤 .

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

    一般来说 , 在构建程序时最好给 Python 进程在主机上的写入权限 , 这样只要源代码没有改变 , 生成的 .pyc 文件就可以被重复利用 , 提高执行效率 , 同时隐藏源代码 .

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

那么 PVM 和 Pickle模块/序列化过程/反序列化过程 有什么联系呢 ?

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

这个轻量级的 PVM 由三个部分组成 , 如下所示

  • 指令处理器( Instruction processor )

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

  • 栈区( stack )

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

  • 标签区( memo )

    由 Python 的字典( dict )实现 , 可以看作是数据索引或者标记 , 为 PVM 的整个生命周期提供存储功能 .

这里需要重点关注指令处理器可读取的操作码 . 完整的指令集可以参考 Python pickle 反序列化实例分析 这篇文章的 , 现在我只讲比较重要的几个 .

     c : 读取本行的内容作为模块名( module ) , 读取下一行的内容作为对象名( object ) . 然后将 module.object 作为可调用对象压入到栈中

     ( : 将一个标记对象压入到栈中 , 用于确定命令执行的位置 . 该标记常常搭配 t 指令一起使用 , 以便产生一个元组

     S : 后面跟字符串 , PVM会读取引号中的内容 , 直到遇见换行符 , 然后将读取到的内容压入到栈中

     t : 从栈中不断弹出数据 , 弹射顺序与压栈时相同 , 直到弹出左括号 . 此时弹出的内容形成了一个元组 , 然后 , 该元组会被压入栈中 .

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

     . : 结束整个 Pickle 反序列化过程 .

以上 6 个符号就是 Pickle 在序列化时最常用到的操作码 ~


序列化过程剖析

现在我们再来看之前的序列化内容 , 就不会感到非常难以理解了 . 我把我的分析思路整理在了下面 , 如果还有错误欢迎您指正 .

整个序列化字符串的生成过程就如上图所示 . 事实上 , 整个序列化的过程可以分为三个步骤 .

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

而反序列化的过程就是序列化过程的逆过程 , 这里就不再过多阐述了 , 我们重点来看反序列漏洞的原理及利用 .


Pickle/CPickle 反序列化漏洞分析


原理分析

反序列化漏洞的根源出在 __reduce__() 魔术方法 .

很多反序列化过程都会存在像 "构造函数" 或者 "析构函数" 这样的函数( 原谅我不知道还能怎么形容 ) . 每当反序列化过程开始或者结束时 , 都会自动调用这类函数 . 而这恰好是反序列化漏洞经常出现的地点 , PHP 的 __wakeup() 魔术方法是这样 , __reduce__() 魔术方法也是这样 .

而在反序列化过程中 , 编程语言需要根据序列化字符串去解析出自己独特的语言数据结构 . 为了实现这点,就必然要在内部把解析出来的结果去执行一下 . 好呗 ! 有了这个执行的操作 , 反序列化过程不出事还好 , 一出事就是一个天大的 RCE ------ 毕竟这些编程语言的应用是非常广泛的~

来看一看 __reduce__() 这个魔术方法 . 官方文档上是这么解释的

借用 k0rz3n 师傅的一句话 : 当序列化以及反序列化的过程中碰到一无所知的扩展类型( 比如使用 C 语言实现的 Python 扩展类 )时,可以通过类中定义的 __reduce__() 方法来告知 Python 如何进行序列化或者反序列化操作

注意上图的红框内的内容 , 当 __reduce__() 函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组 .

是不是非常熟悉 ? 在上文讲 PVM 操作码时 , R 操作码实现的功能不正是这样的吗 ? 事实上 , R操作码就是 __reduce__() 魔术函数的底层实现 . 而在反序列化过程结束的时候 , Python 进程会自动调用 __reduce__() 魔术方法 . 如果我们可以控制被调用函数的参数 , Python 进程就会执行我们的恶意代码 .

举一个例子

通过控制 __reduce__() 的返回值 , 我们成功执行了 /usr/bin/id 命令


反弹 Shell

有了任意代码执行 , 我们很容易反弹 Shell

当然 , 我们还可以通过 python -c 执行任意 Python 代码 .

成功反弹 Shell !


总结

本文的主要内容就到这里了 , 主要谈了谈有关 PVM 的操作码及运行原理 , 以及 Pickle/CPickle 的反序列化漏洞的成因及利用方式 . 感觉能深挖的地方还有很多( 比如 k0rz3n 师傅提到的 Marshal + types.FunctionTyle 这种攻击方式 ) , 后面有时间再来补充 .

最后修改日期:2020年3月9日

作者

留言

师傅你好,我觉得你序列化过程剖析的那部分,编号11的Ntp3那一步,
按照t操作码的定义,弹出栈的数据只到(操作码吧,我觉得copy_reg._reconstructor应该没有被弹出栈,而且我觉得前面的那句”不断从memo中弹出数据”似乎为“不断从栈中弹出数据”?
我是这几天刚在学习pickle反序列化漏洞的相关知识,看到师傅博客的这处地方感到疑惑,特此提出我的看法

    Ntp3 这一步操作应该是依次从栈中弹出数据 , 直到弹出 “(” . 此时弹出的数据组成一个元组 , 最后将该元组压入到栈中.

    当时没有注意到这个点 , 感谢指出错误.

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。