상세 컨텐츠

본문 제목

Boolean Blind SQLi로 한 글자씩 데이터 뽑아내기 — WHS 워게임 5단계

보안

by 코드스키 2026. 7. 26. 10:41

본문

화이트햇스쿨 2주차 웹해킹 기초 시간에 멘토님이 워게임 링크를 하나 공유했다. whs.sakuya.kr, Blind SQL Injection 1단계부터 5단계까지 풀어보는 사이트였다. 로그인부터 하고 시작하는 구조였는데, 단계가 올라갈수록 필터가 하나씩 더 붙는 식이라 처음엔 쉽다가 뒤로 갈수록 손이 많이 갔다.

Blind SQLi는 에러 메시지나 데이터를 직접 화면에 보여주지 않는 쿼리 인젝션이다. 대신 참/거짓에 따라 화면이 다르게 나오거나 응답 시간이 달라지는 걸 이용해서 데이터를 한 글자씩 추측해나간다. 이번 워게임은 Boolean 기반이라 응답 시간이 아니라 화면에 뜨는 결과(로그인 성공/실패, 참/거짓 메시지)가 참거짓 신호였다. 예를 들어 admin' AND SUBSTRING(password,1,1)='a 같은 조건을 걸어서 결과가 참으로 나오면 비밀번호 첫 글자가 a라는 걸 알 수 있는 식이다. 이걸 알파벳과 숫자 조합으로 26~62번씩, 비밀번호 길이만큼 반복하면 전체 값을 뽑아낼 수 있다.

 

Boolean Blind SQLi 워게임 대표 이미지
Boolean Blind SQLi 워게임 대표 이미지

 

1~2단계: 교과서대로 되는 구간

1~2단계는 필터가 거의 없어서 교과서에 나오는 방식 그대로 통했다. PortSwigger Academy의 설명과 거의 똑같은 원리다. 조건이 참인 요청과 거짓인 요청을 각각 보내서 응답이 어떻게 달라지는지만 확인하면 된다. 아래 이미지의 예시처럼 AND '1'='1은 참이라 정상 응답이 오고, AND '1'='2는 거짓이라 다른 응답이 온다. 이 참/거짓 한 비트를 반복해서 물어보는 게 Blind SQLi의 전부다.

 

PortSwigger Academy — 조건에 따라 응답이 달라지는 Blind SQLi 예시 (공식 문서)
PortSwigger Academy — 조건에 따라 응답이 달라지는 Blind SQLi 예시 (공식 문서)

 

3단계부터: 필터를 피해가는 표현 찾기

문제는 3단계부터였다. or라는 단어 자체를 막아놔서 ||로 바꿔야 했고, = 기호도 막혀서 IN으로 우회해야 했다. 이런 WAF 우회는 처음엔 감이 안 왔는데, 결국 SQL 문법에서 같은 의미를 다르게 표현하는 방법을 찾는 게임이라는 걸 알고 나니 패턴이 보이기 시작했다. AND 대신 &&, SUBSTRING 대신 MIDSUBSTR처럼 동의어가 있는 함수들도 마찬가지였다. 아래는 실제로 참고한 PortSwigger의 SQL Injection Cheat Sheet 일부인데, 같은 동작을 DBMS마다 다른 문법으로 표현할 수 있다는 걸 잘 보여준다. 필터를 하나 피하고 나면 그 필터가 정확히 어떤 키워드를 문자열 매칭으로 걸렀는지가 거꾸로 보이는 느낌이었다.

 

PortSwigger SQL Injection Cheat Sheet — DBMS별 문자열/서브스트링 문법 비교 (공식 문서)
PortSwigger SQL Injection Cheat Sheet — DBMS별 문자열/서브스트링 문법 비교 (공식 문서)

 

막힌 표현 우회 표현 비고
or || 단어 자체를 문자열 필터링
= IN (...) 등호 기호 차단
and && 동일 원리
SUBSTRING() MID(), SUBSTR() MySQL 동의어 함수

5단계: 이진 탐색으로 자동화

5단계에서는 손으로 한 글자씩 시도하는 게 도저히 불가능한 수준이라 자동화 스크립트(ch5.py)를 짰다. 무식하게 알파벳 순서대로 26번씩 시도하는 대신 이진 탐색을 썼다. 문자의 아스키 코드 값을 기준으로 > 중간값인지 물어보고, 참이면 위쪽 절반, 거짓이면 아래쪽 절반으로 범위를 좁혀나가는 방식이다. 알고리즘만 놓고 보면 이렇게 정리된다.

def find_char(oracle, position, lo=32, hi=126):
    # oracle(cond)는 조건이 참인지 서버에 물어보고 True/False를 반환
    while lo < hi:
        mid = (lo + hi) // 2
        cond = f"ASCII(SUBSTR(password,{position},1)) > {mid}"
        if oracle(cond):
            lo = mid + 1
        else:
            hi = mid
    return chr(lo)

아스키 코드 범위(32~126, 약 95개)가 넓지 않아서 한 글자당 대략 6~7번 요청이면 값을 확정할 수 있었고, 전체 비밀번호를 뽑는 데 대략 160번 정도의 요청이 들었다. 순차 탐색으로 했으면 한 글자에 최대 90번 넘게 요청을 보내야 했을 걸 생각하면 확실히 체감되는 차이였다. log₂(95) ≈ 6.6이라는 계산이 실제 요청 횟수로 그대로 나타난 셈이다.

자동화가 진짜 벽이었다

이 워게임을 풀면서 새삼 느낀 건, Blind SQLi가 어려운 이유가 SQL 문법을 몰라서가 아니라 자동화 없이는 사람이 감당할 수 없는 규모의 반복 작업이라는 점이었다. 조건 하나 만들고, 요청 보내고, 결과 보고, 다음 조건 만들고를 손으로 하는 건 1~2단계까지가 한계였다. 자동화 스크립트를 짜는 순간부터 문제가 완전히 다른 성격이 됐다. SQL 페이로드를 정확히 짜는 것보다, 요청-응답 루프를 안정적으로 도는 스크립트를 만드는 쪽에 더 공을 들이게 됐다. 세션 쿠키 유지, 응답 파싱 기준(어떤 문자열이 뜨면 참인지)을 정확히 잡는 것, 그리고 요청이 하나 실패했을 때 재시도하는 처리까지 신경 쓸 게 늘어난다.

사라질 워게임을 어떻게 남길까

아쉬운 점은 이 사이트가 곧 종료될 예정이라는 거였다. 멘토님이 공유해준 Notion write-up과 내가 짠 스크립트는 남아 있지만, 실제 서버가 내려가면 오프라인으로 다시 연습할 방법이 없다. PHP와 DB 소스가 따로 공개돼 있지 않으면 로컬 미러를 만들기도 어렵다. 예전에 다른 멘토님이 공유해준 mobilehacking.kr CTF는 로컬 미러를 따로 만들어둔 적이 있는데, 이번에도 비슷하게 미리 보존해뒀어야 했나 싶다. 워게임 사이트들이 언젠가 사라진다는 걸 당연하게 여기고, 풀 수 있을 때 스크립트와 페이로드를 잘 정리해두는 습관을 들여야겠다는 생각이 들었다.

같이 읽기

브라우저 DevTools만으로 숨은 단서를 추적한 다른 CTF 풀이도 있다. Cascading the Seven Seas: 브라우저 DevTools로 숨겨진 플래그 찾기.

공식 자료

 

OWASP Web Security Testing Guide — Testing for SQL Injection (WSTG-INPV-05)
OWASP Web Security Testing Guide — Testing for SQL Injection (WSTG-INPV-05)

 

기법 전체 맥락

PortSwigger Web Security Academy SQL injection 개요
PortSwigger Web Security Academy SQL injection 개요

Boolean blind는 SQL injection의 한 형태다. 응답 본문에 데이터가 직접 나오지 않을 때 참·거짓 차이를 관찰한다는 점이 핵심이며, 실서비스에서는 허가된 환경 외에 적용하면 안 된다.

PortSwigger SQL injection

관련글 더보기