Webhacking.kr 쿠키 변조 / XSS 문제 풀이 기록입니다. 이번 시리즈 주제: 쿠키 위변조, 필터 우회, XSS 인젝션
혹시 쿠키가 뭔지는 아시나요?
서버가 클라이언트에게 "이 사람은 로그인한 사람이야"라고 표시해주는 작은 데이터 조각입니다. 문제는, 그 데이터를 클라이언트가 직접 들고 다닌다는 겁니다.
즉, 개발자 도구를 열면 지금 이 순간 내 쿠키를 마음대로 수정할 수 있습니다.
서버가 쿠키를 신뢰한다면? 그게 바로 이번 문제들의 핵심입니다.
1. Webhacking.kr — old-01
🔗 https://webhacking.kr/challenge/web-01/ 유형: WEB | 핵심: 쿠키값 변조
"소수점 하나가 문을 열어줍니다"
문제에 접속하면 view-source 링크가 있습니다. 친절하게 소스코드를 보여주는 문제입니다. 바로 확인해봤습니다.
if(!is_numeric($_COOKIE['user_lv'])) $_COOKIE['user_lv']=1; if($_COOKIE['user_lv']>=4) $_COOKIE['user_lv']=1; if($_COOKIE['user_lv']>3) solve(1);
조건을 정리하면 이렇습니다.
-
숫자가 아니면 → 1로 초기화
-
4 이상이면 → 1로 초기화
-
3 초과면 → solve
즉, user_lv이 3보다 크고 4보다 작은 값이어야 합니다. 정수로는 불가능합니다.
개발자 도구 → Application → Cookies에서 user_lv을 3.5로 바꿔줬습니다.
조건을 전부 통과하며 문제가 해결됩니다.
핵심 포인트
is_numeric()은 3.5, 3.1 같은 소수점도 숫자로 인식합니다. 정수 범위만 막고 소수점을 고려하지 않은 검증 로직의 허점입니다. 쿠키 값 검증은 타입과 범위를 동시에 처리해야 합니다.
2. Webhacking.kr — old-24
🔗 https://webhacking.kr/challenge/bonus-4/ 유형: WEB | 핵심: 쿠키 → 서버 변수 오염, str_replace 우회
"127.0.0.1을 직접 쓰면 막힙니다. 돌아가면 됩니다."
소스코드를 확인하니 핵심 로직이 두 줄로 요약됩니다.
extract($_SERVER); extract($_COOKIE); // 쿠키로 서버 변수를 덮어쓸 수 있음
extract($_COOKIE)가 extract($_SERVER) 이후에 호출됩니다. 즉, 쿠키에 REMOTE_ADDR 키를 넣으면 서버의 IP 주소를 덮어쓸 수 있습니다.
목표는 $ip == "127.0.0.1"을 만족시키는 것인데, 그냥 넣으면 아래 필터링에 걸립니다.
$ip = str_replace("12","",$ip); $ip = str_replace("7.","",$ip); $ip = str_replace("0.","",$ip);
127.0.0.1을 그대로 쓰면 12, 7., 0.이 전부 제거됩니다. 우회가 필요합니다.
풀이: 중첩 삽입으로 필터 우회
112277...00...00...1
str_replace는 한 번만 실행됩니다. 그래서 삭제될 부분을 안에 끼워넣으면 나머지가 합쳐져서 원하는 문자열이 됩니다.
-
1122→12제거 →12✅ -
77.→7.제거 →7.✅ -
00.→0.제거 →0.✅
최종적으로 127.0.0.1이 완성됩니다. 이 값을 REMOTE_ADDR 쿠키에 넣고 요청하면 해결됩니다.
핵심 포인트
extract()는 배열의 키를 변수명으로 만들어주는 함수입니다. $_COOKIE를 아무런 필터 없이 extract()하면 공격자가 서버 내부 변수를 마음대로 오염시킬 수 있습니다. 실제 서비스에서 이 패턴은 사용하면 안 됩니다.
3. Webhacking.kr — old-32
🔗 https://webhacking.kr/challenge/code-5/ 유형: WEB | 핵심: 쿠키 기반 중복 방지 우회, 자동화
"100번 클릭하면 됩니다. 직접 하시겠습니까?"
리더보드에서 본인 닉네임을 클릭하면 투표 수가 1 올라갑니다. 100에 도달하면 문제가 해결됩니다.
단, 한 번 클릭하면 vote_check 쿠키가 생성되어 이후 투표가 막힙니다.
중복 방지 로직이 서버 세션이 아닌 클라이언트 쿠키로만 구현되어 있습니다. 쿠키를 안 보내면 그만입니다.
풀이 스크립트
import requests URL = 'https://webhacking.kr/challenge/code-5/?hit=닉네임' cookies = {'PHPSESSID': '세션ID'} for i in range(100): res = requests.get(URL, cookies=cookies) print(f"[{i+1}/100] {res.status_code}")
vote_check 쿠키 없이 PHPSESSID만 담아서 100번 요청을 보냅니다. 스크립트를 돌리면 끝입니다.
핵심 포인트
중복 방지, 횟수 제한 같은 로직을 클라이언트 쿠키에만 의존하면 전혀 의미가 없습니다. 공격자는 쿠키를 삭제하거나 아예 안 보내면 그만이거든요. 상태 관리는 반드시 서버 사이드에서 해야 합니다.
4. Webhacking.kr — old-06
🔗 https://webhacking.kr/challenge/web-06/ 유형: WEB | 핵심: Base64 + 치환 인코딩 역산
"20번 인코딩을 풀면 됩니다. 손으로 하시겠습니까?"
소스코드를 보면 쿠키 값 생성 과정이 나옵니다.
-
guest/123qwe를 Base64로 20번 인코딩 -
숫자 → 특수문자로 str_replace 치환
-
그 결과를 쿠키에 저장
서버는 쿠키를 받아서 반대로 특수문자 → 숫자로 치환 후, Base64를 20번 디코딩합니다. 최종 값이 admin / nimda이면 풀립니다.
역산하면 됩니다. admin과 nimda를 Base64로 20번 인코딩하고, 숫자를 특수문자로 치환한 값을 쿠키에 넣으면 됩니다.
풀이 스크립트
import base64 def replace_it(data): data = data.replace("1","!") data = data.replace("2","@") data = data.replace("3","$") data = data.replace("4","^") data = data.replace("5","&") data = data.replace("6","*") data = data.replace("7","(") data = data.replace("8",")") return data ID = "admin" PW = "nimda" for i in range(20): ID = base64.b64encode(ID.encode("UTF-8")).decode("UTF-8") PW = base64.b64encode(PW.encode("UTF-8")).decode("UTF-8") ID = replace_it(ID) PW = replace_it(PW) print("user 쿠키:", ID) print("password 쿠키:", PW)
출력된 값을 user / password 쿠키에 넣고 새로고침하면 해결됩니다.
핵심 포인트
Base64는 암호화가 아닙니다. 인코딩입니다. 누구나 디코딩할 수 있고, 거꾸로 원하는 값을 인코딩해서 집어넣을 수도 있습니다. 쿠키 값을 Base64로 감싼다고 해서 보안이 생기는 게 아닙니다. 검증은 서버에서, 서명은 HMAC이나 JWT로 해야 합니다.
5. Google XSS Game — Level 1
🔗 https://xss-game.appspot.com/ 유형: WEB | 핵심: 기본 XSS
"가장 단순한 XSS입니다"
검색창에 입력값이 그대로 HTML에 반영됩니다. 필터링이 없습니다.
<script>alert(1)</script>
입력하면 alert가 뜨며 통과됩니다.
핵심 포인트
사용자 입력을 HTML에 그대로 출력하면 스크립트 태그가 실행됩니다. 모든 사용자 입력은 출력 전에 HTML 이스케이프(<, > 등) 처리가 필요합니다. 이게 XSS의 기본 중의 기본입니다.
6. Webhacking.kr — old-23
🔗 https://webhacking.kr/challenge/bonus-3/ 유형: WEB | 핵심: Null Byte Injection, eregi 우회
"영문자가 2개 붙으면 막힙니다. 사이에 끼워넣으면 됩니다."
code 파라미터로 값을 전달받아 화면에 출력하는 구조입니다. 목표는 <script>alert(1);</script> 를 실행시키는 것입니다.
문제는 영문자가 2개 이상 연속으로 나오면 필터링된다는 점입니다. script, alert 같은 단어가 전부 막힙니다.
eregi() 함수 기반 필터링인데, 이 함수는 Null Byte(%00)를 만나면 문자열 비교를 그 자리에서 멈춥니다. 영문자 사이마다 %00을 끼워넣으면 필터를 통과하면서 브라우저는 정상적으로 렌더링합니다.
풀이 페이로드
?code=<s%00c%00r%00i%00p%00t>a%00l%00e%00r%00t(1);</%00s%00c%00r%00i%00p%00t>
인접한 모든 영문자 사이에 %00을 삽입했습니다. eregi는 %00 앞까지만 비교하므로 필터를 통과하고, 브라우저는 Null Byte를 무시하고 스크립트를 실행합니다.
핵심 포인트
eregi()는 PHP 5.3에서 deprecated되어 현재는 사용이 권장되지 않습니다. Null Byte에 취약하다는 것도 그 이유 중 하나입니다. 필터링은 블랙리스트 방식보다 화이트리스트 방식이 훨씬 안전합니다. 허용할 것만 명시하는 것이 "막을 것을 전부 나열하는" 것보다 훨씬 견고합니다.
2차 회고
이번 시리즈를 통해 확실히 느낀 게 있습니다.
쿠키는 클라이언트가 들고 다닌다는 사실 자체가 취약점의 출발점입니다.
서버가 쿠키를 검증 없이 신뢰하는 순간, 공격자는 그 쿠키를 원하는 값으로 바꿀 수 있습니다. user_lv=3.5, REMOTE_ADDR=127.0.0.1, vote_check 없는 요청 — 전부 서버가 클라이언트를 신뢰했기 때문에 가능한 공격입니다.
그리고 XSS도 같은 맥락입니다. 사용자 입력을 신뢰하고 그대로 출력하면 안 됩니다.
"클라이언트에서 오는 모든 것은 조작될 수 있다"
이게 웹 보안의 제1원칙이라고 생각합니다.
다음 차시도 열심히 풀어보겠습니다. 🙂
저자 소개
DevOps Engineer. 금융과 여행에 관심이 많습니다.