Prototype Pollution - Эксплуатация цепочки прототипов
Last updated
Last updated
Возможно, вы уже слышали о цепочке прототипов, даже если вы не используете ее непосредственно в своей работе.
Но знали ли вы, что цепочку prototype можно также использовать как средство атаки?
Хоть она и не может непосредственно выполнять JavaScript, она может косвенно влиять на множество потоков выполнения. Объединяя существующий код, можно создать серьезные уязвимости.
Давайте вместе взглянем на эту уникальную уязвимость!
Объектно-ориентированное программирование в JavaScript отличается от других языков программирования. Синтаксис class, был введен в ES6. До этого для этой же цели использовался prototype, также известный как прототипное наследование.
Рассмотрим пример. Вы когда-нибудь задавались вопросом, откуда берутся встроенные функции, когда вы их используете?
Вы даже можете заметить, что метод repeat двух разных строк - это на самом деле одна и та же функция:
Или, если вы когда-либо проверяли MDN, вы бы обнаружили, что этот заголовок не repeat, а String.prototype.repeat:
И все это связано с прототипами.
Когда вы вызываете str.repeat, на самом деле в экземпляре str нет метода под названием repeat. Так как же работает движок JavaScript за кулисами?
Вы помните понятие области действия? Если я использую переменную и она не найдена в локальной области действия, движок JavaScript будет искать в следующей внешней области действия и так далее, пока не дойдет до глобальной области действия. Это называется цепочкой областей действия. JavaScript движок следует этой цепочке, чтобы продолжать поиск вверх, пока не дойдет до вершины.
Концепция цепочки прототипов точно такая же, но разница в том: "Как JavaScript движок знает, где искать дальше?" Если JavaScript движок не может найти функцию repeat в str, где ему нужно искать?
В JavaScript есть скрытое свойство __proto__, которое хранит значение того, где следует искать JavaScript движок.
Например:
То, на что указывает str.__proto__, - это "следующий уровень", где JavaScript должен искать, когда он не может что-то найти в str. И следующий уровень будет String.prototype.
Это объясняет, почему MDN не пишет repeat, а String.prototype.repeat, потому что это полное имя функции repeat. Эта функция repeat на самом деле существует как метод в объекте String.prototype.
Поэтому, когда вы вызываете str.repeat, вы на самом деле вызываете String.prototype.repeat, и это принцип и операция цепочки прототипов.
То же самое применимо к вещам, отличным от строк, например, объектов:
Хотя obj - это пустой объект, почему существует obj.toString? Это потому, что когда JavaScript движок не может найти это в obj, он смотрит в obj.__proto__, и obj.__proto__ указывает на Object.prototype. Поэтому obj.toString в конечном итоге находит Object.prototype.toString.
__proto__ строки - это String.prototype, __proto__ числа - это Number.prototype, и __proto__ массива - это Array.prototype. Эти связи уже предопределены для того, чтобы эти типы могли использовать одни и те же функции.
Если у каждой строки была бы своя функция repeat, то было бы миллион разных функций repeat для миллиона строк, хотя все они делают одно и то же. Звучит не очень разумно, правда? Поэтому, используя прототип, мы можем разместить repeat в String.prototype, чтобы каждая строка, которая использует эту функцию, обращалась в одну и ту же функцию.
Вы можете задаться вопросом, как функция может различать разные строки, когда они вызываются с одной и той же функцией и параметрами.
Ответ - this. Давайте взглянем на пример:
Сначала я добавил метод под названием first в String.prototype. Так что когда я вызываю "".first, движок JavaScript смотрит в String.prototype через __proto__ и обнаруживает, что существует String.prototype.first, поэтому он вызывает эту функцию.
Из-за правил this, когда написано "".first(), this внутри first будет "". Если вызывать "abc".first(), this внутри first будет "abc". Поэтому мы можем использовать this для различия кто вызывает функцию.
Более того, поскольку String.prototype может быть изменен, естественно, что Object.prototype также может быть изменен, например, так:
Так как был изменён Object.prototype, при доступе к obj.a движок JavaScript не может найти свойство a в obj, поэтому он обращается к obj.__proto__, который является Object.prototype, и находит там a, возвращая его значение.
Когда у программы есть уязвимость, которая позволяет злоумышленникам изменять свойства в цепочке прототипов, это называется "загрязнением прототипа". В приведённом выше примере мы "загрязнили" свойство a в прототипе объекта с помощью Object.prototype.a = 123, что может привести к неожиданному поведению при доступе к объектам.
Так, что же за последствия наступают от такого загрязнения?
Представьте, что на веб-сайте есть функция поиска, которая извлекает значение q из строки запроса и отображает его на экране, например:
Код для этой функциональности написан следующим образом:
Вроде бы, код выше вполне нормальный, верно? Мы написали функцию createElement, чтобы упростить некоторые шаги и сгенерировать компоненты на основе предоставленной конфигурации. Чтобы предотвратить XSS, мы использовали innerText вместо innerHTML, так что не должно быть риска XSS!
Похоже, что всё правильно, но что, если до выполнения этого кода была уязвимость загрязнения прототипа, которая позволяла злоумышленнику загрязнять свойства в прототипе? Например, вот так:
Единственное отличие в коде выше - это добавление Object.prototype.innerHTML = '<img src=x onerror=alert(1)>' в начале. Только из-за того, что эта строка загрязнила innerHTML, условие if (config.innerHTML) { оценивается как истинное, изменяя поведение. Исходно использовался innerText, но теперь он был изменён на innerHTML, в результате чего произошла XSS-атака!
Это XSS-атака, вызванная загрязнением прототипа. В общем, загрязнение прототипа относится к уязвимостям в программе, которые позволяют злоумышленникам загрязнять свойства в цепочке прототипов. Однако, помимо загрязнения, злоумышленник должен найти место, где это может оказать импакт, чтобы провести полноценную атаку.
На этом моменте у вас может возникнуть вопрос, какой код может иметь такую уязвимость, позволяя злоумышленникам изменять свойства в цепочке прототипов.
Есть два общих сценария, где возникает подобная проблема. Первым является разбор строки запроса.
Возможно, вы думаете, что строка запроса типа ?a=1&b=2 это просто. Но на самом деле, многие библиотеки для работы со строками запросов поддерживают массивы, такие как ?a=1&a=2 или ?a[]=1&a[]=2, которые можно разобрать как массивы.
Помимо массивов, некоторые библиотеки даже поддерживают объекты, вот так: ?a[b][c]=1, что дает в итоге объект {a: {b: {c: 1}}}.
Если бы вы были ответственны за реализацию этой функциональности, как бы вы это написали? Мы можем начать со схематичной версии, которая обрабатывает только объекты (не учитывая кодировку URL или массивы):
В основном, она создает объект на основе содержимого внутри [] и присваивает значения слой за слоем. Кажется простым.
Но подождите! Если моя строка запроса выглядит так, то все меняется:
Когда строка запроса выглядит так, parseQs изменяет значение obj.__proto__.a, вызывая загрязнение прототипа. В результате, когда я позже объявляю пустой объект и напишу obj.a, он выводит 3, потому что прототип объекта был загрязнен.
Многие библиотеки для разбора строк запросов сталкивались с подобными проблемами. Вот несколько примеров:
Помимо разбора строк запроса, другой общий сценарий, где возникает эта проблема, - это объединение объектов. Простая функция объединения объектов выглядит так:
Если вышеуказанный customConfig подконтролен, могут возникнуть проблемы:
Здесь мы используем JSON.parse, потому что прямое написание:
Не сработает; customConfig будет только пустым объектом. Чтобы создать объект с ключом __proto__, нам нужно использовать JSON.parse:
Аналогично, у многих библиотек, связанных с объединением, была эта уязвимость. Вот несколько примеров:
Помимо этого, почти любая библиотека, которая работает с объектами, сталкивалась с подобными проблемами, например:
Вопрос, повставленный мною в конце предыдущей статьи, также является уязвимой областью:
Злоумышленник может передать {y: '__proto__', x: 'test', color: '123'}, что приведет к screen.__proto__.test = '123', загрязняя Object.prototype.test. Поэтому для значений, передаваемых пользователями, важно проводить проверку.
Теперь, когда мы знаем, где могут возникнуть проблемы с загрязнением прототипа, недостаточно просто загрязнить свойства в прототипе. Нам нужно идентифицировать области, которые могут быть затронуты, то есть места, где после загрязнения свойств изменяется поведение. Это позволяет нам выполнить атаки.
Кажущийся безвредным код Vue hello world, но после загрязнения Object.prototype.template он становится уязвимостью XSS, позволяющей нам внедрять произвольный код.
Или вот так:
Это библиотека, предназначенная для санитизации вводимых данных, но после загрязнения Object.prototype.innerText она становится полезным инструментом для атак XSS.
Почему происходят эти проблемы? Возьмём за пример sanitize-html, это происходит из-за этого участка кода:
Поскольку innerText по умолчанию считается безопасной строкой, он прямо конкатенируется. И когда мы загрязняем это свойство, если свойство не существует, будет использовано значение из прототипа, что приводит к XSS.
Кроме уязвимостей на стороне клиента, есть похожие риски на стороне сервера, например:
Это простой участок кода, который выполняет команду echo и передает параметр. Этот параметр обрабатывается автоматически, поэтому нет необходимости беспокоиться о внедрении команд:
Однако, если есть уязвимость загрязнения прототипа, она может преобразоваться в RCE (Удаленное выполнение кода), позволяя злоумышленникам исполнять произвольные команды (предполагая, что злоумышленник может контролировать параметры):
> If the shell option is enabled, do not pass unsanitized user input to this function. Any input containing shell metacharacters may be used to trigger arbitrary command execution.
Сочетая загрязнение прототипа с гаджетами (child_process.spawn), создается критическая уязвимость.
Если в программе есть функциональность, позволяющая злоумышленникам загрязнять свойства в прототипе, эту уязвимость называют загрязнением прототипа. Prototype Pollution сам по себе не очень полезен и нуждается в сочетании с другими векторами, чтобы быть эффективным, и например код, который можно с ним объединить, называется гаджетами.
Например, внутренняя реализация Vue рендерит что-то на основе свойства template объекта, поэтому, загрязнив Object.prototype.template, мы можем создать уязвимость XSS. Или, в случае с child_process.spawn, она использует shell, так что после его загрязнения он превращается в уязвимость RCE.
Исправление на самом деле не касается гаджетов, которые могут быть использованы, если вы измените каждое место, где происходит доступ к значениям объектов, это не будет являтся фундаментальным решением. Настоящее решение - предотвратить загрязнение прототипа и гарантировать, что прототип не загрязнен, тем самым устраняя эти проблемы.
Есть несколько общих способов защиты. Первый - это блокирование ключа __proto__ при выполнении операций над объектами. Например, ранее упомянутый разбор строк запроса и объединение объектов могут использовать этот подход.
Однако, помимо __proto__, нужно отметить также другой метод обхода, например, такой:
Использование constructor.prototype также может загрязнить свойства в цепочке прототипов, поэтому все эти методы нужно блокировать вместе, чтобы обеспечить безопасность.
Второй метод прост и понятен, это избежание использования объектов, или точнее, "избежание использования объектов с прототипами".
Кто-то, возможно, видел способ создания объектов, такой как Object.create(null). Это позволяет создать пустой объект без свойства __proto__, действительно пустой объект без методов. Именно из-за этого уязвимости загрязнения прототипа не возникает:
> .parse(string, options?) Parse a query string into an object. Leading ? or # are ignored, so you can pass location.search or location.hash directly. > > The returned object is created with Object.create(null) and thus does not have a prototype.
Еще одно предложение - использовать Map вместо {}, но я думаю, что большинство людей все еще привыкли использовать объекты. Object.create(null) немного более удобен, чем Map.
Альтернативой может быть использование Object.freeze(Object.prototype) для замораживания прототипа, предотвращая модификации:
Однако одной из проблем с Object.freeze(Object.prototype) является то, что если пакет сторонних разработчиков модифицирует Object.prototype, например, прямо добавляет к нему свойство для удобства, то отладка будет сложной, потому что модификация после заморозки не вызовет ошибки, она просто не будет успешной.
Итак, вы можете обнаружить, что приложение ломается из-за пакета сторонних разработчиков, но вы не знаете почему. Еще один потенциальный риск, о котором я могу подумать, - это полифилы. Если в будущем из-за проблем с версией необходимо добавить полифил в Object.prototype, он будет недействителен из-за freeze.
Наконец, давайте посмотрим на два реальных примера загрязнения прототипа, чтобы вы лучше понимали данную уязвимость.
На веб-сайте используется пакет сторонних разработчиков, и внутри этого пакета есть часть кода, которая выглядит так:
Он анализирует location.href и document.referrer, где первый контролируется злоумышленником. Функция i.url.parse имеет уязвимость загрязнения прототипа, позволяющую произвольное загрязнение свойства.
После загрязнения автор обнаружил еще одну часть кода, которая похожа на ранее написанный нами createElement. fromObject обходит свойства и помещает их в DOM:
Загрязняя innerHTML, можно создать уязвимость XSS с использованием этого гаджета. Реальная атака включает в себя создание URL, который вызывает загрязнение прототипа + XSS. Отправив URL кому-то и убедив его открыть его, он будет подвержен атаке.
> An attacker with access to the Timelion application could send a request that will attempt to execute javascript code. This could possibly lead to an attacker executing arbitrary commands with permissions of the Kibana process on the host system.
В Kibana есть функция под названием Timelion, которая позволяет пользователям вводить синтаксис и визуализировать его в виде диаграмм. Следующий синтаксис можно использовать для загрязнения прототипа:
Загрязнение прототипа - это только первый шаг. Следующий шаг - найти гаджет. Один из фрагментов кода в Kibana выглядит так:
Этот фрагмент извлекает переменные среды, которые используются для запуска нового процесса Node. Например, если envPairs равно a=1, он будет выполнять команду a=1 node xxx.js.
Поскольку он запускает Node.js, мы можем скрытно ввести файл с помощью переменной среды NODE_OPTIONS:
Следовательно, если мы можем загрузить JavaScript-файл, мы можем выполнить этот файл в сочетании с загрязнением прототипа. Звучит сложно, есть ли другой способ?
Если мы создадим переменную окружения с именем A=console.log(123)//, содержимое /proc/self/environ станет:
Это становится рабочим кодом JavaScript! Мы можем его выполнить с помощью этого метода:
Предоставленная автором полезная нагрузка:
Загрязнены два разных свойства, созданы две переменные среды, одна делает /proc/self/environ рабочим JavaScript и включает код для выполнения, а другая NODE_OPTIONS импортирует /proc/self/environ через --require, что в итоге приводит к уязвимости RCE, позволяющей произвольное выполнение кода!
На самом деле не только существующий код и сторонние библиотеки, но даже некоторые Web API в браузерах могут подвергнуться влиянию загрязнения прототипа.
Как обычно, наиболее полезно предоставить примеры:
Это простая часть кода, которая отправляет GET-запрос, но если есть уязвимость загрязнения прототипа:
Она превращается в POST-запрос!
Это означает, что даже эти Web API могут подвергаться воздействию загрязнения прототипа, расширяя область воздействия.
В конце концов, подобные гаджеты всегда будут существовать, и цепочка прототипов - одна из характеристик JavaScript. Ее сложно обрабатывать специально и намеренно игнорировать вещи в цепочке прототипов при доступе к свойствам. Поэтому реальное решение - начинать с источника и предотвращать загрязнение цепочки прототипов.
Как я упоминал ранее, не все методы атаки включают прямое выполнение JavaScript. Например, уязвимость Prototype Pollution на первый взгляд может показаться не такой уж и значительной - просто добавление свойства к Object.prototype. Ну и что с того?
Однако, как только это сочетается с другим кодом, оно может нарушить существующий поток выполнения и предположения о безопасности, превращая, казалось бы, безобидный код в уязвимый код, который может привести к уязвимостям XSS или даже RCE.
Когда я впервые столкнулся с этой уязвимостью, у меня возникло ощущение "совершенно нового мира". Концепция прототипов, которая всем знакома в разработке фронтенда, стала общей техникой атаки в области безопасности. Как я об этом раньше не знал? И это не только загрязнение прототипа; существует множество других уязвимостей, которые вызывают такое же чувство.
Способ записи String.prototype.first непосредственно изменяет прототип строки, добавляя новый метод, который может использоваться всеми строками. Хотя это удобно, этот подход не рекомендуется в разработке. Есть поговорка: . Например, MooTools сделал что-то подобное, что привело к изменению имени метода массива. Тут вы можете найти больше подробностей:
Например, библиотека
1. 2. 3.
1. 2. 3.
1. 2. 3.
Эти "фрагменты кода, которые могут быть использованы, если мы загрязняем прототип" называются скриптовыми гаджетами. Существует репозиторий GitHub, посвященный сбору этих гаджетов n. Некоторые из этих гаджетов могут быть непонятными. Давайте я продемонстрирую:
Причина этого заключается в том, что третий параметр options метода child_process.spawn имеет параметр вызова shell, который, когда задан как true, вызывает другое поведение. Официальная также заявляет:
На любой странице уязвимости загрязнения прототипа на есть рекомендации по защите. Вы также можете обратиться к этой статье:
Например, загрязнение прототипа в исправлено с использованием этого подхода. Обработка проводится, когда появляется __proto__ или prototype.
Например, в ранее упомянутой библиотеке разбора строк запросов, которую загружают миллионы раз в неделю, , использует этот подход для защиты. говорит:
Что касается Node.js, вы можете использовать опцию --disable-proto, чтобы отключить Object.prototype.__proto__. Для получения дополнительной информации обратитесь к .
В альтернативе, в будущем можно использовать document policy чтобы управлять этим. Вы можете узнать об этом тут: .
Первый пример - это уязвимость, на известной платформе hackerone в 2020 году (да, это уязвимость самой платформы багбаунти). Полный отчет можно найти здесь: via Wistia embed code
Другой случай - это уязвимость в Kibana, сообщенная Michał Bentkowski. Оригинальная статья может быть найдена здесь: . Официальное описание этой уязвимости таково:
Да! Существует общепринятый метод, при котором содержимое определенных файлов контролируется. Например, в PHP содержимое файла сессии может быть контролируемым. Вы можете обратиться к этой статье: . Другой пример - файл /proc/self/environ в системах Linux, который содержит все переменные среды текущего процесса.
Если вас интересуют гаджеты Node.js, вы можете обратиться к этой отличной статье: .
Этот вопрос обсуждался в ошибке Chromium: . Это поведение на самом деле соответствует спецификации и не требует особой обработки.
Для получения дополнительной информации вы можете обратиться к и .
Некоторые люди даже автоматизируют обнаружение уязвимостей загрязнения прототипа и определяют проблемные области, поднимая загрязнение прототипа на новый уровень: . Помимо результатов исследования, также стоит обратить внимание на исследовательские команды за ним. Их можно считать звездами безопасности на фронтенде.