Это случаи из жизни.
Для эксплуатации нам нужно иметь возможность добавлять данные в 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:
А здесь 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