Hoy vamos a ver qué es un ataque CSRF. Es un ataque que ya es viejo pero sin dudas muchos de nosotros seguimos teniendo esa vulnerabilidad en nuestros sitios. No importa el lenguaje de programación que utilicemos, con el simple hecho de realizar operaciones en el servidor ya estamos expuestos.
Tampoco es para preocuparse tanto, pero bueno, vamos allá.
Si quieres ver el ejemplo y probarlo por ti mismo, puedes descargar los archivos, configurar tu servidor local y listo.
Ve a GitHub para descargar el código completo: https://github.com/parzibyte/ataque_csrf_php
No voy a entrar en definiciones tan formales ni cosas de esas. Vamos a hacerlo simple.
Un ataque CSRF es aquel en donde un usuario malicioso, de cualquier manera, logra hacer que un usuario ingenuo realice una acción sin que éste último lo sepa, beneficiando de cualquier manera al primero.
Los casos más comunes son con un link (a veces cargados desde una imagen), aunque igualmente puede ser con un formulario en donde el botón para enviar se disfrace como un link.
Ve más abajo para ver ejemplos.
El ejemplo más básico (aunque no creo que exista en la vida real) es aquel en donde hay un sitio web de un banco y para hacer la transferencia se utiliza un link. Por ejemplo, un enlace como este:
tubanco.com/transferir.php?cantidad=1000¶=parzibyte
Dicho enlace, cuando lo hace el usuario normal, no es peligroso. Pues el usuario sabe que está haciendo una transferencia.
Si un atacante descubre que con el simple hecho de visitar ese link se hace la transferencia, sabrá que con que el usuario ingenuo visite el link (o haga una petición GET al mismo) se realizará el movimiento.
En ese caso le harías una transferencia al usuario parzibyte. Dicho enlace no necesariamente tendría que ser clickeado, podría ser cargado dentro de una imagen que se encuentra en otro sitio. Así:
<img src="tubanco.com/transferir.php?cantidad=1000¶=parzibyte" style="display: none;">
La fuente de la imagen no es una imagen, pero el navegador va a hacer la petición a dicho enlace. Poco nos importa que la imagen cargue o no, ya que está oculta.
Ahora alguien oculta esa imagen en un sitio, te dice que lo visites y listo, sin saberlo ya has hecho la transferencia.
Hay muchos casos y ejemplos, desde los más básicos (como aprobar un comentario de tu blog, eliminar un producto de tu sistema de inventarios) hasta los más sofisticados como dar permisos a un nuevo usuario, eliminar tu propia cuenta, hacer una transferencia, entre otros.
Igualmente hay ataques CSRF por el método POST, es decir, que los valores y parámetros no se envíen en la ruta, sino por un formulario.
Vamos a simular un pequeño banco. En los ejemplos de arriba, el remitente (o no sé cómo se le diga al que hace la transferencia) era tomado de la sesión, pero ahorita no veremos eso de las sesiones porque ese no es el punto.
Simplemente escribiremos en un fichero de texto los movimientos realizados, para darnos una idea.
Y para hacer una transferencia utilizaremos el link:
tubanco.com/transferir.php?cantidad=1000¶=parzibyte&desde=usuario_ingenuo
Comencemos con el código PHP que hará las transferencias. Será muy simple como lo dije, sólo escribirá en un archivo las transferencias que se hacen.
Si quieres profundizar en el tema de cómo escribir, leer y hacer un CRUD con ficheros te recomiendo leer: CRUD de ficheros en PHP
Queda así:
<?php /** * Simular una transferencia de dinero entre usuarios para * explotarlo con un ataque CSRF * * @author parzibyte.me/blog */$desde = $_GET["desde"]; $para = $_GET["para"]; $cantidad = floatval($_GET["cantidad"]); file_put_contents("transferencias.txt", "Se transfieren $cantidad desde $desde para $para" . PHP_EOL, FILE_APPEND); echo "OK"; ?>
Esta será la interfaz legal o verdadera. Aquí es en donde se hacen las transferencias normalmente.
Para llamar al link de arriba (transferir.php) podemos usar un formulario, o un link. En este caso será este formulario:
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>Hacer transferencias de un banco</title> </head> <body> <form action="transferir.php"> <input placeholder="Remitente" type="text" name="desde"><br> <input placeholder="Receptor" type="text" name="para"><br> <input placeholder="¿Cuánto transfieres?" type="number" name="cantidad"><br> <button type="submit">Realizar transferencia</button> </form> </body> </html>
Y cuando el usuario hace la transferencia, todo va bien, porque él sabe que lo está haciendo…
Hasta aquí todo va bien. Ahora vamos a explotar esa “vulnerabilidad” con otra página…
Ahora viene lo bueno. Vamos a mandarle a ese usuario una página muy bonita y no sabrá que cargaremos este link para transferirnos 10000 pesos, dólares o el tipo de moneda que haya en tu país:
transferir.php?desde=ingenuo¶=parzibyte&cantidad=10000
Dicha página tendríamos que hacerla bonita y llamativa, o tal vez no si el usuario confía en nosotros. Lo haré rápido porque el objetivo es explicar el funcionamiento.
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>Mi página bonita</title> </head> <body> <h1>Bienvenido a mi sitio. Aquí puedes ver mi perfil</h1> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quae quam nulla aperiam repellendus iure doloribus. Excepturi reiciendis minima, earum, sapiente sed mollitia totam expedita ut ad ea vel itaque doloremque.</p> <!-- Justo aquí abajo cargamos la imagen para el ataque CSRF :-) --> <img src="transferir.php?desde=ingenuo¶=parzibyte&cantidad=10000" style="width: 0; height: 0;"> <!--La imagen de abajo sí es real y no hace nada--> <img src="https://picsum.photos/200" alt=""> </body> </html>
Ahora le damos al usuario un enlace a nuestra página trampa. Él verá esto y no sospechará nada:
Como vimos, cargamos una imagen con un alto y ancho de 0. Podemos ocultarla como deseemos, eso es lo de menos. Lo que más me gusta es que si vamos ahora al log…
Oh sí. La transferencia fue hecha y el usuario ni cuenta se dio, sólo tuvo que entrar a nuestra página trampa.
Lo sé, es un ejemplo muy básico pero ahora imagina cuántas cosas se podrían hacer con este tipo de ataques, suponiendo que uno estudia bien el tema.
Como lo dije, este ataque ya es viejo y ya existe una solución. Es lo que llamamos token CSRF y lo que hace es que genera una cadena aleatoria que no es predecible con el tiempo.
Ese token no lo sabrá el atacante, y al usuario se lo daremos en el formulario. Vamos paso por paso.
Por cierto, si queremos una alternativa a un token CSRF podemos utilizar un captcha que se supone es más seguro. Te invito a leer cómo implementar el captcha de coinhive.
Ya hice un post de cómo generar un token criptográficamente seguro. Puedes implementar tu función si gustas, aunque no te lo recomiendo.
El token seguro que generamos se lo daremos al usuario en el formulario, como un valor oculto. Pero antes de dárselo lo vamos a inyectar en la sesión así:
<?php #Nota: esta función funciona en PHP7. Visita https://parzibyte.me/blog/2018/06/27/generar-token-criptograficamente-seguro-php/ # si tu versión es menor function generar_token_seguro($longitud) { if ($longitud < 4) { $longitud = 4; } return bin2hex(random_bytes(($longitud - ($longitud % 2)) / 2)); } # Sólo iniciamos la sesión si no la iniciamos antes if(session_status() !== PHP_SESSION_ACTIVE){ session_start(); } $token = generar_token_seguro(40); # La longitud puede variar pero yo la pondré en 40 $_SESSION["token"] = $token; ?> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>Hacer transferencias de un banco</title> </head> <body> <form action="transferir.php"> <!-- Aquí abajo inyectamos el token --> <input type="hidden" value="<?php echo $token; ?>" name="token_csrf"> <input placeholder="Remitente" type="text" name="desde"><br> <input placeholder="Receptor" type="text" name="para"><br> <input placeholder="¿Cuánto transfieres?" type="number" name="cantidad"><br> <button type="submit">Realizar transferencia</button> </form> </body> </html>
El usuario seguirá viendo lo mismo, pero internamente estará el token:
Ese token no lo sabrá el atacante, pues se regenera cada que el usuario entra.
Ahora en el archivo de transferencia primeramente vamos a comparar el token con el que tenemos en la sesión y la haremos en caso de que coincidan:
<?php /** * Prevenir ataque CSRF con un token * * @author parzibyte.me/blog */$desde = $_GET["desde"]; $para = $_GET["para"]; $cantidad = floatval($_GET["cantidad"]); $token = $_GET["token_csrf"]; #Aquí iniciamos la sesión si no está iniciada, esto es para leer el token que guardamos anteriormente if(session_status() !== PHP_SESSION_ACTIVE){ session_start(); } #Comparamos tokens... if(strcmp($token, $_SESSION["token"]) === 0){ file_put_contents("transferencias.txt", "Se transfieren $cantidad desde $desde para $para" . PHP_EOL, FILE_APPEND); echo "OK"; }else{ file_put_contents("transferencias.txt", "NO se ha transferido $cantidad desde $desde para $para porque el token no coincide!" . PHP_EOL, FILE_APPEND); echo "NO"; } ?>
Así queda el script. Por cierto, para comparar los tokens usamos strcmp que devuelve cero si las cadenas son idénticas. Podríamos igualmente usar hash_equals.
Ahora el usuario hace la transferencia y el link cambia a algo como:
transferir_seguro.php?token_csrf=35a719eb761cc5d86c0da53f0565a24692182f8b&desde=nadie¶=parzibyte&cantidad=500
Como esta transferencia fue legal o como se diga, en el log estará el registro.
Sólo queda intentar atacar de nuevo.
Como vimos, ahora el archivo se llama transferir_seguro.php en lugar de transferir.php. Vamos a actualizarlo en la página trampa.
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>Mi página bonita</title> </head> <body> <h1>Bienvenido a mi sitio. Aquí puedes ver mi perfil</h1> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quae quam nulla aperiam repellendus iure doloribus. Excepturi reiciendis minima, earum, sapiente sed mollitia totam expedita ut ad ea vel itaque doloremque.</p> <!-- Justo aquí abajo cargamos la imagen para el ataque CSRF :-) --> <img src="transferir_seguro.php?desde=ingenuo¶=parzibyte&cantidad=10000" style="width: 0; height: 0;"> <!--La imagen de abajo sí es real y no hace nada--> <img src="https://picsum.photos/200" alt=""> </body> </html>
Y si ahora la visitamos, todo sigue igual:
Pero ahora veamos el log:
Oh sí, hemos prevenido el ataque. Y el usuario malicioso nunca sabrá el token (excepto si es un adivino jaja), porque no puede calcularlo de ninguna manera.
Además, el token es distinto por cada usuario y se regenera en cada visita a la página.
De algunos frameworks web que conozco, puedo decir que estos dos implementan protección contra ataques CSRF. Puede haber más, no lo sé, sería cosa de investigar.
Y así es como terminamos este post. Si tienes alguna duda ponla en los comentarios. Espero que haya quedado claro qué es un ataque CSRF.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.
Ver comentarios
Muy bueno tu blog.