PyYaml反序列化
YAML基础
基础语法见附录,借用一个简单的例子快速入门:
1  |  | 

类型转换
所谓类型转换,就是使用符号!!+tag实现数据类型的强制转换,比如将数字强制转换成字符串:
1  |  | 
基于这种特性,PyYAML可以实现拓展解析的能力,选择合适的解析器对数据进行解析处理,可以是简单的数据类型,也可以是模块、实例。doc中列举除了一些基础tag和特殊tag:
- 基础类型转换
 
site-packages/yaml/constructor.py中有24处调用add_constructor()的地方,也就是说PyYAML中支持24中基础的类型转换,也就是上图中的Standard YAML tag与Python-specific tags部分,对于基础的类型转换可以简单抽象成以下过程:
!!module_name args <=> find_function(“module_name”)(args)
- 高级类型转换
 
除了add_constructor()之外,还有add_multi_constructor()的调用,也就是对应上图中的Complex Python tags:
| tag | expression | 
|---|---|
!!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(...) | 
很明显这五种tag都可以引入新的模块,导致PyYAML存在被反序列化攻击的可能。
下面逐一介绍高级类型转换中的五个标签。
危险标签
python/object/apply
对应construct_python_object_apply,它可以引入模块中的方法并执行,可以传入参数并且以列表的方式提供:
1  |  | 
python/object/new
与python/object/apply基本等价,对应函数construct_python_object_new,payload也相同。
python/object
对应函数construct_python_object,可以调用构造函数但无法传参。
1  |  | 
python/module
对应函数construct_python_module,后续调用find_python_module,也就是import,一般情况下import导入模块不会执行py文件里的代码,除了命名空间__name__ == "__main__"的代码部分。这时候想要利用这个标签RCE,自然会联想到结合任意文件上传,那么这就有点类似于php的文件包含了,不过后缀必须是py。举个例子存在如下flask项目:
├── app.py
├── static
├── templates
│ └── index.html
└── uploads
├── exp.py
└── info.py
app.py中包含如下利用代码:
1  |  | 
exp.py为恶意模块
1  |  | 
然后执行app.py就可以实现rce。
1  |  | 
或者可以上传__init__.py至uploads文件夹,不需要指定包名就可以直接触发:
1  |  | 
python/name
对应函数是construct_python_name,后续调用find_python_name,他返回模块下面的属性或方法。它可以用于以下特殊场景:
1  |  | 
这时将payload设置为token=!!python/name:__main__.TOKEN,就可以绕过对于token的check,前提是需要知道变量名是token,当然这种情况下直接RCE也是可以的。
攻击思路
PyYAML < 5.1
默认构造器:Constructor
不支持标签:无,全部支持。
版本小于5.1提供两个方法解析YAML:
yaml.load:加载单个YAML文件yaml.load_all:加载多个YAML文件
两者都可以指定构造器Loader,此版本下构造器有三种:
BaseConstructor:基础构造器,不支持类型转换。SafeConstructor:集成BaseConstructor,同时类型转换和YAML规范一致,不支持complex python tags。Constructor:在YAML规范基础上新增complex python tags。
支持complex python tags的Constructor构造器是最危险的构造器,同时它也是在此版本下默认使用的。
此版本下五种标签看情况选择即可。
PyYAML = 5.1
默认构造器:FullConstructor
不支持标签:无,全部支持。
此版本下的构造器有:
BaseConstructor:没有任何强制类型转换SafeConstructor:只有基础类型的强制类型转换FullConstructor:除了python/object/apply之外都支持,但是加载的模块必须位于sys.modules中(说明已经主动 import 过了才让加载)。这个是默认的构造器。UnsafeConstructor:支持全部的强制类型转换Constructor:等同于UnsafeConstructor
同时,新增了几个加载方法:
yaml.full_loadyaml.full_load_allyaml.unsafe_loadyaml.unsafe_load_all
不使用FullConstructor
这种情况下和版本小于5.1的利用方式相同,但构造器必须是UnsafeConstructor、Constructor。
1  |  | 
突破FullConstructor
goal:触发带参数调用+引入函数
FullConstructor只允许加载sys.modules中的模块,也就是import过的,总结来说利用过程中需要解决如下限制:
- 只引用,不执行的限制:
- 加载进来的 
module必须是位于sys.modules中 
 - 加载进来的 
 - 引用并执行:
- 加载进来的 
module必须是位于sys.modules中 - FullConstructor 下,
unsafe = False,加载进来的module.name必须是一个类 
 - 加载进来的 
 
两个不行的例子:
!!python/name:pickle.loads:pickle不在sys.modules中!!python/object/new:builtins.eval ["print(1)"]:eval虽然在sys.modules中,但是type(builtins.eval)是builtin_function_or_method而不是一个类。
subprocess.Popen()
满足位于sys.modules,同时可以执行命令的类就是subprocess.Popen,因此使用apply标签的payload就是:
1  |  | 
map()
不使用apply标签,在builtins内置模块中还有map、filter等可以触发函数执行。类可以通过python/object/new引入,而函数可以通过python/name引入,最后的payload就是:
1  |  | 
listitems.extend()
使用FullConstructor加载YAML的同时,会调用construct_python_object_apply,而它不仅进行了实例化,如果有listitems还会调用实例中的extend属性指定的方法,比如:
1  |  | 
因此转换成YAML就是:
1  |  | 
__setstate__(state)
除了extend,construct_python_object_apply还会调用setstate,对于这个利用方式picke反序列化中已经提到:
1  |  | 
转换成YAML:
1  |  | 
slotstate.update()
type可以用 staticmethod 来替换,set_python_instance_state 中存在调用:slotstate.update(),只需要将slotstate.update 赋值为 eval,state赋值为payload即可:
1  |  | 
转换成YAML就是:
1  |  | 
PyYAML = 5.2
默认构造器:FullConstructor
不支持标签:python/object/apply
PyYAML = 5.3.1
默认构造器:FullConstructor
不支持标签:python/object/apply
此版本打了个补丁,禁止在使用set_python_instance_state方法时设置任意属性,设置黑名单过滤了extend以及__xxx__方法,详见issue。
 This patch tries to block such attacks in FullLoader by preventing
set_python_instance_statefrom setting arbitrary properties. It implements a blacklist that includesextendmethod (called by construct_python_object_apply) and all special methods (e.g.__set__,__setitem__, etc.).FROM lib/yaml/constructor.py:
blacklist_regex = ['^extend$', '^__.*__$']
PyYAML = 5.4
默认构造器:FullConstructor
不支持标签:python/object/apply、python/object、python/object/new、python/module
仅支持:python/name
PyYAML = 6.0
默认构造器:无默认构造器,使用
yaml.load()时必须指定参数Loader。不支持标签:python/object/apply、python/object、python/object/new、python/module
仅支持:python/name
防御
使用yaml.safe_load()即可。
附录
YAML语法
基本语法
- 大小写敏感
 - 使用缩进表示层级关系
 - 缩进不允许使用tab,只允许空格
 - 缩进的空格数不重要,只要相同层级的元素左对齐即可
 - ‘#’表示注释
 
数据类型
注意 :、?、-后面都需要跟一个空格。
对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
1
2
3
4
5
6
7
8
9
10
11
12#一般对象,包含属性,以键值对表示
obj:
key1: value
key2: value2
#复杂对象,键名可以是一个数组,值也是数组
?
- complexkey1
- complexkey2
:
- complexvalue1
- complexvalue2
# [complexkey1,complexkey2] => [complexvalue1,complexvalue2]数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#以 - 开头表示是一个数组
- A
- B
- C
#对象数组
companies:
-
id: 1
name: company1
price: 200W
-
id: 2
name: company2
price: 500W
#companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}]
#复合结构,对象包含多类型属性纯量(scalars):单个的、不可再分的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21boolean:
- 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连接,最后使用+代表时区
引用
1  |  | 
REF
SecMap - 反序列化(PyYAML) - Tr0y’s Blog