Levels

Source Code

from flask import Flask, request import sqlite3 import re from argon2 import PasswordHasher app = Flask(__name__) @app.route("/") def index(): return "<h2>Levels</h2><ul><li><a href=/level1>Level 1: Classic</li><li><a href=/level1a>Level 1 – Advanced version</a></li><li><a href=/level2>Level 2: A naïve rewriter</a></li><li><a href=/level3>Level 3: Types</a></li></ul><h2>Source Code</h2><plaintext>" + open(__file__, 'r').read() @app.route("/level1") def login(): if "username" not in request.args: return "Login as admin to get the flag! <form action=/level1> Username: <input type=text name=username> Password: <input type=password name=password> <input type=submit value=Login>" username = request.args['username'] password = request.args['password'] # Fetch salt from user database conn = sqlite3.connect("level1.db") rows = conn.execute(f"SELECT salt FROM users WHERE user = '{username}'") r = next(rows) salt = r[0] hash_to_check = PasswordHasher().hash(password, salt=salt.encode('ascii')) # Check if user gave us the correct password rows = conn.execute(f"SELECT * FROM users WHERE user = '{username}' AND passwordhash = '{hash_to_check}'") if len(list(rows)) > 0: rows = conn.execute(f"SELECT flag from flags") r = next(rows) flag = r[0] return f"Login successful! Flag: {flag}" else: return "Login failed!" @app.route("/level1a") def login_slightly_safer(): if "username" not in request.args: return "Login as admin to get the flag! <form action=/level1a> Username: <input type=text name=username> Password: <input type=password name=password> <input type=submit value=Login>" username = request.args['username'] password = request.args['password'] # Fetch salted password hash from user database conn = sqlite3.connect("level1a.db") rows = conn.execute(f"SELECT passwordhash FROM users WHERE user = '{username}'") r = next(rows) password_hash = r[0] if PasswordHasher().verify(password_hash, password): rows = conn.execute(f"SELECT flag from flags") r = next(rows) flag = r[0] return f"Login successful! Flag: {flag}" else: return "Login failed!" @app.route("/level2") def a_naive_rewriter(): if "query" not in request.args: return "Give me a query of the form <tt>SELECT column1[, column2] FROM mixedbag</tt>. Note that columns with special characters such as spaces can be written as \"column with spaces\". Do not attempt to request the flag column, projection will be blocked! <form action=/level2> Query: <input size=50 type=text value=\"SELECT german_of_the_day FROM mixedbag\" name=query> <input type=submit value=\"Rewrite and execute\">" query = request.args['query'].lower() def parse_select_query(query): """ Parse a SELECT query of the form: select col1[, col2] from mixedbag. Ensures column flag cannot be projected. Returns a list of column names if valid, throws a ValidationException otherwise """ # Pattern for the overall structure with verbose mode pattern = r''' ^ # Start of string select # Literal "select" keyword \s+ # Required whitespace ( # Start capture group 1: column list (?: # Non-capturing group for a single column [a-zA-Z_][a-zA-Z0-9_]* # Unquoted identifier (starts with letter or underscore) | # OR "[^"]+" # Quoted identifier (double quotes with anything inside) ) (?: # Non-capturing group for additional columns \s*,\s* # Comma with optional whitespace (?: # Another column (same pattern as above) [a-zA-Z_][a-zA-Z0-9_]* | "[^"]+" ) )* # Zero or more additional columns ) # End capture group 1 \s+ # Required whitespace from # Literal "from" keyword \s+ # Required whitespace mixedbag # Literal table name $ # End of string ''' match = re.match(pattern, query, re.IGNORECASE | re.VERBOSE) if not match: raise ValidationError(f"query does not follow the required structure") # Extract the column list portion columns_str = match.group(1) # Find all columns (both quoted and unquoted) columns = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*|"[^"]+"', columns_str) # strip quotes from quoted identifiers columns = [col.strip('"') if col.startswith('"') else col for col in columns] if any(c == "flag" or c == "mixedbag.flag" for c in columns): raise ValidationError(f"flag column is not allowed in projection, columns parsed: {', '.join(columns)}") return columns try: columns = [f"[{c}]" for c in parse_select_query(query)] rewritten_query = f"SELECT {','.join(columns)} FROM mixedbag" print(rewritten_query) conn = sqlite3.connect("level2.db") rows = conn.execute(rewritten_query) return f'Result: <pre>{"\n".join(str(r) for r in rows)}</pre>' except Exception as e: return f'Error: {e}' @app.route("/level3") def types(): if "query" not in request.args: return "Give me a query of the form <tt>SELECT squirrel_name, flag FROM squirrels WHERE age < [a number]</tt>. Note that only really old squirrels carry the valuable flags. But you cannot SELECT those since we limit the number to a size of three. Or can you? <form action=/level3> Query: <input size=50 type=text value=\"SELECT squirrel_name, flag FROM squirrels WHERE age < 999\" name=query> <input type=submit value=\"Rewrite and execute\">" query = request.args['query'].lower() def parse_squirrel_query(query): """ Parse a query of the form: select squirrel_name, flag from squirrels where age < [age] Returns the age value (max 3 chars) as float if valid, raises a ValidationError otherwise """ pattern = r''' ^ # Start of string select # Literal "select" keyword \s+ # Required whitespace squirrel_name # First column (literal) \s*,\s* # Comma with optional whitespace flag # Second column (literal) \s+ # Required whitespace from # Literal "from" keyword \s+ # Required whitespace squirrels # Literal table name \s+ # Required whitespace where # Literal "where" keyword \s+ # Required whitespace age # Literal column name \s* # Optional whitespace < # Less-than operator \s* # Optional whitespace (.{1,3}) # Capture group: any 1-3 characters $ # End of string ''' match = re.match(pattern, query, re.IGNORECASE | re.VERBOSE) if not match: raise ValidationError("query doesn't follow the necessary structure") # Extract the age value age_value = match.group(1) try: age_float = float(age_value) return age_float except Exception as e: raise ValidationError(f"problem converting age value {age_value} into a float: {e}") try: age = parse_squirrel_query(query) rewritten_query = f"SELECT squirrel_name, flag FROM squirrels WHERE age < {age}" print(rewritten_query) conn = sqlite3.connect("level3.db") rows = conn.execute(rewritten_query) return f'Result: <pre>{"\n".join(str(r) for r in rows)}</pre>' except Exception as e: return f'Error: {e}' @app.route("/.well-known/aws/securityagent-domain-verification.json") def domain_verification(): return '{ "tokens": ["mRuM4TZpdw0NyDcJgPzzEw", "0kYh27yH0DXiTtXT51Xqlw"] }' class ValidationError(Exception): def __init__(self, message): self.message = message