# REPRO-2026-00167: DataEase: Quartz JobStore Java deserialization RCE via QRTZ_JOB_DETAILS ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00167 CVE: CVE-2026-40901 ## Package Name: dataease Ecosystem: github Affected: <= v2.10.20 Fixed: v2.10.21 ## Root Cause # RCA Report: CVE-2026-40901 ## Summary CVE-2026-40901 is a deserialization remote code execution (RCE) vulnerability in DataEase's Quartz scheduler integration. DataEase v2.10.20 ships `commons-collections-3.2.1.jar` inside its Spring Boot application JAR (`app.jar`). The Quartz JDBC JobStore persists `JobDataMap` objects as serialized BLOBs in the `QRTZ_JOB_DETAILS.JOB_DATA` column. When the Quartz scheduler polls for triggers, it calls `StdJDBCDelegate.getObjectFromBlob()`, which uses a raw `ObjectInputStream.readObject()` on the attacker-controlled BLOB. With `commons-collections-3.2.1.jar` on the classpath, a `ysoserial` CommonsCollections6 payload can trigger `Runtime.exec()` during deserialization, achieving RCE as the DataEase process user (root in the default container image). DataEase v2.10.21 fixed this by removing `commons-collections-3.2.1.jar` from the application JAR. ## Impact - **Package/component affected**: DataEase community edition Quartz scheduler (`core-backend` module) - **Affected versions**: DataEase community edition ≤ v2.10.20 - **Fixed versions**: v2.10.21 - **Risk level**: High (CVSS 3.1: 8.8) - **Consequences**: An attacker with write access to the `QRTZ_JOB_DETAILS` table (obtainable via the stacked SQL injection in CVE-2026-40900) can achieve arbitrary code execution inside the DataEase container as root by injecting a malicious serialized Java object into the `JOB_DATA` BLOB. The next time Quartz fires the trigger, the payload is deserialized and the gadget chain executes. ## Root Cause DataEase uses the Quartz scheduler with a JDBC JobStore backend. Quartz stores each job's `JobDataMap` in the `QRTZ_JOB_DETAILS.JOB_DATA` column as a Java-serialized BLOB. The scheduler thread deserializes this BLOB every time it acquires triggers using `StdJDBCDelegate.getObjectFromBlob()`: ```java ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); return ois.readObject(); // No class allowlist, no deserialization filter ``` DataEase v2.10.20's `app.jar` includes `commons-collections-3.2.1.jar`, which contains the `InvokerTransformer` class. This class is the core of the well-known CommonsCollections gadget chain. The `ysoserial` CommonsCollections6 payload creates a `HashSet` containing a `TiedMapEntry` wrapping a `LazyMap` with an `InvokerTransformer`. During `ObjectInputStream.readObject()`, the `HashSet.readObject()` method calls `hashCode()` on all elements. For `TiedMapEntry`, `hashCode()` triggers `getValue()` → `LazyMap.get()` → `InvokerTransformer.transform()` → `Runtime.exec()`. **Fix in v2.10.21**: The `commons-collections-3.2.1.jar` dependency was removed from the application JAR. Without this JAR on the classpath, the `TiedMapEntry` class cannot be resolved during deserialization, causing a `ClassNotFoundException` and safely aborting the attack before the gadget chain can execute. ## Reproduction Steps The reproduction is orchestrated by `repro/reproduction_steps.sh`, which: 1. Generates a `ysoserial` CommonsCollections6 payload that executes `touch /tmp/pruva-cve-2026-40901.txt` 2. Starts the actual DataEase v2.10.20 Docker container with a MySQL backend 3. Waits for DataEase and Quartz to fully initialize (Flyway migrations + scheduler startup) 4. Injects the payload into `QRTZ_JOB_DETAILS.JOB_DATA` via MySQL UPDATE 5. Waits for the Quartz `Datasource` trigger to fire (every 6 minutes) 6. Verifies that `/tmp/pruva-cve-2026-40901.txt` was created inside the running DataEase container 7. Repeats the same payload against a v2.10.21 container classpath, verifying `ClassNotFoundException` for `TiedMapEntry` **Expected evidence**: - Vulnerable v2.10.20: Marker file `/tmp/pruva-cve-2026-40901.txt` exists inside the container; Quartz logs show `JobPersistenceException: Couldn't acquire next trigger: class java.util.HashSet cannot be cast to class java.util.Map` - Fixed v2.10.21: Marker file is NOT created; logs show `ClassNotFoundException: org.apache.commons.collections.keyvalue.TiedMapEntry` ## Evidence ### Classpath Analysis - `repro/evidence/classpath-vulnerable.log`: Shows `BOOT-INF/lib/commons-collections-3.2.1.jar` present in v2.10.20 `app.jar` - `repro/evidence/classpath-fixed.log`: Shows `commons-collections-3.2.1.jar` absent in v2.10.21 `app.jar` - `repro/evidence/cc3-vulnerable.count`: `1` (commons-collections 3.x present) - `repro/evidence/cc3-fixed.count`: `0` (commons-collections 3.x absent) ### Vulnerable Service Test - `repro/evidence/marker-vulnerable.txt`: Empty file created by `touch` inside the running DataEase v2.10.20 container, proving `Runtime.exec()` executed during Quartz deserialization - `repro/evidence/vulnerable-service-container.log`: Full DataEase container logs showing initialization and Quartz activity - `repro/evidence/vulnerable-quartz-evidence.log`: Key log excerpt: ``` org.quartz.JobPersistenceException: Couldn't acquire next trigger: class java.util.HashSet cannot be cast to class java.util.Map (java.util.HashSet and java.util.Map are in module java.base of loader 'bootstrap') Caused by: java.lang.ClassCastException: class java.util.HashSet cannot be cast to class java.util.Map ``` This proves Quartz's `StdJDBCDelegate.getObjectFromBlob()` deserialized the CC6 payload (a `HashSet`), the gadget chain fired (`touch` executed), and then the subsequent cast to `Map` failed. ### Fixed Classpath Test - `repro/evidence/fixed-container-test.log`: Shows `ClassNotFoundException: org.apache.commons.collections.keyvalue.TiedMapEntry` when the same payload is deserialized with the v2.10.21 classpath. The marker file is NOT created. ## Recommendations / Next Steps 1. **Upgrade to v2.10.21 or later**: The fix removes the vulnerable `commons-collections-3.2.1.jar` dependency. 2. **Add deserialization allowlist/filter**: Even with the dependency removed, consider adding an `ObjectInputFilter` to the `ObjectInputStream` used by Quartz's `StdJDBCDelegate.getObjectFromBlob()` to prevent future gadget chains. 3. **Input validation on QRTZ_JOB_DETAILS**: Restrict direct database write access to Quartz tables. The vulnerability is typically chained from CVE-2026-40900 (stacked SQL injection), so patching the SQL injection entrypoint is also critical. 4. **Regression test**: After upgrade, verify that `commons-collections-3.2.1.jar` is not present in the `app.jar` (e.g., `jar tf app.jar | grep commons-collections-3`). ## Additional Notes - **Idempotency**: The reproduction script is idempotent — it cleans up all containers and networks before each run. Running it twice produces the same results. - **Environment**: Tests were performed using the official DataEase Docker images (`registry.cn-qingdao.aliyuncs.com/dataease/dataease:v2.10.20` and `v2.10.21`) with MySQL 8.4.5. - **Limitations**: The full service test for the vulnerable version requires ~7 minutes (DataEase initialization + Quartz trigger wait). The fixed version test uses a classpath-level deserialization harness for speed, but both tests use the actual DataEase application JAR and the same `ObjectInputStream.readObject()` code path that Quartz uses. - **No reverse shells**: The reproduction uses only a benign `touch` command as evidence. No network egress is required. ## Reproduction Details Reproduced: 2026-05-25T20:45:48.508Z Duration: 10750 seconds Tool calls: 555 Turns: 499 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00167 pruva-verify CVE-2026-40901 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00167&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00167/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-40901 ## Artifacts - bundle/repro/rca_report.md (analysis, 7277 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 11602 bytes) - bundle/context.json (other, 3352 bytes) - bundle/metadata.json (other, 941 bytes) - bundle/ticket.md (ticket, 5670 bytes) - bundle/repro/validation_verdict.json (other, 274 bytes) - bundle/repro/evidence/fixed-container-test.log (log, 129 bytes) - bundle/repro/evidence/vulnerable-service-container.log (log, 131204 bytes) - bundle/repro/evidence/vulnerable-service-test.log (log, 131206 bytes) - bundle/repro/evidence/runtime_manifest.json (other, 1389 bytes) - bundle/repro/evidence/validation_verdict.json (other, 274 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00167 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00167/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00167 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev