EnigmaConsultant's picture
Upload README.md with huggingface_hub
9b87e29 verified
|
Raw
History Blame Contribute Delete
4.25 kB

ModelScan H5 RCE blindspot β€” __lambda__ carrier in a non-Lambda TextVectorization preprocessing field

Severity: High (arbitrary code execution; scanner false-clean) Affected tool: modelscan (latest, H5LambdaDetectScan) β€” scanned file is a Keras .h5; victim loader is tf.keras / keras.models.load_model(safe_mode=False) (keras 3.14.1). Category: ModelScan scanner-bypass on .h5/.keras (in-scope, Model File Formats).

Summary

modelscan's HDF5 scanner only treats a layer as dangerous when its class_name == "Lambda". A serialized Python callable ({"class_name":"__lambda__", ...}) planted inside a built-in, non-Lambda TextVectorization layer's standardize or split field is therefore never inspected β€” modelscan reports 0 issues, 0 skipped, 0 errors (a genuine clean bill). At load time, TextVectorization.from_config deserializes those fields through a path that honors __lambda__ β†’ marshal.loads β†’ arbitrary FunctionType, which is then invoked on the first predict()/adapt() = arbitrary code execution.

Root cause (scanner gap)

  • modelscan/scanners/h5/scan.py:105-130 (_get_keras_h5_operator_names) iterates only top-level config.layers[] and appends a finding solely when layer.get("class_name") == "Lambda".
  • modelscan/settings.py:43-45 β†’ unsafe_keras_operators = {"Lambda": "MEDIUM"} only. Every other class and every config sub-field is ignored.

Exec sink (victim side, keras 3.14.1)

  • keras/src/layers/preprocessing/text_vectorization.py:491-505 β€” from_config calls bare serialization_lib.deserialize_keras_object(config["standardize"]) / (config["split"]) with no module_objects.
  • keras/src/saving/serialization_lib.py:660-670 β€” routes class_name == "__lambda__" to python_utils.func_load when safe_mode=False.
  • keras/src/utils/python_utils.py:54-100 β€” func_load β†’ marshal.loads β†’ FunctionType.
  • text_vectorization.py:541-542 / :566-567 β€” _preprocess does if callable(self._standardize): inputs = self._standardize(inputs) (same for split) β†’ executes attacker bytecode.

Why this field and not the obvious ones: planting the same __lambda__ carrier in activation/constraint/initializer/regularizer does not work β€” those route through module_objects deserializers that short-circuit and return the bare class_name string. The TextVectorization.from_config path is distinct precisely because it calls deserialize_keras_object directly. (Control verified in the PoC: a Dense.activation carrier did not fire.)

Reproduce

python poc/poc_final.py (env: tensorflow 2.21.0, keras 3.14.1, h5py, modelscan). Builds evil_standardize.h5 and evil_split.h5 (6472 bytes each). For both:

  • [A1] ModelScan().scan() β†’ 0 issues / 0 skipped / 0 errors (scanner blind, file recorded as scanned).
  • [A2] keras.models.load_model(path, safe_mode=False) then model(tf.constant(["hello world"])) β†’ marker file written with real exfil (uid/host). With a vocabulary shipped in the config, predict() completes silently while the payload fires.

Impact

ACE on anyone who scans a malicious .h5 with modelscan (gets a clean result) and then loads + runs it via the standard load_model(safe_mode=False) + predict(). Defeats modelscan's core promise for a built-in layer with no victim-side custom class; two independent carrier fields (standardize, split). safe_mode=False is the standard/documented path for legacy .h5 (and Keras ignores safe_mode for .h5 per CVE-2025-1550/9905 anyway).

Dup-check

Clean. The dev.to "29 ways" article's only Keras/H5 bypass is Lambda-in-TimeDistributed (structurally different). modelscan docs describe H5 detection as "look for lambda layer" (Lambda-only). The Keras safe_mode-on-h5 CVEs are a different component (Keras honoring Lambda exec, not the scanner) and use actual Lambda layers. Distinct from our prior R1 h5 activation-field lambda (that field short-circuits via module_objects) and R2 modelscan H5 training_config blindspot (different sink). No public source ties a non-Lambda preprocessing-layer __lambda__ carrier to a modelscan H5 miss.