본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] DOM XSS write up

#!/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)

 

 

 

CSP Header


csp header에 이전과 다른 정책이 하나 추가 되었다.

 

strict-dynamic 해당 정책은 동적으로 추가 되는 script 코드에 대해서는 허용하도록 하겠다는 것이다. 이를 이용하면 script 태그를 실행 시킬 수 있다.

 

 

 

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 %}

 

vuln.html에서는 해쉬값 뒤의 문자열을 가져와 id속성이 name인 태그의 innerHTML로 추가하고 있다. 그러나 id속성이 name인 태그는 pre태그 이기때문에 문자열로 인식될 가능성이 크다.

 

{{ param | safe }}에서 param으로 받은 값을 pre태그 보다 먼저 html에 입력하고 있다. 그대로 입력하고 있기 때문에 취약한 부분이다.

 

 

취약점 분석


 

총 세 가지의 취약점이 존재하는 걸로 보인다. csp 헤더에서 두 가지와 vuln.html에서 한 가지가 존재한다.

 

1. base uri 미지정

 

2. script태그 동적 허용

 

3. 사용자 입력 값 html injection

 

 

1. base uri 미지정

 

 

위와 같이 base태그를 이용해서 리소스를 로드하는 기준점을 dreamhack request bin 주소로 변경하니 404에러가 뜨는 걸 볼 수 있다.

 

 

 

2. script 동적허용, 3. 사용자 입력 값 html injection

 

이 부분은 2번과 3번을 묶어서 취약점을 트리거 할 수 있다. url에서 해쉬 부분은 innerHTML로 들어가게 된다. 또한 param은 그대로 페이지에 입력값을 노출시킨다. id속성이 name으로 param을 만든다면 pre태그 보다 id 속성을 우선순위로 보게 된다.

 

 

해쉬 부분에 쓰여진 test 문자열이 param에 전달된 h1태그 안으로 들어간걸 볼 수 있다. 이를 이용해 script 태그를 param 변수에 쓴다면 해쉬 부분이 동적으로 추가 되기 때문에 script 태그 사용이 가능하다.

 

 

//는 스크립트 상에서 주석을 의미한다. 

 

 

 

exploit


 

base uri 미지정

내 서버에 /static/js/jquery.min.js 파일을 만든다. 이 때 해당 파일은 쿠키를 탈취할 수 있는 페이로드를 담고 있어야 한다.

 

base태그를 이용해서 리소스들의 기준이 되는 주소를 내 서버 주소로 변경시켜 내가 만들 js파일이 로드 되게 한다.

 

 

DOM clobbering

 

script태그에 id 속성의 값을 name으로 주고 해쉬 부분에 페이로드를 작성한다.

 

<script id=name></script>#location.href='/memo?memo='+document.cookie//

 

처음 script 태그는 내용이 없는 빈 태그이기 때문에 아무것도 실행하지 않는다. 따라서 CSP에 걸리지 않고 해쉬 부분이 동적으로 추가되어 실행되게 된다. vuln.html에서 해쉬값에 대해 url 디코딩을 거치지 않기 때문에  <, >는 삽입할 수 없다.

 

(더블쿼터는 url 인코딩이 되는데 싱글쿼터는 안 된다. 이거때메 삽질을 했는데 이유는 잘 모르겠다 ㅠㅠ, 일반적으로 싱글쿼터는 안 된다는듯..)