# 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.
