Entrada

XSS guide

Guía sobre la vulnerabilidad XSS

XSS guide

Certificaciones

  • eWPT
  • eWPTXv2
  • OSWE
  • BSCP

Descripción

Explicación técnica de la vulnerabilidad XSS. Detallamos cómo identificar y explotar esta vulnerabilidad, tanto manualmente como con herramientas automatizadas. Además, exploramos estrategias clave para prevenirla


¿Qué es un XSS?

La vulnerabilidad de Cross-site scripting (XSS) es una vulnerabilidad de seguridad web que permite a un atacante comprometer las interacciones que los usuarios tienen con una aplicación vulnerable. Permite evadir la política de mismo origen (same origin policy), diseñada para separar diferentes sitios web entre sí

Los ataques XSS normalmente permiten que un atacante suplante la identidad de un usuario víctima, realice cualquier acción que este pueda ejecutar y acceda a sus datos. Si el usuario afectado tiene privilegios elevados dentro de la aplicación, el atacante podría hacerse con el control total de toda su funcionalidad y datos

¿Para qué puede usarse XSS?

Un atacante que explota una vulnerabilidad de cross-site scripting normalmente puede llevar a cabo estas acciones

  • Hacerse pasar por el usuario víctima

  • Realizar cualquier acción que el usuario pueda ejecutar

  • Leer cualquier dato al que el usuario tenga acceso

  • Capturar las credenciales de inicio de sesión del usuario

  • Hacer un defacement del sitio web

  • Inyectar un funcionalidad tipo troyano en el sitio web

PoC de XSS

Podemos confirmar la mayoría de las vulnerabilidades de XSS inyectando un payload que haga que nuestro propio navegador ejecute algún JavaScript arbitrario. Desde hace tiempo, es común usar la función alert() para este propósito porque es corta, inofensiva y difícil de pasar por alto cuando se ejecuta con éxito

Sin embargo, hay un inconveniente si usamos Chrome. A partir de la versión 92, los cross-origin iframes tienen prohibido llamar a alert() y como estos se utilizan para construir algunos de los ataques XSS más avanzados, a veces debemos usar un payload alternativo de PoC. Es por esto, que se se recomienda la función print()

Contexto

A la hora de testear un reflected XSS o stored XSS, un paso clave es identificar el contexto, para ello, lo primero que necesitamos saber es dónde se refleja nuestro input. Existen estos tipos de contexto :

  • XSS entre etiquetas HTML

  • XSS dentro de los atributos de etiquetas HTML

  • XSS dentro de código JavaScript

    • Finalizar el script existente

    • Escapar de una string

    • Hacer uso de HTML-encoding

    • Inyectar expresiones de JavaScript en template literals

Tipos de ataques XSS

Existen tres tipos principales de ataques XSS:

  • Reflected XSS – Ocurre cuando los datos proporcionados por el usuario se incluyen inmediatamente en la respuesta del servidor sin la validación o escape adecuados

  • Stored XSS – Sucede cuando los datos maliciosos enviados por el usuario se almacenan en el servidor (por ejemplo, en una base de datos) y luego se sirven a otros usuarios sin la sanitización adecuada

  • DOM Based XSS – Este tipo de vulnerabilidad se origina en el lado del cliente, donde el código JavaScript manipula de forma insegura el DOM de la web, permitiendo la ejecución de scripts maliciosos

Reflected XSS

Stored XSS

Dom Based XSS

Sinks que pueden conducir a vulnerabilidades de tipo DOM XSS

Los siguientes son algunos de los principales sinks que pueden provocar vulnerabilidades de tipo DOM XSS

1
2
3
4
5
6
7
document.write()
document.writeln()
document.domain
element.innerHTML
element.outerHTML
element.insertAdjacentHTML
element.onevent

Las siguientes funciones de jQuery algunas de los principales sinks que pueden provocar vulnerabilidades de tipo DOM XSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
add()
after()
append()
animate()
insertAfter()
insertBefore()
before()
html()
prepend()
replaceAll()
replaceWith()
wrap()
wrapInner()
wrapAll()
has()
constructor()
init()
index()
jQuery.parseHTML()
$.parseHTML()

Explotar vulnerabilidades XSS

La forma tradicional de demostrar que hemos encontrado una vulnerabilidad de cross-site scripting es crear un popup usando la función alert(). Esto no es porque el XSS tenga algo que ver con los popups, es simplemente una forma de demostrar que podemos ejecutar código JavaScript arbitrario en un dominio dado. Puede que notes que algunas personas usan alert(document.domain). Esto sirve para dejar explícito en qué dominio se está ejecutando el JavaScript

Para demostrar que una vulnerabilidad XSS es una amenaza real proporcionando un exploit completo. En esta sección exploraremos tres de las formas más populares de explotar un XSS

Robar cookies

Robar cookies es una forma tradicional de explotar XSS. La mayoría de las aplicaciones web usan cookies para el manejo de sesiones. Podemos explotar vulnerabilidades de cross-site scripting para enviar las cookies de la víctima a nuestro propio dominio, luego inyectar manualmente las cookies en el navegador y suplantar a la víctima

En la práctica, este método tiene algunas limitaciones importantes:

  • La víctima podría no estar logueada

  • Muchas aplicaciones ocultan sus cookies de JavaScript usando la flag HttpOnly

  • Las sesiones pueden estar vinculadas a factores adicionales, como la dirección IP del usuario

  • La sesión puede expirar antes de que podamos secuestrarla

En este laboratorio podemos ver como aplicar esta técnica:

Capturar contraseñas

Hoy en día, muchos usuarios tienen gestores de contraseñas que autocompletan sus contraseñas. Podemos aprovechar esto creando un campo de contraseña, leyendo la contraseña autocompletada y enviándola a nuestro propio dominio. Esta técnica evita la mayoría de los problemas asociados con robar cookies y puede incluso obtener acceso a todas las demás cuentas donde la víctima haya reutilizado la misma contraseña

La principal desventaja de esta técnica es que solo funciona con usuarios que tienen un gestor de contraseñas que realiza autocompletado. Si un usuario no tiene guardada una contraseña, podemos intentar obtenerla mediante un ataque de phishing

En este laboratorio podemos ver como aplicar esta técnica:

Bypassear las protecciones contra CSRF

Un XSS permite a un atacante hacer casi todo lo que un usuario legítimo puede hacer en un sitio web. Al ejecutar código JavaScript arbitrario en el navegador de la víctima, el XSS nos permite realizar una amplia variedad de acciones como si fuéramos ese usuario. Por ejemplo, podemos hacer que la víctima envíe un mensaje, acepte una solicitud de amistad o transfiera algunos Bitcoins

Algunos sitios web permiten a los usuarios logueados cambiar su dirección de correo electrónico sin volver a ingresar su contraseña. Si encontramos un XSS en uno de estos sitios web, podemos explotarlo para robar un token CSRF. Con ese token, podemos cambiar el correo electrónico de la víctima a una que controlemos. Luego, podemos activar un restablecimiento de contraseña para obtener acceso a la cuenta

Este tipo de exploit combina XSS (para robar el token CSRF) con la funcionalidad normalmente atacada por CSRF. Mientras que el CSRF tradicional es una vulnerabilidad de “una sola vía”, donde el atacante puede inducir a la víctima a enviar peticiones pero no puede ver las respuestas, el XSS permite una comunicación bidireccional. Esto permite al atacante tanto enviar peticiones como leer las respuestas, resultando en un ataque híbrido que evade las defensas anti-CSRF

En este laboratorio podemos ver como aplicar esta técnica:

¿Cómo detectar y explotar un XSS?

Es posible detectar XSS de varias formas, en mi caso sigo estos pasos:

  1. Añadir el dominio y sus subdominios al scope

  2. Hacer un escaneo general con Burpsuite. Como tipo de escaneo marcaremos Crawl and audit y como configuración de escaneo usaremos Deep

  3. Escanearemos partes específicas de la petición usando el escáner de Burpsuite. Para escanear los insertion points debemos seleccionar en tipo de escaneo la opción Audit selected items

  4. Con el objetivo de encontrar vulnerabilidades de tipo DOM XSS usamos el DOM Invader para testear todos los inputs

  5. Una vez hecho esto, usamos XSStrike con el parámetro --crawl para poder identificar si hay alguna vulnerabilidad o algún sink, el cual pueda conducir a un XSS

  6. Usamos XSStrike nuevamente, pero esta vez con el objetivo de detectar un XSS

  7. Si no encontramos nada y la URL es de este estilo https://0a42008c0326fbeb803d129600e6006e.web-security-academy.net/?search=test o de este otro http://stock.0a1b001e03ee4b4480f30dd1005a0015.web-security-academy.net/?productId=3&storeId=1, vamos a usar Loxs y XSSuccessor

  8. Si Loxs y XSSuccessor no encuentran nada, podemos usar Dalfox, el cual tiene soporte para DOM XSS, Reflected XSS y Stored XSS. Además, cuenta con los payloads de PayloadBox y Portswigger para descubrir XSS. También cuenta con los diccionarios de Burpsuite y Assetnote para descubrir parámetros en la URL vulnerables a XSS. Desde mi experiencia, esta herramienta no me ha dado muy buenos resultados, pero podemos probar a ver si encuentra algo o usar el comando dalfox payload -h para listar varias opciones que nos permitirán ver los payloads que usa Dalfox y usarlos con otras herramientas. Por ejemplo, con el Intruder de Burpsuite, aunque puede ser complicado encontrar el payload correcto si mandamos muchos a la vez. Por eso, recomiendo usar la herramienta PayloadSplitter https://github.com/Justice-Reaper/PayloadSplitter.git para dividir una gran lista de payloads en listas más pequeñas y manejables

  9. Si sospechamos de un stored XSS, usaremos los paylods de Loxs, XSSuccessor o los de Dalfox con el Intruder de Burpsuite y seleccionaremos Pitchfork como tipo de ataque

  10. En el caso en que haya algunos tags o atributos blacklisteados, podemos usar XSSDynaGen o el fuzzer de XSStrike para ver que caracteres podemos usar. Sin embargo, yo prefiero usar el Intruder de Burpsuite junto con la cheatsheet de Portswigger para averiguarlo, debido a que esta forma es más precisa

  11. Si no encontramos nada, nos centraremos en buscar los XSS de forma manual utilizando la metodología de Hacktricks y usando como apoyo las cheatsheets de Portswigger y de PayloadsAllTheThings

  12. En el caso de que sospechemos de un Blind XSS, podemos usar varias herramientas para identificarlo. Si disponemos de un VPS, podemos usar XSSHunter Express y si no disponemos de uno, podemos usar XXHunter, BXSSHunter o XSSReport

Cheatsheets para XSS

En Hacktricks tenemos una metodología para encontrar XSS y explotarlos. En Portswigger tenemos diferentes payloads que, combinados con Burpsuite, nos permiten identificar qué tags y eventos están permitidos y de esta forma construir un payload válido. En PayloadsAllTheThings tenemos payloads que podemos usar y herramientas recomendadas

Herramientas

Tenemos estas herramientas para automatizar la explotación de XSS:

Prevenir ataques XSS

La prevención de XSS puede lograrse, en general, mediante dos capas de defensa:

  • Codificar los datos en la salida

  • Validar el input de datos al recibirlo

La codificación debe aplicarse justo antes de que los datos controlados por el usuario se escriban en una página, porque el contexto en el que se escriben determina el tipo de codificación que debemos usar. Por ejemplo, los valores dentro de una cadena JavaScript requieren un tipo de escapado diferente al de un contexto HTML

En un contexto HTML, debemos convertir los valores no permitidos en entidades HTML

  • < se convierte en: &lt;

  • > se convierte en: &gt;

En un contexto de cadena JavaScript, los valores no alfanuméricos deben escaparse en Unicode

  • < se convierte en: \u003c

  • > se convierte en: \u003e

A veces, debemos aplicar múltiples capas de codificación, en el orden correcto. Por ejemplo, para incrustar de forma segura el input del usuario dentro de un manejador de eventos, debemos tratar tanto el contexto JavaScript como el contexto HTML. En este caso, primero debemos escapar en Unicode el input y luego codificarlo en HTML

1
<a href="#" onclick="x='This string needs two layers of escaping'">test</a>

Validar el input al recibirlo

La codificación probablemente sea la línea de defensa más importante contra XSS, pero no es suficiente para prevenir vulnerabilidades en todos los contextos. También debemos validar el input de la forma más estricta posible en el momento en que se recibe por primera vez del usuario

Ejemplos de validación de input:

  • Si un usuario envía una URL que se devolverá en las respuestas, validar que empiece con un protocolo seguro como HTTP o HTTPS. De lo contrario, alguien podría explotar el sitio usando un protocolo peligroso como javascript o data

  • Si un usuario proporciona un valor que se espera sea numérico, validar que el valor contenga realmente un entero

  • Validar que el input solo contenga un conjunto de caracteres esperado

La validación del input debería funcionar bloqueando el input no válido. La alternativa que consiste en intentar limpiar el input inválido para hacerlo válido, es más propensa a errores y debe evitarse siempre que sea posible

Whitelisting vs Blacklisting

La validación de input debe usar preferiblemente whitelists en lugar de blacklists. Por ejemplo, en lugar de intentar crear una lista con todos los protocolos peligrosos (javascript, data, etc.), debemos hacer una lista con los protocolos seguros (HTTP, HTTPS) y bloquear todo lo que no esté en la lista. Esto asegura que la defensa no falle cuando aparezcan nuevos protocolos peligrosos y reduce el riesgo frente a ataques que intenten ofuscar valores inválidos para evadir la blacklist

Permitir HTML “seguro”

Permitir que los usuarios publiquen HTML debería evitarse siempre que sea posible, aunque a veces es un requisito de negocio. Por ejemplo, un blog podría permitir que los comentarios contengan HTML limitado

El enfoque clásico es intentar filtrar las etiquetas peligrosas y JavaScript. Podemos implementar esto usando una whitelist de etiquetas y atributos seguros, pero debido a las diferencias en los motores de parseo de navegadores y a técnicas como mutation XSS, este método es extremadamente difícil de implementar de forma segura

La opción menos mala es usar una librería JavaScript que realice el filtrado y la codificación en el navegador del usuario, como DOMPurify. Otras librerías permiten que los usuarios escriban en Markdown y luego lo convierten a HTML. Sin embargo, todas estas librerías tienen vulnerabilidades XSS de vez en cuando, por lo que no es una solución perfecta. Por lo tanto, si usamos una, debemos vigilar de cerca las actualizaciones de seguridad

Además de JavaScript, CSS e incluso HTML pueden ser peligrosos en algunas situaciones https://portswigger.net/research/detecting-and-exploiting-path-relative-stylesheet-import-prssi-vulnerabilities#badcss

Prevenir XSS usando un motor de plantillas

Muchos sitios web modernos usan motores de plantillas del lado del servidor como Twig o Freemarker para incrustar contenido dinámico en HTML. Estos suelen tener su propio sistema de escapado, por ejemplo, en Twig podemos usar el filtro e() con un argumento que define el contexto

1

Otros motores de plantillas, como Jinja o React, escapan el contenido dinámico por defecto, lo que previene la mayoría de los casos de XSS. Se recomienda revisar cuidadosamente las funciones de escapado al evaluar si usar un motor de plantillas o framework concreto

Si se concatena directamente el input del usuario en templates strings, nos volvemos vulnerables a SSTI (Server-Side Template Injection), que a menudo es más grave que el XSS

Prevenir XSS en PHP

En PHP existe la función incorporada htmlentities para codificar entidades. Debemos llamarla para escapar el input cuando estemos en un contexto HTML. Debe llamarse con tres argumentos:

  1. El input

  2. ENT_QUOTES, una bandera que indica que se deben codificar todas las comillas

  3. El conjunto de caracteres, que normalmente debe ser UTF-8

1
<?php echo htmlentities($input, ENT_QUOTES, 'UTF-8');?>

En un contexto de cadena JavaScript, debemos escapar en Unicode el input. Desafortunadamente, PHP no incluye una API para hacer Unicode-escape a un string. Así se puede implementar manualmente en PHP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php 
function jsEscape($str) {
    $output = '';
    $str = str_split($str);
    
    for ($i = 0; $i < count($str); $i++) {
        $chrNum = ord($str[$i]);
        $chr = $str[$i];
        
        // Manejo de caracteres especiales Unicode (U+2028, U+2029)
        if ($chrNum === 226) {
            if (isset($str[$i+1]) && ord($str[$i+1]) === 128) {
                if (isset($str[$i+2]) && ord($str[$i+2]) === 168) {
                    $output .= '\u2028';
                    $i += 2;
                    continue;
                }
                if (isset($str[$i+2]) && ord($str[$i+2]) === 169) {
                    $output .= '\u2029';
                    $i += 2;
                    continue;
                }
            }
        }
        
        // Escapado de caracteres especiales
        switch ($chr) {
            case "'":
            case '"':
            case "\n":
            case "\r":
            case "&":
            case "\\":
            case "<":
            case ">":
                $output .= sprintf("\\u%04x", $chrNum);
                break;
            default:
                $output .= $str[$i];
                break;
        }
    }
    
    return $output;
}
?>

Así se usa la función jsEscape en PHP

1
<script>x = '<?php echo jsEscape($_GET['x'])?>';</script>

Alternativamente, podríamos usar un motor de plantillas

Prevenir XSS del lado del cliente en JavaScript

Para escapar el input del usuario en un contexto HTML en JavaScript, necesitamos un codificador HTML propio porque JavaScript no proporciona una API para codificar HTML. Esta función de JavaScript convierte una cadena en entidades HTML

1
2
3
4
5
6
function htmlEncode(str) {
    return String(str)
        .replace(/[^\w. ]/gi, function(char) {
            return '&#' + char.charCodeAt(0) + ';';
        });
}

Podríamos usar la función anterior así:

1
<script>document.body.innerHTML = htmlEncode(untrustedValue)</script>

Si el input está dentro de una cadena JavaScript, necesitaremos un codificador que realice el escape de los caracteres en Unicode. Este sería un ejemplo:

1
2
3
4
5
function jsEscape(str) {
    return String(str).replace(/[^\w. ]/gi, function(c) {
        return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
    });
}

Podríamos usar la función anterior así:

1
<script>document.write('<script>x="'+jsEscape(untrustedValue)+'";<\/script>')</script>

Prevenir XSS en jQuery

La forma más común de XSS en jQuery ocurre cuando pasamos el input del usuario a un selector de jQuery. Los desarrolladores web a menudo usaban location.hash y lo pasaban al selector, lo que causaba un XSS porque jQuery interpretaba ese contenido como HTML

jQuery reconoció este problema y corrigió la lógica del selector para verificar si el input comienza con un símbolo de hash (#). Ahora, jQuery solo renderiza HTML si el primer carácter es un <

Si le pasamos datos no confiables al selector de jQuery, debemos asegurarnos de escapar correctamente el valor usando la función jsEscape

Mitigación de XSS usando la política de seguridad de contenido (CSP)

La política de seguridad de contenido (CSP) es la última línea de defensa contra el cross-site scripting. Si la prevención de XSS falla, podemos usar CSP para mitigar XSS restringiendo lo que un atacante puede hacer

CSP permite controlar varias cosas, por ejemplo, si se pueden cargar scripts externos y si se ejecutarán inline scripts. Para desplegar CSP, necesitaremos incluir un encabezado HTTP llamado Content-Security-Policy con un valor que contenga tu política

Un ejemplo de CSP sería este:

1
default-src 'self'; script-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none';

Esta política especifica que los recursos, como imágenes y scripts, solo pueden cargarse desde el mismo origen que la página principal. Por lo tanto, aunque un atacante logre inyectar un payload XSS, solo podrá cargar recursos desde el origen actual. Esto reduce significativamente la posibilidad de que un atacante pueda explotar el XSS

Si necesitamos cargar recursos externos, debemos asegurarnos de permitir solo scripts que no ayuden al atacante a explotar nuestro sitio web. Por ejemplo, si permitimos explícitamente ciertos dominios, un atacante podría cargar cualquier script desde esos dominios. Siempre que sea posible, debemos alojar los recursos en nuestro propio dominio

Si eso no es posible, podemos usar una política basada en hash o nonce para permitir scripts en diferentes dominios. Un nonce es una cadena aleatoria que se añade como atributo a un script o recurso, y solo se ejecutará si esa cadena coincide con la generada por el servidor. Un atacante no puede adivinar esa cadena aleatoria, por lo que no podrá ejecutar un script o recurso con un nonce válido

Esta entrada está licenciada bajo CC BY 4.0 por el autor.