YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
CWE-502 Unsafe Java Deserialization RCE in DL4J J7FileStatsStorage via the SQLite StatsStorage (.sqlite) file format
Summary
org.deeplearning4j.ui.model.storage.sqlite.J7FileStatsStorage reads Persistable
objects out of an on-disk SQLite database by pulling a raw BLOB column
(ObjectBytes) and passing it directly to an unfiltered
ObjectInputStream.readObject(). Because the BLOB lives inside the .sqlite
file the victim opens, its contents are fully attacker-controlled. Any
Serializable gadget on the classpath executes its readObject() during a
plain SELECT, yielding remote code execution the moment an application opens or
re-opens a StatsStorage database.
- Target (verified): Maven Central
org.deeplearning4j:deeplearning4j-ui-model:1.0.0-M2.1(latest released version). Classorg/deeplearning4j/ui/model/storage/sqlite/J7FileStatsStorage.classconfirmed present in the released jar. - Weakness: CWE-502 Deserialization of Untrusted Data β RCE
- Sink:
J7FileStatsStorage.deserialize(byte[])βJ7FileStatsStorage.java:132-138 - Reachable from public API: constructor
new J7FileStatsStorage(File)+getStaticInfo/getAllStaticInfos/getUpdate/getUpdates/getStorageMetaData
Root cause
The private deserialize helper (from the released -sources.jar):
// J7FileStatsStorage.java:132-138
private static <T> T deserialize(byte[] bytes) {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
There is no ObjectInputFilter, no allow-list, no class validation. The
bytes come straight from a BLOB column read out of the SQLite file:
private <T> T queryAndGet(String sql, int columnIndex) {
...
ResultSet rs = statement.executeQuery(sql);
...
byte[] bytes = rs.getBytes(columnIndex);
return deserialize(bytes); // attacker-controlled bytes -> readObject()
}
queryAndGet(...) -> deserialize(...) is invoked by every StatsStorage read
accessor (getStaticInfo, getAllStaticInfos, getUpdate, getUpdates,
getStorageMetaData). The (T) cast happens after readObject() returns,
so the ClassCastException that a non-Persistable gadget triggers is
post-execution β the gadget's readObject() has already run by then. The
constructor new J7FileStatsStorage(File) opens any attacker-supplied file via
DriverManager.getConnection("jdbc:sqlite:..."), so no additional trust
boundary protects the deserialization.
Attack surface
Any application that opens/loads a DL4J StatsStorage .sqlite file with
J7FileStatsStorage is exploitable:
- Re-opening a previously-saved training-UI stats database.
- A shared, downloaded, or uploaded stats artifact fed to
J7FileStatsStorage. - Any remote/untrusted
.sqlitepassed to the constructor.
No gadget beyond what the victim classpath already provides is required; the
PoC's self-contained Evil demonstrates the readObject() execution sink
directly.
PoC β verified by ACTUAL EXECUTION
Dependencies (all released Maven Central artifacts):
deeplearning4j-ui-model:1.0.0-M2.1, deeplearning4j-core:1.0.0-M2.1
(Persistable/StatsStorage interfaces), nd4j-api:1.0.0-M2.1,
nd4j-common:1.0.0-M2.1, org.xerial:sqlite-jdbc:3.36.0.3.
Harness files (poc/):
Evil.javaβSerializablegadget whosereadObject()runsRuntime.exec(id)and drops a marker file.MakeStatsDB.javaβ builds a malicious.sqlitewith the exactJ7FileStatsStorageschema (CREATE TABLE StaticInfo (... ObjectBytes BLOB ...)) and inserts a java-serializedEvilinto theObjectBytescolumn atSessionID='s', TypeID='t', WorkerID='w'. Abenignmode inserts a serializedString(negative control).Victim.javaβ uses ONLY the public API:new J7FileStatsStorage(new File(path))thengetStaticInfo("s","t","w").
Captured evidence (verbatim)
=== RUN VICTIM ON MALICIOUS ===
[Victim] new J7FileStatsStorage(new File("malicious-stats.sqlite"))
[Victim] sessions=[s]
[Victim] calling getStaticInfo("s","t","w")
[Evil.readObject] COMMAND EXECUTED -> /home/kali/hunt-workspace/dl4j-statsstorage-audit/poc/PWNED_statsstorage_16473214206027.txt
Exception in thread "main" java.lang.ClassCastException: class Evil cannot be cast to class org.deeplearning4j.core.storage.Persistable ...
at org.deeplearning4j.ui.model.storage.sqlite.J7FileStatsStorage.getStaticInfo(J7FileStatsStorage.java:436)
at Victim.main(Victim.java:14)
--- marker (PWNED_statsstorage_16473214206027.txt) ---
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)
dl4j-J7FileStatsStorage-deserialize-rce
=== NEGATIVE CONTROL: benign DB ===
[Victim] new J7FileStatsStorage(new File("benign-stats.sqlite"))
[Victim] sessions=[s]
[Victim] calling getStaticInfo("s","t","w")
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to ... Persistable
at org.deeplearning4j.ui.model.storage.sqlite.J7FileStatsStorage.getStaticInfo(J7FileStatsStorage.java:436)
--- markers: NO MARKER DROPPED (control clean) ---
Malicious run: Evil.readObject fires β id executes β marker written;
ClassCastException (Evil β Persistable) thrown after execution at
J7FileStatsStorage.java:436.
Negative control: benign String deserializes, no command runs, no
marker dropped β only the same post-cast ClassCastException (String β Persistable). Clean. This proves the RCE is attributable to the gadget's
readObject(), not to any side effect of opening the DB or the cast itself.
Suggested fix
Install a strict ObjectInputFilter / allow-list on the ObjectInputStream
(restricting to the expected Persistable/StorageMetaData implementation
types), or replace Java serialization with a schema-bound format. Never call
readObject() on bytes read from a file whose provenance is not trusted.
Dedup / prior-work note
This is a distinct sink from other DL4J deserialization issues: it is not
ModelSerializer, not the SameDiff/FlatBuffers path, not the WordVectors
loader, and not the Arbiter result reference. It is specifically the SQLite
StatsStorage BLOB read in deeplearning4j-ui-model's
J7FileStatsStorage.deserialize(byte[]), reachable through the public
StatsStorage read API. No public CVE was found for this specific class/sink at
the time of writing.