Пентест gRPC
В последнее время в современных веб-приложениях с микросервисной архитектурой всё чаще в качестве API используется фреймворк удалённого вызова процедур gRPC. Данный фреймворк чаще всего использует protocol buffers (protobuf) в качестве языка определения интерфейсов и в качестве основного формата обмена сообщениями. Однако, в отличие от более привычных форматов обмена сообщениями (JSON, XML и тд), которые являются текстовыми, в protobuf фигурируют бинарные данные. Помимо этого, gRPC в качестве транспорта использует исключительно HTTP/2. Чаще всего эти обстоятельства вызывают затруденения при тестировании безопасности веб-приложений, которые используют данный фреймворк.
Данная статья поможет разобраться в том, как тестировать веб-приложения, использующие gRPC. Содержание статьи частично пересекается с моим докладом на OFFZONE 2023, однако, если в нём был сделан упор на рассказ об аспектах protobuf и gRPC и обзор существующих инструментов для тестирования безопасности, то здесь мы рассмотрим практический пример по поиску уязвимостей на демонстрационном стенде с использованием Burp Suite и расширения prototbuf-magic, разработанным нами. Для ознакомления с основами protobuf и gRPC рекомендую послушать мой доклад.
О проблемах тестирования безопасности gRPC с использованием Burp Suite
Как всем известно, Burp Suite является де-факто стандартным инструментом для исследования безопасности веб-приложений, однако, без дополнительных расширений он оказывается беспомощным, когда сталкивается с gRPC. Причин для этого несколько:
Burp Suite не поддерживает так называемые trailing headers в HTTP/2 — заголовки, отправляемые после тела запроса. В gRPC данный механизм используется для отправки заголовка grpc-status. Это также необходимо для потоковой передачи, так как позволяет отправить заголовок посреди потока в случае возникновения ошибки.
По умолчанию Burp Suite не умеет корректно декодировать protobuf.
Первая проблема не позволяет нам использовать данный инструмент для тестирования «голого» gRPC. В рамках Burp Suite эта проблема не решается расширениями и скорее всего сами разработчики возможность взаимодействия с «голым» gRPC в обозримом будущем не добавят. Мы же будем решать эту проблему через дополнительные надстройки в виде envoy прокси.
Вторая проблема затрудняет тестирование gRPC-web. Несмотря на то, что мы можем успешно перехватывать запросы от gRPC-Web клиента (страницы веб-приложения), из-за специфики формата данных проблематично осуществлять фаззинг, особенно, если речь идет о тестировании без доступа к .proto файлу. Данную проблему решает наше расширение protobuf-magic.
Описание тестового стенда
Для демонстрации процесса тестирования мы будем использовать веб-приложение, использущее gRPC. Оно представляет собой чат с несколькими функциями: регистрацией, поиском пользователей и созданием чатов. Хочу отдельно отметить, что приложение написано исключительно с целью демонстрации процесса тестирования gRPC.
Схема приложения представлена ниже. Для упрощения в ней не представлена СУБД MySQL, которая используется серверной частью веб-приложения и экземпляр которой развёрнут в отдельном docker-контейнере.
Веб-приложение состоит из 3 частей:
Серверная часть. Она представлена gRPC-сервисом, написанным на Go.
Envoy-прокси. Данная прослойка нужна нам для трансляции grpc-Web запросов в голый gRPC.
Клиентская часть. Она представлена веб-страницей, использующей gRPC-Web.
Как вы могли заметить, на данный момент у нас gRPC-приложение, использующее gRPC-Web. С точки зрения тестирования безопасности это наименее труднозатратный сценарий. По ходу статьи мы будем убирать составные части для демонстрации всех возможных сценариев.
Практика
Как правило, когда речь идёт о расширениях Burp Suite, я описываю как пользоваться инструментом. Однако, поскольку protobuf-magic довольно прост в использовании, я опущу инструкцию. Все возможные взаимодействия с расширением и так будут продемонстрированы в ходе практики. Для начала мы рассмотрим наименее сложные сценарии.
Тестирование gRPC-Web
Перед тестированием gRPC-Web следует упомянуть несколько особенностей.
Во-первых, клиентская часть может передавать protobuf в двух Content-Type
application/grpc-web+[proto, json, thrift]
application/grpc-web-text+[proto, thrift]
Вот так они выглядят в Burp Suite
Демонстрация protobuf для различных Content-Type
Как вы могли заметить, различие только в кодировании base64. Также стоит отметить, что в случае в application/grpc-web желательно указывать тип передаваемых данных (типы указаны в квадратных скобках). В противном случае по умолчанию будет считаться, что в запросе используется тип данных proto (protobuf).
Во-вторых, в зависимости от Content-Type клиент с gRPC-Web может поддерживать только следующие типы вызовов функций gRPC:
application/grpc-web — только унарные вызовы
application/grpc-web-text — унарные и потоковые серверные вызовы
Давайте теперь откроем наше приложение. Как мы видим, у нас несколько предполагаемых функций и полей для ввода данных. Попробуем зарегистрировать пользователя с именем Test1
.
Страница тестового приложения
Перехватим запрос в Burp Suite, отправим его через Repeater и посмотрим содержимое запроса и ответа. В данном примере и далее мы будем иметь дело с application/grpc-web-text. Здесь gRPC-Web вызов представляет собой POST-запрос к конечной точке /vulnchat.VulnChat/registerUser
. Содержимое тел запроса и ответа представляет собой закодированные в base64 данные.
Содержимое запроса и ответа
После перехода на вкладку Protobuf Magic (требуется предварительная установка расширения) у нас отображается JSON, в котором представлено декодированное protobuf сообщение.
Декодированное protobuf-сообщение
При изменении тела запроса в данной вкладке изменяется и само тело запроса.
Теперь попробуем воспользоваться полем Find Users и перехватить запрос. Данный gRPC-Web вызов представляет собой POST-запрос к конечной точке /vulnchat.VulnChat/findUser
. Ниже на рисунках изображены перехваченные запрос и ответ.
Перехваченный запрос
Декодированное тело перехваченного запроса
Перехваченный ответ
Декодированное тело перехваченного ответа
Стоит упомянуть, что protobuf-magic автоматически преобразует JSON в gRPC-вызовах, что позволяет использовать расширение вместе со встроенным сканером Burp Suite (есть в PRO версии) или с Intruder. Давайте попробуем запустить сканер. Выбираем значение параметра value, выделяем его и нажимаем «Open scan launcher with selected insertion point».
Запускаем сканер вместе с protobuf-magic
После завершения сканирования мы можем увидеть результат — сканер Burp Suite нашёл SQL-инъекцию.
Запрос с полезной нагрузкой, вызывающей исполнение sleep на 20 секунд
Ответ пришёл через 20 секунд
Тестирование «голого» gRPC
Теперь разберём более сложный случай, в котором у нас отсутствует gRPC-Web. В таком случае Burp Suite без предварительной подготовки использовать не получится. Поскольку поддержка gRPC в данном инструменте в обозримом будущем не появится, нам требуется свести текущие условия к похожим на те, которые были описаны выше.
Первый случай: мы имеем .proto файл
Предположим, у нас есть .proto файл. В таком случае нам будут известны все необходимые методы и фигурирующие в них данные. Единственная проблема, которая остаётся — это связь Burp Suite с нашим приложением с «голым» gRPC. У нас есть два решения:
Сгенерировать страницу gRPC-Web на основе .proto файла и поднять envoy proxy.
Использовать gRPC-клиент с веб-интерфейсом.
Мы естественно выбираем второй вариант. В качестве gRPC-клиента с веб-интерфейсом мы рассмотрим grpcui. Он написан на Go, поэтому данный метод подойдёт для любой операционной системы. Схема компонентов будет выглядеть следующим образом.
Чтобы запустить его с подгрузкой методов из .proto файла, следует ввести следующую команду
grpcui -proto VulnChat.proto -plaintext localhost:50051
где VulnChat.proto
— .proto файл тестируемого сервиса, localhost:50051
— адрес и порт тестируемого сервиса.
Сразу стоит оговориться, флаг -plaintext
нужно применять только если gRPC-сервис работает без TLS. В продуктовой среде скорее всего вы такого не встретите.
На рисунке ниже представлен интерфейс grpcui. Попробуем сделать gRPC вызов findUser, в котором мы обнаружили уязвимость.
Интерфейс grpcui
Вот так выглядит перехваченый запрос
Попробуем снова запустить сканер на уязвимый параметр
Запрос к grpcui с полезной нагрузкой, вызывающей исполнение sleep на 20 секунд
Аналогично нам удалось обнаружить SQL-инъекцию.
Второй случай: у нас нет .proto файла.
В данном случае мы не имеем .proto файла. И мы снова имеем два сценария, однако, в этот раз выбор сценария зависит от конфигурации gRPC-сервиса.
gRPC reflection
У gRPC существует механизм рефлексии, который позволяет узнать доступные методы сервиса. Если у тестируемого сервиса данный механизм включен, то grpcui автоматически обнаружит доступные методы и всё сведётся к случаю из предыдущего раздела. Нам просто не требуется указывать .proto файл в качестве параметра grpcui.
grpcui -insecure vulnchat:50051
Интерфейс и производимые действия не будут ничем отличаться от описанного выше.
Рефлексия в grpcui
gRPC reflection отсутствует
Теперь рассмотрим самый сложный сценарий: gRPC-сервис не имеет включённого механизма рефлексии и у нас отсутствует .proto файл.
В таком случае нам остаётся только фаззинг параметров и эндпоинтов. Инструмент grpcui не позволяет подключиться к gRPC-сервису без .proto файла и без включённой gRPC рефлексии, поэтому нам снова понадобится наше расширение protobuf-magic.
Вывод ошибки grpcui
В таком случае схема компонентов будет выглядеть следующим образом.
Сделаем предварительную подготовку окружения.
Для начала нам понадобится извлечь TLS-сертификат сервиса для проксирования трафика через envoy. Извлечение сертификата осуществляется следующим образом.
grpc_cert.crt
Затем нам нужно поднять envoy-прокси. Предполагается, что сервис имеет доменное имя vulnchat и расположен на 50051 порту. Используем следующую конфигурацию:
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: vulnchat
timeout: 0s
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: vulnchat
connect_timeout: 0.25s
type: logical_dns
# HTTP/2 support
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
lb_policy: round_robin
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
load_assignment:
cluster_name: cluster_0
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: vulnchat
port_value: 50051
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
alpn_protocols: h2
validation_context:
trusted_ca:
filename: /etc/ssl/certs/grpc_cert.crt
sni: vulnchat
Сертификат нужно предварительно переместить в директорию /etc/ssl/certs/. Также для удобства можно поднять envoy в docker-контейнере. Для этого в Dockerfile прописываем копирование полученного сертификата и конфига envoy.
FROM envoyproxy/envoy:v1.22.0
COPY ./envoy.yaml /etc/envoy/envoy.yaml
COPY ./grpc_cert.crtt /etc/ssl/certs/
RUN chmod go+r /etc/envoy/envoy.yaml
ENTRYPOINT [ "/usr/local/bin/envoy" ]
CMD [ "-c /etc/envoy/envoy.yaml", "-l trace", "--log-path /tmp/envoy_info.log" ]
Теперь билдим образ в директории с файлами и запускаем его. Не забудьте пробросить порты.
sudo docker build --tag 'envoy_grpc_blackbox' .
sudo docker run -dit -p 8080:8080 envoy_grpc_blackbox
Теперь мы можем при помощи Burp Suite общаться с gRPC посредством envoy. Взаимодействие будет происходить по gRPC-Web точно также, как и в начале статьи, за исключением того, что мы не знаем названия сервисов и методов. Для их поиска придётся применить фаззинг. Запрос gRPC-Web, как мы видели ранее, представляет собой POST-запрос к эндпоинту следующего вида:
/package.ServiceName/rpcMethod
где package
— имя пакета в .proto файла, ServiceName
— имя сервиса, rpcMethod
— имя метода. Обратите внимание на регистр символов в примере, за исключением имени метода скорее всего регистр символов будет таким же. Это важно, так как gRPC чувствителен к регистру.
Рассмотрим ключевые отличия запросов при фаззинге при дефолтной конфигурации gRPC-сервиса. Ниже можно увидеть содержимое заголовка grpc-message ответа envoy при разных ситуациях: неверное имя пакета, верное имя пакета, но неверное имя сервиса и так далее.
Неверное имя пакета
Верное имя пакета, но неверное имя сервиса даст аналогичный ответ
Верное имя пакета, но неверное имя сервиса даст аналогичный ответ
Верный эндпоинт, некорректные данные
Для фаззинга параметров в удобном виде можно поместить в тело запроса следующий шаблон запроса, который protobuf-magic автоматически преобразует в protobuf с одним параметром.
[ {
"index" : 1,
"type" : "insert_type",
"start" : 5,
"end" : 11,
"value" : "insert_value"
} ]
Также не забудьте указать корректные значения заголовков Content-Type и Accept. В данном случае application/grpc-web-text
.
Теперь нам остаётся только определить тип данных и валидное значение. Далее действия начинают повторять описанные выше.
Таким образом, мы можем исследовать gRPC-сервис режиме полного blackbox.
Заключение
Вышеперечисленные техники тестирования gRPC покрывают большую часть возможных сценариев. Единственный нерассмотренный сценарий — это наличие клиентской TLS-аутентификации в совокупности с условиями последнего описанного случая. Данный сценарий является очень маловероятным и экзотическим, однако, envoy имеет возможность настройки клиентской TLS-аутентификации. Более подробно об этом можно прочитать здесь. Также рекомендую посмотреть примеры различных конфигураций envoy в OpenSource проектах.