Пентест gRPC

58c52f2e44148ee69d193c311223d0b5.png

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

3d77dab760a9c4d150733d0be9221f3f.png

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

69f8032a72873eacef70b1c584759c84.png

Чтобы запустить его с подгрузкой методов из .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

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

b7dd2e04819063b994edc5eddc480c1a.png

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

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

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 проектах.

© Habrahabr.ru