본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] CSP Bypass 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, 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.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}'"
    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 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")
        if not check_xss(param, {"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)

 

 

XSS에 대한 보호기법이 CSP 정책 밖에 없다. CSP 헤더를 살펴보자.

 

default-src 'self'; img-src https://dreamhack.io; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{nonce}'

 

default-src 'self': 기본 src에 대해 같은 자원만 로드하도록 되어 있다.

 

img-src https://dreamhack.io: img 자원에 대한 출처를 dreamhack.io만으로 제한한다.

 

style-src 'self' 'unsafe-inline': style 자원에 대한 출처를 같은 출처로만 제한하며 inline코드를 허용한다. inline 코드는 스크립트 안에 직접적으로 코드를 삽입하는 것을 말한다. ex) <script>alert(1)</script>

 

script-src 'self' 'nonce-{nonce}': 스크립트에 대한 출처를 같은 출처와 nonce값이 일치할때만으로 제한한다.

 

 

먼저, 위 CSP 헤더를 보고 3가지를 이용해서 우회가 가능하다고 생각했다.

 

1. nonce값을 알아내기

 

2. Nonce Retargeting (base-uri 미지정)

 

3. 동일 출처에대한 자원 로드

 

1번의 경우에는 nonce값이 매 요청마다 갱신되며 16바이트로 너무많은 경우의수가 있기 때문에 불가능하다고 생각했다.

 

 

2번의 경우.. 나름 시도를 해봤다. 우선 param에 base 태그를 넘겨주어야 한다. 

 

내 깃헙주소를 넘겨주었다. 

 

내 깃헙에는 다음과 같은 js 코드가 작성되어 있다.

 

 

따라서 서버 자원의 상대경로가 변경됨으로 내 깃헙에 방문할거라 생각했지만, 잘 되지 않았다.

아마 /vuln페이지에 base태그가 삽입은 되나, 상대 경로로 지정되어 있는 스크립트가 다른 경로에 있기 때문에 /vuln페이지를 탈출하면 base태그가 삭제 되어서 그런 것 같다. 또한, admin이 그 경로를 방문할 수도 없다..

 

 

3번의 경우 바로 alert(1)을 띄웠다.

 

 

 

따라서 3번의 방법을 이용해 페이로드를 작성했다.

 

<script src="/vuln?param=location.href='/memo?memo='%2bdocument.cookie"></script>

 

+를 URL 인코딩해서 넣은 이유는 src가 URL을 로드할때 url 디코딩을 하기 때문에 +가 공백으로 디코딩이 되기때문이다.