File size: 6,199 Bytes
b28a9c7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | # 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.<init>(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`.
|