# Eclipse Deeplearning4j (.zip model) — unsafe Java deserialization (CWE-502) in `ModelSerializer` restore path: loading a malicious model executes attacker-controlled `ObjectInputStream.readObject` ## Summary Eclipse **Deeplearning4j** model files are `.zip` archives loaded with `org.deeplearning4j.util.ModelSerializer.restoreMultiLayerNetwork(...)` / `restoreComputationGraph(...)`. On the **standard, default load path**, the restorer reads the archive entry **`preprocessor.bin`** and passes its bytes straight to **`new ObjectInputStream(...).readObject()`** with **no `ObjectInputFilter`, no class allowlist**. A malicious model `.zip` therefore drives **Java native deserialization of fully attacker-controlled bytes** the moment a victim loads it — a textbook **CWE-502** untrusted-deserialization sink, executing attacker code inside `readObject` (demonstrated below). Neither picklescan nor modelscan inspects Java-serialized data, so the malicious model passes both scanners. ## Affected / verified versions - **deeplearning4j-nn / deeplearning4j-core 1.0.0-M2.1** (latest published release, Jan 2023) on **OpenJDK 21** - Sink confirmed **identical and unpatched on `master`** (the restore helpers still call `ObjectInputStream.readObject` on `preprocessor.bin`) ## Root cause `ModelSerializer.class` (bytecode, both `restoreMultiLayerNetwork` and `restoreComputationGraph` helpers): ``` 319: ldc "preprocessor.bin" 347: new java/io/ObjectInputStream 353: invokespecial java/io/ObjectInputStream.(InputStream) 360: invokevirtual java/io/ObjectInputStream.readObject() // <-- unfiltered deser of attacker bytes ``` The archive member `preprocessor.bin` is deserialized via a raw `ObjectInputStream` with no `setObjectInputFilter`, no whitelist, no validation. This runs on **every** standard restore call, **independent of the `loadUpdater` flag** (only `updaterState.bin` is flag-gated, and it goes through `Nd4j.read`, not `readObject`). Java deserialization runs the graph's `readObject`/`readResolve`/finalize chains during reconstruction, so attacker-reachable gadget classes execute code **before** any type check on the result. ## Proof of concept `poc/` contains the malicious model, a benign control, and the Java harness. - `poc/malicious_model.zip` — a valid DL4J model (`configuration.json` + `coefficients.bin`) with one **added** `preprocessor.bin` holding a serialized object whose `readObject` runs `Runtime.exec`. - `poc/Evil.java` — the demonstration gadget class (its `readObject` executes a command and writes a marker). - `poc/Load.java` — `ModelSerializer.restoreMultiLayerNetwork(new File(argv[0]), true)` (the standard API call). - `poc/benign_control.zip` — same model **without** `preprocessor.bin`. ### Observed (reproduced) ``` $ java -cp out:libs/* Load malicious_model.zip MARKER_DL4J_DESER_7Q3X note=served-as-preprocessor.bin whoami=uid=1002(hacnho) gid=1002(hacnho) groups=...,docker,kvm Exception in thread "main" java.lang.ClassCastException: class Evil cannot be cast to ...DataSetPreProcessor ``` The command executed **inside `readObject`** (marker written with the running uid) **before** the cosmetic `ClassCastException` — i.e. attacker code runs to completion during deserialization; the cast failure afterward is irrelevant. A real gadget chain returns a value of the expected type (or the exception is simply swallowed by the caller) and leaves no trace. **Control:** `java -cp out:libs/* Load benign_control.zip` → `LOADED_OK params=23`, exit 0, no marker — isolating the sink to the attacker-added `preprocessor.bin`. ## Impact Untrusted Java deserialization (CWE-502) on the default model-load path → **remote code execution** when a victim loads a malicious DL4J `.zip` model. This is the Java analog of the untrusted-pickle RCE class: the model file is attacker-controlled (the MFV threat model), and loading it deserializes attacker bytes through an unfiltered `ObjectInputStream`. **Honest scope on the gadget precondition:** full host RCE requires a Serializable gadget chain reachable on the victim's classpath. The bare `deeplearning4j-core` dependency set ships `commons-collections4-4.1`, whose `InvokerTransformer` is **not** `Serializable` without `-Dorg.apache.commons.collections4.enableUnsafeSerialization=true`, and no other classic serializable-gadget library (commons-collections 3.x, commons-beanutils, c3p0, Spring, groovy, …) is in the default 172-jar set; JDK-21 serialization filters neutralize the JRE-internal chains. So on a **bare** library install the demonstrated impact is an **arbitrary-deserialization primitive** (CWE-502) that executes attacker code when a gadget is present (shown above with a class on the classpath). In practice DL4J is embedded in JVM applications/serving stacks that routinely pull gadget-bearing libraries (Spring, c3p0, commons-beanutils, etc.), making the precondition commonly satisfiable → RCE. The vulnerability is the unfiltered `readObject` sink itself; gadget availability is environmental and continually growing. ## Remediation - Wrap the `preprocessor.bin` (and any other `readObject`) deserialization in a strict `ObjectInputFilter` allowlisting only the expected `org.nd4j…DataSetPreProcessor` types (and primitives), or replace native Java serialization with a safe format. - Reject/serialize preprocessors via a schema-validated, non-`ObjectInputStream` mechanism. ## Dedup / novelty - **No CVE / GHSA** for `ModelSerializer` Java deserialization at any version; **no huntr per-repo report** (`huntr.com/repos/…/deeplearning4j` shows only generic NPE/EOF bug reports). The sink is present and unpatched on `master`. - Distinct from the other model-deserialization CWEs (pickle, etc.) — this is **Java native** `ObjectInputStream` on a `.zip` DL4J model, an in-scope huntr MFV format (`DL4J – Eclipse`). - Scanner blind spot (bonus): `modelscan` → "No issues found"; `picklescan` → "Scanned files: 0" — neither understands Java `ObjectOutputStream` data. ## Artifacts - `poc/malicious_model.zip`, `poc/benign_control.zip`, `poc/Evil.java`, `poc/Load.java`, `poc/MakeEvil2.java`.