baby-sqlite
Enumeration
Reading app.py
reveals its mechanics.
- When first setup, DB is deleted and recreated.
- In recreation, uid of admin is not inserted.
- User inputs
uid
,upw
andlevel
are lower cased, and searched against certain keywords. - When keywords are not found in user inputs, inputs are used in a query to get the uid.
- If query result is
admin
, return the flag.
┌──(m0nk3y@kali)-[~/DH/baby-sqlite]
└─$ cat app.py
#!/usr/bin/env python3
from flask import Flask, request, render_template, make_response, redirect, url_for, session, g
import urllib
import os
import sqlite3
app = Flask(__name__)
app.secret_key = os.urandom(32)
from flask import _app_ctx_stack
DATABASE = 'users.db'
def get_db():
top = _app_ctx_stack.top
if not hasattr(top, 'sqlite_db'):
top.sqlite_db = sqlite3.connect(DATABASE)
return top.sqlite_db
try:
FLAG = open('./flag.txt', 'r').read()
except:
FLAG = '[**FLAG**]'
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
uid = request.form.get('uid', '').lower()
upw = request.form.get('upw', '').lower()
level = request.form.get('level', '9').lower()
sqli_filter = ['[', ']', ',', 'admin', 'select', '\'', '"', '\t', '\n', '\r', '\x08', '\x09', '\x00', '\x0b', '\x0d', ' ']
for x in sqli_filter:
if uid.find(x) != -1:
return 'No Hack!'
if upw.find(x) != -1:
return 'No Hack!'
if level.find(x) != -1:
return 'No Hack!'
with app.app_context():
conn = get_db()
query = f"SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};"
try:
req = conn.execute(query)
result = req.fetchone()
if result is not None:
uid = result[0]
if uid == 'admin':
return FLAG
except:
return 'Error!'
return 'Good!'
@app.teardown_appcontext
def close_connection(exception):
top = _app_ctx_stack.top
if hasattr(top, 'sqlite_db'):
top.sqlite_db.close()
if __name__ == '__main__':
os.system('rm -rf %s' % DATABASE)
with app.app_context():
conn = get_db()
conn.execute('CREATE TABLE users (uid text, upw text, level integer);')
conn.execute("INSERT INTO users VALUES ('dream','cometrue', 9);")
conn.commit()
app.run(host='0.0.0.0', port=8001)
Exploitation
In order to perform a SQL injection, there are a few hurdles we need to overcome.
uid
column fromusers
table does not contain the valueadmin
.- User inputs
uid
andupw
cannot be used in SQL injection since the input ‘ is filtered. - User inputs cannot contain any form of whitespaces, including spaces, tabs, and new lines.
admin
cannot be directly supplied as the wordadmin
is filtered.- UNION [ALL] SELECT is not an option since the word
select
is also filtered. - Chaining queries using ; is impossible as the execute() function in python’s SQLite module disables the usage of multiple queries by default.
- SQLite does not perform URL, hex, or unicode decoding by default.
Some of the hurdles can be bypassed using simple bypass techniques.
- User input
level
can be used to cause a SQL injection. - Whitespaces can be supplied by using comments (/**/).
admin
can be forged by using SQLite’s built-in function char() and concat operator (||).
With these bypasses, we only need to find a way to add admin
to our query result.
Since sending multiple queries is not an option, it seems finding a way to use UNION without SELECT is our best bet.
According to the SQLite’s official documentation, UNION statement should directly be followed by select-core
.
Searching for the select-core
reveals that it can start from values() instead of SELECT.
With all the information gathered, I’ll send a request that unions empty result for ‘uid’ with the word admin
.
Post Exploitation
After a successful exploitation, I’m able to view the flag.