먼저 아래 서버 코드를 보자.
DOM XSS라 그런지 기분 탓인지 모르게 코드가 별게 없어 보인다.
#!/usr/bin/python3
from flask import Flask, request, render_template
from selenium import webdriver
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:
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome("/chromedriver", 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)
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 %}
DOM-XSS는 브라우저 단에서 일어나기 때문에 아무래도 웹 페이지에 스크립트 코드가 핵심일 것이다.
확실한 건 일반적인 XSS는 render template로 인해 막혀있고 render_template는 html로 랜더링 해주는 것이기 때문에 DOM-XSS 취약점이 가능할 수 있다는 걸 안다.
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'"
CSP는 위와 같이 설정이 되어있는데 눈이 가는 것은 strict-dynamic이다.
script에 대해 src까지 신뢰해준다는 것 같다.
자세한 건 더 나중에 알아보겠다.
근데 코드를 보아하니 base태그를 삽입할 수 있을 것만 같다..
아래는 param에 ㄴㅇㄹㄴㅇㄹ이라는 아무 문자열이나 넣어 본 것이다.
아무래도 될 것만 같다.
아래와 같은 base를 flag에서 제출해보았는데
아래처럼 base가 먹혔다.
일단 이렇게 푸는 것은 아니니 DOM-XSS방법으로 다시 풀어보겠다.
코드를 살펴보면
<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>
위와 같이 nonce로 신뢰되는 id가 name인 태그에 # 뒤를 html로 삽입한다.
<p id = name>#내용</p>
위처럼 말이다.
아래와 같이 div태그를 id=name으로 하고 #뒤에는 jckim2을 입력했다.
dvwa때 했던 것과 같이 # 뒤에는 서버로 전송되지 않고 브라우저를 랜더링 할 때 읽혀지게 된다.
그럼 위와 같이 jckim2 is my name이라는 html이 div태그 안에 쓰여지게 된다.
그리고 패킷을 잡아보면 정말로 #뒤는 잘리고 서버에게 요청이 간 것을 볼 수 있다.
그럼 이제 다른 생각을 할 수 있다.
strict dynamic이 되어 있기 때문에 나는 script태그를 이용할 수 있을 것 같다.
만약 스크립트 태그의 id를 name으로 한 뒤 #뒤로 악성 코드를 주입하여 쿠키를 탈취할 수 있다면 어떨까
먼저 아래처럼
param에 작성한 뒤 alert(1)을 성공시켰다.
<script> 태그 안에 alert(1)// 이라는 문자열이 성공적으로 들어가게 된 것이다.
그럼 이제 로컬 호스트가 url을 클릭한다는 시나리오로 쿠키를 memo에 적고 내가 탈취할 수 있는 flag 코드를 작성해보자
코드는 아래와 같다.
<script id=name></script>#location='/memo?memo='+document.cookie//
뒤에 //은 스크립트 상에서 주석이다.
왜 주석을 다는지는 아까 div태그에서 jckim2 is my name이 안에 들어있었던 걸 생각하자.
is my name은 기본으로 들어가는 문자열이기 때문에 주석처리를 해주어야 스크립트 상에서 오류를 피할 수 있다.
그럼 최종적으로
<script id=name>
location='/memo?memo='+document.cookie//is my name
</script>
위와 같은 코드가 innerHTML 될 것이다.
그렇게 아래와 같이 익스플로잇 해주면
flag를 얻을 수 있다.