В последнее время в современных веб-приложениях с микросервисной архитектурой всё чаще в качестве 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. Причин для этого несколько:

  1. Burp Suite не поддерживает так называемые trailing headers в HTTP/2 - заголовки, отправляемые после тела запроса. В gRPC данный механизм используется для отправки заголовка grpc-status. Это также необходимо для потоковой передачи,так как позволяет отправить заголовок посреди потока в случае возникновения ошибки.

  2. По умолчанию Burp Suite не умеет корректно декодировать protobuf.

Первая проблема не позволяет нам использовать данный инструмент для тестирования “голого” gRPC. В рамках Burp Suite эта проблема не решается расширениями и скорее всего сами разработчики возможность взаимодействия с “голым” gRPC в обозримом будущем не добавят. Мы же будем решать эту проблему через дополнительные надстройки в виде envoy прокси.

Вторая проблема затрудняет тестирование gRPC-web. Несмотря на то, что мы можем успешно перехватывать запросы от gRPC-Web клиента (страницы веб-приложения), из-за специфики формата данных проблематично осуществлять фаззинг, особенно, если речь идет о тестировании без доступа к .proto файлу. Данную проблему решает наше расширение protobuf-magic.

Описание тестового стенда

Для демонстрации процесса тестирования мы будем использовать веб-приложение, использущее gRPC. Оно представляет собой чат с несколькими функциями: регистрацией, поиском пользователей и созданием чатов. Хочу отдельно отметить, что приложение написано исключительно с целью демонстрации процесса тестирования gRPC.

Схема приложения представлена ниже. Для упрощения в ней не представлена СУБД MySQL, которая используется серверной частью веб-приложения и экземпляр которой развёрнут в отдельном docker-контейнере.

Веб-приложение состоит из 3 частей:

  1. Серверная часть. Она представлена gRPC-сервисом, написанным на Go.

  2. Envoy-прокси. Данная прослойка нужна нам для трансляции grpc-Web запросов в голый gRPC.

  3. Клиентская часть. Она представлена веб-страницей, использующей gRPC-Web.

Как вы могли заметить, на данный момент у нас gRPC-приложение, использующее gRPC-Web. С точки зрения тестирования безопасности это наименее труднозатратный сценарий. По ходу статьи мы будем убирать составные части для демонстрации всех возможных сценариев.

Практика

Как правило, когда речь идёт о расширениях Burp Suite, я описываю как пользоваться инструментом. Однако, поскольку protobuf-magic довольно прост в использовании, я опущу инструкцию. Все возможные взаимодействия с расширением и так будут продемонстрированы в ходе практики. Для начала мы рассмотрим наименее сложные сценарии.

Тестирование gRPC-Web

Перед тестированием gRPC-Web следует упомянуть несколько особенностей.

Во-первых, клиентская часть может передавать protobuf в двух Content-Type

  1. application/grpc-web+[proto, json, thrift]

  2. application/grpc-web-text+[proto, thrift]

Вот так они выглядят в Burp Suite

Демонстрация protobuf для различных Content-Type
Демонстрация protobuf для различных Content-Type

Как вы могли заметить, различие только в кодировании base64. Также стоит отметить, что в случае в application/grpc-web желательно указывать тип передаваемых данных(типы указаны в квадратных скобках). В противном случае по умолчанию будет считаться, что в запросе используется тип данных proto (protobuf).

Во-вторых, в зависимости от Content-Type клиент с gRPC-Web может поддерживать только следующие типы вызовов функций gRPC:

  1. application/grpc-web - только унарные вызовы

  2. application/grpc-web-text - унарные и потоковые серверные вызовы

Давайте теперь откроем наше приложение. Как мы видим, у нас несколько предполагаемых функций и полей для ввода данных. Попробуем зарегистрировать пользователя с именем Test1.

Страница тестового приложения
Страница тестового приложения

Перехватим запрос в Burp Suite, отправим его через Repeater и посмотрим содержимое запроса и ответа. В данном примере и далее мы будем иметь дело с application/grpc-web-text. Здесь gRPC-Web вызов представляет собой POST-запрос к конечной точке /vulnchat.VulnChat/registerUser. Содержимое тел запроса и ответа представляет собой закодированные в base64 данные.

Содержимое запроса и ответа
Содержимое запроса и ответа

После перехода на вкладку Protobuf Magic(требуется предварительная установка расширения) у нас отображается JSON, в котором представлено декодированное protobuf сообщение.

Декодированное 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
Запускаем сканер вместе с protobuf-magic

После завершения сканирования мы можем увидеть результат - сканер Burp Suite нашёл SQL-инъекцию.

Запрос с полезной нагрузкой, вызывающей исполнение sleep на 20 секунд
Запрос с полезной нагрузкой, вызывающей исполнение sleep на 20 секунд
Ответ пришёл через 20 секунд
Ответ пришёл через 20 секунд

Тестирование “голого” gRPC

Теперь разберём более сложный случай, в котором у нас отсутствует gRPC-Web. В таком случае Burp Suite без предварительной подготовки использовать не получится. Поскольку поддержка gRPC в данном инструменте в обозримом будущем не появится, нам требуется свести текущие условия к похожим на те, которые были описаны выше.

Первый случай: мы имеем .proto файл

Предположим, у нас есть .proto файл. В таком случае нам будут известны все необходимые методы и фигурирующие в них данные. Единственная проблема, которая остаётся - это связь Burp Suite с нашим приложением с “голым” gRPC. У нас есть два решения:

  1. Сгенерировать страницу gRPC-Web на основе .proto файла и поднять envoy proxy.

  2. Использовать 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
Вот так выглядит перехваченый запрос
Вот так выглядит перехваченый запрос

Попробуем снова запустить сканер на уязвимый параметр

Запрос к grpcui с полезной нагрузкой, вызывающей исполнение sleep на 20 секунд
Запрос к grpcui с полезной нагрузкой, вызывающей исполнение sleep на 20 секунд

Аналогично нам удалось обнаружить SQL-инъекцию.

Второй случай: у нас нет .proto файла.

В данном случае мы не имеем .proto файла. И мы снова имеем два сценария, однако, в этот раз выбор сценария зависит от конфигурации gRPC-сервиса.

gRPC reflection

У gRPC существует механизм рефлексии, который позволяет узнать доступные методы сервиса. Если у тестируемого сервиса данный механизм включен, то grpcui автоматически обнаружит доступные методы и всё сведётся к случаю из предыдущего раздела. Нам просто не требуется указывать .proto файл в качестве параметра grpcui.

grpcui -insecure vulnchat:50051

Интерфейс и производимые действия не будут ничем отличаться от описанного выше.

Рефлексия в grpcui
Рефлексия в grpcui

gRPC reflection отсутствует

Теперь рассмотрим самый сложный сценарий: gRPC-сервис не имеет включённого механизма рефлексии и у нас отсутствует .proto файл.

В таком случае нам остаётся только фаззинг параметров и эндпоинтов. Инструмент grpcui не позволяет подключиться к gRPC-сервису без .proto файла и без включённой gRPC рефлексии, поэтому нам снова понадобится наше расширение protobuf-magic.

Вывод ошибки grpcui
Вывод ошибки grpcui

В таком случае схема компонентов будет выглядеть следующим образом.

Сделаем предварительную подготовку окружения.

Для начала нам понадобится извлечь 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 при разных ситуациях: неверное имя пакета, верное имя пакета, но неверное имя сервиса и так далее.

Untitled
Неверное имя пакета
Верное имя пакета, но неверное имя сервиса даст аналогичный ответ
Верное имя пакета, но неверное имя сервиса даст аналогичный ответ
Untitled
Верные имена пакета и сервиса, но некорретный или отсутствующий метод.
Untitled
Верный эндпоинт, некорректные данные

Для фаззинга параметров в удобном виде можно поместить в тело запроса следующий шаблон запроса, который 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 проектах.

Комментарии (0)