Python yaml 反序列化
Py Yaml 反序列化漏洞
Python 中的强制类型转换
- Py Yaml 的漏洞本质上是因为这一点产生的
可以通过 !!
来进行类型转换。
通过上面的测试可以发现,如果识别到一个数字,那么按照 YAML 格式来处理,这个类型就是数字类型。如果我们想把数字类型变为字符串类型就可以这样:a: !!str 1
,它的结果和 a: "1"
是一样的。
由于 YAML 仅仅是一种格式规范,所以理论上一个支持 YAML 的解析器可以选择性支持 YAML 的某些语法,也可以在 YAML 的基础上利用 !!
来扩展额外的解析能力。本文主要聚焦于 PyYAML,所以直接看源码就可以知道它在 !!
上做了哪些魔改。
在 site-packages/yaml/constructor.py
中可以看到使用了 add_constructor
的有 24 多个地方,这些都是用来支持基础的类型转换(带有 tag:yaml.org,2002:python/
的说明是 PyYAML 自定义的类型转换)
这些基础类型转换的功能非常好理解,看上面那张图即可,就不多说了,我们下面简单写一个 demo
test.py
1 | # test.yaml 文件内容 |
Run test.py
我们可以调试代码,简单看一下流程与一些值,传入的参数 node 格式为
所以对于一个 !!x x
来说,类型转换执行的伪代码就是:find_function("x")(x)
。这个也很好理解。
高级类型转换
在理解了基础的类型转换之后,查看源码可以发现还有一个 add_multi_constructor
函数,一共有 5 个:
python/name
python/module
python/object
python/object/new
python/object/apply
从上面那张图可以看到,这几个都可以引入新的模块。这就是 PyYAML 存在反序列化的本质原因。
- 用 Pycharm 来安装两个 Py Yaml 版本,先从 Py Yaml < 5.1 的版本说起
PyYaml < 5.1 的序列化与反序列化
在 Python 中的 PyYAML 库中提供这几种方式实现 Python 和 Yaml 这两种语言的转换。
yaml -> python 使用的方法是 yaml.dump
1 | yaml.dump(data) |
dump()
方法将 Python 对象 data
转换为 YAML 格式的方法,data
是一个 Python 对象,可以是字典、列表、元组、整数、浮点数、字符串、布尔值等基本数据类型,也可以是自定义的类的实例。
举个例子:
1 | import yaml |
输出结果:
在这个代码中,我们定义了一个 Python 字典 data
,然后使用 yaml.dump
方法将 data
对象转换为 YAML 格式,并将其赋值给变量 yaml_data
。最后,我们打印 yaml_data
,输出转换后的 YAML 格式数据。
其实通俗点来说,就是将 Python 的对象实例转化为 YAML 格式的字符串,也就是序列化。
深入挖掘 Py YAML 序列化与反序列化
- 其实 Py YAML 并不是只有一个
yaml.load()
一个反序列化的方法
<5.1 版本中提供了几个方法用于解析 YAML:
yaml.load
:加载单个 YAML 配置yaml.load_all
:加载多个 YAML 配置
以上这两种均可以通过 Loader
参数来指定加载器。一共有三个加载器,加载器后面对应了三个不同的构造器:
BaseConstructor
:最最基础的构造器,不支持强制类型转换SafeConstructor
:集成 BaseConstructor,强制类型转换和 YAML 规范保持一致,没有魔改Constructor
:在 YAML 规范上新增了很多强制类型转换
Constructor
这个是最危险的构造器,却是默认使用的构造器。
所有的 construct_python_xxx()
所有方法都调用了 constructor()
方法,这让我想起来 CVE-2018-1284 这个洞,大致上差不多。
我们可以先用这个 EXP 调试看一下
1 | import yaml |
到 make_python_instance()
的调用栈如下
1 | make_python_instance, constructor.py:552 |
中间的调用栈也很简单,这里不再赘述,直接看漏洞触发点,其实是从 find_python_name()
方法中开始被调用
跟进 find_python_name()
方法,里面触发点是在 __import__
上,在 SSTI 里面我们就会用到 __import__
来导入恶意类,从而实现漏洞攻击。
利用成功
针对!!python/module标签
这个标签对应的是源码中的 construct_python_module
,针对这个标签的利用方法和前两个不同,它没有调用逻辑,但是再搭配任意文件上传有奇效。
首先写入执行目录,yaml 中指定同名模块,例如上传一段恶意代码,叫 exp.py
,然后通过 yaml.load('!!python/module:exp')
加载。
在实际的场景中,由于一般用于存放上传文件的目录和执行目录并不是同一个,例如:
1 | app.py |
这个时候只需要上传一个 .py 文件,这个文件会被放在 uploads 下,这时只需要触发 import uploads.header
就可以利用了:
接着运行 python3 app.py
即可
PyYaml >= 5.1 的序列化与反序列化
大部分东西是一样的,区别在于 PyYaml >= 5.1 的时候,它并不是以 Constructor
作为默认使用的构造器。所以我们在请求的时候,是需要加上 Loader
这个参数的
这里说明一下 PyYaml >= 5.1 都有哪些加载器
1 | BaseLoader:不支持强制类型转换 |
- 从加载器里面就可以比较明显得看出来,如果要进行漏洞挖掘的话,一定是从下面三个加载器去做文章,其中第二个
SafeLoader
需要我们去找 bypass 的方法,而剩下两个,则是寻找能利用的方法。
在 PyYaml >= 5.1的版本之后,Fullloader 这个加载器对于 payload 的限制比较多了,我们延用PyYaml 的 poc 还是可以的,只不过要修改一下。
我们还可以利用 python 内置的 builtins 模块(因为之前我们审计 constructor.py 时,发现定义的find_python_name() 中,如果不用”.”将模块名和对象名分开,会默认调用 builtins 模块)
1.沿用 PyYaml<=5.1 的 poc
1 | from yaml import * |
2.利用 builtins 模块中的内置函数
首先,我们先明确 builtins 中的所有的类,从而筛选出可以利用的类
1 | import builtins |
输出结果:
1 | [<class 'ArithmeticError'>, <class 'AssertionError'>, <class 'AttributeError'>, <class 'BaseException'>, <class 'BaseExceptionGroup'>, <class 'BlockingIOError'>, <class 'BrokenPipeError'>, <class 'BufferError'>, <class 'BytesWarning'>, <class 'ChildProcessError'>, <class 'ConnectionAbortedError'>, <class 'ConnectionError'>, <class 'ConnectionRefusedError'>, <class 'ConnectionResetError'>, <class 'DeprecationWarning'>, <class 'EOFError'>, <class 'EncodingWarning'>, <class 'OSError'>, <class 'Exception'>, <class 'ExceptionGroup'>, <class 'FileExistsError'>, <class 'FileNotFoundError'>, <class 'FloatingPointError'>, <class 'FutureWarning'>, <class 'GeneratorExit'>, <class 'OSError'>, <class 'ImportError'>, <class 'ImportWarning'>, <class 'IndentationError'>, <class 'IndexError'>, <class 'InterruptedError'>, <class 'IsADirectoryError'>, <class 'KeyError'>, <class 'KeyboardInterrupt'>, <class 'LookupError'>, <class 'MemoryError'>, <class 'ModuleNotFoundError'>, <class 'NameError'>, <class 'NotADirectoryError'>, <class 'NotImplementedError'>, <class 'OSError'>, <class 'OverflowError'>, <class 'PendingDeprecationWarning'>, <class 'PermissionError'>, <class 'ProcessLookupError'>, <class 'RecursionError'>, <class 'ReferenceError'>, <class 'ResourceWarning'>, <class 'RuntimeError'>, <class 'RuntimeWarning'>, <class 'StopAsyncIteration'>, <class 'StopIteration'>, <class 'SyntaxError'>, <class 'SyntaxWarning'>, <class 'SystemError'>, <class 'SystemExit'>, <class 'TabError'>, <class 'TimeoutError'>, <class 'TypeError'>, <class 'UnboundLocalError'>, <class 'UnicodeDecodeError'>, <class 'UnicodeEncodeError'>, <class 'UnicodeError'>, <class 'UnicodeTranslateError'>, <class 'UnicodeWarning'>, <class 'UserWarning'>, <class 'ValueError'>, <class 'Warning'>, <class 'OSError'>, <class 'ZeroDivisionError'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'bool'>, <class 'bytearray'>, <class 'bytes'>, <class 'classmethod'>, <class 'complex'>, <class 'dict'>, <class 'enumerate'>, <class 'filter'>, <class 'float'>, <class 'frozenset'>, <class 'int'>, <class 'list'>, <class 'map'>, <class 'memoryview'>, <class 'object'>, <class 'property'>, <class 'range'>, <class 'reversed'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'str'>, <class 'super'>, <class 'tuple'>, <class 'type'>, <class 'zip'>] |
现在就是找可以利用的类了,继续回到我们的constructor.py中,
进一步审计 construct_python_object_apply()时,存在一个漏洞点
果listitems不为空,则调用instance的extend()方法,将listitems中的所有元素添加到instance列表对象的末尾,instance是我们创建的实例,这里并没有定义listitems是什么,如果我们的listitems是一个字典,而且其内容有”{‘extend’:function}”,这样的话,我们就可以利用extend进行任意函数的执行了。
但是,经过测试,我发现上面 builtins 中的这些类中,只有frozenset,bytes,tuple这三个类可以进行命令执行,为了搞清楚为什么,我们继续审计源码。
我们接着看 make_python_instance()
这里只要我们的内置类可以执行cls.__new__(cls, *args, **kwds)
这段代码,就可以根据参数来动态创建新的Python对象,从而进行命令执行。
所以猜测frozenset,bytes,tuple应该是可以执行上述代码,所以可以进行命令执行的
测试payload:
1 | !!python/object/new:frozenset |
1 | !!python/object/new:tuple |
再附上一些网上存在的 payload
1 | #报错但是执行了 |
1 | - !!python/object/new:yaml.MappingNode |
1 | #创建了一个类型为z的新对象,而对象中extend属性在创建时会被调用,参数为listitems内的参数 |
PS:
当利用上述payload测试到5.2b1版本时,发现无法利用针对!!python/object/apply标签的payload了,别的都可以利用
上述payload当我测试到5.4b1版本时,发现只有利用针对!!python/name标签
的payload可以命令执行,别的payload都不能用了
当版本大于等于5.3.1:https://github.com/yaml/pyyaml/pull/386
当版本大于等于6.0: https://github.com/yaml/pyyaml/pull/386
3. 利用ruamel.yaml读写yaml文件
在网上搜资料的时候,Evi1s7 师傅发现了也可以利用 ruamel.yaml 读写 yaml 文件,进行测试一下。
这里还是沿用 PyYaml>5.1 的 poc
1 | import ruamel.yaml |
虽然回显报错,但是仍然可以执行,所以利用ruamel.yaml读写yaml文件也是存在上述的漏洞的,这里就不再测试。
小结
入职后实在是没精力写这些文章了,时间太碎了,后续或许有时间会再回过头来看看
Ref
https://xz.aliyun.com/t/12481
https://www.tr0y.wang/2022/06/06/SecMap-unserialize-pyyaml/
- 本文标题:Python yaml 反序列化
- 创建时间:2023-06-02 16:29:18
- 本文链接:2023/06/02/Python-yaml-反序列化/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!