# CWE-502 Unfiltered Java Deserialization RCE in DL4J `MapDBStatsStorage` via a malicious MapDB (`.db`) StatsStorage file **Target:** Eclipse Deeplearning4j — `deeplearning4j-ui-model` **Artifact:** `org.deeplearning4j:deeplearning4j-ui-model:1.0.0-M2.1` (released to Maven Central, Aug 12 2022 — latest release) **Vulnerable classes:** `org.deeplearning4j.ui.model.storage.mapdb.MapDBStatsStorage$SessionTypeWorkerIdSerializer` and `$SessionTypeIdSerializer` **Sink file:** `MapDBStatsStorage.java:266-270` (and `288-292`) **Vulnerability class:** CWE-502 Deserialization of Untrusted Data → Remote Code Execution **Trigger:** Victim opens/reads an attacker-supplied MapDB `.db` StatsStorage file through the public DL4J UI API. --- ## Summary `MapDBStatsStorage` is DL4J's on-disk (MapDB-backed) implementation of the training-UI `StatsStorage` interface. Its constructor `new MapDBStatsStorage(File)` opens **any** attacker-supplied `.db` file, and it registers two **custom MapDB key serializers** for its on-disk `HTreeMap`s: - `SessionTypeWorkerIdSerializer` — key serializer for the `"staticInfo"` `HTreeMap` - `SessionTypeIdSerializer` — key serializer for the `"storageMetaData"` `HTreeMap` Both `deserialize()` methods perform: ```java ObjectInputStream in2 = new ObjectInputStream(new DataInput2.DataInputToStream(in)); return (SessionTypeWorkerId) in2.readObject(); ``` with **no `ObjectInputFilter`, no allow-list, and no class validation**. The bytes fed to `readObject()` are the raw stored map-key bytes read straight out of the `.db` (MapDB) file. Because the `(SessionTypeWorkerId)` / `(SessionTypeId)` cast happens **after** `readObject()` returns, any `Serializable` gadget's `readObject()` executes **before** the cast is even attempted — so the `ClassCastException` is post-exploitation noise, not a defense. Any public read accessor that iterates a map's `keySet()` forces MapDB to deserialize every stored key via the vulnerable serializer: `getAllStaticInfos`, `listTypeIDsForSession`, `listWorkerIDsForSession`, `listWorkerIDsForSessionAndType`, and `checkStorageEvents` (called on every `put*`). Opening the file plus one ordinary read is enough to reach `readObject()` on attacker-controlled bytes. ## Root cause (verbatim released source) From `deeplearning4j-ui-model-1.0.0-M2.1-sources.jar`, `MapDBStatsStorage.java`: ```java //Simple serializer, based on MapDB's SerializerJava private static class SessionTypeWorkerIdSerializer implements Serializer { @Override public void serialize(@NonNull DataOutput2 out, @NonNull SessionTypeWorkerId value) throws IOException { ObjectOutputStream out2 = new ObjectOutputStream(out); out2.writeObject(value); out2.flush(); } @Override public SessionTypeWorkerId deserialize(@NonNull DataInput2 in, int available) throws IOException { try { ObjectInputStream in2 = new ObjectInputStream(new DataInput2.DataInputToStream(in)); return (SessionTypeWorkerId) in2.readObject(); // <-- UNFILTERED readObject on file bytes } catch (ClassNotFoundException e) { throw new IOException(e); } } ... } ``` `SessionTypeIdSerializer.deserialize` (lines 288-292) is identical with a `(SessionTypeId)` cast. The constructor opens the untrusted file with the exact same MapDB options an attacker can replicate: ```java DBMaker.fileDB(f).closeOnJvmShutdown().transactionEnable().make(); ``` ## Attack scenario The DL4J training UI stores/loads stats from disk. A `.db` StatsStorage file is a natural artifact to share, host, or restore (bug reports, shared experiment runs, checkpoints). A victim who opens an attacker-supplied `.db` with `new MapDBStatsStorage(file)` and performs any normal read (or any `put*`, which internally calls `checkStorageEvents`) triggers deserialization of attacker-controlled key bytes → arbitrary code execution in the victim JVM. ## Proof of Concept All dependencies are real released Maven Central artifacts: `deeplearning4j-ui-model:1.0.0-M2.1`, `deeplearning4j-core:1.0.0-M2.1`, `org.mapdb:mapdb:3.0.5` and its runtime deps (`kotlin-runtime`/`kotlin-stdlib` 1.0.7, `eclipse-collections` 7.1.2, `guava` 19.0, `lz4` 1.3.0, `elsa` 3.0.0-M5). - **`Evil.java`** — a `Serializable` gadget whose `readObject()` runs `Runtime.exec`. Stand-in for any real gadget-chain (Commons-Collections, etc.); here it self-demonstrates execution timing (runs during `readObject`, before the cast). - **`MakeMalicious.java`** — opens a MapDB `fileDB` with the **exact** `DBMaker` options the victim constructor uses, and writes the `"staticInfo"` `HTreeMap` with a key serializer that emits a serialized `Evil` gadget as the key bytes. - **`Victim.java`** — public API only: `new MapDBStatsStorage(new File("malicious.db"))` then `getAllStaticInfos("s","t")`. - **`MakeBenign.java`** — negative control: identical structure but a benign `String` key. ### Malicious run (RCE) ``` $ java -cp Victim verify_mal.db [Victim] new MapDBStatsStorage(new File("verify_mal.db")) [Victim] sessions=[] [Victim] calling getAllStaticInfos("s","t") -> iterates staticInfo.keySet() [Evil.readObject] EXECUTED -> `id; echo PWNED_MAPDB_VERIFY_1929118783` output: uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),102(scanner),118(wireshark),119(kaboxer),982(bluetooth),999(lpadmin) PWNED_MAPDB_VERIFY_1929118783 Exception in thread "main" java.lang.ClassCastException: class Evil cannot be cast to class org.deeplearning4j.ui.model.storage.BaseCollectionStatsStorage$SessionTypeWorkerId at org.deeplearning4j.ui.model.storage.mapdb.MapDBStatsStorage$SessionTypeWorkerIdSerializer.deserialize(MapDBStatsStorage.java:269) at org.deeplearning4j.ui.model.storage.mapdb.MapDBStatsStorage$SessionTypeWorkerIdSerializer.deserialize(MapDBStatsStorage.java:257) at org.mapdb.HTreeMap$leafValueExternalSerializer$1.deserialize(HTreeMap.kt:228) at org.mapdb.HTreeMap$leafValueExternalSerializer$1.deserialize(HTreeMap.kt:214) at org.mapdb.StoreDirectAbstract.deserialize(StoreDirectAbstract.kt:229) at org.mapdb.StoreWAL.get(StoreWAL.kt:527) at org.mapdb.HTreeMap.leafGet(HTreeMap.kt:1374) at org.mapdb.HTreeMap$htreeIterator$1.moveToNextLeaf(HTreeMap.kt:1057) at org.mapdb.HTreeMap$htreeIterator$1.(HTreeMap.kt:1047) at org.mapdb.HTreeMap.htreeIterator(HTreeMap.kt:1037) at org.mapdb.HTreeMap$KeySet.iterator(HTreeMap.kt:973) at org.deeplearning4j.ui.model.storage.BaseCollectionStatsStorage.getAllStaticInfos(BaseCollectionStatsStorage.java:155) at Victim.main(Victim.java:13) ``` The `id` output and the unique marker `PWNED_MAPDB_VERIFY_1929118783` are printed **inside** `SessionTypeWorkerIdSerializer.deserialize` (from `readObject`) **before** the post-execution `ClassCastException`. Command execution completed with no gadget class ever being successfully cast. ### Negative control (benign `String` key) ``` $ java -cp Victim verify_benign.db [Victim] new MapDBStatsStorage(new File("verify_benign.db")) [Victim] sessions=[] [Victim] calling getAllStaticInfos("s","t") -> iterates staticInfo.keySet() Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class org.deeplearning4j.ui.model.storage.BaseCollectionStatsStorage$SessionTypeWorkerId at org.deeplearning4j.ui.model.storage.mapdb.MapDBStatsStorage$SessionTypeWorkerIdSerializer.deserialize(MapDBStatsStorage.java:269) at org.deeplearning4j.ui.model.storage.BaseCollectionStatsStorage.getAllStaticInfos(BaseCollectionStatsStorage.java:155) at Victim.main(Victim.java:13) ``` No `[Evil.readObject] EXECUTED`, no `id` / marker output — only the identical post-deserialization `ClassCastException`. This proves the command execution is **gadget-driven** (the attacker's `readObject`), not an artifact of the harness or of the cast. ## Impact Remote code execution in the context of any application/user that opens an untrusted `MapDBStatsStorage` `.db` file and performs any read or write through the public API. No authentication or special configuration required; the default constructor path is vulnerable. ## Suggested fix Install a strict `ObjectInputFilter` (allow-list of only the expected `SessionTypeWorkerId` / `SessionTypeId` and their field types) on the `ObjectInputStream` in both `deserialize()` methods, or replace Java serialization of these keys with a fixed non-reflective wire format (e.g. explicit field read/write via `DataInput2`/`DataOutput2`), matching the guidance already applied to other DL4J deserialization sinks. ## Deduplication note - Distinct from the sibling **`J7FileStatsStorage`** SQLite deserialization sink (different storage backend — SQLite vs MapDB, different class, different on-disk format; there the `readObject` lives in a direct BLOB read, here it lives inside a MapDB `HTreeMap` **key serializer**). - Distinct from the DL4J `ModelSerializer`, SameDiff, WordVector, and Arbiter deserialization findings (different classes, entry points, and file formats). - No CVE currently assigned to `MapDBStatsStorage`. The class is present in the latest released jar (`1.0.0-M2.1`) — confirmed via `unzip -l`: `org/deeplearning4j/ui/model/storage/mapdb/MapDBStatsStorage$SessionTypeWorkerIdSerializer.class` and `$SessionTypeIdSerializer.class`. ## Reproduction files `Evil.java`, `MakeMalicious.java`, `MakeBenign.java`, `Victim.java` are included in this repo. Compile against the released jars listed above and run `MakeMalicious` then `Victim` (malicious) and `MakeBenign` then `Victim` (negative control).