Введение
В первой части статьи мы дали краткое описание механизма encrypted SNI (eSNI). Показали каким образом на его основе можно уклоняться от детектирования современными DPI-системами (на примере билайновского DPI и запрещенного РКН рутрекера), а также исследовали новый вариант домен-фронтинга на основе данного механизма.
Во второй части статьи перейдем к более практическим вещам, которые будут полезны RedTeam специалистам в их нелегкой работе. В конце концов наша цель — не получение доступа к заблокированным ресурсам (для таких банальных вещей у нас есть старый добрый VPN). Благо что VPN-провайдеров существует великое множество, как говорится, на любой вкус, цвет и бюджет.
Мы постараемся применить механизм домен-фронтинга для современных инструментов RedTeam, например, таких как Cobalt Strike, Empire и т.д., и дать им дополнительные возможности по мимикрированию и уклонению от современных систем фильтрации контента.
В прошлый раз мы внедрили механизм eSNI в библиотеку OpenSSL и успешно использовали ее в знакомой всем утилите curl. Но одним курлом, как говориться, сыт не будешь. Конечно же хочется реализовать нечто подобное в языках высокого уровня. Но, к сожалению, беглый поиск по просторам сети нас разочаровывает, потому что поддержка механизма eSNI полноценно реализована только в GOLANG. Таким образом, выбор у нас не особо и большой: либо мы пишем на чистом С или С++ с использованием пропатченной библиотеки OpenSSL, либо используем отдельный форк GOLANG от CloudFlare и пытаемся портировать наш инструментарий туда. В принципе, есть еще один вариант, более классический, но одновременно и трудоемкий – это реализовать поддержку eSNI для питона. В конце концов, Python так же использует OpenSSL для работы с https. Но этот вариант мы оставим на разработку кому-нибудь еще, а сами будем довольствоваться реализацией на голанге, тем более что наш любимый Cobalt Strike прекрасно умеет работать с каналом связи, построенным сторонними средствами (External C2 channel) – об этом расскажем в конце статьи.
Try Harder…
Один из реализованных на Go инструментов – наша разработка для пивотинга внутрь сети – тунеллер rsockstun, которая, кстати говоря, нынче детектируется средствами от компании Microsoft и Symantec, как очень вредоносное ПО, направленное на нарушение мировой стабильности…
Было бы здорово использовать предыдущую разработку и в данном случае. Но тут возникает небольшая проблема. Дело в том, что изначально rsockstun подразумевает использование синхронного SSL канала связи с сервером. Это означает, что соединение устанавливается один раз и существует на протяжении всего времени работы туннелера. А, как вы понимаете, протокол https немного не предназначен для такого режима работы – он работает в режиме запрос-ответ, где каждый новый http-запрос существует в рамках нового tcp-соединения.
Основной недостаток такой схемы состоит в том, что сервер не может передать данные клиенту, пока клиент не пришлет новый http-запрос. Но, к счастью, существуют много вариантов решения данной проблемы – потоковой передачи данных через http протокол (в конце концов мы же умудряемся как то смотреть наши любимые сериалы и слушать музыку с порталов, работающих на https, а передача видео и аудио — это не что иное как потоковая передача данных). Одной из технологий для эмуляции работы полноценного tcp-соединения поверх протокола http является технология веб-сокетов (WebSockets), основная суть которой в организации полноценного сетевого соединения между клиентом и веб-сервером.
На наше везение (ура-ура!!!), эта технология включена по умолчанию во все тарифные планы CloudFlare и прекрасно работает в сочетании с eSNI. Вот как раз ее то мы и будем использовать для того, чтобы научить наш туннелер использовать домен-фронтинг и скрываться от современных DPI.
Немного о WebSockets
Прежде всего, мы вкратце и простыми словами расскажем о веб-сокетах, чтобы все имели представление о том, с чем мы будем работать.
Технология веб-сокетов позволяет временно переключиться с http-соединения на стандартную потоковую передачу данных через сетевой сокет, не разрывая при этом установленного tcp-соединения. Когда клиент хочет переключиться на веб-сокет, он в своем http-запросе выставляет несколько http-заголовков. Два обязательных заголовка – Connection: Upgrade и Upgrade: websocket. Так же он может указать принудительно версию websocket протокола (Sec-Websockset-Version: 13) и нечто вроде base64 идентификатора веб-сокета (Sec-WebSocket-Key: DAGDJSiREI3+KjDfwxm1FA==). Сервер ему отвечает http-кодом 101 Switching Protocols и так же устанавливает заголовки Connection, Upgrade и Sec-WebSocket-Accept. Наглядно процесс переключения продемонстрирован на скрине ниже:
После этого установку WebSocket соединения можно считать завершенной. Любые данные как от клиента, так и от сервера теперь будут снабжаться не http, а WebSocket заголовками (они начинаются с байта 0x82). Теперь серверу нет необходимости ждать запроса от клиента, чтобы передать данные, т.к. tcp-соединение не разрывается.
В голанге для работы с веб-сокетами есть несколько библиотек. Наиболее популярные из них это Gorilla WebSocket и стандартная WebSocket. Мы воспользуемся последней, т.к. она проще, меньше и работает, как говорят, немного пошустрее.
В коде клиента rsockstun нам необходимо заменить вызовы net.dial или tls.dial на соответствующие вызовы WebSocket:
Мы хотим сделать клиентскую часть нашего туннелера универсальной и способной работать как через прямое ssl-соединение, так и через протокол WebSockset. Для этого мы создадим отдельную функцию func connectForWsSocks(address string, proxy string) error {…} по аналогии с connectForSocks() и будем ее использовать для работы с веб-сокетами в случае, если адрес сервера, заданный при запуске клиента, будет начинаться с ws: или wss: (в случае с Secure WebSocket).
Для серверной части туннелера мы также сделаем отдельную функцию для работы с веб-сокетами. В ней будет создаваться экземпляр класса http и задаваться обработчик http-соединения (функция wsHandler):
А всю логику обработки подключения (авторизация клиента по паролю, установка и завершение yamux-сессии) мы поместим в обработчик подключения WebSocket:
Компилируем проект, запускаем серверную часть:
./rsockstun –listen ws:127.0.0.1:8080 –pass P@ssw0rd
И затем клиентскую часть:
./rsockstun -connect ws:127.0.0.1:8080 –pass P@ssw0rd
И проверяем работу на локальном хосте:
Переходим к домен-фронтингу
С веб-сокетами мы вроде бы как разобрались. Теперь давайте перейдем непосредственно к eSNI и домен-фронтингу. Как уже говорилось ранее, для работы с DoH и eSNI нам необходимо взять специальную ветку голанга от компании CloudFlare. Нам нужна ветка с поддержкой eSNI (pwu/esni).
Клонируем ее к себе локально или качаем и разжимаем соответствующий zip:
git clone -b pwu/esni https://github.com/cloudflare/tls-tris.git
Затем нам необходимо скопировать каталог GOROOT, заменить соответствующие файлы из склонированной ветки и установить его как основной. Чтобы избавить разработчика от этой головной боли ребята из CloudFlare приготовили специальный скрипт – _dev/go.sh. Просто запускаем его. Скрипт вместе с makefile все сделают сами. Для интереса — можете заглянуть внутрь makefile за подробностями.
После отработки скрипта, при компиляции проекта, нам необходимо будет указывать в качестве GOROOT локальный каталог, подготовленный скриптом. В нашем случае это выглядит так:
GOROOT="/opt/tls-tris/_dev/GOROOT/linux_amd64" go build ….
Далее, нам необходимо реализовать в туннелере функционал запроса и парсинга публичных eSNI ключей для нужного домена. В нашем случае, это будут бубличные eSNI ключи от фронтенд-серверов CloudFlare. Для этого мы создадим три функции:
func makeDoTQuery(dnsName string) ([]byte, error)
func parseTXTResponse(buf []byte, wantName string) (string, error)
func QueryESNIKeysForHost(hostname string) ([]byte, error)
Названия функций, в принципе, говорят сами за себя. Наполнение же мы возьмем из файла esni_query.go, входящего в состав tls-tris. Первая функция создает сетевой пакет с запросом к DNS серверу CloudFlare, используя протокол DoH (DNS-over-HTTPS), вторая – парсит результаты запроса и получает значения публичных ключей домена, а третья является контейнером для первых двух.
Далее, мы вносим в нашу, вновь созданную, функцию подключения веб-сокета connectForWsSocks функционал запроса eSNI ключей для домена. Там, где функционирует серверная часть, устанавливаем параметры TLS, а также задаем имя фейкового «домена прикрытия»:
Тут следует отметить, что изначально, ветка tls-tris не рассчитана на применение домен-фронтинга. Следовательно, в ней не уделено внимание фейковому имени сервера (в составе client-hello пакета передается пустое поле serverName). Для того чтобы это исправить нам придется добавить в структуру TlsConfig соответствующее поле FakeServerName. Стандартное поле ServerName структуры мы использовать не можем, т.к. его используют внутренние механизмы tls и если оно будет отличаться от исходного, то tls-хендшейк закончится с ошибкой. Описание структуры TlsConfig содержится в файле tls/common.go – его нам и предстоит поправить:
Дополнительно нам придется внести изменения в файл tls/handshake_client.go, чтобы использовать наше поле FakeServerName при формировании TLS хендшейка:
На этом всё! Можно компилировать проект и проверять работу. Но прежде чем запускать проверку, необходимо настроить аккаунт CloudFlare. Ну как сказать настроить – просто создать аккаунт на клаудфларе и привязать свой домен к нему. Все фишки, связанные с DoH, WebSocket и ESNI, включены в CloudFlare по умолчанию. После того как DNS записи обновятся – можно проверить работу домена выполнив запрос eSNI ключей:
dig +short txt _esni.df13tester.info
Если вы видите нечто похожее для вашего домена – значит все у вас работает и можно переходить к тестированию.
Запускаем Ubuntu VPS, например, на DigitalOcean. P.S. В нашем случае только что выданный провайдером IP-адрес VPS оказался в блек-листах РКН. Так что не удивляйтесь, если с вами случится что-то похожее. Пришлось воспользоваться VPN чтобы попасть на свою VPS.
Копируем на VPS уже скомпилированный rsockstun (в этом, кстати, еще одна прелесть голанга – можно скомпилировать проект у себя и запустить на любом линуксе, соблюдая только разрядность системы) и запускаем серверную часть:
А затем клиентскую часть:
Как мы видим, клиент успешно подключился к серверу через фронтенд-сервер CloudFlare с использованием веб-сокета. Для проверки что туннель работает именно как туннель можно сделать curl-запрос через локальный socks5, открытый на сервере:
Теперь посмотрим, что же DPI видит в канале связи:
Сначала туннелер, используя механизм DoH обращается к DNS серверу Cloudflare за eSNI ключами для домена назначения (пакеты №1-19), а затем обращается к фронтенд-серверу и устанавливает TLS-соединение, прикрываясь при этом доменом www.google.com (это значение по умолчанию, когда при запуске клиента не задан фейковый домен). Для указания своего фейкового домена необходимо использовать параметр -fronfDomain:
Теперь еще один момент. По умолчанию, в настройках аккаунта на CloudFalre установлен режим работы Flexible SSL. Это означает, что https-запросы к фронтенд-серверам Cloudflare от клиентов будут перенаправлены в нешифрованном (http) виде нашему серверу. Именно поэтому мы и запускали серверную часть туннелера в non-ssl режиме ( -listen ws:0.0.0.0), а не ( -listen wss:0.0.0.0).
Для того, чтобы переключиться в режим полного шифрования необходимо выбрать Full, или Full (strict) в случае наличия настоящего сертификата на сервере. После переключения режима мы сможем принимать подключения от CloudFlare по https протоколу. Не забудьте сгенерировать self-signed сертификат для серверной части туннелера.
Въедливый читатель спросит: «А как на счет клиента под Windows? Ведь наверняка, основное применение туннелера – поднимать бек-коннект с корпоративных машин и серверов, а там, как правило, всегда винда. Как мне скомпилировать туннелер под винду, да еще и со специфическим стеком TLS?» А теперь мы представим еще одну фишку, которая показывает, насколько удобен голанг. Компилируем под windows прямо из Kali, просто добавив параметр GOOS=windows:
GOARCH=amd64 GOROOT="/opt/tls-tris/_dev/GOROOT/linux_amd64" GOOS=windows go build -ldflags="-s -w"
Или 32-битный вариант:
GOARCH=386 GOROOT="/opt/tls-tris/_dev/GOROOT/linux_amd64" GOOS=windows go build -ldflags="-s -w"
Всё! И больше никаких заморочек не надо. Это реально работает!
Флаги компилятора –w и –s нужны для того, чтобы убрать лишний мусор из исполняемого файла, сделав его меньше на пару мегабайт. Дополнительно его потом можно упаковать с помощью UPX, чтобы еще уменьшить размер.
Вместо заключения
В статье мы, на примере написанного на голанге туннелера, наглядно показали применение новой технологии домен-фронтинга, реализованной на довольно интересной особенности протокола TLS 1.3. Аналогичным образом можно адаптировать существующий инструментарий написанный на голанге для работы через сервера CloudFlare, например Merlin — известный C2, или заставить CobaltStrike Beacon использовать eSNI домен-фронтинг при работе с Teamserver через External C2 Channel, реализованный на голанге, или же на стандартном C++ с использованием пропатченной версии OpenSSL, о которой мы рассказывали в прошлой части статьи. В общем, фантазии нет предела.
Пример с туннелером и CloudFlare представлен в виде концепции и пока что сложно сказать о далеких перспективах подобного вида домен-фронтинга. На данный момент поддержка eSNI реализована только у CloudFlare и, по идее, ничто не мешает им отключить подобного рода фронтинг и, например, разрывать tls-соединения при несовпадении SNI и eSNI. В общем, будущее покажет. Но пока что перспектива работы под «прикрытием kremlin.ru» выглядит довольно таки заманчивой. Не так ли?
Обновленный код туннелера, а так же скомпилированные исполняемые exe-файлы размещены в отдельной ветке проекта на github. Обо всех возможных проблемах туннелера лучше писать issue на странице проекта на GitHub.