Tags: cache flask 

Rating:

**Description**

> Dear all
>
> Welcome to our new enterprise solution.
> You can leave your business notes here.
> Data is safe - cause we use strong encryption !
> It is soooo safe, that even I am using this system.
> You even can audit the code [here](files/webapp.py)
> Plz DO NOT hax this - cause it is impossible !1111
>
> Regards
>
> Martin, the Webmaster
>
> URL: http://solution.hackable.software:8080

**Files provided**

- [`webapp.py`](https://github.com/EmpireCTF/empirectf/blob/master/writeups/2018-09-29-Teaser-Dragon-CTF/files/webapp.py)

**Solution** (by [Aurel300](https://github.com/Aurel300))

On our first visit to the webpage, we can only login or register. We can find out that the server is running Flask, although this is immediately obvious from the source code given.

The registration is normal enough:

![](https://github.com/EmpireCTF/empirectf/raw/master/writeups/2018-09-29-Teaser-Dragon-CTF/screens/3nterprise-1.png)

The login is slightly unusual though, it emulates a 2FA system:

![](https://github.com/EmpireCTF/empirectf/raw/master/writeups/2018-09-29-Teaser-Dragon-CTF/screens/3nterprise-2.png)

(No screenshot for the second form or anything else after, because the challenge is not working anymore. The form asks for the password and a 2FA token that is not actually implemented.)

Once we login, we can add notes, list all of our notes, and show specific notes. Whenever we look at a note, the browser first shows it encrypted, then it obtains our encryption key via AJAX, then proceeds to animate XOR-decryption of the note.

Even without looking at the source code, we can spot the first vulnerability: consecutive, non-encrypted IDs for notes. If we change the URL to `/note/show/0`, we get to see a note added by `admin`:

07D8B68CDB92A687DFC74217C9D7F47E84540A3C97BA3D2B8B5B3E1C110A4C54F09392
ADC910461BF61AA4AC6D921591556D1AAFCB8495144C27748369FC101847D7C2A9508F
6534FFB7BCF859FD3ED8863611400F9ECB56064C20EDF0B6F6B1BF1CBB522A91F0C9B2

The browser still animates XOR-decryption, but it uses our own key instead of `admin`'s, so the decrypted data is just garbage.

The note is 105 bytes, and our own key is only 20 bytes - perhaps this cipher could be broken? Well, after some playing around it is clear that the key is not 20 bytes, and is at least 100 bytes long. The simplest possible explanation is that the admin note is actually encrypted using proper [OTP](https://en.wikipedia.org/wiki/One-time_pad), unlike our own notes.

Now let's finally have a look at the source code. Most of it is basic Flask stuff. The `@loginzone` decorator is applied consistently, and it explains why `/note/show/0` was accessible to us - it simply checks IF whe are logged in, not WHO we are.

The one strange thing is in the bit that seemed unusual before - namely the two-step authentication process:

```python
# first part of authentication
@app.route('/login/user', methods=['POST'])
def do_login_user_post():
username = get_required_params("POST", ['login'])['login']
backend.cache_save(
sid=flask.session.sid,
value=backend.get_key_for_user(username)
)
state = backend.check_user_state(username)
if state > 0:
add_msg("user has {} state code ;/ contact backend admin ... ".format(state))
return do_render()
flask.session[K_LOGGED_IN] = False
flask.session[K_AUTH_USER] = username
return do_302("/login/auth")

#second part of authentication
@app.route("/login/auth", methods=['POST'])
def do_auth_post():
flask.session[K_LOGGED_IN] = False
username = flask.session.get(K_AUTH_USER)
params = get_required_params("POST", ["password", "token"])
hashed = backend.password_hash(params['password'])
record = sql_session.query(model.Users).filter_by(
username=username,
password=hashed,
).first()
if record is None:
add_msg("Fail to login. Bad user or password :-( ", style="warning")
return do_render()
# well .. not implemented yet
if 1 == 0 and not backend.check_token(username, token=1):
add_msg("Fail to verify 2FA !")
return do_render()
flask.session[K_LOGGED_IN] = True
flask.session[K_LOGGED_USER] = record.username
return do_302("/home/")
```

The `backend.cache_save(...)` call is odd. Even before we are properly logged in, the cache contains the encryption key for that username, if it exists. (Also note that the challenge is probably called `3NTERPRISE` because caching becomes relevant with large-scale projects.) We cannot simply call the first step and get the encryption key, however, since the `getkey` API endpoint does not rely on the cache, not to mention that it requires a full login (`@loginzone`).

```python
@app.route("/note/getkey")
@loginzone
def do_note_getkey():
return flask.jsonify(dict(
key=backend.get_key_for_user(flask.session.get(K_AUTH_USER))
))
```

But there is a place where the cached key is used:

```python
@app.route("/note/add", methods=['POST'])
@loginzone
def do_note_add_post():
text = get_required_params("POST", ["text"])["text"]
key = backend.cache_load(flask.session.sid)
if key is None:
raise WebException("Cached key")
text = backend.xor_1337_encrypt(
data=text,
key=key,
)
note = model.Notes(
username=flask.session[K_LOGGED_USER],
message=backend.hex_encode(text),
)
sql_session.add(note)
sql_session.commit()
add_msg("Done !")
return do_render()
```

So whichever key is in the cache (which may not be the one for our username!) will be used to encrypt the notes we submit. Since the encryption method is XOR, submitting a known plaintext will allow us to recover an unknown key.

But how to ensure a different key is cached?

The reason caching can be problematic is because there is a lot of things that can go wrong. The wrong user can be served personal details of another. Old information may be shown to the user, misinforming them. In the case of this challenge, the problem is time-based: there exists a race condition between the note adding (using the cached key) and the first step of the authentication (setting the cached key).

The first step of authentication is supposed to log out the user, which would prevent us from submitting notes and using the cached key. But before it logs us out, it puts the encryption key into the cached, and then does a state check of some sort, presumably a slow database operation.

So the plan is:

1. Login completely as our own user (`/login/user`, then `/login/auth`)
2. Do the first step of authentication as `admin` (`/login/user` again)
3. Create a known-plaintext note (`/note/add`)

The key is that 3 needs to happen a very short time after 2.

([Full exploit here](https://github.com/EmpireCTF/empirectf/blob/master/writeups/2018-09-29-Teaser-Dragon-CTF/scripts/3nterprise.sh))

During the CTF, there were some issues with the server being very slow (10+ seconds for a page load), so I didn't even try to exploit this. After some maintenance downtime, the service was slightly faster, though a page could still take up to 5 seconds to load, so I was somewhat sceptical. Much to my surprise, the exploit worked on the first try - there was a note encrypted with the admin key in the list of notes for our user. Then simply XORing that note with `aaa`... (which was the known plaintext) revealed the `admin` key, and XORing the `admin` key with the `admin` note revealed:

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

`DrgnS{L0l!_U_h4z_bR4ak_that_5upr_w33b4pp!Gratz!}`

Original writeup (https://github.com/EmpireCTF/empirectf/blob/master/writeups/2018-09-29-Teaser-Dragon-CTF/README.md#278-web--3nterprise-solution).