Every Capture the Flag competition has to have an obligatory to-do list application, right???
Solution
Once again, we were welcomed with a login prompt. No sign of being able to access using SSTI or SQLi, I created an account and got logged in.
All functions work great, no sign of vulnerabilities when I tried to create, delete, modify the state of the to-do event. The bug here is the success notification:
There is a DB folder, and a db.sqlite database inside it. I can quickly grab the flag by calling strings DB/*. (I really wonder why I can not use cat DB/db.sqlite)
We got the flag.
Beyond the Flag
This is the page's source code:
from flask import (
Flask,
request,
session,
make_response,
render_template,
render_template_string,
redirect,
jsonify,
abort,
)
from models import db, User, Task
import re
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///DB/db.sqlite"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = ">HN&Ngup3WqNm6q$5nPGSAoa7SaDuY"
app.config["SESSION_COOKIE_NAME"] = "auth-token"
@app.before_first_request
def create_tables():
db.create_all()
db.init_app(app)
def current_user():
if "id" in session:
uid = session["id"]
user = User.query.get(uid)
return user
return None
def jinja_safe(s):
blacklist = [
"{{\s*config\s*}}",
".*class.*",
".*mro.*",
".*import.*",
".*builtins.*",
".*popen.*",
".*system.*",
".*eval.*",
".*exec.*",
".*\..*",
".*\[.*",
".*\].*",
".*\_\_.*",
]
temp = "(?:% s)" % "|".join(blacklist)
print(s)
if re.match(temp, s):
abort(
400,
"HACKER DETECTED!!!!\nThe folowing are not allowed: [ % s ]"
% ",".join(blacklist),
)
@app.route("/")
def home():
user = current_user()
if not user:
return redirect("/signin?error=Invalid session please sign in")
success = request.args.get("success", None)
error = request.args.get("error", None)
filter = request.args.get("filter", None)
tasks = user.tasks
if filter == "active":
tasks = [t for t in tasks if not t.completed]
elif filter == "completed":
tasks = [t for t in tasks if t.completed]
if success:
jinja_safe(success)
template = '<div class="text-success text-center mb-3">' + success + "</div>"
success = render_template_string(template)
if error:
jinja_safe(error)
template = '<div class="text-danger text-center mb-3">' + error + "</div>"
error = render_template_string(template)
resp = make_response(
render_template(
"index.html",
user=user,
tasks=tasks,
success=success,
error=error,
)
)
return resp
@app.route("/new", methods=["POST"])
def new():
user = current_user()
if not user:
return redirect("/signin?error=Invalid session please sign in")
if request.method == "POST":
task_name = request.form.get("task", None)
if not task_name:
return redirect("/?error=Missing task name")
task = Task(name=task_name, user_id=user.id)
db.session.add(task)
db.session.commit()
return redirect("/?success=Task created")
@app.route("/delete", methods=["GET"])
def delete():
user = current_user()
if not user:
return redirect("/signin?error=Invalid session please sign in")
if request.method == "GET":
task_id = request.args.get("id", None)
if not task_id:
return redirect("/?error=Missing task id")
task = Task.query.filter(Task.id == task_id, Task.user_id == user.id).first()
if not task:
return redirect("/?error=Task not found")
db.session.delete(task)
db.session.commit()
return redirect("/?success=Task deleted")
@app.route("/complete", methods=["POST"])
def complete():
user = current_user()
if not user:
response = make_response(
jsonify({"message": "You must be signed in"}),
403,
)
response.headers["Content-Type"] = "application/json"
return response
if request.method == "POST":
task_id = request.get_json().get("id", None)
completed = request.get_json().get("completed", None)
if not task_id or completed == None:
response = make_response(
jsonify({"message": "Missing parameters"}),
400,
)
response.headers["Content-Type"] = "application/json"
return response
task = Task.query.filter(Task.id == task_id, Task.user_id == user.id).first()
if not task:
response = make_response(
jsonify({"message": "Task not found"}),
400,
)
response.headers["Content-Type"] = "application/json"
return response
task.completed = completed
db.session.commit()
response = make_response(
jsonify({"success": True}),
200,
)
response.headers["Content-Type"] = "application/json"
return response
@app.route("/signup", methods=("GET", "POST"))
def signup():
if request.method == "POST":
username = request.form.get("username", None)
password = request.form.get("password", None)
password2 = request.form.get("password2", None)
if not username or not password or not password2:
return redirect("/signup?error=Missing parameters")
# Check if user exists
user = User.query.filter(User.username == username).first()
if user:
return redirect(
"/signup?error=This username is already taken please choose another one"
)
# Check if passwords match
if password != password2:
return redirect("/signup?error=Passwords do not match")
user = User(username=username, password=password)
db.session.add(user)
db.session.commit()
return redirect("/signin?success=User created successfully")
elif request.method == "GET":
success = request.args.get("success", None)
error = request.args.get("error", None)
return render_template("signup.html", error=error, success=success)
@app.route("/signin", methods=("GET", "POST"))
def signin():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
# Check if user exists
user = User.query.filter(
User.username == username, User.password == password
).first()
if not user:
return redirect("/signin?error=Invalid credentials")
session["id"] = user.id
return redirect("/")
elif request.method == "GET":
success = request.args.get("success", None)
error = request.args.get("error", None)
return render_template("signin.html", success=success, error=error)
@app.route("/signout")
def signout():
session.pop("id", None)
return redirect("/signin?success=Signed out successfully")