웹 보안
Step 146. 인증 공격 종합 — 정문을 두드리는 네 가지 방법
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★☆☆ | 예상 소요 시간 4시간
전제: Step 122(온라인 브루트포스), Step 133(Burp Intruder), Step 134(쿠키와 세션 공격)를 마쳤다.
- 준비물: 파이썬 3 + Flask (로컬 재현), Burp Suite (랩), 텍스트 에디터
- 주의: ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
로그인은 공격 표면의 정문입니다. 지금까지 정문 공격의 부품들 — 브루트포스(Step 122), 세션 쿠키(Step 134), 프록시 가로채기(Step 132~133) — 를 따로 배웠습니다. 오늘은 그 부품들을 "인증 공격"이라는 하나의 체계로 조립합니다. 기본 자격증명 점검, 사용자명 열거, 브루트포스, 클라이언트 신뢰 결함 — 네 가지를 직접 만든 로그인 서버에서 연달아 실측하고, 각 공격에 맞서는 방어를 표로 정리합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 기본 자격증명 점검이 실전 침투의 "첫 5분"인 이유를 설명한다
- 로그인 실패 메시지 차이로 사용자명을 열거하는 원리를 실증한다
- 서버가 클라이언트 값(role, success 플래그)을 신뢰하는 설계 결함을 재현하고 우회한다
- 브루트포스에서 "성공 판별 기준"을 잡는 법을 안다
- 공격 4종과 방어(레이트 리밋, 잠금, 통일된 메시지, MFA)를 표로 매칭한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 + Flask (로컬 로그인 서버) / Burp Suite Intruder (랩) |
| 오늘의 명령 | 로그인 POST 반복, ?role=admin 파라미터 조작, 응답 메시지 비교 |
| 필요한 개념 | 기본 자격증명, 사용자명 열거, 클라이언트 신뢰 결함, 레이트 리밋 |
| 오늘의 산출물 | 인증 공격 4종 기록 + 공격↔방어 매칭 표 |
2-1. 기본 자격증명 — 공격의 첫 5분
실전 점검에서 로그인 창을 만나면 공격자가 제일 먼저 하는 일은 브루트포스가 아닙니다. admin/admin, admin/password, test/test를 손으로 넣어 보는 것입니다. 라우터, 관리 콘솔, 사내 도구에는 출고 때 비밀번호가 그대로인 경우가 지금도 흔합니다. 시도 비용은 몇 초, 성공하면 끝 — 비용 대비 효과가 가장 큰 공격이라 항상 첫 번째입니다.
2-2. 사용자명 열거 — 실패 메시지가 말해 주는 것
로그인에 실패했을 때 서버가 "존재하지 않는 계정입니다"와 "비밀번호가 틀렸습니다"를 구분해 말해 준다면, 공격자는 비밀번호를 하나도 몰라도 계정 존재 여부를 알아낼 수 있습니다. 이것이 사용자명 열거(username enumeration)입니다. 메시지가 같아도 응답 시간이나 응답 본문 길이가 미묘하게 다르면 역시 열거됩니다. 방어는 단순합니다 — 실패 메시지를 "계정 또는 비밀번호가 올바르지 않습니다" 하나로 통일하는 것.
2-3. 클라이언트 신뢰 결함 — 서버가 유리문인 경우
로그인 성공 후 서버가 {"success": true, "role": "user"}를 주고, 이후 요청에서는 클라이언트가 보내는 role 값을 그대로 믿는 설계가 있습니다. 공격자는 프록시에서 role=user를 role=admin으로 바꿔치기만 하면 됩니다. 클라이언트가 보낸 값은 전부 조작 가능하다 — Step 132에서 요청을 가로채 바꿔 본 여러분은 이미 압니다. 권한 판단은 반드시 서버 안의 세션 저장소에서 이루어져야 합니다.
2-4. 방어의 삼각 — 느리게, 멈추게, 모르게
인증 공격에 대한 방어는 세 층입니다.
- 레이트 리밋(rate limit): 시도 속도를 제한 — 브루트포스의 "시도당 비용"을 폭등시킵니다.
- 계정 잠금: 연속 실패 시 계정을 잠급니다 — 공격 중단 + 방어자에게 알림 (Step 122).
- 통일된 에러 메시지 + MFA: 열거를 막고, 비밀번호가 뚫려도 두 번째 문을 댑니다.
3. 따라 하기
3-1. 모사 대상 — 결함 네 개를 심은 로그인 서버
Step 122의 로그인 서버를 확장합니다. 계정 셋(admin/admin, alice/correct-horse-battery, test/test1234)과, 일부러 심은 결함 둘 — 에러 메시지 구분, 클라이언트 role 신뢰 (본 교재는 2026-09-09에 실측했습니다).
입력 (auth_lab.py의 핵심)
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
USERS = {"admin": "admin", "alice": "correct-horse-battery", "test": "test1234"}
@app.route("/login", methods=["POST"])
def login():
data = json.loads(request.data.decode())
u, p = data.get("username", ""), data.get("password", "")
if u not in USERS:
return jsonify({"success": False, "error": "존재하지 않는 계정입니다"}), 401
if USERS[u] != p:
return jsonify({"success": False, "error": "비밀번호가 틀렸습니다"}), 401
return jsonify({"success": True, "role": "user"})
@app.route("/admin")
def admin_panel():
role = request.args.get("role", "guest") # 취약: 클라이언트 값을 그대로 신뢰
if role == "admin":
return "관리자 패널 — flag{클라이언트_신뢰_결함}"
return "권한이 없습니다", 403
읽는 법: 결함은 두 군데입니다. ① 실패 이유가 메시지로 구분됩니다. ② /admin은 "누구인가"를 세션에서 찾지 않고 요청의 role 파라미터에서 읽습니다. 둘 다 실제 서비스에서 반복적으로 발견되는 고전입니다.
3-2. 기본 자격증명 점검 — 공격의 첫 5분
입력
for u, p in [("admin", "admin"), ("admin", "password"),
("test", "test"), ("test", "test1234")]:
code, body = post("/login", {"username": u, "password": p})
print(f"{u} / {p} -> {code}")
출력 (2026-09-09 실측):
admin / admin -> 200 {"role":"user","success":true}
admin / password -> 401 (실패)
test / test -> 401 (실패)
test / test1234 -> 200 {"role":"user","success":true}
읽는 법: 네 번의 시도 중 두 계정이 열렸습니다. 브루트포스 도구를 꺼내기도 전입니다. 실전 보고서에서 "첫 시도 조합으로 로그인 성공"은 가장 강력한 한 줄이며, 방어자에게는 "출고 비밀번호 변경 강제"라는 가장 값싼 조치가 됩니다.
3-3. 사용자명 열거 — 실패 메시지가 말해 주는 것
입력
for u in ["admin", "nosuchuser", "alice", "hacker"]:
code, body = post("/login", {"username": u, "password": "wrongpw"})
print(u, "->", body)
출력 (2026-09-09 실측):
admin -> {"error":"비밀번호가 틀렸습니다","success":false}
nosuchuser -> {"error":"존재하지 않는 계정입니다","success":false}
alice -> {"error":"비밀번호가 틀렸습니다","success":false}
hacker -> {"error":"존재하지 않는 계정입니다","success":false}
읽는 법: 비밀번호는 전부 틀렸는데, 응답이 계정 존재 여부를 정확히 알려 줍니다. admin과 alice는 "있다", nosuchuser와 hacker는 "없다". 공격자는 이제 열거된 계정에만 브루트포스를 집중할 수 있습니다 — 사전 공격의 표적 선별이 공짜로 된 것입니다. 메시지를 통일하면 이 채널은 닫힙니다.
3-4. 클라이언트 신뢰 결함 — role 바꿔치기
alice로 정상 로그인한 뒤, 관리자 페이지에 다른 role을 붙여 요청합니다.
출력 (2026-09-09 실측):
정상 로그인 응답: {"role":"user","success":true}
GET /admin?role=user -> 403 권한이 없습니다
GET /admin?role=admin -> 200 관리자 패널 — flag{클라이언트_신뢰_결함}
읽는 법: 로그인은 "user"로 했는데, 파라미터 하나로 관리자 패널이 열렸습니다. 서버가 "이 요청을 보낸 사람의 권한"을 세션 저장소에서 확인하지 않고, 요청 안의 값을 믿었기 때문입니다. Burp Repeater로 요청 한 줄을 고치는 일과 정확히 같은 공격입니다. 방어: 권한은 서버 측 세션에서만 읽고, 클라이언트 값은 어디에도 쓰지 않는 것.
3-5. 브루트포스와 성공 판별 기준
Step 122의 브루트포서를 이 서버에 돌립니다. 포인트는 "성공을 어떻게 판별하는가"입니다 — 응답 본문의 "success":true 문자열을 기준으로 잡습니다.
출력 (2026-09-09 실측):
[+] 7번째 시도에서 돌파! alice / correct-horse-battery
시도 7회, 0.06초, 시도당 8.9ms
읽는 법: 여기서 정직하게 고백할 것이 있습니다. 이 실측을 준비하다 판정 문자열을 "success": true(공백 포함)로 적었다가, 실제 응답은 "success":true(공백 없음)라서 성공을 놓치는 사고가 있었습니다. 성공 판별 기준이 한 글자라도 틀리면 브루트포스는 뚫고도 모릅니다. Burp Intruder에서도 같습니다 — 실패 응답의 길이를 먼저 재고, 길이가 다른 응답이나 리다이렉트(302)를 성공 후보로 표시하도록 설정하는 것이 핵심입니다.
3-6. 공격↔방어 매칭 표
오늘 실측을 표로 접습니다.
입력 (노트에 작성)
공격 | 오늘의 실측 | 방어 | 우회 가능성
기본 자격증명 | admin/admin 성공 | 초기 비번 변경 강제 | 낮음
사용자명 열거 | 에러 메시지로 구분됨 | 실패 메시지 통일 | 응답 시간 차이로 잔존 가능
클라이언트 신뢰 결함 | ?role=admin 통과 | 서버 측 세션에서 권한 판단 | 서버 설계만 바르면 없음
브루트포스 | 7번째에 돌파 | 레이트 리밋 + 잠금 + MFA | 느린 공격·분산 IP로 부분 우회
읽는 법: 오른쪽 열이 중요합니다. 완벽한 방어는 없고, 방어는 공격의 비용을 올리는 일입니다. 레이트 리밋도 "분산 IP로 천천히"라는 우회가 있지만, 그 순간 공격 비용은 며칠로 불어나고 로그는 길어집니다 — 탐지의 승리입니다.
4. 미션과 연습문제
미션 — 인증 공격 4종 세트
- 3-1의 로그인 서버를 만들고, 기본 자격증명 네 조합을 시도해 성공한 계정을 기록한다
- 사용자명 열거: 다섯 개의 후보 이름으로 실패시켜 "존재하는 계정" 목록을 만들어 본다
?role=admin조작으로 관리자 패널의 플래그를 획득한다- 브루트포스 판별 기준을 직접 정해(문자열 또는 응답 길이) alice 계정을 돌파한다
- 3-6 형식의 공격↔방어 표를 완성한다 — 우회 가능성 열 포함
연습문제
문제 1. 기본 자격증명 점검이 브루트포스보다 항상 먼저인 이유를 "비용 대비 효과"로 설명해 보세요.
문제 2. 실패 메시지를 통일했는데도 사용자명 열거가 가능할 수 있는 채널을 두 가지 말해 보세요.
문제 3. "클라이언트가 보낸 값은 전부 조작 가능하다"가 왜 인증 설계의 제1원칙인지, 오늘의 ?role=admin 실측과 연결해 설명해 보세요.
문제 4. 계정 잠금 정책이 공격자에게 주는 이중 타격과, 방어자가 주의할 부작용(정상 사용자 관점)을 말해 보세요.
해설과 완료 기준 · 펼치기/접기
5. 모범 답안과 완료 기준
미션 모범 답안
기본 자격증명에서 admin/admin과 test/test1234가 성공합니다. 사용자명 열거에서는 admin, alice, test가 "비밀번호가 틀렸습니다"로 살아 있는 계정임을 확인합니다. 플래그는 flag{클라이언트_신뢰_결함}입니다.
검증하는 법: ① 네 공격 각각의 실측 출력이 기록됐는가. ② 열거 실험에서 "없는 계정"도 대조군으로 넣었는가. ③ 브루트포스의 판별 기준이 명시됐는가. ④ 방어 표에 우회 가능성이 적혀 있는가.
연습문제 해답
문제 1 해답. 기본 자격증명 시도는 몇 초의 손동작이고, 성공 확률이 의외로 높습니다. 브루트포스는 수천 번의 시도·시간·로그 흔적이 듭니다. 비용이 가장 작고 효과가 큰 수단부터 쓰는 것이 공격의 기본 순서입니다.
문제 2 해답. 응답 본문의 길이 차이, 그리고 응답 시간 차이입니다. 존재하는 계정은 비밀번호 해시 비교까지 수행하느라 미세하게 느리고, 없는 계정은 일찍 거절됩니다. 메시지 통일은 필요조건이지 충분조건이 아닙니다.
문제 3 해답. 요청의 모든 필드 — 파라미터, 쿠키, 헤더 — 는 공격자의 프록시에서 자유롭게 수정됩니다. 오늘 ?role=user가 ?role=admin으로 바뀌며 관리자 패널이 열린 것이 그 증거입니다. 그래서 권한·가격·소유권 같은 판단은 반드시 서버가 보관하는 상태(세션 저장소, DB)에서만 내려야 합니다.
문제 4 해답. 공격자에게는 ① 시도가 물리적으로 멈추고 ② 잠금 이벤트가 로그·알림으로 방어자에게 통보되는 이중 타격입니다. 부작용: 공격자가 남의 계정을 일부러 반복 실패시켜 잠그는 서비스 거부 악용이 가능하므로, 실무에서는 잠금 대신 지연(대기 시간 증가)이나 출발지 차단을 조합합니다.
완료 기준 체크리스트
- [ ] 기본 자격증명 점검으로 성공한 계정을 기록했다
- [ ] 에러 메시지 차이로 사용자명 열거를 재현했다
- [ ] role 파라미터 조작으로 관리자 패널에 들어갔다
- [ ] 브루트포스의 성공 판별 기준을 직접 정해 돌파했다
- [ ] 실패 메시지 통일이 왜 방어인지 설명할 수 있다
- [ ] 공격 4종과 방어를 표로 매칭했다
- [ ] 각 방어의 우회 가능성까지 한 줄씩 적었다
6. 흔한 실수와 해결
벽 1. 브루트포스가 성공했는데도 "실패"로 끝난다
증상: 정답 비밀번호가 목록에 있는데 돌파 메시지가 안 뜹니다.
원인: 성공 판별 문자열이 실제 응답과 한 글자라도 다르면 안 됩니다. 오늘 실측 준비에서도 "success": true(공백)와 "success":true의 차이로 성공을 놓쳤습니다.
해결: 판별 기준을 정하기 전에 성공 응답 원문을 curl로 한 번 받아 그대로 베끼세요. Burp Intruder라면 "실패 응답의 길이"를 기준으로 잡는 것이 더 견고합니다.
벽 2. 사용자명 열거가 안 된다 — 메시지가 똑같다
증상: 어떤 이름을 넣어도 같은 에러가 나옵니다.
원인: 대상이 이미 메시지를 통일한 것입니다. 정상적인 방어입니다.
해결: 응답 본문 길이와 응답 시간을 재 보세요. 둘 다 같다면 이 채널은 닫힌 것 — 그것도 "방어가 작동함"이라는 기록입니다.
벽 3. ?role=admin인데도 403이다
증상: 파라미터를 바꿨는데 막힙니다.
원인: 서버가 세션에서 권한을 읽는 올바른 구현이거나, 파라미터 이름이 다릅니다.
해결: 로그인 응답과 이후 요청을 프록시에서 비교해, 권한 정보가 어느 필드(쿠키? 헤더? 파라미터?)로 오가는지 먼저 관찰하세요. 없다면 이 결함은 없는 것입니다.
벽 4. 공격 도중 전부 실패로 바뀐다 — 잠금 발동
증상: 어느 순간부터 모든 로그인이 실패합니다.
원인: 레이트 리밋이나 계정 잠금이 발동했습니다.
해결: 랩이라면 서버를 재시작하고 시도 간격을 늘리세요. "잠금이 발동했다"는 사실 자체가 방어 평가의 수확입니다.
벽 5. admin/admin이 바로 열려서 오히려 의심스럽다
증상: 첫 시도에서 성공합니다.
원인: 연습용 랩은 일부러 약하게 만들어져 있습니다. 실전에서도 기본 비밀번호는 단골 발견입니다.
해결: 정상입니다. 다만 기록하세요 — "첫 조합에서 로그인 성공"은 보고서의 첫 페이지감입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 기본 자격증명 | admin/admin류 출고 비밀번호 — 공격의 첫 5분 |
| 사용자명 열거 | 실패 메시지·길이·시간 차이로 계정 존재를 알아내는 기법 |
| 클라이언트 신뢰 결함 | role 같은 권한 정보를 클라이언트 값으로 판단하는 설계 오류 |
| 성공 판별 기준 | 브루트포스에서 성공 응답을 구분하는 기준(문자열·길이·리다이렉트) |
| 레이트 리밋 | 시도 속도 제한 — 브루트포스 비용 폭등 |
| 통일된 에러 메시지 | "계정 또는 비밀번호가 올바르지 않습니다" 하나로 열거 차단 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
POST /login (반복) |
기본 자격증명·브루트포스 시도 |
GET /admin?role=admin |
클라이언트 신뢰 결함 시험 |
| 실패 응답 본문 비교 | 사용자명 열거 |
| 응답 길이·시간 측정 | 메시지 통일 후의 잔여 채널 확인 |
Burp Intruder § 위치 지정 |
웹폼 브루트포스 (랩) |
명령어보다 중요한 감각
인증 공격의 네 수법은 따로 배운 기술이 아니라 하나의 질문에서 나옵니다 — "이 로그인은 무엇을 믿고 있는가?" 비밀번호를 믿으면 사전으로 두드리고, 에러 메시지를 믿으면 계정을 열거하고, 클라이언트 값을 믿으면 바꿔치기합니다. 방어 설계는 이 질문을 뒤집은 것입니다: 서버는 자기가 보관한 상태 외에는 아무것도 믿지 않는다.
그리고 오늘의 작은 사고 — 판별 문자열 하나 틀려 성공을 놓친 일 — 를 기억하세요. 공격 도구는 대답해 주는 사람이 아니라, 여러분이 정해 준 기준을 세는 기계입니다. 기준을 잡는 눈이 곧 실력입니다.
전부 체크되면 Step 146 완료입니다.
한 단계 더 가까이
수업을 마쳤나요?
본문의 완료 기준을 확인한 뒤 직접 표시하세요.