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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.