Scapy p.09

Scapy and DNS

May - 2019 (~5 minutes read time)

We've been able to work with Ethernet, ARP, IP, ICMP, and TCP pretty easily so far thanks to Scapy's built in protocol support. Next on our list of protocols to work with are UDP and DNS.

DNS Request and Response

Using the sr1() function, we can craft a DNS request and capture the returned DNS response. Since DNS runs over IP and UDP, we will need to use those in our packet:

#! /usr/bin/env python3

from scapy.all import DNS, DNSQR, IP, sr1, UDP

dns_req = IP(dst='8.8.8.8')/
    UDP(dport=53)/
    DNS(rd=1, qd=DNSQR(qname='www.thepacketgeek.com'))
answer = sr1(dns_req, verbose=0)

print(answer[DNS].summary())

Console Output:

Begin emission:
..Finished to send 1 packets.
..*
Received 5 packets, got 1 answers, remaining 0 packets
DNS Ans "198.71.55.197"

The DNS layer summary is printed showing the IP address of the hostname requested

Without too much work we were able to write a short script to query some DNS name to IP address resolutions. Just think about what this means; we could read in a list of hostnames to resolve and send the IP addresses off to some function to do some more tests such as ping or TCP scans.

DNS Forwarding and Spoofing

Ok, so sending a DNS query was fun, but let's build on that. How about hand building a DNS service that can handle DNS forwarding, but with the added functionality of handing out a custom IP address for a certain domain name. This is similar to how the DNS server part of the PlexConnect utility works to hijack some communications from the AppleTV. This is going to jump up in complexity quite a bit from our previous example but the process is still pretty simple:

We're doing a lot of field replacements, especially on the handcrafted spoof response. All I did to figure out all those fields is capture a DNS response from a request I made and used the show() function to figure out what fields are expected in the DNS response.

#! /usr/bin/env python3

from scapy.all import DNS, DNSQR, DNSRR, IP, send, sniff, sr1, UDP

IFACE = "lo0"   # Or your default interface
DNS_SERVER_IP = "127.0.0.1"  # Your local IP

BPF_FILTER = f"udp port 53 and ip dst {DNS_SERVER_IP}"


def dns_responder(local_ip: str):

    def forward_dns(orig_pkt: IP):
        print(f"Forwarding: {orig_pkt[DNSQR].qname}")
        response = sr1(
            IP(dst='8.8.8.8')/
                UDP(sport=orig_pkt[UDP].sport)/
                DNS(rd=1, id=orig_pkt[DNS].id, qd=DNSQR(qname=orig_pkt[DNSQR].qname)),
            verbose=0,
        )
        resp_pkt = IP(dst=orig_pkt[IP].src, src=DNS_SERVER_IP)/UDP(dport=orig_pkt[UDP].sport)/DNS()
        resp_pkt[DNS] = response[DNS]
        send(resp_pkt, verbose=0)
        return f"Responding to {orig_pkt[IP].src}"

    def get_response(pkt: IP):
        if (
            DNS in pkt and
            pkt[DNS].opcode == 0 and
            pkt[DNS].ancount == 0
        ):
            if "trailers.apple.com" in str(pkt["DNS Question Record"].qname):
                spf_resp = IP(dst=pkt[IP].src)/UDP(dport=pkt[UDP].sport, sport=53)/DNS(id=pkt[DNS].id,ancount=1,an=DNSRR(rrname=pkt[DNSQR].qname, rdata=local_ip)/DNSRR(rrname="trailers.apple.com",rdata=local_ip))
                send(spf_resp, verbose=0, iface=IFACE)
                return f"Spoofed DNS Response Sent: {pkt[IP].src}"

            else:
                # make DNS query, capturing the answer and send the answer
                return forward_dns(pkt)

    return get_response

sniff(filter=BPF_FILTER, prn=dns_responder(DNS_SERVER_IP), iface=IFACE)

With this running on a host, I used the DNS dig utility to make some DNS requests:

localhost:~ packetgeek$ dig @172.16.20.40 www.thepacketgeek.com

; <<>> DiG 9.8.5-P1 <<>> @172.16.20.40 www.thepacketgeek.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29980
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;www.thepacketgeek.com.   IN  A

;; ANSWER SECTION:
www.thepacketgeek.com.  20411 IN  A 198.71.55.197

;; Query time: 90 msec
;; SERVER: 172.16.20.40#53(172.16.20.40)
;; WHEN: Thu Oct 10 19:39:38 PDT 2013
;; MSG SIZE  rcvd: 76

localhost:~ packetgeek$ dig @172.16.20.40 trailers.apple.com

; <<>> DiG 9.8.5-P1 <<>> @172.16.20.40 trailers.apple.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12688
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: Messages has 34 extra bytes at end

;; QUESTION SECTION:
;trailers.apple.com.    IN  A

;; ANSWER SECTION:
trailers.apple.com. 20  IN  A 172.16.20.40

;; Query time: 561 msec
;; SERVER: 172.16.20.40#53(172.16.20.40)
;; WHEN: Thu Oct 10 19:39:45 PDT 2013
;; MSG SIZE  rcvd: 104<

This example uses a lot of Python, so if you're not familiar with that take some time to look at the code and look up anything you don't know in the Python Docs.