Загрязнение прототипа — это ошибка, которая еще не так хорошо задокументирована, как некоторые из основных, известных широкой публике, таких как SQL-инъекции (SQLI), межсайтовый скриптинг (XSS), включение локальных файлов (LFI) и многие другие.

Уязвимость затрагивает языки на основе прототипов, наиболее известным и используемым из которых является JavaScript (JS). Это начало обсуждаться исследователями примерно в 2017 году, а первые уязвимости начали появляться в начале 2018 года.
У нас есть, например, эта атака типа «отказ в обслуживании (DoS)» (2018). Но она может стать намного более серьезной, чем просто DoS, например, этауязвимость Lodash, которая имеет оценку CVSS 7,3 на Snyk. Учитывая тот факт, что Lodash — такая популярная библиотека и, следовательно, она касается многих веб-приложений, мы можем понять, почему это может стать очень проблематичным.

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

Объекты

Прежде всего, нам нужно понять, что такое объект. Объект представляет собой набор пар, содержащих ключи и соответствующие им значения. Допустим, у нас есть объект car и цвет автомобиля, который является ключом, имеет значение red. Пары ключ-значение называются свойствами указанного объекта, они определяют характеристики объекта.
Этот объект можно создать очень просто:

car = {color : "red"}

Давайте попробуем визуализировать объект как простую коробку. У нас есть коробка с ключами внутри. Допустим, мы открываем эту коробку, чтобы достать ключ, который нам нужен для доступа к значению. Указание объекта осуществляется вызовом его имени (автомобиль), открытие ящика для доступа к любой клавише осуществляется путем ввода точки (.).
Осталось только указать, какой ключ мы хотим захватить (цвет).
Результат такой:

car.color

Затем это вернет нам значение для назначенного ключа. В нашем случае будет показан цвет красный, потому что это характеристика нашего объекта автомобиля. Конечно, мы можем добавить к нашему объекту больше пар ключ/значение, например, марку автомобиля или его мощность. Также очень важно отметить, что значения могут быть чем-то другим, кроме строк или чисел, значения также могут быть функциями, массивами или даже другими объектами. Однако ключи объекта должны быть строками.

Прототипы

Прототипы — это механизм, с помощью которого объекты могут наследовать свойства друг друга. Это означает, что объекты могут выступать в качестве шаблона для других наследующих объектов, которые сами могут быть унаследованы и так далее. Такой порядок действий называется цепочка прототипов. Именно по этой причине одни объекты будут иметь свойства других объектов и поэтому так легко передать эти свойства.

Я попытаюсь объяснить эту концепцию на примере, продолжая наши автомобили. Пишем конструктор Car():

function Car(brand, color) {
    this.brand = brand;
    this.color = color;
    this.drive = function() {
        return "The " + this.brand + " is driving away."
    }
}

Это то же самое, что и создание класса с конструктором, включенное в ECMAScript 2015 (ES6), которое просто призвано облегчить разработчикам переход между языками.

У нас есть конструктор Car, первая буква которого по соглашению пишется с заглавной буквы.
Теперь можно создать экземпляр объекта Car с ключевым словом new и указав название бренда и цвет.

let redCar = new Car("BMW", "red")

Как видно на снимке экрана выше, объект redCar, который является экземпляром Car, может вызывать свои свойства (red и bmw), но самое главное использовать метод drive(), который унаследован благодаря к прототипам.

Наследование

Как объяснялось выше, объекты могут наследовать свойства друг друга через прототипы. Важно отметить, что методы и свойства не копируются из одного объекта в другой, доступ к ним осуществляется путем восхождения по цепочке прототипов. В нашем примере redCar наследуется от Car, Car, а с другой стороны наследуется от Object.prototype, который является базовым объектом и шаблоном для всех вновь созданных объектов, а значит, он находится на вершине цепочки прототипов. Если вы выйдете за пределы Object.prototype, вы получите null.

Это можно увидеть в нашем примере с автомобилями, использующими свойство __proto__.
Обратите внимание, что __proto__ и prototype — это не одно и то же. __proto__ — это фактический объект, который используется в цепочке поиска, prototype, с другой стороны, покажет прототипы объекта, на котором он используется (это объект, используемый для build __proto__).
Короче говоря, это означает, что прототип нельзя запускать в экземплярах (например, redCar), а только в функции-конструкторе (например, Car).
__proto__ однако может запускаться на всех объектах и ​​является собой и объектом.

redCar.__proto__ == Car.prototype
redCar.__proto__.__proto__ == Object.prototype
Car.prototype.__proto__ == Object.prototype
Car.prototype.__proto__.__proto__ == null

Теперь отчетливо видны различные уровни цепочки прототипов.
Вот почему наш экземпляр redCar имеет возможность вызывать методы из Object.prototype, например: toString() , valueOf() или getPropertyOf().

Теперь у нас есть прочная основа для борьбы с загрязнением прототипов.

Когда возникают уязвимости Prototype Pollution?

Злоумышленник должен иметь возможность манипулировать __proto__. Злоумышленник может с этой возможностью изменить поведение приложений.
Как объяснялось выше, каждый объект наследует прототипы от своего прототипа (redCar из Car и Car из Object). Злоумышленники могут воспользоваться этой функцией, чтобы испортить Object.prototype, чтоприведет к заражению каждого объекта javascript.

__proto__ загрязнение имущества

Возвращаясь к нашему конструктору Car(), можно получить доступ к Object.prototype, используя этот код:

redCar.__proto__.__proto__

Это также работает в конструкторе:

Car.__proto__.__proto__

Следовательно, прототип загрязнения может выглядеть так:

redCar.__proto__.__proto__.toString = function() {
    console.log("Polluted with the __proto__ property")
};

Теперь при вызове метода toString() для любого объекта после выполнения этого кода в консоль будет записываться сообщение «Загрязнено свойством __proto__» вместо возвращая строку, которая будет представлять объект, как обычно.

Мы видим, что он влияет даже на объекты, которые будут созданы позже, потому что все объекты наследуются от прототипа объекта.
Чтобы это работало, злоумышленнику нужно найти способ изменить некоторые свойства (пары ключ-значение) объекта. Это довольно сложно найти, если бэкенд-код не виден, поэтому это так опасно для проектов с открытым исходным кодом.

Загрязнение имущества прототипа

Мы уже видели свойство прототипа выше, оно также может быть использовано злоумышленниками. Это может быть полезно в случаях, когда ключевое слово «__proto__» ограничено. Это гораздо менее эффективно, потому что, как объяснялось выше, он работает с конструктором и может испортить только свойство прототипа функции, а в большинстве случаев не искомый прототип объекта. Это означает, что если вы можете изменить свойства функции, в нашем случае Car, каждый экземпляр этой функции будет загрязнен (например, redCar).< br /> В нашем примере загрязнение будет выглядеть так:

Car.prototype.toString = function() {
    console.log("Polluted with prototype")
}

Как видно на снимке экрана выше, экземпляры конструктора Car загрязнены (даже новые), но экземпляры, созданные непосредственно из прототипа Object’s, — нет. Однако возможно загрязнить все объекты, если мы используем прототип для Object. Это очень просто, но, как вы понимаете, невероятно в дикой природе.

Object.prototype.toString = function() {
    console.log("Polluted with Object.prototype.")
}

Использование свойства конструктора

Другой способ добиться загрязнения прототипа всех объектов JS свойством прототипа без использования Object — использовать свойство конструктора.
Это свойство возвращает ссылку на функцию конструктора объекта, которая создала объект экземпляра. . Это означает, что для нашего экземпляра redCar это будет функция-конструктор Car. Но запуск его на объекте, созданном непосредственно из Object.prototype, например Car или любом объекте, созданном без промежуточных функций-конструкторов, даст нам желаемый прототип объекта обратно.

simpleObject.constructor.prototype.toString = function() {
    console.log("Polluted with the constructor property.")
}

Типы атак

Отказ в обслуживании (DoS)

Как мы видели выше, прототип объекта содержит функции, которые можно наследовать и вызывать для различных операций, например, метод toString(). Давайте представим, что мы изменили бы этот метод на случайную функцию, такую ​​как простая console.log(), как это было сделано ранее. Впоследствии это вызовет проблемы, если метод будет вызван для другого объекта.
Это может и, скорее всего, сделает приложение неработоспособным.

Внедрение свойств

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

  • печенье
  • жетоны
  • другие атрибуты кода, например, атрибут user.isAdmin, могут привести к повышению привилегий, если мы установим для него значение true.

Удаленное выполнение кода (RCE)

Это самая страшная ситуация, но она возможна только в определенных сценариях. Базе кода нужно будет оценить определенный атрибут объекта, который мы могли бы испортить, с помощью eval().
Например, eval(<object>.<attribute>) будет уязвим для RCE, если атрибут может быть загрязнен.

Некоторые примеры

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

Небезопасное рекурсивное слияние объектов

Относительно часто бывает так, что нужно объединить 2 объекта, это может быть уязвимо и использовано, как это произошло в lodash.
Доказательство концепции (PoC):

В этом PoC мы видим, что функция слияния перебирает исходный объект и добавит свойство в целевой объект. Это делается рекурсивно, поэтому он проверяет, существует ли значение, и если это объект, он продолжает слияние, пока не примет все свойства source.
Злоумышленник может загрязнить это с помощью полезная нагрузка, которая должна выглядеть примерно так, чтобы ключ «isAdmin» имел значение true:

{
 "proto" : "pollution",
 "__proto__" : {
      "isAdmin" : "true"
     }
}

Определение свойства по пути

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

function(object, path, value)

Если значение path можно изменить, можно будет перезаписать важные существующие свойства или добавить новые. Если мы, например, введем __proto__.pollution, pollution является случайным методом, pollution будет присвоен прототипу конструктора класса из загрязненный объект, если таковой имеется. Если это экземпляр из класса конструктора, ввод __proto__.__proto__.pollution приведет к загрязнению прототипа объекта.
Известный случай — пакет Mpath. уязвимости.

Другой

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

Снижение уязвимости прототипа к загрязнению:

  • Самое главное, у нас должна быть кодовая база с хорошими практиками программирования.
  • Дезинфекция пользовательского ввода является ключевым фактором, поскольку уязвимость в основном возникает на стороне клиента.
  • Обновляйте библиотеки новыми исправлениями, а также регулярно выполняйте проверки npm для сканирования вашего проекта на наличие уязвимостей.
  • Избегание рекурсивных слияний снижает риски такой уязвимости.
  • Также рекомендуется использовать метод Object.freeze(), так как замороженный объект больше нельзя изменить и, следовательно, нельзя будет создавать, удалять, перечислять, настраивать или записывать свойства. Замораживание объекта также предотвратит изменение его прототипа.
  • Представленная в EcmaScript 6 (ES6) структура данных map теперь хорошо поддерживается и является лучшей альтернативой объектам, поскольку действует как хэш-карта (пары ключ-значение) без всех предостережения безопасности.
  • Наконец, создание объектов без свойств прототипа, например Object.create(null), не повлияет на цепочку прототипов и, таким образом, уменьшит риск загрязнения прототипа.

использованная литература