{"repro_id":"REPRO-2026-00167","version":7,"title":"DataEase: Quartz JobStore Java deserialization RCE via QRTZ_JOB_DETAILS","repro_type":"security","status":"published","severity":"high","description":"DataEase uses the Quartz scheduler with the **JDBC JobStore** backend so its\nperiodic tasks (refresh datasets, send digest emails, run maintenance) survive\nrestarts. Quartz persists each job's `JobDataMap` in the\n`qrtz_job_details.JOB_DATA` column as a Java-serialized blob and calls\n`ObjectInputStream.readObject()` on that blob every time the trigger fires.\n\nThe DataEase classpath ships a version of `commons-collections` that contains\nthe well-known transformer-based gadget chain. The only thing standing\nbetween an attacker and code execution is the table contents — and the\npreceding step in the chain (CVE-2026-40900, stacked SQL injection) hands\nthat to them. With write access to a single row of `qrtz_job_details`, the\nnext scheduler tick deserializes the attacker's payload and runs whatever\nmethod chain it encodes in the JVM running the DataEase backend.\n\nThis is the terminal step of the 4-CVE chain (auth bypass → blocklist bypass\n→ stacked SQLi → **Quartz deserialization RCE**). The end result is code\nexecution inside the DataEase container as the DataEase process user (root,\nin the default container image).","root_cause":"# RCA Report: CVE-2026-40901\n\n## Summary\n\nCVE-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.\n\n## Impact\n\n- **Package/component affected**: DataEase community edition Quartz scheduler (`core-backend` module)\n- **Affected versions**: DataEase community edition ≤ v2.10.20\n- **Fixed versions**: v2.10.21\n- **Risk level**: High (CVSS 3.1: 8.8)\n- **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.\n\n## Root Cause\n\nDataEase 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()`:\n\n```java\nObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));\nreturn ois.readObject();  // No class allowlist, no deserialization filter\n```\n\nDataEase 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()`.\n\n**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.\n\n## Reproduction Steps\n\nThe reproduction is orchestrated by `repro/reproduction_steps.sh`, which:\n\n1. Generates a `ysoserial` CommonsCollections6 payload that executes `touch /tmp/pruva-cve-2026-40901.txt`\n2. Starts the actual DataEase v2.10.20 Docker container with a MySQL backend\n3. Waits for DataEase and Quartz to fully initialize (Flyway migrations + scheduler startup)\n4. Injects the payload into `QRTZ_JOB_DETAILS.JOB_DATA` via MySQL UPDATE\n5. Waits for the Quartz `Datasource` trigger to fire (every 6 minutes)\n6. Verifies that `/tmp/pruva-cve-2026-40901.txt` was created inside the running DataEase container\n7. Repeats the same payload against a v2.10.21 container classpath, verifying `ClassNotFoundException` for `TiedMapEntry`\n\n**Expected evidence**:\n- 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`\n- Fixed v2.10.21: Marker file is NOT created; logs show `ClassNotFoundException: org.apache.commons.collections.keyvalue.TiedMapEntry`\n\n## Evidence\n\n### Classpath Analysis\n- `repro/evidence/classpath-vulnerable.log`: Shows `BOOT-INF/lib/commons-collections-3.2.1.jar` present in v2.10.20 `app.jar`\n- `repro/evidence/classpath-fixed.log`: Shows `commons-collections-3.2.1.jar` absent in v2.10.21 `app.jar`\n- `repro/evidence/cc3-vulnerable.count`: `1` (commons-collections 3.x present)\n- `repro/evidence/cc3-fixed.count`: `0` (commons-collections 3.x absent)\n\n### Vulnerable Service Test\n- `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\n- `repro/evidence/vulnerable-service-container.log`: Full DataEase container logs showing initialization and Quartz activity\n- `repro/evidence/vulnerable-quartz-evidence.log`: Key log excerpt:\n  ```\n  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')\n  Caused by: java.lang.ClassCastException: class java.util.HashSet cannot be cast to class java.util.Map\n  ```\n  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.\n\n### Fixed Classpath Test\n- `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.\n\n## Recommendations / Next Steps\n\n1. **Upgrade to v2.10.21 or later**: The fix removes the vulnerable `commons-collections-3.2.1.jar` dependency.\n2. **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.\n3. **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.\n4. **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`).\n\n## Additional Notes\n\n- **Idempotency**: The reproduction script is idempotent — it cleans up all containers and networks before each run. Running it twice produces the same results.\n- **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.\n- **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.\n- **No reverse shells**: The reproduction uses only a benign `touch` command as evidence. No network egress is required.\n","cve_id":"CVE-2026-40901","cwe_id":"CWE-502 (Deserialization of Untrusted Data)","package":{"name":"dataease","ecosystem":"github","affected_versions":"<= v2.10.20","fixed_version":"v2.10.21","tested_patched":"v2.10.21"},"reproduced_at":"2026-05-25T20:45:48.508975+00:00","duration_secs":10749.96726489067,"tool_calls":555,"turns":499,"handoffs":2,"total_cost_usd":10.968809329999996,"agent_costs":{"repro":10.835600179999997,"support":0.13320915},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":10.835600179999997},"support":{"accounts/fireworks/models/kimi-k2p6":0.13320915}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-25T20:45:50.815693+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":7277,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":11602,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":3352,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":941,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":5670,"category":"ticket"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":274,"category":"other"},{"path":"bundle/repro/evidence/fixed-container-test.log","filename":"fixed-container-test.log","size":129,"category":"log"},{"path":"bundle/repro/evidence/vulnerable-service-container.log","filename":"vulnerable-service-container.log","size":131204,"category":"log"},{"path":"bundle/repro/evidence/vulnerable-service-test.log","filename":"vulnerable-service-test.log","size":131206,"category":"log"},{"path":"bundle/repro/evidence/runtime_manifest.json","filename":"runtime_manifest.json","size":1389,"category":"other"},{"path":"bundle/repro/evidence/validation_verdict.json","filename":"validation_verdict.json","size":274,"category":"other"}]}