YAML是一种直观的能够被电脑识别的的数据序列化格式,容易被人类阅读,并且容易和脚本语言交互,通常运用在一些数据代码分离场合:用于配置文件,但也用于数据存储(例如调试输出)或传输(例如文档标题)。YAML 的配置文件后缀为 .yml或.yaml
1、大小写敏感;
2、使用缩进表示层级关系,缩进只可以使用空格,不允许使用tab,遵守左对齐即可;
3、列表中项通过“-”表示,字典中的通过“:”表示;
4、# 表示注释,和python表示注释一样。
使用PyYAML>=5.1显示的结果
yaml.dump():将一个Python对象序列化生成为yaml文档。
yaml.load():将一个yaml文档反序列化为一个Python对象。
可以看到,User对象经过yaml序列化之后内容为一行字符串,简单解释一下:“!!python/object”为yaml标签,yaml.load()会识别该标签并调用相应的方法执行反序列化操作;冒号后面的“__main__”为py文件名,这里为本文件的意思;“User”为序列化的对象类型,后面紧跟的大括号即为该对象的属性及其属性值。
这里使用PyYAML==4.2b4进程测试,PyYAML历史版本可以参考:
https://pypi.org/project/PyYAML/#history
在PyYAML 5.1版本之前我们有以下不安全的反序列化方法:
这里编写简单的Demo,一个py文件用于将恶意类序列化为字符串保存到yaml文件中,另一个py文件用于反序列化yaml文件内容为恶意类对象从而达到利用反序列化漏洞的目的。
先创建一个poc对象再调用yaml.dump()将其序列化为一个字符串,其中第9行代码为将默认的“__main__”替换为该文件名“yaml_test”,目的是为了后面yaml.load()反序列化该字符串的时候会根据yaml文件中的指引去读取yaml_ test.py中的poc这个类。
simple.yml文件内容如下所示:
通过yaml.load()读取目标yaml文件,之后"!!python/object"标签解析其中的名为yaml_test的module中的poc类,最后执行了该类对象的init()方法从而执行了命令。
有如下几种类型:
(1)BaseLoader:仅加载最基本的YAML;
(2)SafeLoader:安全地加载YAML语言的子集,建议用于加载不受信任的输入(safe_load);
(3)Loader:可以通过不受信任的数据输入轻松利用。
PyYAML<5.1默认的加载器使用的是Loader,调试一下。
跟踪到Constructor,可以得到其针对Python语言特有的标签解析的处理函数对应列表:
(1)!!python/object: => Constructor.construct_python_object;
(2)!!python/object/apply: => Constructor.construct_python_object_apply;
(3)!!python/object/new: => Constructor.construct_python_object_new。
1、从上面的代码中可以看到" !!python/object/new " 标签的代码实现其实就是" !!python/object/apply "标签的代码实现,只是最后newobj参数值不同而已
2、查看官方文档,!!python/object标签的使用格式和另外两个根本就是两码事,其接收参数是使用大括号{}而非中括号[],且并没有对参数args进行接收。也就是说,!!python/object标签只针对于对象类进行使用。
那么对应的!!python/object/new和!!python/object/apply标签的payload可以写成:
!!python/object/apply:os.system ["calc.exe"]
!!python/object/new:os.system ["calc.exe"]
这3个Python标签中都是调用了make_python_instance()函数
还以刚才simple.yml文件内容为例
那我们的payload就可以写成如下图所示的样子
回来看make_python_instance()函数
接下来查看find_python_name()函数
可见最后返回值结果为<class ‘yaml.poc’>即代表yaml.py文件(模块)中的poc类
回到make_python_instance()函数中,cls值就是<class ‘yaml.poc’>,如下图所示,最后返回结果就是恶意poc类生成的对象
这里使用的是PyYAML5.2版本
有如下几种类型(多了两个):
(1)BaseLoader:仅加载最基本的YAML
(2)FullLoader:加载完整的YAML语言,避免任意代码执行,默认加载器
(3)SafeLoader:安全地加载YAML语言的子集,建议用于加载不受信任的输入(safe_load)
(4)Loader:可以通过不受信任的数据输入轻松利用
(5)UnsafeLoader(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)
在5.1之后,使用load()进行序列化操作时我们需要在方法里面加一个loader的请求参数,直接使用load请求时会显示以下warning,默认FullLoader
不想显示warning加上Loader参数即可。
在PyYAML>=5.1版本中,提供了以下不安全的反序列化方法:
还是以之前在小于5.1的Demo为例,在YAML 5.2版本中使用之前的Payload发现已无法实现RCE了,如下图
因为YAML5.2版本默认使用加载器FullLoader,下面就来分析一下
使用的是FullConstructor
没有加载yaml_test文件(模块),所以会报错,如下图
若使用Loader加载器的话,还是可以实现RCE了,如下图
那么下面就分析一下,PyYAML5.2版本中Loader加载器为什么实现RCE,跟踪到Constructor
继承了UnsafeConstructor类
查看UnsafeConstructor类,发现其继承了FullConstructor类,使用super()函数调用父类FullConstructor的方法,可以观察到find_python_name()函数的参数unsafe值变为True,而我们在分析FullLoader时,若这个参数值为True,就会执行__import__(module_name)代码从而加载yaml_test文件(模块)
如下图,这样就不会报错
查看make_python_instance()函数,也避免了报错。
加载器FullLoader与Loader不同之处就在于参数unsafe的值是不同的!
1、在这里,着重演示了yaml模块中load()函数中加载器的使用与实现恶意对象的分析过程
(1)其中包括如下函数(调用过程):
construct_python_object()
----make_python_instance()
--------find_python_name()
(2)先执行find_python_name()函数再到make_python_instance()函数最后到construct_python_object()函数
(3)重点在于是否导入了自定义的模块,即在find_python_name()函数中,__import__()函数:__import__()函数用于动态加载类和函数,若一个模块经常变化就可以使用__import__()来动态载入。
(4)sys.modules是一个全局字典,该字典是python启动后就加载在内存中,每当导入新的模块,sys.modules都将记录这些模块。
2、最后说一下在处理YAML数据过程中的防御策略。
1、要序列化数据,可以使用下面的安全函数:
2、要反序列化数据,可以使用下面的安全函数: