October 11, 2023

Инструментарий JavaScript на стороне клиента

Существует масса кода, который не стоит вашего времени и умственных способностей. Разработчики двоичного реверса обычно сразу переходят к важному коду, используя ltrace, strace или frida. Вы можете сделать то же самое для клиентского JavaScript, используя только общие функции браузера. Это сэкономит время, сделает тестирование более увлекательным и поможет сосредоточить ваше внимание на коде, который заслуживает вашего внимания.

В этом блоге представлены мои мыслительные процессы и практические методы инструментирования клиентского JavaScript. Эти процессы помогли мне относительно легко находить глубоко заложенные ошибки в сложных кодовых базах. Многие из этих приемов я использую так долго, что реализовал их в веб-расширении под названием Eval Villain . Я познакомлю вас с некоторыми совершенно новыми функциями Eval Villain, а также покажу, как добиться тех же результатов без Eval Villain.

Общий метод и мышление

Тестирование приложения часто вызывает вопросы о том, как оно работает. Клиент должен знать ответы на некоторые из этих вопросов, чтобы приложение функционировало. Рассмотрите следующие вопросы:

  • Какие параметры принимает сервер?
  • Как параметры кодируются/шифруются/сериализуются?
  • Как модуль Wasm влияет на DOM?
  • Где находятся приемники DOM XSS и какая санация применяется?
  • Где находятся обработчики почтовых сообщений?
  • Как осуществляется перекрестное взаимодействие между объявлениями?

Чтобы веб-страница работала, ей необходимо знать ответы на эти вопросы. Это означает, что мы можем найти ответы и в JavaScript. Обратите внимание, что каждый из этих вопросов подразумевает использование определенных функций JavaScript. Например, как бы клиент реализовал обработчик почтового сообщения, не вызывая его addEventListener? Итак, «Шаг 1» — это подключение этих интересных функций, проверка того, какой вариант использования нас интересует, и обратное отслеживание. В JavaScript это будет выглядеть так:

(() => {
    const orig = window.addEventListener;
    window.addEventListener = (a, b) => {
        if (a === "message") {
            console.lgo("postMessage handler found");
            console.log(b); // You can click the output of this to go directly to the handler
            console.trace(); // Find where the handler was registered.
        }
        return orig(..arguments);
    };
})

Просто вставка приведенного выше кода в консоль будет работать, если обработчик еще не зарегистрирован. Однако крайне важно подключить функцию еще до ее использования. В следующем разделе я покажу простой и практичный способ всегда выигрывать в этой гонке.

Подключение собственного JavaScript — это «Шаг 1». Это часто помогает найти интересный код. Иногда вам захочется инструментировать этот код, но он не является нативным. Для этого требуется другой метод, который будет описан в разделе «Шаг 2».

Шаг 1. Подключаем собственный JavaScript

Создайте свое собственное расширение

Хотя вы можете использовать одно из многих веб-расширений, которые добавляют на страницу произвольный JavaScript, я не рекомендую это делать. Эти расширения часто содержат ошибки, имеют состояния гонки и их сложно разрабатывать. В большинстве случаев мне проще написать собственное расширение. Не пугайтесь, это действительно легко. Вам понадобится всего два файла, и я уже сделал их для вас здесь .

Чтобы загрузить код в Firefox, перейдите about:debugging#/runtime/this-firefoxв строку URL-адреса, щелкните Load Temporary Add-onи перейдите к manifest.json файлу в верхнем каталоге расширения.

Для Chrome перейдите в раздел chrome://extensions/, включите режим разработчика справа и нажмите load unpacked.

Расширение должно появиться в списке дополнений, где вы сможете быстро включить или отключить его. Если этот параметр включен, script.jsфайл будет загружаться на каждой веб-странице. Следующие строки кода регистрируют все входные данные в document.write.

	/*********************************************************
	 ***  Your code goes goes here to run in pages scope  ***
	 *********************************************************/

	// example code to dump all arguments to document.write
	document.write = new Proxy(document.write, {
		apply: function(_func, _doc, args) {
			console.group(`[**] document.write.apply arguments`);
				for (const arg of args) {
					console.dir(arg);
				}
			console.groupEnd();
			return Reflect.apply(...arguments);
		}
	});

Замените эти строки кода тем, чем захотите. Ваш код будет выполняться на каждой странице и в каждом кадре до того, как страница получит возможность запускать собственный код.

Как это работает

Шаблон использует файл манифеста для регистрации сценария содержимого . Манифест сообщает браузеру, что сценарий содержимого должен выполняться в каждом кадре и до загрузки страницы. Сценарии контента не имеют прямого доступа к области страницы, на которую они загружаются, но имеют прямой доступ к DOM. Таким образом, стандартный код просто добавляет новый скрипт в DOM страниц. CSP может запретить это, поэтому расширение проверяет, работает ли оно. Если CSP блокирует вас, просто отключите CSP с помощью настроек браузера, веб-расширения или перехватывающего прокси-сервера.

Обратите внимание, что код инструментирования в конечном итоге получает те же привилегии, что и веб-сайт. Таким образом, на ваш код будут распространяться те же ограничения, что и на страницу. Например, та же политика происхождения.

Асинхронность и гонки

Краткое предупреждение. Приведенный выше сценарий содержимого предоставит вам первый доступ к единственному потоку JavaScript. Сам веб-сайт не сможет запускать какой-либо JavaScript, пока вы не закроете этот поток. Попробуйте, посмотрите, сможете ли вы создать веб-сайт, который будет работать document.writeдо того, как его зацепит шаблон.

Первый доступ является огромным преимуществом: вы можете отравить среду, которую собирается использовать веб-сайт. Не отказывайтесь от своего преимущества, пока не закончите отравление. Это означает отказ от использования асинхронных функций.

Вот почему многие веб-расширения, предназначенные для внедрения пользовательского JavaScript на страницу, содержат ошибки. Получение конфигурации пользователя в веб-расширении выполняется с помощью асинхронного вызова. Пока async просматривает пользовательскую конфигурацию, страница выполняет свой код и, возможно, уже выполнила приемник, который вы хотели перехватить. Вот почему Eval Villain доступен только в Firefox. Firefox имеет уникальный API , который может зарегистрировать сценарий содержимого в конфигурации пользователя.

Эваль Злодей

Очень редко я сталкиваюсь с ситуацией «Шага 1», которую невозможно решить с помощью Eval Villain. Eval Villain — это просто скрипт контента, который перехватывает приемники и ищет источники входных данных. Вы можете настроить практически любую встроенную функциональность JavaScript в качестве приемника. Источники включают в себя строки конфигурации пользователя или регулярные выражения, параметры URL-адреса, локальное хранилище, файлы cookie, фрагмент URL-адреса и имя окна. Эти источники рекурсивно декодируются на наличие важных подстрок. Давайте посмотрим на ту же страницу примера выше, на этот раз с Eval Villain в конфигурации по умолчанию.

Обратите внимание, что эта страница загружается из локального файла file://. Исходный код показан ниже.

<script>
let x = (new URLSearchParams(location.search)).get('x');
x = atob(x);
x = atob(x);
x = JSON.parse(x);
x = x['a'];
x = decodeURI(x);
x = atob(x);
document.write(`Welcome Back ${x}!!!`);
</script>

Несмотря на то, что на странице нет веб-запросов, Eval Villain по-прежнему успешно перехватывает настроенный пользователем приемник document.writeдо того, как страница его использует. Нет состояния гонки.

Также обратите внимание, что Eval Villain не просто отображает вводимые данные document.write. Он правильно выделил точку инъекции. Параметр URL xсодержал закодированную строку, которая попала в приемник document.write. Eval Villain выяснил это, рекурсивно декодировав параметры URL. Поскольку параметр был декодирован, encoderпользователю предоставляется функция. Вы можете щелкнуть правой кнопкой мыши, скопировать сообщение и вставить его в консоль. Использование encoderфункции позволяет быстро опробовать полезные нагрузки. Ниже показана функция кодировщика, используемая для внедрения marqueeтега на страницу.

Если вы читали предыдущие разделы, вы знаете, как все это работает. Eval Villain просто использует скрипт контента для внедрения JavaScript на страницу. Все, что он делает, вы можете сделать в своем собственном сценарии контента. Кроме того, теперь вы можете использовать исходный код Eval Villain в качестве шаблонного кода и настраивать его функции для вашей конкретной технической задачи.

Шаг 1.5: Быстрый совет

Допустим, вы использовали «Шаг 1», чтобы получить console.traceинтересную встроенную функцию. Возможно, параметр URL-адреса попал в ваш decodeURIприемник, и теперь вы возвращаетесь к функции анализа URL-адресов. В этой ситуации я регулярно допускаю ошибку, и я хочу, чтобы вы поступили лучше. Когда вы получите след, пока не начинайте читать код!

Современные веб-приложения часто содержат полифилы и другую ерунду в верхней части файла console.trace. Например, трассировка стека, которую я получаю на странице результатов поиска Google, начинается с функций iAa, ka, c, ng, getAll. Не становитесь туннельным видением и начинайте читать kaтогда, когда getAllэто очевидно, что вы хотите. Когда смотрите getAll, не читайте исходник! Продолжайте сканирование, обратите внимание, что getAll это метод, а его родственный брат — , get, , и все остальные методы, перечисленные в документации .setsizekeysentriesURLSearchParams . Мы только что нашли несколько пользовательских парсеров URL-адресов, повторно реализованных в минимизированном коде без фактического чтения кода. «Сканируйте» как можно больше, не начинайте глубоко читать код, пока не найдете нужное место или сканирование вас не подвело.

Шаг 2. Перехват неродного кода

Инструментирование машинного кода не привело к появлению уязвимостей. Теперь вы хотите инструментировать саму неродную реализацию. Позвольте мне проиллюстрировать это примером.

Допустим, вы обнаружили функцию анализатора URL-адресов, которая возвращает объект с именем url_params. Этот объект содержит все пары «ключ-значение» для параметров URL-адреса. Мы хотим контролировать доступ к этому объекту. Это может дать нам хороший список всех параметров URL-адреса, связанных с URL-адресом. Таким образом мы можем обнаружить новые параметры и разблокировать скрытые функции сайта.

Сделать это в JavaScript не сложно. С помощью 16 строк кода мы можем получить хорошо организованный уникальный список параметров URL-адресов, связанных с соответствующей страницей и сохраненных для быстрого доступа в формате localStorage. Нам просто нужно придумать, как вставить наш код прямо в парсер URL.

function parseURL() {
    // URL parsing code
    // url_params = {"key": "value", "q": "bar" ...

    // The code you want to add in
    url_params = new Proxy(url_params, {
        __testit: function(a) {
            const loc = 'my_secret_space';
            const urls = JSON.parse(localStorage[loc]||"{}");
            const href = location.protocol + '//' + location.host + location.pathname;
            const s = new Set(urls[href]);
            if (!s.has(a)) {
                urls[href] = Array.from(s.add(a));
                localStorage.setItem(loc, JSON.stringify(urls));
            }
        },
        get: function(a,b,c) {
            this.__testit(b);
            return Reflect.get(...arguments);
        }
    };
    // End of your code

    return url_params;
}

Инструменты разработки Chrome позволяют вам вводить собственный код в исходный код JavaScript, но я не рекомендую это делать. По крайней мере, у меня добавленный код исчезнет при загрузке страницы. Кроме того, таким образом непросто управлять какими-либо контрольно-измерительными точками.

У меня есть решение получше, оно встроено в Firefox и Chrome. Возьмите код вашего инструмента, заключите его в круглые скобки и добавьте && falseв конец. Приведенный выше код становится таким:

(url_params = new Proxy(url_params, {
    __testit: function(a) {
        const loc = 'my_secret_space';
        const urls = JSON.parse(localStorage[loc]||"{}");
        const href = location.protocol + '//' + location.host + location.pathname;
        const s = new Set(urls[href]);
        if (!s.has(a)) {
            urls[href] = Array.from(s.add(a));
            localStorage.setItem(loc, JSON.stringify(urls));
        }
    },
    get: function(a,b,c) {
        this.__testit(b);
        return Reflect.get(...arguments);
    }
}) && false

Теперь щелкните правой кнопкой мыши номер строки, в которую вы хотите добавить свой код, нажмите «условная точка останова».

Вставьте туда свой код. Из-за того, что && falseусловие никогда не будет истинным, вы никогда не получите точку останова. Браузер по-прежнему будет выполнять наш код в той области функции, в которую мы вставили точку останова. Условия гонки отсутствуют, и точка останова будет продолжать действовать. Он появится на новых вкладках, когда вы откроете инструменты разработчика. Вы можете быстро отключить отдельные сценарии инструментирования, просто отключив вспомогательную точку останова. Или отключите их все, отключив точки останова или закрыв окно инструментов разработчика.

Я использовал этот конкретный пример, чтобы показать, как далеко вы можете зайти. Инструментированный код сохранит параметры URL для каждого сайта в записи локального хранилища. На любой странице вы можете автоматически заполнить все известные параметры URL-адреса в строке URL-адреса, вставив следующий код в консоль.

(() => {
const url = location.protocol + '//' + location.host + location.pathname;
const params = JSON.parse(localStorage.getItem("my_secret_space"))[url];
location.href = url + '?' + params.flatMap( x => `${x}=${x}`).join('&');
})()

Если вы часто этим пользуетесь, то можете даже поместить код в букмарклет .

Объединение собственного и неродного инструментария

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

Это подводит нас к последней особенности Eval Villain . Ваше условное выражение может использовать функцию рекурсивного декодирования Eval Villains. Во всплывающем меню нажмите «Настроить» и перейдите в раздел «Глобальные». Убедитесь, что строка «sourcer» включена, и нажмите «Сохранить».

Я часто включаю/отключаю эту функцию, поэтому в самом всплывающем меню есть второй флаг «включить». Он находится в меню «включить/выключить» как «Пользовательские источники». Это заставляет Eval Villain экспортировать evSourcerфункцию в область глобального имени. Это добавит любой произвольный объект в список рекурсивно декодированных источников.

Как видно, первый аргумент — это то, что вы называете источником. Второй — это сам объект, который вы хотите обыскать. Если нет специальной кодировки, которую Eval Villain не понимает, вы можете просто поместить ее в необработанном виде. Существует необязательный третий аргумент, который будет заставлять источник источника console.debugкаждый раз вызываться. Эта функция возвращает false, поэтому вы можете использовать ее в качестве условной точки останова где угодно. Например, вы можете добавить это как условную точку останова, которая запускается только в интересующем обработчике почтового сообщения при получении сообщений из определенного источника, чтобы определить, попадет ли какая-либо часть сообщения в приемник DOM XSS. Использование этого в нужном месте может облегчить ограничения SOP, налагаемые на ваш инструментальный код.

Точно так же, как evSourcerесть evSinker. Я редко использую это, поэтому во всплывающем меню нет пункта «включить/отключить». Он принимает имя приемника и список аргументов и действует как ваш собственный приемник. Он также возвращает false, поэтому его можно легко использовать в условных точках останова.

Заключение

Написание собственного инструментария — мощный навык для исследования уязвимостей. Иногда достаточно пары строк JavaScript, чтобы укротить огромную кодовую базу. Зная, как это работает, вы сможете лучше понять, что могут и чего не могут делать такие инструменты, как Eval Villain и DOM захватчик . При необходимости вы также можете адаптировать свой собственный код, когда инструмента не хватает.