XSS в Sappy (частичный writeup)

Введение

Недавно прошел Google CTF, после которого были выложены исходные коды и exploit’ы к заданиям.

В этой статье я хотел бы подробнее рассмотреть web task с недавно прошедшего Google CTF, который называется «Sappy».

fe16f51ec1735261553fa67815dc4070.png

На момент решения задания, участника выдавался ограниченный исходный код задачи.

На данный момент полный исходный код проекта доступен в GitHub репозитории. Сейчас можно сказать, что была доступна директория challenge.

Анализ кода

Прежде, чем начать, введем основные определения.

Осваиваем DOM Invader: ищем DOM XSS и Prototype Pollution на примере пяти лабораторных и одной уязвимости на Хабре

Source
Свойство JavaScript, которое принимает данные, потенциально контролируемые пользователем. Пример источника — свойство location.search, поскольку оно считывает ввод из строки запроса, которой относительно просто управлять. В конечном итоге любое свойство, которым может управлять пользователь, является потенциальным Source. К этому относятся URL-адрес источника (document.referrer), Cookie пользователя (document.cookie) и WebMessages (подробнее про WebMessages написано здесь).

Sink
Потенциально опасная функция JavaScript или объект DOM, которые могут вызвать уязвимость, если в них передаются данные, контролируемые пользователем. Например, функция eval () является Sink’ом, поскольку она обрабатывает аргумент, который в него передается, как JavaScript. Примером HTML-Sink является document.body.innerHTML, так как это потенциально позволяет внедрить HTML и выполнить произвольный JavaScript.

Gadget
Небольшие фрагменты кода, которые могут быть использованы для эксплуатации уязвимостей. «Гаджеты» часто применяются в цепочках уязвимостей для достижения более значительного импакта. Еще их используют для обхода защитных мер, повышения привилегий или выполнения произвольного кода.

После знакомства с исходным кодом нас должен был заинтересовать файл sap.html, который подтягивает файл sap.js.

Потенциальный sink находится в данном участке кода:

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
);

Sink:

output.innerHTML = json.html;

Цепочка гаджетов:

  • Передача пользовательских данных в event listener

  • Переопределение API.host

  • Формирование параметра url с использованием API.host и data.page

  • AJAX запрос на url с использованием fetch()

  • Ответ запроса в формате json содержит ключ html, значение которого подставляется в sink

Подготовка exploit’а

Передача пользовательских данных в event listener

Чтобы проэксплуатировать данный участок кода, необходимо как-то передать в него пользовательские данные (source). Для этого используется метод addEventListener()

MDN:

Метод EventTarget.addEventListener() регистрирует определённый обработчик события, вызванного на EventTarget.

EventTarget может быть Element,  Document,  Window, или любым другим объектом, поддерживающим события (таким как XMLHttpRequest).

Синтаксис

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

type
Чувствительная к регистру строка, представляющая тип обрабатываемого события.

listener
Объект, который принимает уведомление, когда событие указанного типа произошло. Это должен быть объект, реализующий интерфейс EventListener или просто функция JavaScript.

Controlling the web message source (дословный перевод)

Если страница обрабатывает входящие веб-сообщения небезопасным способом, например, не проверяя корректно origin входящих сообщений в event listener, свойства и функции, вызываемые event listener’ом, могут стать sink’ами. Например, злоумышленник может разместить вредоносный iframe и использовать метод postMessage() для передачи данных веб-сообщения уязвимому event listener, который затем отправляет полезную нагрузку в sink на родительской странице. Такое поведение означает, что вы можете использовать веб-сообщения в качестве source для распространения вредоносных данных в любой из этих sink’ов.

Т.е. для выполнения метода postMessage() в нашем exploit’е необходимо выполнение следующих условий:

  • Наличие event listener’а типа «message» на атакуемом приложении

    addEventListener("message", ...)
  • Использование данных из event’а

    addEventListener("message", funciton(event) {
    	eval(event.data);
    })
  • Отсутствие защитных мер от использования iframe’ов

Тогда наш exploit на данном этапе (гаджете) может выглядеть примерно так:



Примечание. Здесь я перенес основной payload в значение ключа "page" для удобства.

Ответ запроса в формате json содержит ключ html, значение которого подставляется в sink

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

Если мы используем метод fetch() для запроса ресурса в формате json, то после получения ответа сервера можно использовать метод Response.json().

Есть ресурс для тестирования подобного функционала: https://mdn.github.io/dom-examples/fetch/fetch-json/.

Адрес https://mdn.github.io/dom-examples/fetch/fetch-json/products.json возвращает массив JSON объектов.

4d5e8398b4080557bff17425096a36ad.png

Примечание 1. В аргументе fetch() указан относительный адрес, т.к. запрос выполнялся в консоли панели инструментов конкретного ресурса.

Примечание 2. Метод json() так же возвращает promise, поэтому используем await для получения объекта.

Теперь используем схему data:,{payload} с json данными в качестве payload’а {"foo":"bar"} в URI и метод json():

ca1b60eb27e5c2b993996fd3ac6b9549.png

И мы видим, что данные из url вернулись в виде JSON объекта из ответа.

Получается, что код вида:

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

позволяет нам создавать различные json объекты в рамках одного и того же домена, используя схему data:, и promise’ы метода fetch().

Помним, что url формируется конкатенацией двух source со строковым значением "/sap/":

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

Чтобы payload имел вид типа:

'data://sappy-web.2024.ctfcompetition.com/sap,{"html":""}'

Тогда конечный exploit будет выглядеть примерно следующим образом:





    
    
    Document


    
    












4ea5eca5b8113ea201460073f3d57947.png

На момент написания writeup’а exploit от Google выглядит следующим образом:

window.postMessage('{"method": "initialize","host": "data://sappy-web.2024.ctfcompetition.com/,{\\"html\\":\\""}');
window.postMessage('{"method": "render", "page": "page1\\"}"}')

В завершении, чтобы получить флаг, нужно было изменить exploit таким образом, чтобы xss отправляла cookie жертвы на подконтрольный нам ресурс.

Далее сохранить этот exploit на подконтрольном ресурсе, передать url данного ресурса в поле URL блока «Share your learnings»:

fca4ec68904d60800059620f5958e539.png

Если все сделать верно, то вместе с cookie будет добыт флаг.

© Habrahabr.ru