В последнее время в современных веб-приложениях с микросервисной архитектурой всё чаще в качестве 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
Как вы могли заметить, различие только в кодировании 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 сообщение.
При изменении тела запроса в данной вкладке изменяется и само тело запроса.
Теперь попробуем воспользоваться полем Find Users и перехватить запрос. Данный gRPC-Web вызов представляет собой POST-запрос к конечной точке /vulnchat.VulnChat/findUser
. Ниже на рисунках изображены перехваченные запрос и ответ.
Стоит упомянуть, что protobuf-magic автоматически преобразует JSON в gRPC-вызовах, что позволяет использовать расширение вместе со встроенным сканером Burp Suite(есть в PRO версии) или с Intruder. Давайте попробуем запустить сканер. Выбираем значение параметра value, выделяем его и нажимаем “Open scan launcher with selected insertion point”.
После завершения сканирования мы можем увидеть результат - сканер Burp Suite нашёл SQL-инъекцию.
Тестирование “голого” 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, в котором мы обнаружили уязвимость.
Попробуем снова запустить сканер на уязвимый параметр
Аналогично нам удалось обнаружить SQL-инъекцию.
Второй случай: у нас нет .proto файла.
В данном случае мы не имеем .proto файла. И мы снова имеем два сценария, однако, в этот раз выбор сценария зависит от конфигурации gRPC-сервиса.
gRPC reflection
У gRPC существует механизм рефлексии, который позволяет узнать доступные методы сервиса. Если у тестируемого сервиса данный механизм включен, то grpcui автоматически обнаружит доступные методы и всё сведётся к случаю из предыдущего раздела. Нам просто не требуется указывать .proto файл в качестве параметра grpcui.
grpcui -insecure vulnchat:50051
Интерфейс и производимые действия не будут ничем отличаться от описанного выше.
gRPC reflection отсутствует
Теперь рассмотрим самый сложный сценарий: gRPC-сервис не имеет включённого механизма рефлексии и у нас отсутствует .proto файл.
В таком случае нам остаётся только фаззинг параметров и эндпоинтов. Инструмент grpcui не позволяет подключиться к gRPC-сервису без .proto файла и без включённой gRPC рефлексии, поэтому нам снова понадобится наше расширение protobuf-magic.
В таком случае схема компонентов будет выглядеть следующим образом.
Сделаем предварительную подготовку окружения.
Для начала нам понадобится извлечь TLS-сертификат сервиса для проксирования трафика через envoy. Извлечение сертификата осуществляется следующим образом.
</dev/null openssl s_client -connect ip_or_domain:port | openssl x509 > 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.crt /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 проектах.