Глава 6. Порча прототипа

Введение

Порча (загрязнение, pollution) прототипа это уязвимость, происходящая когда вы осуществляете ранимое рекурсивное слияние, а один или несколько объектов являются контролируемыми. При произведении рекурсивного слияния код зачастую применяет ключи свойств не соблюдая безопасность, что может приводить к непреднамеренному назначению свойств. Например, в JavaScript имеется волшебное свойство с названием __proto__, которое в действительности выступает получателем/ установщиком (getter/ setter), позволяющим вам получать и устанавливать прототип объекта. Когда управляемый вами объект способен получить данное свойство, сама функция рекурсивного слияния фактически будет манипулировать одним из глобальных прототипов, чаще всего Object.prototype. Затем это делает для вас возможным контроль неожиданных свойств, которые по мнению их разработчика безопасны и, следовательно, способны приводить к XSS (cross-site scripting, сценариям перекрёстных сайтов) DOM на стороне клиента или даже к RCE на уровне соответствующего сервера.

Обычно вы не можете устанавливать простое свойство с названием __proto__, потому как,что уже упоминалось, это и в самом деле получатель/ установщик. Однако при использовании JSON.parse создаётся обычное свойство когда вы пользуетесь свойством __proto__, причём это один из тех компонентов, который может приводить к осквернению (pollution) прототипа. Это можно продемонстрировать следующим кодом:


1 ({__proto__:"foo"}).hasOwnProperty('__proto__')//false
2 (JSON.parse('{"__proto__":"foo"}')).hasOwnProperty('__proto__')//true
 	   

Когда некая уязвимая функция слияния занимается перечислением указанного объекта, по той причине, что __proto__ выступает обычным свойством, это делает возможным применять его, но, что важно, когда его пытаются присваивать, оно снова превращается в установщик (setter) для своего целевого объекта, а именно это и вызывает осквернение прототипа. Стоит отметить, что __proto__ не единственный подвергающийся атаке вектор, он самый распространённый, но существует и альтернатива. __proto__, по- существу, выступает сокращением для constructor.prototype и, когда рекурсивное слияние позволяет вам применять множество свойств, вы можете применять свойства constructor.prototype также для того чтобы вызывать загрязнение прототипа. Теперь мы имеем основу того как это происходит и давайте рассмотрим как вы можете пользоваться этим.

  Методика

Методика указывает на тот способ, которым она загрязняет прототип. Например, вы применяете __proto__ или constructor.prototype, или иную новую технологию.

  Источник

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


?__proto__[property]=value
 	   

  Гаджет

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

  Потенциальный гаджет

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

Порча прототипа стороны клиента

Существуют два вида порчи прототипа: стороны клиента и стороны сервера. Данный раздел имеет дело с видом стороны клиента. Основной целью употребления порчи прототипа стороны клиента является общий XSS DOM. Вы выясняете, позволяет ли сайт добавлять свойства к прототипу Object, а затем смотрите, попадает ли такое свойство в слив (sink), что в результате приводит к управлению произвольным JavaScript или HTML.

  Поиск порчи прототипа

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


?__proto__[foo]=bar
 	   

Далее, после загрузки данной страницы, вы можете воспользоваться консолью своего браузера для инспекции прототипа Object:


1 console.log(Object.prototype)
2 //{foo: 'bar', constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnPrope\
3 rty: ƒ, …}
 	   

Если прототип Object содержит ваше свойство foo, значит вы удачно выявили порчу прототипа на стороне клиента. Применение свойства __proto__ это не единственный способ обнаружить порчу прототипа, вы можете пользоваться свойствами constructor[prototype], которые по своей сути являются тем же самым, они менее распространены по сравнению со свойством __proto__, поскольку они требуют три ключа свойства, а сайты часто будут применять два. Для проверки этого вы можете выполнить тот же процесс, просто заменив __proto__ на constructor[prototype]:


?constructor[prototype][foo]=bar
 	   

Что приводит к манипулированию прототипом объекта, если сайт уязвим для этой техники:


1 console.log(Object.prototype)
2 //{foo: 'bar', constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnPrope\
3 rty: ƒ, …}
 	   

Различные библиотеки выполняют синтаксический анализ строки запроса по-разному, я сталкивался с сайтами, которые будут применять в строке запроса точки, чтобы проверить это, просто удалите квадратные скобки и добавьте точки для доступа к различным ключам свойств:


?__proto__.foo=bar
 	   

Также стоит проверить хэш, я встречал сайты, которые выполняют синтаксический разбор хэша как параметры запроса. Если вы испробуете каждую комбинацию как со строкой запроса, так и с хэшем, то это должно приводить к 99% уязвимостей прототипа на стороне клиента.

  Модификация естественных методов

Когда сайт обладает уязвимостью к порче прототипа вы не ограничены лишь манипуляциями с прототипом Object, вам даже не требуется добавлять некое свойство. Вы можете изменять один из встроенных методов глобального Object или ещё каких- то. Например, представьте что сайт пользуется методом Object.keys, тогда, воспользовавшись порчей прототипа источника вполне возможно перекрыть этот метод. Атака может выглядеть следующим образом:


?__proto__[keys]=0//Object.keys === "0"
 	   

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

  Естественные API браузера

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


1 Object.prototype.body='foo=bar'
2 const request = new Request('/myEndpoint', {
3     method: 'POST',
4 });
5 fetch(request);
 	   

Приведённый выше код пользуется имеющимся объектом Request, потому как такой объект Request не определяет свойство body; имеется возможность контроля над таким свойством при помощи некого наследуемого свойства через порчу прототипа. При отправке запроса POST, он будет обладать body POST со значением foo=bar. В точности та же самая техника может быть применена со вторым параметром функции fetch():


1 Object.prototype.body='foo=bar';
2 const init = {
3     method: 'POST'
4 };
5 fetch('/end-point', init)
 	   

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


1 Object.prototype.headers={foo:'bar'};
2 const init = {
3     method: 'POST',
4 };
5 fetch('/end-point', init)
 	   

Приведённый выше пример отправляет заголовок "foo" со значением "bar". Такие методы полезны в цепочках ошибок, когда у вас присутствует порча прототипа, но нет XSS DOM. Вы можете применять данный метод чтобы связать в цепочку порчу прототипа с CSRF {cross-site request forgery, подделкой запросов перекрёстных сайтов} через fetch() или прочие уязвимости.

Иными уязвимыми для порчи прототипа методами выступают методы ES5, например, defineProperty(). При вызове такой функции вы указываете целевой объект в первом параметре, второй параметр принимает имя свойства, а третий позволяет вам применять дескриптор (который просто является литералом объекта). Такой литерал объекта может быть испорчен, а поскольку некоторые свойства являются не обязательными, и по умолчанию, когда не определены, обладают значением false, это исключительный приступ для порчи прототипа.

Давайте здесь рассмотрим некий образец кода как применять такую функцию:


1 let obj = {};
2 Object.defineProperty(obj, 'foo', {configurable:false, writable: false, value:123});
 	   

В этом приведённом выше образце кода мы определяем свойство с названием "foo" и превращаем его в не настраиваемое, что подразумевает, что вы не сможете переопределять данное свойство при помощи defineProperty(). writable указывает на то, что его значение можно перекрывать. Как уже упоминалось, однако, данные свойства и являются не обязательными, что означает что по умолчанию они равны false. Давайте взглянем на этот код снова с опущенными в его дескрипторе свойствами:


1 let obj = {};
2 Object.defineProperty(obj, 'foo', {value:123});
3 
4 obj.foo//123
5 obj.foo=0;
6 obj.foo//123
 	   

Поскольку выше у нас опущены свойства configurable и writable, механизм JavaScript предполагает, что они рассматриваются равными false, однако, как мы наблюдаем при помощи fetch(), эти свойства могут наследоваться. Это означает, что мы и в самом деле можем изменять установленное поведение defineProperty() через порчу прототипа:


1 Object.prototype.configurable=true;
2 Object.prototype.writable=true;
3 
4 let obj = {};
5 Object.defineProperty(obj, 'foo', {value:123});
6 
7 obj.foo//123
8 obj.foo='overwritten';
9 obj.foo//overwritten
 	   

Как мы наблюдаем выше, поведение заданного свойства изменилось, теперь можно перекрыть значение, причём невзирая на пропущенные в его дескрипторе свойства. Это действительно похоже на проблему с самой спецификацией, поскольку унаследованные свойства не следует применять при использовании естественных API, ибо это может приводить к порче прототипа. У меня сомнения что это будет исправлено, так как наследование лежит в основе JavaScript.

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

Для данной методики уязвим даже localStorage, когда такой сайт пользуется getter соответствующейform вместо метода getItem(). Часто разработчики будут пользоваться получателем формы, например localStorage.foo, потому как это довольно удобно, но это будет наследоваться из прототипа Object, а следовательно вы сможете управлять таким свойством через порчу прототипа, а потому вы бы обладали потенциальным гаджетом.


1 if(localStorage.foo) {
2     let foo = localStorage.foo;
3 }
 	   

Приводимый выше образец создаёт потенциальный гаджет с названием "foo". Поскольку данный код проверяет присутствие свойства "foo" и применяет соответствующий получатель во всех экземплярах, он подвержен порче.

  Поиск гаджетов

Итак, вы обнаружили потенциальный гаджет и у вас присутствует порча прототипа, как вы можете выявлять реальные гаджеты? Прежде всего, вам требуется выполнить некоторый JavaScript, прежде чем сама страница исполнит их. Вы можете осуществить это воспользовавшись прокси- сервером, например Burp и добавляя до исполнения чего бы то ни было оператора отладчика. Такой оператор отладчика - это особая команда JavaScript, которая активирует отладчик JavaScript (обычно Devtools), по достижению отлаживаемого оператора исполнение JavaScript будет приостановлено в той точке, в которой встречается данный оператор и затем вы вольны вводить свой собственный код JavaScript применяя этот отладчик. Чтобы определить является ли ваш потенциальный гаджет в действительности гаджетом, вы можете воспользоваться методом Object.defineProperty() для выявления того где применяется это метод. Вы можете вызывать данный метод и осуществлять трассировку стека при каждом доступе к получателю данного свойства:


1 Object.defineProperty(Object.prototype,'potentialGadget', {__proto__:null, get(){
2     console.trace();
3     return 'test';
4 }})
 	   

Применяя трассировку стека вы способны определять ту часть исходного кода, в котором применяется данное свойство и посмотреть попадает ли это значение в уязвимый слив (sink). Существуют автоматизирующие данный процесс инструменты. Я написал инструмент с названием DOM Invader, который способен выявлять источники и гаджеты подвергающихся порче прототипов. Это некое расширение для браузера как часть встраиваемого браузера в Комплекте Burp.

Порча прототипа стороны сервера

Порчу прототипа на стороне клиента довольно легко обнаружить, поскольку у вас имеется исходный код, тогда как на стороне сервера это гораздо сложнее, потому как порча происходит на сервере, и обычно у вас нет доступа к исходному коду. Проблема усугубляется, когда дело доходит до DoS: с DoS на стороне клиента вы можете обновить браузер, и всё снова заработает, тогда как, если DoS происходит на уровне сервера, приложение может быть сломано и недоступно для вас или других пользователей и часто единственное решение — перезапустить процесс данного node.

Когда у вас имеется исходный код всё намного проще. К порче прототипа на стороне сервера применимы те же принципы, вы всё ещё располагаете методикой, источником и гаджетом. Когда у вас имеется контроль над процессом node и вы сами размещаете приложение, вы даже можете применять тот же способ определения успешности порчи прототипа при помощи консоли браузера. Для командной строки node имеются два значительных флага, которые чрезвычайно важны:


1. –inspect
2. –inspect-brk
 	   

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


node –inspect your-app.js
 	   

Затем вы можете для отладки этого node пользоваться Chrome посредством посещения: chrome://inspect/.

Когда вы правильно запустили процесс своего node, вы должны увидеть появившейся удалённую цель. Затем вы можете проверить эту цель и получить доступ к консоли. Как и на стороне клиента, вы можете применять консоль для проверки Object.prototype. Даже работает трассировка стека и вы способны получать точную строку из которой осуществляется доступ к свойству при помощи описанной ранее в этой главе методики Object.defineProperty().

Бывают случаи, когда вам может потребоваться отладить часть уже запущенного приложения, а потому отладчик пропустит ту часть, которую вы желаете отладить. Именно в этом случае полезен второй флаг командной строки - –inspect-brk, который укажет процессу node приостановить работу над выполнением своего приложения, что очень удобно при поиске в приложениях node гаджетов. Затем вы снова можете применить отладчик для пошагового исполнения или для его возобновления, даже добавив свои собственные точки останова.

Применение поиска по всему сайту devtools также очень удобно при тестировании, как только вы откроете, как было указано ранее, удалённую консоль, вы сможете запускать поиск во всех запущенных приложениях node. Нажмите на три точки в правой части окна devtools и выберите параметр поиска. Вы можете применять это только для поиска потенциальных гаджетов. Помочь их идентификации должно сочетании этого с упомянутой ранее методикой defineProperty().

Выводы

В данной главе мы представили порчу прототипа, определили что представляет собой методика, источник и гаджет. Мы показали вам как выявлять порчу прототипа и как вы можете менять естественные функция, пользуясь уязвимостью порчи прототипа. Затем мы рассказали о том, что даже естественные API JavaScript сами по себе уязвимы порчей прототипа, когда применяют в своих параметрах объекты. Мы показали как вы способны вручную выявлять потенциальные гаджеты, а затем мы завершили порчей прототипа на стороне сервера и тем, как вы можете отлаживать приложения node и применять те же самые методы, что и на стороне клиента.