Обновление2023-08-10: Фичу с __proto__ и eval прикрыли... Тупо выпилили вызов setTimeout(event_callback).
Это случаи из жизни.
Для эксплуатации нам нужно иметь возможность добавлять данные в dataLayer или менять config_id в url загрузки GTAG/GTM.
Обычно это делается помощью postMessage (общение между окнами).
Бонусом ко всему это отлично работает когда задан Content-Security-Policy (CSP): nonce и вот это вот все. Обязательное условие наличие unsafe-eval. Все это так же будет в примерах.
GTM (www.googletagmanager.com/gtm.js) можно все, в том числе custom javascript, нам это не интересно.
GTAG (www.googletagmanager.com/gtag/js) же жестко ограничен модулями: ["google", "gtagfl", "lcl", "zone"]
Более того, если первая загрузка конфига происходит через GTM, то загрузка других конфигов через этот GTM происходит уже через GTAG.
Об этом и пойдет речь.
Важно! GTAG не должен быть уже загружен. Или window.GoogleAnalyticsObject должен быть undefined.
Вот пример страницы на которой ожидаются сообщения для загрузки GTAG
Настраиваем Google Tag Manager:
Google Analytics: Universal Analytics
.Enable overriding settings in this tag
Tracking ID
вписываем любое вида UA-000000000-00
Advanced Configuration
в поле Global Function Name
вписываем eval
- это будет функция в GoogleAnalyticsObject
вместо ga по умолчаниюSet Tracker Name
в trueTracker Name
вписываем полезную нагрузку 1;eval(location.hash.substring(1));//
- это то, что будет в eval из пункта 3.1;
- это требование tagmanageriframe.location.hash
где загружается уязвимая страницаА здесь 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")
И что? Нам нужно чтобы create
не был undefined
, чтобы он прекрасно отрабатывался в eval. И GTAG пошел дальше, до места основного запуска нашего пейлоад.
И пришла идея, заменить dataLayer (параметр l в url при загрузке gtag), на наш create.
То есть, посылаем сообщение с любым gtag_id (GTM-FAKE) и l=create, а после посылаем наш итоговый gtag_id. Обязательное условие это именно разные gtag_id.
Здесь уже не нужны никакие сторонние конфиги. Нужно два условия. Чтобы была возможность добавлять данные в dataLayer.
И второе, возможность передать __proto__
, и JSON.parse
здесь отлично помогает. Прямая передача object в message не передает __proto__.
Работает в GTAG и в GTM.
Вот пример такой страницы, где ожидаются сообщения для dataLayer.push()
Через dataLayer можно загружать очередной tag_id. И фишка в том, что в коде есть интересный момент про event_callback
. И то, что в нем находится запускается через setTimeout
, а это эквивалент eval.
Проблема заключается в том, что event_callback
удаляется из переданного массива данных.
Ладно, для начала загрузим этот конфиг. Нам нужен любой с префиксом G
. Почему G? Это выявилось при исследовании случайно, путем проб и ошибок.
Если мы посылаем сообщение вида: window.postMessage('{"config":"G-0000000000000"}', '*')
то это не соответсвуем хотя бы одному критерию.
Это обычный object
, а не object Arguments
, и нет свойства callee
.
Значит добавляем: window.postMessage('{"config":"G-0000000000000","callee":{}}', '*')
.
А далее требует наличия length
и строковое значение по индексу = 0
. Которое должно равняться config
Итого мы имеем такое сообщение: window.postMessage('{"0":"config","1":"G-0000000000000","length":2,"callee":{}}', '*')
Загружка пошла, но event_callback удаляется. А он нам нужен.
Как это обходится? Конечно же всем уже известный метод prototype pollution. Он искался, и он нашелся.
window.postMessage('{"0":{"__proto__":{"event_callback":"alert(origin)"}}}', '*');
Проверяем в консоле:window.event_callback
Object.prototype