[Dreamhack] Safe CSP

😁 문제 설명

  • Google says this CSP is safe.

img


✏️ 풀이

문제에 접속하면 로그인 폼과 /login 경로로 리다이렉트 된다.

image-20250820200636559


소스 코드에서 USER_DATA에 admin이 아닌 user의 계정이 존재한다. 또한 NONCE 값도 base64로 암호화되어 있는 것을 알 수 있다.

NONCE = base64.b64encode(random.getrandbits(32).to_bytes(8, 'big')).decode()

USER_DATA = {
    "admin": "[**redacted**]",
    "user": "pass"
}


user 계정으로 로그인 해보면 /memo 경로로 리다이렉트되고 admin이 아니라고 나온다.

image-20250820201342296


소스 코드에서 read_url 함수를 살펴보면 admin 계정의 관리자 권한으로 읽을 수 있는 함수라는 것을 알 수 있다.

def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    try:
        service = Service(executable_path="/usr/local/bin/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({'name': 'username', 'value': 'admin'})
        driver.add_cookie(cookie)
        driver.get(url)
    except Exception as e:
        driver.quit()
        return False
    driver.quit()
    return True


check_xss 함수에서는 /memo 경로에서 text 파라미터 값을 url인코딩하여 관리자 계정으로 url을 읽는 것을 알 수 있다. 아래 add_header 함수는 CSP를 설정하고 있다.

def check_xss(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/memo?text={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';base-uri 'none';style-src 'none';img-src *;script-src 'nonce-{NONCE}'"
    return response


/memo 경로에서는 username의 쿠키 값을 가져와서 username 값을 가져와 admin이 아니라면 you are not admin 문자열을 출력한다. 또한 text 파라미터도 값을 가져와 xor 암호화를 하여 출력해주는 것을 알 수 있다.

@app.route('/memo')
def memo():
    username = request.cookies.get('username')
    memo_text = request.args.get('text', '')
    
    if not username or username not in USER_DATA:
        return redirect('/login')

    if username != 'admin':
        return 'You are not admin!!!'

    try:
        r = random.getrandbits(32)
        memo_bytes = memo_text.encode('utf-8')
        memo_int = bytes_to_long(memo_bytes)
        xor_result = memo_int ^ r
        xor_hex = hex(xor_result)
    except Exception as e:
        xor_hex = f"Conversion fail: {e}"

    resp = make_response(render_template("memo.html", username=username, memo_text=memo_text, xor_result=xor_hex))
    return resp


username의 쿠키 값을 admin으로 변경해주면 아래와 같이 Welcome, admin 문자열과 memo를 입력할 수 있는 폼이 나온다.

image-20250820202819213

image-20250820202740848


test를 입력하여 요청해보면, 입력한 값이 text 파라미터에 포함되어 요청되고 아래에는 입력한 값과 암호화된 값이 출력된다.

image-20250820202933846


소스 코드에서는 /debug 라우터 경로에서도 param 파라미터를 통해서 쿠키가 flag인 값으로 시도하는 것을 알 수 있다.

@app.route('/debug')
def debug():
    global NONCE
    param = request.args.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.html에서 보면 memo_text 부분 옆에 safe가 설정되어 있는데 이는 이스케이프를 설정하지 않는 것이기 때문에 XSS가 발생할 수 있다.

  


logout 부분의 소스 코드와 응답 헤더 부분의 CSP를 추가하는 소스 코드를 보면 로그아웃하기 전까지 Nonce 값이 계속 응답헤더에 나타나고 재사용하기 때문에 Nonce값을 재사용해서 script 태그를 사용하여 쿠키 값을 얻을 수 있다.

@app.route('/logout', methods=['POST'])
def logout():
    global NONCE
    random.seed(time.time())
    NONCE = base64.b64encode(random.getrandbits(32).to_bytes(8, 'big')).decode()

    resp = make_response(redirect('/login'))
    resp.delete_cookie('username')
    return resp


아래 nonce값을 확인하여 /debug 경로에서 쿠키값을 flag 값으로 설정하고 있기 때문에 param 파라미터를 통해서 script 태그를 사용하여 XSS 페이로드를 작성하면 된다. 주의할점은 nonce 값에서 +가 포함되어 있기 때문에 인코딩하여 %2b로 보내야한다.

image-20250820223348898

image-20250820224157985

image-20250820224140306

댓글남기기