After my last report for work went out the door and my company entered its end-of-year shutdown period, I found myself at my parents house for several days for the holidays, relaxed and with nothing to do. I saw some people on Twitter talking about the SANS Holiday Hack Challenge, and decided I would finally give it a try.
I started on Christmas Eve and after several days of borderline dangerous obsessive completion-compulsion, I had solved all the challenges. All challenges were amazingly well done, and unlike some other CTFs I’ve participated in, they were all based on “real-world” examples and techniques which gave me a chance to practice and hone my skills and learn some new ones.
The challenge starts here:
So without further ado, here is my write up on solving the SANS Holiday Hack Challenge 2016.
Note: Sorry for the long writeup, but I wanted to try to fully capture my thought process on each of these challenges, and discuss the failures and rabbit holes I went down. I’m not a huge fan of solution writeups that just say “here’s the solution, all it takes is this oneliner”. They completely ignore all the buildup and discovery, which are super important parts for pentesters. We’re not infallible and we definitely don’t get the solution immediately and in one try.
Part 1: A Most Curious Business Card
I started the the hack challenge by entering the game world and finding my first clue, a business card from Santa:
- What is the secret message in Santa’s tweets?
When I saw his Twitter account, my heart sank a little since I hate crypto and stego challenges. But I wasn’t going to give up on step 1, so I figured the first step would be to grab all his tweets and run some frequency analysis and maybe some sort of code will come out of it.
I had heard of Tweepy before but hadn’t used it. I knew it had to be fairly straightforward to download a user’s tweets. I found this great walkthrough about data mining in Twitter using Tweepy. Then I actually found a public Gist which does exactly what I wanted. It needs Twitter API credentials, so I created a new Twitter app on https://apps.twitter.com/ to get an API key, API secret, and access token.
Once I had all that, I modified the script to dump tweets from @santawclaus (not @santawclause as I first tried - really confused why it wasn’t working):
The script downloaded the tweets to
santawslcaus_tweets.csv. The CSV format is
id,created_at,text so I used awk to ignore the first two fields and dump the text:
A “B”? Sweet it’s ASCII art! No crypto or stego needed :D Scrolling down through the tweet content, it spells out: BUGBOUNTY
- What is inside the ZIP file distributed by Santa’s team?
I was supposed to find a ZIP file, but I didn’t know where it was yet. I hadn’t looked at Santa’s Instagram account. There was one picture in particular that probably contained clues: https://www.instagram.com/p/BNpA2kEBF85/
I don’t think I ever realized until doing this challenge that Instagram doesn’t give you an easy way to download images locally. I used a free online tool to grab a local copy of the image:
Zooming in and looking at Santa’s laptop screen, I saw the tail end of a command that lists a ZIP file:
SantaGram_v4.2.zip. On the right side of the picture, tucked into Violent Python (great book, btw), was the top of an Nmap scan report for www.northpolewonderland.com.
Putting the two together, I ventured a guess and tried to download the Zip file from the URL:
Great - I found the Zip file! I can see that inside the Zip is an APK file, but trying to inflate it prompts for a password. I tried the secret message from the tweets (“BUGBOUNTY”) but no luck. Then I tried it lower case and it worked!
I now have the first two answers:
- What is the secret message in Santa’s tweets?
- What is inside the ZIP file distributed by Santa’s team?
An APK file: SantaGram_4.2.apk
Part 2: Awesome Package Konveyance
What username and password are embedded in the APK file?
What is the name of the audible component (audio file) in the SantaGram APK file?
Time for some Android analysis! First, I extracted the APK contents to look for the audible component. Although an APK is just a ZIP file, simply unzipping it won’t work as expected. A lot of the files, like the manifest, will still be encoded and unreadable. Instead, I used
apktool to extract and decompile all the contents:
Next, I used
find to list all the static files inside the newly created SantaGram_4.2 directory. I didn’t care about
.xml files right now, so I ignored them:
It was mostly image files (.png), but scrolling through the list I finally saw the audio file:
To find the embedded username and password I could search through the smali code since strings should be preserved, but I’d much rather look at Java. The in-game elves gave some great hints for this part, including using
jadx, but my favorite tool to use when looking at Android apps is ByteCodeViewer. I downloaded the latest release and launch it from the command line, specifying using additional memory:
When it opened, I simply dropped the entire APK file in and it decompiled everything. The Search feature was my friend here. I just searched for the string “password” and clicked through the files until I found what looked like some hard coded credentials for something related to Analytics:
Now I’ve answered the next 2 questions:
- What username and password are embedded in the APK file?
guest : busyreindeer78
- What is the name of the audible component (audio file) in the SantaGram APK file?
Part 3: A Fresh-Baked Holiday Pi
For Part 3, I had to go back into the game world and find all the pieces for the Cranberry Pi. This took me wayyyy longer than I’d care to admit and I almost gave up since I just wanted to get back to “hacking” :)
(hint: there’s a secret room behind a fireplace)
Once I had all the pieces, the elf at the beginning (Holly Evergreen) gave me a download link to the SD card image: https://www.northpolewonderland.com/cranbian.img.zip
One of the other elves gave a huge hint to a great blog post about mounting Raspberry Pi images. I followed this guide and got the image mounted in a Kali system.
First, I used
fdisk -l on the unzipped .img file to see what offset the filesystems were. There was a Linux filesystem at offset 137216. Since the sector size is 512 bytes, the true offset is at 70254592 bytes (512*137216).
Knowing that, I mounted the filesystem to a folder called
mnt with the following command:
Once it was mounted, I had full access to the filesystem under
Since I wanted the password to the
cranpi account, I pull it out of
/etc/shadow from the mounted file system:
The prefix on the hash (“$6”) means the format is sha512crypt. Since cracking on a VM is slow, I copied the entire hash back to my Mac and used
hashcat. It’s entirely possible to use
john, but I prefer
hashcat since it takes advantage of my MBP’s GPU.
Looking at the hashcat wiki I see that sha512crypt is mode 1800. One of the elves gave a hint in-game about using the wordlist “rockyou.txt”, which is included in the awesome Git repo SecLists. So putting it all together:
After about 2 minutes, my MBP spit out the cracked hash:
So the local account on the CranPi is:
cranpi : yummycookies
In game, I told the password to Holly Evergreen and she told me I now had access to the Cranberry Pi terminals.
Accessing the Terminals
Armed with the Cranberry Pi, I now had access to each terminal and could try to get the passwords needed to open the doors. These terminal “mini-games” were really fun and well-designed. I’ll outline how I got the past each one.
Elf House 2 Terminal - Peacoats and PCAPs
The first terminal was in the back of Elf House #2. The banner told me how to open the door:
Easy enough…but there’s immediately a problem. I don’t have permissions to read the file:
So I might need to elevate privileges?
In my talk at Thotcon and Derbycon this year, I talked about my favorite way to elevate privs - using misconfigured sudo commands. Running
sudo -l on this terminal showed me some interesting commands:
So I could run
strings as the user
itchy without requiring a password! My first thought was that I could use
tcpdump to run a command with the
-z option and get a shell as
itchy. There was a good discussion on this on SecLists here. Unfortunately I realized that I would need to capture packets in order to run the command, and the user
itchy doesn’t have permissions to capture on any interface, so I abandoned that idea and decided to just focus on the two commands I could run.
tcpdump isn’t really the best tool to examine pcap files (
tshark is what I really wanted), but I could do some basic investigation using builtin Linux tools. There’s a great SANS whitepaper on doing just that here.
First I scraped out the endpoints:
Seeing that there’s HTTP traffic, I then pulled out the HTTP requests:
So there were two HTTP requests, one for
firsthalf.html and another for
secondhalf.bin. Since it’s plaintext HTTP, I could use the
strings command to pull out the HTML for the first half:
So Part 1 was “santasli".
Part 2 took me a long time to figure out. I knew I probably had to carve out
secondhalf.bin to examine it. I looked around and realized there’s no real easy way to carve out a file using only
tcpdump. I found a Perl script here which can read PCAP files and carve out files, but even though Perl was installed, I couldn’t run it as
itchy to actually read the file.
Playing around, I realized that I could use
tcpdump to write the contents of
out.pcap to another file, and that file would have global read permissions:
Now I could run the Perl script as the
scratchy user and still access the file!
I copy/pasted to code, but when I tried to run it, none of the required Perl modules (like Net::Pcap) were installed - d’oh! Scratch that idea.
I got pretty frustrated and thought it was dumb I couldn’t use tshark or wireshark, so I started trying to see if I could pull the entire pcap file off to my local machine. The only real way I could do that was to base64 encode it and copy/paste from the terminal. The file is over 1 Mb and translated to about 19k lines of b64 encoded content. That seemed like big hassle and a lot of copy/paste so I revisited the two commands I could use, knowing I had to be missing something obvious.
Reading up on what
strings could do, I stumbled across this SANS blogpost called Strings are wonderful things. Reading it, it dawned on me I never tried looking for unicode strings on the pcap (strings defaults to ASCII). I ran the command from the blog and:
Ugh…I felt dumb. So simple. Lesson to self: always make sure to look for both ASCII and Unicode strings.
Combining the two parts, I had the password for the first console:
Workshop Terminal 1 - Gone Spelunking
The next terminal I tried was in Santa’s Workshop, next to the reindeer pen. The banner told me what’s needed:
There was a file called
wumpus in my home directory. The
file command was not present, but I could run
head to see the magic bytes and know that this was an ELF binary:
When running the binary and trying to view the instructions, it told me that the instructions file has “disappeared”:
Figuring the password was somewhere in the binary, I ran
strings but didn’t see anything that looked like a password. However, I did see some strings related to the instructions:
Just by looking at the strings, I could kind of guess that the binary is trying to open
PAGER string was probably an environment variable being read too.
I created a file called
wump.info in the same directory and ran the binary again, getting a different error:
So it was trying to run
less doesn’t exist. What about that
PAGER environment variable?
Interesting…I found a command injection vuln in Wumpus! But wait…it’s still running as the
elf user. D’oh! It’s not a
setgid binary! So I found a way to run commands as myself. That’s not gonna do me any good. Back to the drawing board!
gdb is not installed on the console, so I can’t run the binary in a debugger. But I could do some static analysis on it.
objdump are installed, so I decided to take a closer look at the binary. Fortunately the symbols haven’t been stripped, so I used
objdump to look at symbols in the
.text section (where code would be):
I saw the
instructions function, which is probably where I found that useless command injection vuln, and a potentially interesting function called
I assumed the point of the game was to kill the wumpus, so I decided to take a closer look at that function. I disassembled the binary to a text file using:
I wanted to use
vim to search and explore, but quickly realized there was no text editors installed in the console, so I used
more instead to look at the
I’m still pretty noobish when it comes to assembly, but what really jumped out at me was all the crazy
mov commands before calling
puts. It seemed to me maybe it was doing some sort of decryption/deobfuscation, which would explain why I didn’t see the password in the strings output.
At this point, I knew I needed to call the
kill_wump function, and also that I didn’t want to actually “play” the game to get there. If I had
gdb on this console it would be simple. Set a breakpoint on
main and then just
jump kill_wump. But how to jump to that function without a debugger? I’d have to modify the binary to call that function.
After looking around on the internet, I found a great resource about modifying ELF binaries and changing callq instructions.
At this point, I knew that the binary probably called the
instructions function when it started (or when I pressed ‘y’), and that I wanted it to instead call the
kill_wump function. Looking through the disassembly of
main, I saw the call to
instructions at offset
I wanted to change the address it calls from
kill_wump. The offset of
kill_wump I got from the symbols table above:
As the link I found pointed out, and what I didn’t know, was that the
callq operation jumps to a location relative to the location of the next instruction. I saw in the assembly that the next instruction after the
callq is located at
401013, so the difference between that and
402644 - 401013 = 1631, or
31 16 00 00 in little endian.
callq opcode (
e8) wouldn’t change, but the address needed to. So I needed to change the hex instruction on that line from
e8 8d 14 00 00 to
e8 31 16 00 00. This would cause the binary to jump to the start of
kill_wump instead of the start of
Knowing what I needed to do, I went to modify the binary and unfortunately realized I didn’t have any hex editing tools (or even a text editor like vim, for that matter) on the console. Ugh, why is nothing ever easy?!? Fortunately, I love “living off the land” and working with what I have, and noticed that perl is installed, as well as text manipulation tools like
So to modify the binary, I needed to dump it to hex, use
sed to replace the instruction I needed changed, and then decode the hex back into binary. My perl knowledge is non-existent, so I turned to a pentester’s best friend (Stackoverflow) and found out how to encode/decode files to/from hex with perl.
Putting it all together, I used a combination of perl and sed to modify the hex opcodes in the binary. Instead of calling the
instructions function, it would now call the
kill_wump function and hopefully spit out the password:
Jackpot! The game now jumped ahead to the end instead of asking me if I want instructions, and I had recovered the passphrase from the wumpus:
WUMPUS IS MISUNDERSTOOD
Phew! Probably would have been easier to just play the game (or yank the binary off the console and use a debugger), but this way was more fun and I learned a ton :)
Workshop Terminal 2 - The One Who Knocks
After spending a half day on Wumpus, it was nice to see a new challenge that seemed pretty straight forward:
Seemed simple enough. Often times on pentests when I gain access to someone’s home directory, I like to spit out a list of all files to see if anything jumps out at me as maybe containing passwords or other sensitive information. I use the surprisingly powerful
find command. The command will recursively search for everything under a directory. To see what files were under my current directory (using the
-type f option to only display files, not directories):
Ah there it is! I could try using
cat on it, but all those special characters would be a pain to escape on the command line. Instead,
find has the
-exec option, which executes a command on each file it finds. I’ll use
cat all files under the “.doormat” directory for me:
Done! The key for the next door is:
Sant’s Office Terminal - Chess?
Inside Santa’s office I found the next terminal. Instead of a shell prompt, I got a greeting. The first thing I tried was a “?” and got another response:
Turning to Google, I realized that “GREETINGS PROFESSOR FALKEN” is a reference to Wargames. I found the exact clips on Youtube where the main character is interacting with the computer:
If I freeze framed, I could see exactly what was typed on screen. The terminal was very picky on capitalization and punctuation, but I just kept typing out what Matthew Broderick did and eventually started the game:
Once the game started, I copied the moves from the movie. I chose Soviet Union, and when it came time to choose targets, I chose the first city chosen in the movie: Las Vegas.
The key for the door in Santa’s Workshop (which is behind the left bookshelf, btw) was:
LOOK AT THE PRETTY LIGHTS
Train Terminal - OUTATIME
The door behind Santa’s office did not have a console, so I saved that one for later and instead went to the last terminal by the train.
I was placed in a “Train Management Console”:
The first thing I did was read the HELP:
What immediately jumped out at me was the filename highlighted in the bottom left. I was inside the
less command. The clue on the 6th line confirms it (‘less’ is unnecessarily capitalized).
less enough to know there are several different commands you can use from within it. You can read more about
less, I can open and view other files with the
:e command. Tab complete even works when opening files, so I tab-completed to see what else was inside the
/home/conductor folder. There were two other files: “ActivateTrain” and “Train_Console”:
Opening that file revealed the password necessary to start the train:
It looked like a hash, but I verified in the code that when the comparison is made it was not hashing my input, but rather just doing a string comparison. So that was the actual password:
With that password, I could dis-engage the brakes and start the train to go back in time to 1978!
Once in 1978, I made my way back to Santa’s workshop and found him locked in the reindeer pen:
He didn’t remember how he had gotten there, so I still needed to find out how kidnapped him…
- What is the password for the “cranpi” account on the Cranberry Pi system?
- How did you open each terminal door and where had the villain imprisoned Santa?
See Above Sections :)
Part 4: My Gosh… It’s Full of Holes
Now that I had found Santa in-game, I had to compromise targets identified in the APK to recover the other audio files. The online write up outlines 6 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)
My first step was to identify where these servers are. Going back to my decompiled APK, I just did a recursive grep for “http://” and “https://” to see what URLs were hardcoded in the application. I found several URLs defined in
I verified the associated IP addresses of these URLs with Tom Hessman (as instructed) and had my list of servers to target:
- Analytics -https://analytics.northpolewonderland.com
- Dungeon - http://dungeon.northpolewonderland.com
- Debug - http://dev.northpolewonderland.com/index.php
- Ads - http://ads.northpolewonderland.com
- Exception Handler - http://ex.northpolewonderland.com/exception.php
Note: I’m going to write up my solutions in the order that I found them. I focused on the analytics server first and didn’t move on until I had both mp3 files from it
Analytics - Part 1
Visiting the analytics server at https://analytics.northpolewonderland.com/, I was presented with a login page:
Fortunately, I had already recovered some hardcoded credentials from the APK file way back in Part 2:
guest / busyreindeer78
Using these creds, I was able to login to the server and see a link to download an mp3 file:
Easy enough! Two mp3’s down (I had one from the APK), five to go.
Analytics - Part 2
At this point, I decided to keep focusing on the Analytics server. Seeing a PHP application with “Query” functionality just screamed SQLi to me so I explored that path.
I used BurpSuite to capture requests while I explored the site functionality. I was able to run custom queries by making POST requests like this:
Since I wasn’t really concerned about being stealthy or careful, I YOLO’d it and sent that entire request to SQLmap, certain I was about to dump the database through SQLi:
Imagine my shock when sqlmap came back with no vulnerable parameters! Well there was also a “view” functionality that takes a GET param. Surely that was the one? Nope, same results from sqlmap.
At this point I started to think maybe the SQLi (and I was still convinced it was SQLi) wasn’t that easy. I was still only the “guest” user, maybe I need to focus on privilege escalation.
I decided I needed to step back and do some more reconnaissance on the server. Maybe there was another service listening or some other pages on the site that I couldn’t reach through the home page. I was hoping for something like “/admin” or “/manage”, or an exposed API.
I ran an nmap scan with the
-A option to enumerate everything I could:
An exposed Git directory! I’ve seen this several times on external pentests, and it almost always leads to something good :)
I visited the site in my browser and was pleasantly surprised that not only was the .git folder present and unprotected, but nginx was indexing it for me:
Since Dir Indexing is enabled, it was easy to grab an entire copy of the .git directory. There’s an excellent writeup from Netspi on Dumping Git Data from Misconfigured Web Servers.
Had directory indexing not been enabled, it’s still possible to get some really good info from the directory. There’s a great tool called DVCS-Ripper that automates the process.
To pull down a local copy of the entire git repo, I used wget:
Then once, downloaded. I was able to
cd into the directory and do a
git reset --hard to get the latest source code for the web app:
Now that I had the source code, I started reading through it to better understand the app. Although there were a lot of SQL queries, the app seemed to always be using the
mysqli_real_escape_string function to prevent straightforward SQLi.
One page in the source code that I hadn’t seen yet was
edit.php. Trying to view it I got “Access denied!” and looking through the source, I see that only the “administrator” user would be able to access it.
At this point, I knew I had to elevate myself to the “administrator” user. Authorization was being handled through the “AUTH” cookie, and the username was encrypted in the value. The relevant PHP code was in the functions “get_username” in
A hardcoded encryption key! This key is used to encrypt and decrypt the AUTH cookie. I now had everything I needed to forge a cookie (hopefully). First, I decrypted my AUTH cookie for the “guest” account by importing
crypto.php in an interactive PHP session and following along with the above
Note: I had to
apt-get install php-mcrypt in Kali
Looked pretty straightforward. The cookie was just a JSON string. I changed the “username” to “administrator” and re-encrypted:
Armed with a new cookie, I used the Cookies Manager+ Addon to change my cookie:
I refreshed, and voila - a new page to attack!
This new page let me edit an existing query’s name and description by entering its ID. I got a query ID by running a launch query and checking “Save this query”. Viewing the results showed me the details, including the ID of the query I just ran:
I edited this query to change the name and description to “foobar”. The request looked like this:
What’s nice is the page actually shows you the SQL that was executed to modify the DB:
The SQL statement showed that the application is building an UPDATE query from my GET parameters.
Naturally, since I’m lazy, I YOLO’d it again and fed that request into sqlmap. Unfortunately I didn’t get any results. I was seriously starting to doubt this was a straightforward SQL injection, so I decided I needed to take a closer look at the source code, specifically for this new
edit.php page. Every SQL query was correctly using
mysqli_real_escape_string, so SQLi probably wasn’t possible. However, as I worked my way through the code I started piecing together what it was doing and noticed a vulnerability:
(I marked up the interesting bits and I’ll explain below)
- It takes the
idget parameter and pulls the first matching query from the database. If the row doesn’t exist, it aborts.
- It iterates over each value (column name) in the row and checks to see if there is a GET parameter with that name (
if(isset($_GET[$name]))). It prints “Checking for (columnName)” and “Yup!” if there’s a matching GET param.
- If there is a GET parameter that matches the column name, it adds the string “(columnName) = (getParamValue)” to a
- It builds a SQL UPDATE statement by joining all the strings in
$setwith a comma, and
- Adding a WHERE clause with the supplied
- It prints the statement and executes the query
The interesting part is that even though the “edit” page only lets you enter “name” and “description”, the code will actually iterate over all GET params and add them to the SQL statement if they match column names.
Since the SQL Schema is included in the source code (
sprusage.sql), I knew that the reports table had a column named “query”. It’s also verified on the output page of edit (“Checking for query…"). I assumed this “query” value was actual SQL that was executed, and verified it by looking at
view.php where it executes the SQL statement directly from the “query” value:
Using Burp repeater, I added a GET parameter for “query”. I decided to try a simple MySQL query to get the hostname of the server:
SELECT @@hostname; Since “query” is both a column name and a GET parameter now, the app added it to the UPDATE statement.Looking at the response, it looks like the SQL statement worked and I changed the query for the report with ID
Now all I had to do was the view the results of that report by visiting https://analytics.northpolewonderland.com/view.php?id=ae8ad368-46ff-4bad-88bd-fba7d055cafd
Woohoo! Arbitrary SQL execution! At this point my mind started racing and wondering if I could get local code execution by writing PHP or maybe reading sensitive system files, but then I remembered the whole point of this challenge was just to get an mp3 file…
Looking back at the SQL schema in the Git repo, I knew mp3 files were stored in a table called audio:
So I used Burp Repeater to update the query again for this report to be
SELECT * from audio; and this was the output:
Excellent, now I had the ID and filename for the audio file. I tried using the same
getaudio.php link I used for the first mp3 file and changing the ID, but got this:
Looking at the source code for
getaudio.php I realized the problem. I could only access the page if I was the “guest” user, and I couldn’t download the mp3 file I needed because the username defined in that row was “administrator”.
I got frustrated because I knew where the audio file was but couldn’t access it. Then I realized that in my report output, there was a column for “mp3” but no content was displayed. I assumed this was because the mp3 column was type
MEDIUMBLOB and the
format_sql command in
view.php didn’t know how to output it. I looked around online and realized that MySQL has a built in
HEX() function. So once again I used Repeater to change the report query to:
SELECT HEX(mp3) FROM audio WHERE username='administrator';. That should pull just the mp3 file I care about and hex encode it for me. Viewing the report again:
There we go! The hex encoded mp3 file! I viewed-source and copied the entire hex string to a file. Then simply used
xxd and got the raw mp3 file back!
Done with the analytics server!
Next I looked at the ads server located at http://ads.northpolewonderland.com/
Running it through Burp, I saw a request for a static JS file:
I had not heard of the Meteor Framework before, but one of the in-game elves gave some really good hints about it. Specifically, this blogpost about abusing misconfigurations:
Like most client-side app frameworks, if the developer isn’t careful, too much sensitive information can be sent from the server and stored client-side. The author of the blog released an awesome Tampermonkey script called MeteorMiner to help in parsing and viewing data from Meteor subscriptions.
I installed the extension and re-visited the page. I now had a pretty graphical representation of collections, routes, subscriptions, etc from the Meteor app:
Using my browser console, I could view the contents of the collections, for example “Quotes”:
These appeared to be the quotes that were displayed on the home page. I didn’t see anything interesting in either the “HomeQuotes” or “Satisfaction” collections, so I started to look at the other pages. The nice thing about MeteorMiner is that it lists the “Routes”, or pages that I could visit. I clicked through to each one and MeteorMiner kept updating with the collections/subscriptions on that page.
On the page “/admin/quotes”, I noticed a new Subscription: “adminQuotes”. Despite not being logged in and the page not displaying, it looked like Meteor was still fetching some data. I looked at the “HomeQuotes” collection again using my browser console:
It contained the path to the next mp3 file! http://ads.northpolewonderland.com/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3
The MeteorMiner extension and great SANS blogpost made this challenge pretty easy. I hadn’t known about Meteor before this, but now I can’t wait to come across it on an assessment and test this out “for real” :)
Next I targeted the “debug” server located at: http://dev.northpolewonderland.com/
I did some initial recon with nmap and nse scripts, but found nothing interesting. There was an “index.php” page but it was blank. I used Burp and tried various HTTP verbs with various payloads, but never got anything other than a blank 200 Response from the server.
I knew the Android application must communicate with the server (I got the URL from the APK after all), so I decided to do some dynamic analysis on the application. I was at my parents house and didn’t have an Android device with me, so I downloaded a personal edition of Genymotion to quickly spin up virtual Android devices. I used a virtual device with API level 18 (JellyBean) and installed SantaGram using adb:
I wanted to intercept all the HTTP/S traffic from SantaGram and hopefully see a request go to the Debug server. If you’re reading this and haven’t used Burp with an Android device before, I strongly recommend this blogpost from Bugcrowd.
Once I had SantaGram installed I opened it up and saw requests start coming through Burp. Phew! SantaGram doesn’t do cert-pinning :)
I was presented with a login page, but there was an option to create an account, so I registered a test account:
Once I had an account, I clicked around and on everything I possibly could. I updated my profile, I searched for people, and just generally tried to do absolutely everything the app let me. What I wanted was to generate as much traffic as I could in Burp to (hopefully) catch a debug request.
Sadly, in all my clicking, I never saw a single request to dev.northpolewonderland.com. That didn’t make sense to me - the URL was in the APK, when did it talk to the server?
I looked again at
strings.xml where the server was defined and saw the reason why:
Debug was set to “false” in the application. I bet if I switched it to “true” I would start seeing requests! To change it, I would need to modify the value in the XML value, re-compile the application, and re-install it on my device.
I’ve modified Android applications before (usually to disable cert-pinning) and always referred to this great blog series from SANS on modifying Android apps.
First, I used
apktool d to decompile the application, then modified the
strings.xml file to change the value of
<string name="debug_data_enabled">false</string> to “true”. Then I used
apktool b to re-build the application:
The rebuilt APK is in the
dist/ directory. But it can’t be installed until it’s signed. To sign it, I generated a self-signed cert using Java’s
keytool and then signed the APK using
Then I uninstalled the “stock” SantaGram app from my device, and installed the modified one using adb:
Now I used the app as before (clicking on everything and capturing traffic in Burp). This time, however, I finally saw a Debug request!
Excellent! Now I had a request I could replay to the server that actually gave me a response! I sent the request to Repeater and started tampering with some parameters. I wanted to see what was reflected back to me and if I could cause any verbose errors:
I never saw any errors, but noticed a few interesting things. The entire “request” was echoed back to me in the response, and there was reference to a “filename”. Pointing my browser to the filename, I saw the request echoed back to me in a txt file:
Alright, so I can write values to a text file on the server - so what? Looking at the response in Burp, the content-type is correctly set as
I went back to Repeater and started trying to inject different special chars hoping I could cause something interesting to happen. I was really hoping for a PHP error that leaked info or maybe a SQL exception. That’s when I noticed something I missed the first time. In the response, when it echoes back the “request”, it has a parameter named “verbose” that I didn’t send. Where did that come from?
On a hunch, I resent the same request but specified
Nice! Manually setting “verbose” to true spit out a bunch of information, including files on the server :)
Now I had the next mp3:
Exception Handler Server
Next up I decided to target the exception handler server:
Looking through my Burp state, I didn’t see any requests to that server from my Android device, so I was hoping there was enough to go on just looking at the server (unlike Debug where I needed to capture a valid request)
First I noticed when visiting the site that only POST requests were accepted:
Burp it is. I sent the request to Repeater and right clicked to select “Change Request Method”. Now I got an error:
That’s useful. So it’s expecting to receive some JSON data via POST. I set the content-type to “application/json”, sent some dummy JSON and got another error:
Very useful! These verbose error messages are telling me exactly what params I need to send. I added
"operation":"WriteCrashDump" and got an error that “data” was missing. So I added
"data":"foobar" and finally got a successful response:
It looked like I had written a PHP file to the “docs” folder? I confirmed it with my browser:
Yep! Looked like my “data” parameter. Knowing I could write to a PHP file in the web directory, the next thing I tried was sending
"data":"<?php echo 1;?>" to see if I could execute arbitrary PHP code:
No, I can see that it’s just returning the content of “data” wrapped up in quotes as HTML. I tried sending some special characters to see if I could “break out” of the string, but no luck.
Then I remembered there was another “operation” available to me when making POST requests:
ReadCrashDump. I changed the operation to “ReadCrashDump” and the server responded that another key was needed: “crashdump”. I set that one to “foobar” as well and got an interesting error:
It is set! Why doesn’t it recognize it? Unless it’s looking for it in the wrong place… It wanted a “data” parameter too…maybe “crashdump” needs to inside that one:
Well now it 500’s. But my hunch was confirmed - it didn’t complain about the “crashdump” key missing. Obviously, “foobar” isn’t a valid crashdump to read. Thankfully I had written one earlier (“crashdump-d6I79O.php”) so I tried to read it:
Oh cool! Now it complained about a duplicate PHP extension. That told me there was server-side code trying to add “.php” to the end of the value of “crashdump” and then trying to read it. Removing the “.php” from my crashdump parameter spits out the correct content. This had local file inclusion written all over it! First, I wanted to see if it honored path traversal using “..". Since I was reading crashdump files in the “docs” directory, could I read PHP files in the parent directory (the web root)?
Perfect, I could read files from the parent directory. If I removed the extension from “exception.php”, I would get a 500 error though and nothing would be displayed.
Talking to the elves in game, I got a big hint and a great resource about PHP LFI vulnerabilities in the form of another excellent SANS blogpost. In the post, the author talks about using the
php://filter/base64-encode/resource= stream filter to display contents as base64. As the post discussed, it can allow PHP source code to be exfiltrated instead of parsed and ran when doing LFI on PHP files. It sounded like exactly what I needed. I wanted to read the source code of
exception.php so I passed the php filter as the “crashdump” parameter to the “ReadCrashDump” operation:
w00t! It spit out a bunch of base64 encoded data. Highlighting it and sending it to Burp Decoder, I could see it was the source code of
And right there in a comment is the location of the next mp3 file :)
Six mp3s down, one more to go!
Note: This challenge by far took me the longest, but it turns out it took me so long because SANS was having some server issues…once the server was working I realized I had the solution pretty early on. Oh well :/
The challenge I saved for last was Dungeon. One of the elves gave you an “older” version of the Dungeon game, located here:
The URL from the APK pointed you to directions for the game:
Dungeon was an ELF64 binary and looked like a text based adventure game:
There was also another file in the Zip called
dtextc.dat. I wasn’t sure what kind of file it was or what data it contained yet, but if I removed or renamed it the Dungeon game wouldn’t work, so I knew it contained important information that was necessary for the game.
Just like Wumpus, the last thing I wanted to do was play a text based game, so I decided to cheat. Since I had a local copy of the binary this time, I threw it into Hopper to take a look at the disassembly. I figured the link to the next mp3 had to be somewhere in the binary. I looked the strings that Hopper found, but found nothing related to an mp3. One string jumped out as me (as it would to any good pentester): “You are not an authorized user”
The string above it looked like it could be a hardcoded password also. In Hopper, pressing ‘x’ on a string let’s you see where it is referenced and jump to it. I followed the reference and it was in a function called
I felt like this was an important function, so I started tracing it (by pressing ‘x’) to see when/if it was called from within the game. It’s called only once: inside the
game_ function at offset
4049a7 (highlighted in blue at the bottom):
Working my way up the instructions, I can see that it’s only called if a
strcmp comes back true (at
40499e). Since this is the main function of the game, and it’s calling
rdline_ earlier, I assumed this was checking my input on the main line. It’s not easy to tell just by looking at the assembly what it’s comparing with
strcmp, so I used a debugger (GDB). I use the awesome GDB Peda framework to show me much more relevant information such as register contents, stack contents, and guessed arguments for function calls.
I set a breakpoint on the
strcmp call at
40499e and ran the game. When I entered “?” and hit enter, my breakpoint hit and I could see what it was comparing my input ("?") against:
Duh, easy enough! It’s checking to see if I entered “GDT”. So that’s how you call this mysterious “GDT” function…by typing it’s dang name! (I kinda felt stupid for not guessing that…)
I ran the game normally this time and entered “GDT”. Then I tried “?” and “HELP” and got a list of commands I can use inside GDT (I also saw these in the strings in the binary):
Looking at what I could do, the command that seemed most valuable to me was “Display text”. I was still convinced the URL of the mp3 file was buried/encrypted/encoded somewhere in this game. Typing “DT” it prompted me for an “Entry”. I took a super scientific guess and entered “1”, and then “1000”:
Now these didn’t look too interesting at first to me, but then I realized I had never seen these strings before. They were not in the binary - they must be being read/decrypted from the .dat file!
Alright, I thought, the path to the mp3 file has to be somewhere in there and I though I could just use “DT” to print it out. I just needed to know the Entry number. Thinking it might be the very last one, I tried to find the maximum entry number I could enter.
First I tried 2000 and got a Segmentation Fault. Then I tried 1500 and got a Segmentation Fault. 1250? No SegFault, but nothing displayed. 1125? Nothing. 1063? Nothing. 1032? Nothing. 1016? Got a string. Then I just bounced between 1016 and 1032 until I found the “last” string, 1027:
Alright! That looks like the string that’s displayed when someone beats the game. I worked my way backwards a few entries, but still didn’t see any reference to an mp3. I was still convinced it had to be in there somewhere, so I decided to spit out every text entry between 1 and 1027.
Now instead of typing “DT” 1027 times, I wanted to script it. I thought at first about using GDB, then realized there was a much easier way using a tool that’s saved me many times: expect
expect lets you interact with programs on the command line and send/receive input. Since I knew exactly what to type to get the string displayed (“GDT\nDT\n1\n”) I could use
expect to send the keystrokes for me and display the output to the STDOUT. I had to google a little to figure out how to do a loop in
expect, but ultimately got a script together that would run “DT” 1026 times from the command line:
The script basically runs the dungeon binary, waits for the prompt (“>”), enters “GDT\n”, waits for that prompt, then loops through entering 1-1030 for “DT”. Everything is spit out to STDOUT so I used
tee to capture it to a file:
vim I stripped out the first few lines, and the “GDT>DT” and “Entry: " lines to just leave me with every string from the game.
I browsed through the file and was quite dismayed when there was no reference to an mp3 file :(
Instead, the only real interesting string that jumped out at me was:
Reading this I thought there might be an online version of this Dungeon game I needed to “play”. I never really explored the server
dungeon.northpolewonderland.com, so I kicked off an Nmap scan on the server:
Note: this is where I got stupidly stuck when I first tried it. Nmap didn’t return anything other than port 80 open and there was nothing in the webserver except the instructions. I went back into the online minigame thinking the game existed there, but couldn’t find it. So I turned back to the local copy and kept trying to reverse it looking for something else. Turns out there is an online version, SANS was just having network troubles and it was down when I ran nmap. I’m gonna skip ahead and pretend the service was up from the start for me
Port 11111 seemed odd, so I tried connecting with
There it is! An online version of the game. I checked and GDT worked and I could spit out strings. I wanted to dump everything from the online version and diff to my local version, so I wrote a Python script to repeatedly run “GDT>DT” (just like my expect script) but over a network socket:
I ran this script and tee’d the output to another file as well.
Now that I had all the strings from both the local version AND the online version, I used a visual editor to diff them and look at the differences. They were almost identical, but one change jumped out at me:
So instead of going online for the prize, I just “send email to email@example.com”.
I sent a blank email to that address (no subject, no body) and got an auto-reply!
YES! I now had the final mp3 and had completed all the challenge servers :D
Summary of Part 4
To summarize, I now had 7 audio files from the different targets:
SantaGram APK. The mp3 file discombobulatedaudio1.mp3 was inside the APK at
Analytics Server (via credentialed access). I used credentials hardcoded in the APK at
com/northpolewonderland/santagram/SplashScreen.classto login to https://analytics.northpolewonderland.com/. The mp3, discombobulatedaudio2.mp3 was linked on the home page
The Dungeon Game. Using a special function,
GDT, I was able to display decoded/decrypted text from the associated
.datfile. Strings pointed me to an online version of the game at
dungeon.northpolewonderland.com:11111. Using a Python script I dumped the strings from the online version using
GDTand saw instructions to email firstname.lastname@example.org. The auto-reply gave me discombobulatedaudio3.mp3 as an attachment.
The Debug Server. To generate debug requests, I modified
res/values/strings.xmlin the APK to change
true, then recompiled and self-signed the app. I ran it in Genymotion and captured debug requests to http://dev.northpolewonderland.com/index.php through Burp. I observed a parameter in the response called
verboseand when I replayed a request specifying that parameter and setting it to
true, the response listed files on the server, including debug-20161224235959-0.mp3
The Banner Ad Server. The ads server at http://ads.northpolewonderland.com/ used the Meteor Framework to run a client side application. I used MeteorMiner to view routes, and on
/admin/quotes, found a link to the mp3 file in the “HomeQuotes” collection which led me to discombobulatedaudio5.mp3
The Uncaught Exception Handler Server. Using verbose errors which told me what keys/params were missing, I was able to build valid POST requests to http://ex.northpolewonderland.com/exception.php. The
ReadCrashDumpoperation was vulnerable to Local File Inclusion when reading crashdump PHP files in the “docs” folder. I used the php filter technique here to read the source code for
exception.php. In a comment in the source was a link to discombobulated-audio-6-XyzE3N9YqKNH.mp3
Mobile Analytics Server (post auth). An unprotected and indexed Git directory, https://analytics.northpolewonderland.com/.git/, let me grab source code for the application. Using the hardcoded encryption key, I forged a cookie for the “administrator” user and gained access to the
edit.phppage. Looking at the source code for that page, I saw it was possible to modify the query for an existing report and execute arbitrary SQL statements. From the
.sqlfile, I knew the mp3 existed as a binary blob in the database, so I executed
SELECT HEX(mp3) FROM audio WHERE username='administrator';in a report to display and download a hex encoded version of discombobulatedaudio7.mp3
Part 5: Discombobulated Audio
I now had all seven mp3 fragments! I renamed the two files that had different filenames to match the “discomboluatedaudioX.mp3” format:
To combine them all, I simply used
cat and redirected output to a new file:
Listening to the combined audio file, it sounded like it had really been slown down. So I loaded it up in Audacity and looked at the waveform:
Within Audacity, I went to “Effect -> Change Speed” and sped it up by 400%. Unfortunately when I played it it still sounded awful. I realized that by changing the speed I was also changing the pitch so what I thought might be “voices” were just high pitched shrieks.
I Googled how to change speed without changing pitch and learned that Audacity has the feature built in called “Filter->Change Tempo”. I undo’d the change in speed and instead did a 400% increase on “Change Tempo”
This time when playing it, I understood voices! It was still a little slow and a bit distorted, but I could clearly make out some words:
“?? Christmas…Santa Claus…or as I’ve ?? known him…??”
There seemed like there was some background music too. It sounded like a quote from a movie, so I googled the words I could pick out:
The top result was a quote from an episode of Dr. Who called “A Christmas Carol”:
The Doctor: Father Christmas, Santa Claus. Or, as I’ve always known him, Jeff.
I’ve never watched Dr. Who, so I was hoping at this point I didn’t need to know anything else about Dr. Who and that this was all I needed to get through the final door.
I went back in the mini-game and went to the final locked door in the corridor behind Santa’s office. It prompted me for a password. I started taking some guesses:
- “Dr. Who”
- “The Doctor”
- “A Christmas Carol”
- “Tardis” (that’s a Dr. Who thing right?)
Nothing was working and I was starting to think I missed something. I was even considering watching the whole episode of Dr. Who to see if there was another reference or clue. Finally I just tried the whole quote, with punctuation:
- “Father Christmas, Santa Claus. Or, as I’ve always known him, Jeff.”
It worked! I got through the door, went up the ladder and met Santa’s kidnapper face to face:
Found him! It was Dr. Who who kidnapped Santa Clause. Talking to him he reveals why:
He just wanted to create a world where the “Star Wars Holiday Special” never existed and I foiled it. Sorry :/
- Who is the villain behind the nefarious plot.
- Why had the villain abducted Santa?
He wanted to use Santa’s magic to stop the “Star Wars Holiday Special” from being created. Santa didn’t agree with him so he kidnapped him.
So after countless hours, I had finally completed all the challenges and found Santa’s kidnapper.
I had an absolute blast playing the hack challenge. Ed Skoudis and the entire SANS team really went above and beyond to make an entertaining, but still realistic CTF style challenge. I especially liked that there was a great balance to the challenges: An android application, web apps, Unix command breakouts, binary exploitation, etc.
Anyways, if you made it this far, let me know if you have any questions! I really enjoyed playing the challenge and writing this massive post, and I hope somebody got something out of it. Also, if you found a different/better way to solve these let me know that too!