заметки

xss с помощью gtag/gtm, eval и prototype pollution

2023-03-25
Обновление

2023-08-10: Фичу с __proto__ и eval прикрыли... Тупо выпилили вызов setTimeout(event_callback).

Предисловие

Это случаи из жизни. Это не статья, это заметка.

Для эксплуатации нам нужно иметь возможность добавлять данные в dataLayer или менять config_id в url загрузки GTAG/GTM.

Обычно это делается помощью postMessage (общение между окнами).

Бонусом ко всему это отлично работает когда задан Content-Security-Policy (CSP): nonce и вот это вот все. Обязательное условие наличие unsafe-eval. Все это так же будет в примерах.

Что позволено GTM, не позволено GTAG

GTM (www.googletagmanager.com/gtm.js) можно все, в том числе custom javascript, нам это не интересно.

GTAG (www.googletagmanager.com/gtag/js) же жестко ограничен модулями: ["google", "gtagfl", "lcl", "zone"]

Более того, если первая загрузка конфига происходит через GTM, то загрузка других конфигов через этот GTM происходит уже через GTAG.

Об этом и пойдет речь.

GTAG, config, eval

Важно! GTAG не должен быть уже загружен. Или window.GoogleAnalyticsObject должен быть undefined.

Вот пример страницы на которой ожидаются сообщения для загрузки GTAG

Настраиваем Google Tag Manager:

  1. В tagmanager.google.com добавляем tag Google Analytics: Universal Analytics.
  2. Включаем Enable overriding settings in this tag
  3. Tracking ID вписываем любое вида UA-000000000-00
  4. И в Advanced Configuration в поле Global Function Name вписываем eval - это будет функция в GoogleAnalyticsObject вместо ga по умолчанию
  5. Set Tracker Name в true
  6. В поле Tracker Name вписываем полезную нагрузку 1;eval(location.hash.substring(1));// - это то, что будет в eval из пункта 3.
    1; - это требование tagmanager
    JS для выполнения будет браться из iframe.location.hash где загружается уязвимая страница

Настройки Universal AnalyticsНастройка eval в UA

А здесь PoC для работы через postMessage с уязвимой страницей

Но этого оказалось недостаточно. Вылезает ошибка

ReferenceError: create is not defined at eval (eval at Rv (https://www.googletagmanager.com/gtag/js?id=GTM-MS2BRZ6:229:141), :1:1)

Оказывается, перед тем как сработает наш eval с нашей полезной нагрузкой, он пытается отработать create.

В дебаггере все прекрасно видно:

k - наш eval, createOnlyFields - наш пейлоад, но это не важно, потому что происходит eval("create")

eval вызывается в GTAGpayload для eval

И что? Нам нужно чтобы create не был undefined, чтобы он прекрасно отрабатывался в eval. И GTAG пошел дальше, до места основного запуска нашего пейлоад.

И пришла идея, заменить dataLayer (параметр l в url при загрузке gtag), на наш create.

То есть, посылаем сообщение с любым gtag_id (GTM-FAKE) и l=create, а после посылаем наш итоговый gtag_id. Обязательное условие это именно разные gtag_id.

В общем вот такой PoC

GTAG/GTM, __proto__ и setTimeout (eval)

Здесь уже не нужны никакие сторонние конфиги. Нужно два условия. Чтобы была возможность добавлять данные в dataLayer.
И второе, возможность передать __proto__, и JSON.parse здесь отлично помогает. Прямая передача object в message не передает __proto__.

Работает в GTAG и в GTM.

Вот пример такой страницы, где ожидаются сообщения для dataLayer.push()

Через dataLayer можно загружать очередной tag_id. И фишка в том, что в коде есть интересный момент про event_callback. И то, что в нем находится запускается через setTimeout, а это эквивалент eval.

Вызов event_callbackВызов event_callback через setTimeout

Проблема заключается в том, что event_callback удаляется из переданного массива данных.

Удаление свойства event_callback

Ладно, для начала загрузим этот конфиг. Нам нужен любой с префиксом G. Почему G? Это выявилось при исследовании случайно, путем проб и ошибок.

Если мы посылаем сообщение вида: window.postMessage('{"config":"G-0000000000000"}', '*') то это не соответсвуем хотя бы одному критерию.
Это обычный object, а не object Arguments, и нет свойства callee.

Проверка на callee и object Arguments в GTM

Значит добавляем: window.postMessage('{"config":"G-0000000000000","callee":{}}', '*').
А далее требует наличия length и строковое значение по индексу = 0. Которое должно равняться config

Проверка на length и config в GTM

Итого мы имеем такое сообщение: window.postMessage('{"0":"config","1":"G-0000000000000","length":2,"callee":{}}', '*')

Загружка пошла, но event_callback удаляется. А он нам нужен.

Как это обходится? Конечно же всем уже известный метод prototype pollution. Он искался, и он нашелся.

window.postMessage('{"0":{"__proto__":{"event_callback":"alert(origin)"}}}', '*');

Место prototype pollution в GTM/GTAG

Проверяем в консоле:
window.event_callback
Object.prototype

Object.prototype в GTM/GTAG

Итоговый PoC