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_load
yaml.full_load_all
yaml.unsafe_load
yaml.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_state
from setting arbitrary properties. It implements a blacklist that includesextend
method (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