optimus-fulcria/modelscan-keras-bypass-poc
024
ModelScan Keras Scanner Bypass PoC
Vulnerability
ModelScan v0.8.8's Keras scanner (KerasLambdaDetectScan) only checks for Lambda layers at the top level of model_config["config"]["layers"]. Lambda layers nested inside sub-models (Functional, Sequential) or wrapper layers (TimeDistributed, Bidirectional) are not detected.
Bypass Techniques
1. Nested Model (nested_lambda_bypass.keras)
Lambda layer hidden inside a nested Functional sub-model. ModelScan sees the outer layer as class_name: "Functional" and skips it.
2. Wrapper Layer (wrapper_lambda_bypass.keras)
Lambda layer wrapped in a TimeDistributed wrapper. ModelScan sees class_name: "TimeDistributed" and skips it.
Root Cause
In modelscan/scanners/keras/scan.py lines 119-131:
lambda_layers = [
layer.get("config", {}).get("function", {})
for layer in model_config_data.get("config", {}).get("layers", {})
if layer.get("class_name", {}) == "Lambda"
]This only iterates the top-level layers list. It does NOT recursively check:
- Nested model configs (layers inside
config.layers[n].config.layers) - Wrapper layer configs (the
layerfield in TimeDistributed/Bidirectional)
Impact
When a user loads a .keras file that passes ModelScan with 0 issues:
import keras
model = keras.models.load_model("nested_lambda_bypass.keras", safe_mode=False)
# Arbitrary code executes via marshal.loads in Lambda.from_config()The Lambda function is deserialized via:
Functional.from_config()→process_layer()→ recursivedeserialize_keras_object()Lambda.from_config()→python_utils.func_load()func_load()→codecs.decode(base64)→marshal.loads()→types.FunctionType()→ ACE
Reproduction
pip install modelscan[keras]
modelscan -p nested_lambda_bypass.keras
# Output: No issues found (0 Lambda layers detected)
# But the file contains a malicious Lambda layer with marshal.loads code execution