BSidesSF CTF - DNSCap Walkthrough

Of all the BSidesSF CTF challenges, I think this one has to be my favourite. Combining a mix of packet capture analysis, scripting, frustration, and trying to beat the clock.

The brief provided by the challenge was quite straight forward:

Found this packet capture. Pretty sure there's a flag in here. Can you find it!?

We are provided with a PCAP file, which we will start analysing with TShark:

Straight away we notice the unusual DNS traffic within the capture file, with what appears to be subdomains encoded using hex. Let's extract the hex and see what, if anything, it decodes to:

tshark -r dnscap.pcap -Tfields -e dns.qry.name > names.txt

To decode the ASCII hex to raw, we can use the following Python script:

import re
import binascii

with open('names.txt', 'r') as f:
    for name in f:
        m = re.findall('([a-z0-9\.]+)\.skull', name)
        if m:
            print binascii.unhexlify(m[0].replace('.', ''))

Running this, we can see a few strings which look interesting:

Good luck! That was dnscat2 traffic on a flaky connection with lots of re-transmits. Seriously, good luck. :)

and

\x89PNG

So we now know that this is dnscat2 traffic, which lucky for us is very well documented by iagox32 on his github repo here. We will use this information to try and hunt for the PNG file which was transferred.

Reviewing the documentation, we see that dnscat2 traffic is encrypted by default, although this can be disabled. We know that encryption was likely disabled as we can see plain text traffic, so now all we need to do is to parse the traffic and output the contents... easier said than done :)

Before we get started, we need a way to parse the PCAP file in Python. To do this, I used Scapy's rdpcap class, which allows us to iterate through structured packets quite nicely. We also know that the file being exfiltrated will be sent from the client to the server, so we can focus on parsing traffic in this direction:

pkts=rdpcap(sys.argv[1]) 
for pkt in pkts:
    if pkt[UDP].dport == 53 and pky[IP].dst == "4.2.2.4":

Now we have a steady stream of DNS traffic, we need to unpack the data from the specification.

Within the documentation, we see that there are a number of packet types:

#define MESSAGE_TYPE_SYN    (0x00)
#define MESSAGE_TYPE_MSG    (0x01)
#define MESSAGE_TYPE_FIN    (0x02)
#define MESSAGE_TYPE_ENC    (0x03)
#define MESSAGE_TYPE_PING   (0xFF)

For the purposes of our parser, we will focus on MSG packets only. All packets share a common 24 bit header, which we will extract to determine the packet type:

(id, type) = struct.unpack(">Hb", data[0:3])

For MSG packets, the documentation shows the following structure:

(uint16_t) packet_id
(uint8_t) message_type [0x01]
(uint16_t) session_id
(uint16_t) seq
(uint16_t) ack
(byte[]) data

This can be easily extracted by expanding our unpack statement to the following:

(id, type, session_id, seq, ack) = struct.unpack(">HbHHH", data[:9])

Now we have extracted the meaningful fields, I usually find it helpful to output the contents so we can see what we are dealing with:

We see that session_id 65013 hold the bulk of the data, so this is the stream we will focus on with the best hopes of capturing this flag.

During the CTF, the plan was to parse packets for a matching session_id, and output the data to a file, reading the contents. Of course all I was greeted with was a corrupted binary blob, mostly because of the teaser provided within the ASCII dump we saw earlier, re-transmits.

Let's see just how many we are talking about, by printing out a hash of the data transmitted:

As we can see, there is are a lot of data transfers which contains identical hashes. Although this method isn't perfect, as it may be perfectly valid to transmit the same data more than once in sequence, time is against us during a CTF, so we will attempt to strip out anything which is duplicated.

After adding our simple checks, the final script is run, but we are still not able to view the PNG. Let's take a look at the file binary and see what may be happening:

flag

So we have a PNG header at offset 8 into the binary, let's strip the first 8 bytes:

dd if=/tmp/out.png of=/tmp/out2.png bs=1 skip=8

And opening the file, we find our flag:

flag

The final script can be downloaded from Github here.