Making a DNS Query with scapy

Crafting a DNS request

In scapy, you can use "/" to stack up the packets in different layers.


#!/usr/bin/python3
# dns_pkt.py

from scapy.all import *

# IP layer packet. Need to set only the destination addr
# 208.80.153.231 is ns1.wikimedia.org
ip = IP(dst="208.80.153.231")

# UDP layer packet. Need to set only the detination port
udp = UDP(dport=53)

# DNS layer packet. 
# Need to set qd (question section). Set qname only 
dns = DNS(qd=DNSQR(qname="en.wikipedia.org"))

# stack up the layers
req = ip/udp/dns
# print the packet 
req.show()

# send-and-receive once (sr1 is a great scapy function)
ans = sr1(req)

# print the answer packet
ans.show()

DNS packet construction: DNS and DNSQR

According to the scapy documentation, the member variable qd for the question section is of type DNSQR. Here, QR stands for "Question Record".
Note: In the lab, you need to spoof the answer section, which uses type DNSRR
In the documentation: 
 Search for "DNS(", "DNSQR(", and "DNSRR(" 

sr1() function

For sending and receving packets using function sr1(), you can check this documentation for more detail.
$ sudo ./dns_pkt.py
##[ IP ]### ...(omitted)...
###[ DNS ]### 
        id        = 0
        qr        = 0
        opcode    = QUERY
        aa        = 0
        tc        = 0
        rd        = 1
        ra        = 0
        z         = 0
        ad        = 0
        cd        = 0
        rcode     = ok
        qdcount   = 1
        ancount   = 0
        nscount   = 0
        arcount   = 0
        \qd        \
         |###[ DNS Question Record ]### 
         |  qname     = 'en.wikipedia.org'
         |  qtype     = A
         |  qclass    = IN
        an        = None
        ns        = None
        ar        = None

Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]### ...(omitted)...
###[ DNS ]### 
        id        = 0
        qr        = 1
        opcode    = QUERY
        aa        = 1
        tc        = 0
        rd        = 1
...(omitted)...
        \qd        \
         |###[ DNS Question Record ]### 
         |  qname     = 'en.wikipedia.org.'
         |  qtype     = A
         |  qclass    = IN
        \an        \
         |###[ DNS Resource Record ]### 
         |  rrname    = 'en.wikipedia.org.'
         |  type      = CNAME
         |  rclass    = IN
         |  ttl       = 86400
         |  rdlen     = None
         |  rdata     = 'dyna.wikimedia.org.'
        ns        = None
        ar        = None

Simple DNS Server with scapy

As a preparation for the lab that will have you launch a DNS cache poisoning attack, let's write a simple DNS server.

We will simulate ns1.wikimedia.org. Recall the dig result we saw:

$ dig @ns1.wikimedia.org en.wikipedia.org

; <<>> DiG 9.11.3-1ubuntu1.13-Ubuntu <<>> @ns1.wikimedia.org en.wikipedia.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27943
;; 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: 1024
; COOKIE: 2db0c1bf9d498a555cbc370a3e8ec13b (good)
;; QUESTION SECTION:
;en.wikipedia.org.              IN      A

;; ANSWER SECTION:
en.wikipedia.org.       86400   IN      CNAME   dyna.wikimedia.org.

;; Query time: 43 msec
;; SERVER: 208.80.153.231#53(208.80.153.231)
;; WHEN: Wed Jul 27 16:03:26 EDT 2022
;; MSG SIZE  rcvd: 94

Step 1. Creating a socket and receive packets

In IT430, we learned how to create a UDP socket and receive packets. We will not use the standard DNS port 53; instead, we will use 3000.

#!/usr/bin/python3
# dns_server.py

from scapy.all import *
from socket import *

sock = socket(AF_INET, SOCK_DGRAM)
sock.bind( ("0.0.0.0", 3000) )

pkt, addr = sock.recvfrom(4096)
Note that pkt in the above contains a DNS message. Therefore, we can construct a scapy DNS object from pkt as follows:

req = DNS(pkt)
req.show()
Now, let's run a dig command.
dig @127.0.0.1 -p 3000 en.wikipedia.org
Note -p option is used to specifiy the special port 3000 that our name server uses.

The result is shown on the right.

$ sudo ./dns_server.py 
###[ DNS ]### 
  id        = 42534
  qr        = 0
  opcode    = QUERY
  ...(omitted)...
  nscount   = 0
  arcount   = 1
  \qd        \
   |###[ DNS Question Record ]### 
   |  qname     = 'en.wikipedia.org.'
   |  qtype     = A
   |  qclass    = IN
  an        = None
  ns        = None
  \ar        \
   |###[ DNS OPT Resource Record ]### 
   |  rrname    = '.'
   |  type      = OPT
   |  rclass    = 4096
   |  extrcode  = 0
   |  version   = 0
   |  z         = 0
   |  rdlen     = None
   |  \rdata     \
   |   |###[ DNS EDNS0 TLV ]### 
   |   |  optcode   = 10
   |   |  optlen    = 8
   |   |  optdata   = '5\x9deP\x82e\xa2A'

Step 2. Question and Answer Section

We would like to the DNS reply message contain a question section and an answer section. There is an additional section containing DNS cookie, but we choose not to include it in our reply.

For the question section, we can simply copy the question section from the request packet (req.qd)


ans_qd = req.qd

For the answer section, we need more work. First, we need to use DNSRR (reponse record) class for this section.


class scapy.layers.dns.DNSRR(_pkt, /, *, rrname=b'.', type=1, rclass=1, ttl=0, rdlen=None, rdata=None)
Recall we need to reply with something like the following:
en.wikipedia.org.       86400   IN      CNAME   dyna.wikimedia.org.
Therefore, we construct the answer section as follows:

ans_an = DNSRR( rrname = "en.wikipedia.org", ttl=86400, type="CNAME", rdata="dyna.wikimedia.org")

Step 3. Finish the DNS packet construction

Building on top of what we have, we can finish the DNS packet construction. Note we need to use the DNS class for this.

class scapy.layers.dns.DNS(_pkt, /, *, length=None, id=0, qr=0, opcode=0, aa=0, tc=0, rd=1, ra=0, z=0, ad=0, cd=0, 
  rcode=0, qdcount=None, ancount=None, nscount=None, arcount=None, qd=DNSQR(), an=None, ns=None, ar=None)
For the most parts, we can just use the default values except for the fields important to us:

ans = DNS( id = req.id, qr=1, aa=1, qdcount=1, ancount=1, qd=ans_qd, an=ans_an)

Step 4: Putting it all together

Here is the full code.

#!/usr/bin/python3
# dns_server.py

from scapy.all import *
from socket import *

sock = socket(AF_INET, SOCK_DGRAM)
sock.bind( ("0.0.0.0", 3000) )

pkt, addr = sock.recvfrom(4096)

req = DNS(pkt)

# construct the DNS reply
# -- question section
ans_qd = req.qd
# -- answer section
ans_an = DNSRR( rrname = "en.wikipedia.org", ttl=86400, type="CNAME", rdata="dyna.wikimedia.org")
# DNS reply packet
ans = DNS( id = req.id, qr=1, aa=1, qdcount=1, ancount=1, qd=ans_qd, an=ans_an)

# send the reply back
sock.sendto(bytes(ans), addr)
Here are the resuts:
$ dig @127.0.0.1 -p 3000 en.wikipedia.org

; <<>> DiG 9.16.1-Ubuntu <<>> @127.0.0.1 -p 3000 en.wikipedia.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37357
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;en.wikipedia.org.		IN	A

;; ANSWER SECTION:
en.wikipedia.org.	86400	IN	CNAME	dyna.wikimedia.org.

;; Query time: 11 msec
;; SERVER: 127.0.0.1#3000(127.0.0.1)
;; WHEN: Thu Aug 04 19:26:07 EDT 2022
;; MSG SIZE  rcvd: 83

DNS Spoofing in the Local Network

Against an attacker who is on the path of the DNS query sequence (usually called an "on-path" attacker), DNS is completely insecure. Since everything is sent over plaintext, attacker can read the request, construct a malicious response message with malicious records and the correct ID field, and race to send the malicious reply before the legitimate response.

The DNS spoofing proceeds as follows:

  1. The attacker sniffs the DNS query from the user.
  2. The attacker quickly responds with address 20.20.20.20 (perhaps an address over which the attcker has control).
  3. Since the user already received the response, the response from the actual name server will be ignored.

DNS Cache Poisoning

Attack using DNS spoofing in the local network

In the above picture, if the user is changed to the local DNS server, the cache of the local DNS server will be poisoned. If the time-to-live (TTL) of the malicious records is set to a very high number, then the local DNS server (i.e., the victim) will cache those malicous records for a very long time. Now, for all subsequent DNS queries, this DNS server will give wrong answers based on the poisoned cache.

Attack using the Additional Section

There were some more subtle attacks that uses the additional section to poison the cache with malicious IP addresses. Suppose that there is a DNS server that handles the zone for *.real-website.com.
$ dig @192.176.10.30 www.real-website.come

...
;; ADDITIONAL SECTION:
www.real-website.com 		       172800   IN   A    192.176.10.31
www.google.com       999999   IN   A    123.123.123.123
...
If the querier is implemented to believe whatever information from a DNS response packet, this attack will be successful.
To prevent any malicious name server from doing too much damage, resolvers often implement bailiwick checking.
The word bailiwick means the district or jurisdiction (of a bailie or bailiff).
For example: The DNS resolver will ignore all the answers that fails this check.

DNS Attack: Pharming

In a Pharming attack, the adversary install malicious code in a DNS server to misdirect users to fraudulent websites without their knowledge or consent.

This can be done by exploiting vulnerabilities in the DNS protocol or by compromising insecure DNS servers and adding entries that redirect traffic. By causing the DNS server to give the user the incorrect answer, the pharmer can send users to a fake site for a malicious purpose.