Downloads · 30 days
29
32% of all-time downloads
EnigmaConsultant/huntr-poc-modelscan-keras-vectorizedmap-lambda-carrier
huntr-poc-modelscan-keras-vectorizedmap-lambda-carrier is a machine learning model from EnigmaConsultant. Use it for the machine learning task on the model card, and read the license before you ship it in a product. It is set up for keras.
Severity: High (arbitrary code execution; scanner reports a fully clean bill) Affected tool: modelscan 0.8.8 — KerasLambdaDetectScan (native .keras v3 zip). Also applies to the H5 path (H5LambdaDetectScan). Victim loa…
Downloads · 30 days
29
32% of all-time downloads
All-time downloads
91
Public
Repo size
—
Likes
0
Public
Click a slice to open those files.
.keras23.4 KB · 68%
From the Hugging Face model README
.keras RCE blindspot — __lambda__ carrier in a keras.ops.vectorized_map (VectorizedMap) operationSeverity: High (arbitrary code execution; scanner reports a fully clean bill)
Affected tool: modelscan 0.8.8 — KerasLambdaDetectScan (native .keras v3 zip). Also applies to the H5 path (H5LambdaDetectScan).
Victim loader: keras.models.load_model(path, safe_mode=False) (keras 3.15.0 / TF 2.21.0) — the documented path for loading Lambda-bearing models.
Category: ModelScan scanner-bypass on Keras .keras/.h5 (Model File Formats, in-scope).
modelscan flags a Keras model as dangerous only when a top-level config.layers[] entry has class_name == "Lambda". keras.ops.vectorized_map(fn, x) used inside a functional model serializes as a top-level layer entry whose class_name is "VectorizedMap" (a keras.src.ops.core.Operation), carrying the attacker's Python callable as a {"class_name":"__lambda__", ...} value in its function config field. Because the class name is not "Lambda", modelscan never inspects it and returns total_issues: 0, errors: [], scanned: [evil] — a genuine clean result. At load, VectorizedMap.from_config bare-deserializes that field (no module_objects, so no short-circuit) via serialization_lib.deserialize_keras_object → __lambda__ branch → python_utils.func_load → marshal.loads → a real FunctionType, which VectorizedMap.call then invokes (backend.core.vectorized_map(self.function, elements)) during model build/first inference = arbitrary code execution.
modelscan/scanners/keras/scan.py _get_keras_operator_names — only layer.get("class_name") == "Lambda" in top-level config.layers.modelscan/scanners/h5/scan.py _get_keras_h5_operator_names — same Lambda-only top-level check.modelscan/settings.py — unsafe_keras_operators = {"Lambda": "MEDIUM"} only. "VectorizedMap" is not listed and sub-fields are never inspected.keras/src/ops/core.py:1163-1167 — VectorizedMap.from_config: config["function"] = serialization_lib.deserialize_keras_object(config["function"]) (bare, no module_objects → does not short-circuit __lambda__).keras/src/saving/serialization_lib.py:686 — class_name == "__lambda__" → python_utils.func_load(inner_config["value"]).keras/src/utils/python_utils.py func_load → marshal.loads → types.FunctionType.keras/src/ops/core.py:1139 — VectorizedMap.call → backend.core.vectorized_map(self.function, elements) calls the reconstructed function.TextVectorization standardize/split carrier (different layer, different field)."module"-key legacy→modern deserializer fork on .h5 activation (this needs no injected "module" key; VectorizedMap.from_config bare-deserializes natively, and the class is not a Dense/activation layer).Lambda carrier (there is no Lambda layer anywhere — class_name is "VectorizedMap", a distinct built-in operation).training_config/compile loss/metric blindspot (this is a forward-pass model op firing at build/inference, not a compile-time callable).keras.ops operations, VectorizedMap is the one that both stores its function in get_config and bare-deserializes + invokes it (Map/Scan/Cond/Switch/… take functions as call-time args and do not round-trip a stored function through model config).Env: pip install tensorflow keras 'modelscan==0.8.8', KERAS_BACKEND=tensorflow (Python 3.10–3.12).
python build_poc.py # writes evil_vectorizedmap.keras + control_lambda.keras
modelscan -p evil_vectorizedmap.keras # -> No issues found! (total_issues: 0, errors: [])
modelscan -p control_lambda.keras # -> 1 MEDIUM 'Lambda' (control: detection works)
python demo_load.py # load_model(safe_mode=False) + predict -> /tmp/VMAP_PWN.txt written
evil_vectorizedmap.keras — modelscan JSON: total_issues: 0, total_issues_by_severity all 0, errors: [], scanned_files: ["evil_vectorizedmap.keras"] (see modelscan_evil_result.json). Top-level layer class_names are ["InputLayer", "VectorizedMap"] — no "Lambda".keras.models.load_model("evil_vectorizedmap.keras", safe_mode=False) fires the payload during load (functional trace) and predict() completes normally (output shape (2,3)); marker /tmp/VMAP_PWN.txt = VMAP_RCE_1000 (real os.system / id -u).control_lambda.keras (identical payload wrapped in a real Lambda) → modelscan total_issues: 1, MEDIUM, "Use of unsafe operator 'Lambda'" (see modelscan_control_result.json).load_model(...) (safe_mode=True) raises ValueError: Requested the deserialization of a Python lambda ... and the marker is not written — payload only fires under the documented safe_mode=False.ACE against any pipeline that scans an untrusted .keras (or .h5) model with modelscan — receiving a clean, 0-issue, file-marked-scanned result — and then loads it via the standard load_model(safe_mode=False). No victim-side custom class and no Lambda layer present, so it defeats modelscan's "look for Lambda layers" detection model with a benign-looking built-in Keras operation.
build_poc.py — regenerates both models (payload is benign: writes /tmp/VMAP_PWN.txt with id -u).evil_vectorizedmap.keras — malicious model (class_name VectorizedMap, no Lambda).control_lambda.keras — negative control (same payload as a real Lambda; modelscan flags it).demo_load.py — loads the evil model and triggers the payload.modelscan_evil_result.json / modelscan_control_result.json — captured scanner output.In both Keras scanners, recurse all layer/operation entries and normalize sub-fields, flagging any class_name == "__lambda__" anywhere in the config (and treat function-carrying operations like VectorizedMap as unsafe), rather than matching only a top-level class_name == "Lambda".