Step 147. ★ DVWA 전 난이도 + 3단 정리표 — 취약점 하나를 "완전히" 안다는 것
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★★☆ | 예상 소요 시간 5시간
전제: Step 135~146을 마쳤다. DVWA의 Security 난이도(Low/Medium/High)를 바꿔 본 적이 있다.
- 준비물: DVWA가 뜬 랩, 파이썬 3 (sqlite3 — 방어 원리 실측용), 노트 앱
- 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
취약점 하나를 진짜로 안다는 것은 세 가지를 다 말할 수 있다는 뜻입니다. 왜 생기는가(원리), 어떻게 터는가(공격), 어떻게 막는가(방어). 지금까지 DVWA의 여러 모듈을 Low 난이도 중심으로 털어 왔다면, 오늘은 남은 모듈을 마무리하고 Medium·High의 방어 코드를 읽습니다. 그리고 그 지식 전부를 한 장의 "3단 정리표"로 접습니다 — 이 표는 앞으로 모든 웹 공부의 골격이 되는 평생 자산입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- DVWA 난이도가 올라갈 때 방어 코드가 어떻게 진화하는지 코드 수준에서 설명한다
- 같은 공격이 Medium·High에서 왜 막히는지 실측으로 확인한다
- 블랙리스트 방어의 한계(우회 사례)를 하나 이상 든다
- 취약점 8종 이상을 "원리 | 공격 | 방어" 3단 표로 정리한다
- 각 방어의 우회 가능성까지 평가하는 시각을 갖는다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | DVWA (워게임) + 파이썬 3 sqlite3 (방어 원리 실측) |
| 오늘의 명령 | DVWA Security 난이도 전환, "View Source" 버튼, sqlite3 파라미터 바인딩 |
| 필요한 개념 | 준비된 문장(prepared statement), 출력 인코딩, 화이트리스트, 블랙리스트의 한계 |
| 오늘의 산출물 | 취약점 3단 정리표 (최소 8행) |
2-1. DVWA의 난이도 구조 — 같은 취약점, 네 벌의 코드
DVWA의 각 모듈은 난이도마다 다른 서버 코드를 갖습니다. 왼쪽 메뉴의 "DVWA Security"에서 난이도를 바꾸고, 각 모듈 화면 아래의 "View Source"로 그 난이도의 코드를 읽을 수 있습니다. Low는 방어가 없고, Medium은 블랙리스트 필터를, High는 더 촘촘한 필터나 대체 구현을 둡니다. 공격만 하던 화면에서 코드를 읽는 화면으로 — 오늘의 전환점입니다.
2-2. 방어의 세 가지 근본 원리
함수 이름은 언어마다 다르지만, 방어의 원리는 셋뿐입니다.
- 준비된 문장(prepared statement): SQL의 뼈대와 데이터를 분리해 전달 — 데이터가 명령이 될 수 없습니다. SQLi 방어의 정답.
- 출력 인코딩:
<를<처럼 "화면 표시용 문자"로 바꿔 출력 — 스크립트가 태그로 해석되지 않습니다. XSS 방어의 정답. - 화이트리스트: 허용 목록 외 전부 거부 — Step 144의 파일 포함 방어와 같은 원리입니다.
2-3. 블랙리스트의 운명 — 왜 Medium은 뚫리는가
Medium 난이도의 전형은 블랙리스트입니다. <script> 문자열을 지우고, ;와 &&를 제거하고, 따옴표를 이스케이프합니다. 그런데 공격자는 <scr<script>ipt>(지우면 다시 합쳐짐), 따옴표 없는 숫자형 인젝션 같은 우회를 찾아냅니다. "나쁜 것의 목록"은 언제나 불완전합니다 — 오늘 실측에서 이것이 실제로 일어나는 것을 봅니다.
3. 따라 하기
3-1. 방어 난이도 실험 — sqlite3로 보는 Low/Medium/High
DVWA 없이도 방어 원리의 차이는 정직하게 재현됩니다. 파이썬 내장 sqlite3로, 같은 공격 문자열을 세 종류의 코드에 넣어 봅니다 (본 교재는 2026-09-09에 실측했습니다).
입력 (defense_levels.py의 핵심)
import sqlite3
db = sqlite3.connect(":memory:")
cur = db.cursor()
cur.execute("CREATE TABLE users (name TEXT, pw TEXT, role TEXT)")
cur.executemany("INSERT INTO users VALUES (?, ?, ?)", [
("admin", "s3cr3t-admin", "관리자"),
("alice", "alice-pass", "사용자"),
("guest", "guest123", "손님"),
])
ATTACK = "' OR '1'='1"
# Low: 문자열 이어 붙이기 (방어 없음)
q = f"SELECT name, role FROM users WHERE name = '{ATTACK}'"
print("Low :", cur.execute(q).fetchall())
# Medium: 따옴표 이스케이프 (' -> '')
escaped = ATTACK.replace("'", "''")
q = f"SELECT name, role FROM users WHERE name = '{escaped}'"
print("Med :", cur.execute(q).fetchall())
# High/Impossible: 준비된 문장
print("High :", cur.execute(
"SELECT name, role FROM users WHERE name = ?", (ATTACK,)).fetchall())
출력 (2026-09-09 실측):
Low : 실행된 쿼리: SELECT name, role FROM users WHERE name = '' OR '1'='1'
결과 3행: [('admin', '관리자'), ('alice', '사용자'), ('guest', '손님')]
Med : 실행된 쿼리: SELECT name, role FROM users WHERE name = ''' OR ''1''=''1'
결과 0행: []
High : 결과 0행: []
읽는 법: Low에서는 입력의 따옴표가 문자열을 닫아 버려 '1'='1'이 조건으로 실행됐고, 전 회원 3행이 털렸습니다. Medium의 이스케이프는 따옴표를 무효화해 막았고, High의 준비된 문장은 쿼리 구조 자체가 바뀌지 않아 막았습니다. 겉으로는 Medium과 High가 같은 결과지만, 막힌 방식이 다릅니다 — 바로 다음 실험에서 그 차이가 드러납니다.
3-2. 블랙리스트의 구멍 — 따옴표가 필요 없는 인젝션
Medium 이스케이프는 "따옴표가 있는 인젝션"만 막습니다. 숫자 자리라면 따옴표 자체가 필요 없습니다.
입력
uid_attack = "1 OR 1=1" # 따옴표가 없으므로 이스케이프가 무의미
q = f"SELECT item, price FROM items WHERE id = {uid_attack}"
print("실행된 쿼리:", q)
print("결과:", cur.execute(q).fetchall())
출력 (2026-09-09 실측):
실행된 쿼리: SELECT item, price FROM items WHERE id = 1 OR 1=1
결과: [('연필', 500), ('지우개', 700), ('비밀노트', 9999)]
읽는 법: 따옴표를 이스케이프하는 Medium급 방어가 숫자형 인젝션 앞에서는 완전히 무력합니다. 이것이 "블랙리스트는 불완전하다"의 실체입니다. 반면 준비된 문장은 이 경우에도 입력을 데이터로만 취급해 안전합니다. 방어 코드를 읽을 때 "무엇을 막았나"보다 "무엇을 못 막았나"를 묻는 습관 — 오늘의 핵심 감각입니다.
3-3. DVWA 남은 모듈 클리어 (워게임 실습, 출력 예시)
여러분의 DVWA 랩에서 진행하세요. 출력은 출력 예시입니다. 아직 못 끝낸 모듈(Brute Force, File Upload, CSP, JavaScript 등)을 마무리합니다.
Brute Force (Low): hydra로 웹폼 공격 — Step 122와 146의 결합입니다.
hydra -l admin -P rockyou.txt DVWA주소 http-get-form \
"/vulnerabilities/brute/:username=^USER^&password=^PASS^:F=incorrect"
[80][http-get-form] host: DVWA주소 login: admin password: password
File Upload (Low): 확장자 검사가 없어 .php 웹쉘이 그대로 올라갑니다 (Step 141~142 복습). Medium은 Content-Type 검사만 하므로 프록시에서 헤더만 바꾸면 우회됩니다.
읽는 법: 각 모듈을 클리어할 때마다 "View Source"를 열어 방금 통과한 공격이 코드의 어느 줄에서 허용됐는지 찾으세요. 이 습관이 3-4의 정리표 재료가 됩니다.
3-4. High 코드 읽기 — 방어의 진화를 추적한다
DVWA 모듈 하나를 골라 Low→Medium→High 코드를 나란히 읽습니다. 명령 인젝션 모듈의 예 (화면 예시):
// Low: 그대로 실행
$target = $_REQUEST['ip'];
$cmd = shell_exec('ping -c 4 ' . $target);
// Medium: 블랙리스트 치환
$substitutions = array('&&' => '', ';' => '');
$target = str_replace(array_keys($substitutions), $substitutions, $target);
// High: 더 긴 블랙리스트 ('|', '&', ' ', '$' 등 추가)
읽는 법: Low→Medium은 "아무것도 없음 → 나쁜 문자 지우기"의 진화이고, High는 그 목록을 늘린 것입니다. 그래서 High도 이론상 완벽하지 않습니다 — 목록에 없는 연결 문자(예: 줄바꿈 %0a)가 남을 수 있습니다. Impossible 코드에서는 explode로 IP 네 덩이를 쪼개 숫자인지 검사하는 화이트리스트적 검증이 등장합니다. 진화의 종착지는 언제나 "허용할 것을 정의하는 방식"입니다.
3-5. 3단 정리표 작성 — 오늘의 산출물
노트에 표를 만듭니다. 취약점 | 원리(한 줄) | 대표 페이로드 | 방어 코드/원리 | 우회 가능성.
입력 (작성 예 — 여러분의 표로 다시 만드세요)
취약점 | 원리 | 대표 페이로드 | 방어 원리 | 우회 가능성
SQLi | 데이터가 쿼리가 됨 | ' OR '1'='1 | 준비된 문장 | 준비된 문장이면 사실상 불가
XSS | 입력이 스크립트가 됨 | <script>alert(1)</script> | 출력 인코딩 | 컨텍스트별 인코딩 누락 시 존재
CSRF | 브라우저가 인증을 자동 첨부 | <img src=이전URL> | CSRF 토큰 | 토큰 검증 누락 엔드포인트
업로드 | 파일이 실행 가능 경로에 | shell.php 업로드 | 확장자 화이트리스트+저장소 분리 | 검사 우회(이중 확장자 등)
명령 인젝션 | 입력이 셸 명령이 됨 | 127.0.0.1; id | 인자 배열 전달(셸 경유 금지) | 블랙리스트면 잔존
LFI/RFI | 입력이 파일 경로가 됨 | ?page=../../../../etc/passwd | 경로 화이트리스트 | 화이트리스트면 불가
인증 | 기본 비번·열거·신뢰결함 | admin/admin | 잠금+통일 메시지+서버측 권한 | 느린 분산 공격은 잔존
정보 노출 | 남겨진 파일이 공개됨 | /.git/config | 배포 정제+접근 차단 | 새 경로는 계속 생김
읽는 법: "방어 원리" 열에 함수 이름이 아니라 원리(준비된 문장, 출력 인코딩, 화이트리스트)가 적혀 있어야 합니다. 함수는 언어가 바뀌면 달라지지만, 원리는 어디서나 같습니다.
4. 미션과 연습문제
미션 — 전 난이도 완주와 평생 자산
- 3-1의 sqlite3 실험을 직접 실행해 Low/Medium/High의 결과 차이를 기록한다
- 3-2의 숫자형 인젝션으로 "Medium급 방어의 구멍"을 재현하고, 준비된 문장으로 막히는 것까지 확인한다
- DVWA 미완료 모듈을 전부 클리어하고, 모듈마다 난이도별 "View Source" 코드 차이를 한 줄씩 적는다
- 3-5 형식의 3단 정리표를 완성한다 — 최소 8종, 우회 가능성 열 포함
- 표의 각 행에 대해 "내가 직접 재현해 본 것"과 "코드로만 확인한 것"을 구분해 표시한다
연습문제
문제 1. 준비된 문장이 SQLi를 원천 차단하는 원리를 "쿼리의 뼈대와 데이터의 분리"로 설명해 보세요.
문제 2. 블랙리스트 방어가 구조적으로 불완전한 이유를, 오늘 실측한 숫자형 인젝션 사례로 설명해 보세요.
문제 3. XSS 방어가 "출력 인코딩"인 이유 — 입력을 막는 것이 아니라 출력에서 처리하는 이유를 말해 보세요.
문제 4. "완벽한 방어는 없다"는 시각에서, 정리표의 "우회 가능성" 열이 왜 필요한지 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
sqlite3 실험의 기대 결과: Low 3행 유출 → Medium 0행 → High 0행, 단 숫자형 변형(1 OR 1=1)은 이스케이프 방어를 통과해 3행 유출, 준비된 문장에서는 0행 (2026-09-09 실측). 정리표는 3-5의 예시를 뼈대로 하되, 여러분이 실제로 클리어한 모듈의 페이로드와 코드 문구로 채워야 합니다.
검증하는 법: ① 실험 출력이 베껴져 있는가. ② 숫자형 우회까지 확인했는가. ③ 표가 8행 이상인가. ④ 방어 열이 "원리"로 적혀 있는가. ⑤ 직접 재현/코드 확인 구분 표시가 있는가.
연습문제 해답
문제 1 해답. 준비된 문장은 SQL 뼈대(SELECT ... WHERE name = ?)를 먼저 DB에 보내 문법을 확정하고, 데이터는 그 뒤에 "값"으로만 전달합니다. 입력에 따옴표가 있어도 이미 확정된 문법을 바꿀 수 없어, 데이터가 명령이 되는 길 자체가 없습니다.
문제 2 해답. 블랙리스트는 "나쁜 입력"을 미리 전부 열거해야 하는데, 공격 표현은 무한히 변형됩니다. 오늘 실측에서 따옴표 이스케이프(Medium급)는 따옴표 있는 인젝션을 막았지만, 따옴표가 필요 없는 숫자형 인젝션 1 OR 1=1에는 무의미했습니다. 목록이 불완전하면 방어가 불완전합니다.
문제 3 해답. 입력은 검색어·닉네임처럼 정상 용도로도 저장돼야 하므로 입력 단계에서 차단하면 정상 사용이 깨지고, 저장된 옛 데이터도 위험합니다. 위험은 "브라우저가 태그로 해석하는 출력 순간"에 발생하므로, 출력 시점에 <를 <로 바꾸는 인코딩이 정확한 방어 지점입니다.
문제 4 해답. 방어를 기록할 때 우회 가능성까지 적어야 "이 방어가 있으니 안전"이라는 착각을 피할 수 있기 때문입니다. 레이트 리밋은 분산 공격에, 필터는 변형 페이로드에 부분적으로 뚫립니다. 우회 가능성 열은 방어의 겸손이자, 다음 공격 기법을 공부할 목록이 됩니다.
완료 기준 체크리스트
- [ ] sqlite3로 Low/Medium/High 방어 차이를 직접 재현했다
- [ ] 숫자형 인젝션으로 블랙리스트 방어의 구멍을 확인했다
- [ ] DVWA 전 모듈을 클리어했다
- [ ] 모듈별 난이도 코드 차이를 "View Source"로 읽고 기록했다
- [ ] 취약점 8종 이상의 3단 정리표를 완성했다
- [ ] 각 방어의 우회 가능성을 한 줄씩 적었다
- [ ] 방어의 세 원리(준비된 문장·출력 인코딩·화이트리스트)를 말할 수 있다
6. 흔한 실수와 해결
벽 1. 난이도를 바꿨는데 공격이 그대로 통한다
증상: Medium으로 올렸는데 Low 페이로드가 먹힙니다.
원인: DVWA의 Security 설정이 저장되지 않았거나(쿠키 기반), 다른 모듈을 보고 있을 수 있습니다.
해결: DVWA Security 페이지에서 난이도를 바꾸고 Submit을 눌렀는지 확인 후, 모듈 화면의 "View Source"로 현재 코드가 진짜 Medium 코드인지 확인하세요. 코드가 바뀌지 않았다면 난이도가 안 바뀐 것입니다.
벽 2. "View Source"의 PHP 코드가 읽히지 않는다
증상: 코드를 봐도 어디가 취약한지 모르겠습니다.
원인: 정상입니다. PHP 문법에 익숙하지 않으면 코드가 벽처럼 보입니다.
해결: 한 줄씩이 아니라 "입력이 들어오는 줄"($_GET, $_REQUEST)과 "입력이 쓰이는 줄"(shell_exec, SQL문, echo)만 찾으세요. 그 사이에 아무 처리가 없으면 Low, str_replace 같은 치환이 있으면 블랙리스트입니다.
벽 3. Medium에서 필터에 막혀 진행이 안 된다
증상: <script>가 지워져서 XSS가 안 됩니다.
원인: 블랙리스트는 지운 뒤 재검사하지 않는 경우가 많습니다 — 그래서 <scr<script>ipt>처럼 가운데를 잘라내면 다시 합쳐지는 우회가 성립합니다.
해결: "서버가 뭘 지웠나"를 에코로 확인하고, 지워진 문자열을 페이로드 안에 겹쳐 넣어 보세요. 막히는 지점을 하나씩 지워 가며 확인하는 것이 필터 우회의 정석입니다.
벽 4. sqlite3 실험에서 Medium 결과가 0행이 아니다
증상: 이스케이프했는데도 결과가 나옵니다.
원인: 이스케이프 대상을 공격 문자열에 적용하지 않았거나, f-string 안에 원본을 넣었을 수 있습니다.
해결: "실행된 쿼리"를 반드시 출력해 눈으로 확인하세요. 쿼리문이 ''' OR ''1''=''1' 형태(따옴표가 두 개씩)인지 보는 것이 디버깅의 전부입니다.
벽 5. 정리표가 "배운 것 베끼기"가 된다
증상: 표는 채웠는데 설명하라고 하면 막힙니다.
원인: 직접 재현 없이 글로만 채운 행은 기억에 남지 않습니다.
해결: 각 행에 "재현한 날짜와 환경"을 적으세요. 재현 못 한 행은 "코드 확인만"이라고 표시하고, 시간이 날 때 하나씩 직접 손으로 확인해 나가면 됩니다. 표는 완성품이 아니라 평생 고쳐 쓰는 노트입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 준비된 문장 | 쿼리 뼈대와 데이터를 분리 — SQLi 원천 차단 |
| 출력 인코딩 | 출력 시점에 위험 문자를 표시용 문자로 변환 — XSS 방어 |
| 화이트리스트 | 허용할 것을 정의하고 나머지 전부 거부 |
| 블랙리스트 | 나쁜 것을 열거해 제거 — 불완전, 우회 존재 |
| 숫자형 인젝션 | 따옴표가 필요 없는 인젝션 — 이스케이프 우회의 고전 |
| 3단 정리표 | 원리-공격-방어(+우회 가능성)로 취약점을 정리하는 평생 자산 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
| DVWA Security → 난이도 변경 | 방어 코드 수준 전환 |
| 모듈 화면 "View Source" | 해당 난이도의 서버 코드 읽기 |
cur.execute("... WHERE name = ?", (입력,)) |
준비된 문장 (sqlite3 실측) |
입력.replace("'", "''") |
이스케이프 방어 (Medium 모사) |
1 OR 1=1 (숫자 자리에) |
이스케이프 우회 시험 |
명령어보다 중요한 감각
지난 챕터들에서 여러분은 공격자였습니다. 오늘 코드를 읽으며 시야가 하나 늘었습니다 — 같은 화면이 방어자에게는 "내 필터가 무엇을 못 막는가"의 문제라는 것. 공격과 방어는 한 취약점의 앞뒷면이고, 둘 다 보는 사람만이 "왜 막혔는가"를 설명할 수 있습니다.
3단 정리표는 여기서 끝나는 문서가 아닙니다. 앞으로 새 취약점을 만날 때마다 행을 추가하세요. 표가 길어지는 속도가 여러분의 성장 속도이고, 언젠가 이 표는 여러분이 다른 누군가를 가르칠 때의 교재가 됩니다.
전부 체크되면 Step 147 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.