
Пользователь включил двухфакторную аутентификацию, привязал приложение-аутентификатор и успокоился. Через месяц в его аккаунт входят без единого правильного кода. Криптографию TOTP при этом никто не ломал: атакующий отправил пустое поле вместо кода, подменил ответ сервера или зашёл через «Войти с Google», где второй фактор никто не спросил. Такие баги стабильно всплывают в программах bug bounty HackerOne, Meta, Shopify и Slack.
29 октября в 19:00 МСК CyberED проводит открытый практикум «Обход MFA: атака и защита». Кирилл Николаев, лид команды пентестеров и эксперт по веб-безопасности, разберёт основные классы обхода MFA на примерах из публичных bug bounty-отчётов, покажет, где проверять второй фактор в основном и альтернативных сценариях входа, и объяснит, какие меры снижают риск. Зарегистрироваться можно здесь.
Уязвимой чаще оказывается логика вокруг второго фактора: шаг проверки можно пропустить, flow не привязан к сессии, а endpoint отключения MFA защищён слабее самого логина. Ниже семь ошибок, на которых второй фактор ломается чаще всего, с примерами из открытых отчётов и тем, что проверить у себя.
Сервер доверяет решению клиента
Сервер проверяет код и отправляет результат в теле ответа, например {"success": false, "mfa_verified": false}. Решение «пускать или нет» при этом принимает фронтенд: читает поле и делает редирект.
Атакующий перехватывает ответ в Burp Suite, меняет false на true, и интерфейс открывает личный кабинет без правильного кода. Тот же класс включает подмену статус-кода, когда 401 или 403 в ответе на шаг проверки заменяют на 200 OK.
Что проверить: факт прохождения MFA хранится в серверной сессии, и каждый следующий запрос сверяется с ним. Поля в JSON-ответе ничего не решают.
Код можно перебирать
Четырёхзначный код даёт 10 000 комбинаций, шестизначный миллион. Без лимита попыток на шаге MFA такой код перебирается за минуты через Burp Intruder. Учебный пример есть в PortSwigger Web Security Academy: лаба с брутфорсом 2FA, где блокировку обходят повторной авторизацией через макрос.
Бывает, что лимит есть, но считается неправильно. Если бэкенд считает попытки по IP и доверяет заголовкам X-Forwarded-For или X-Real-IP, их ротация снимает ограничение. Если лимитер считает HTTP-запросы, десятки проверок кода упаковывают в один batch-запрос или одну GraphQL-операцию с алиасами.
Что проверить: лимит стоит отдельно на шаге кода, считается по аккаунту и сессии, применяется к каждой попытке внутри batch-запроса и не сбрасывается при новой сессии или новом коде.
Пустые, тестовые и повторные коды
В отчёте #897385 на HackerOne обход выглядел так: на экране ввода 2FA вводятся любые цифры, запрос перехватывается, значение кода удаляется. Бэкенд трактовал пустое поле как пройденную проверку вместо явного отказа.
Рядом живут статические коды для QA и staging: 0000, 1111, 123456, 000000. Их оставляют для тестов, и иногда они продолжают приниматься в продакшене.
Третий вариант описан в отчёте #2529780: OTP из аутентификатора не инвалидировался после успешного входа. Тот же код срабатывал повторно, в том числе из другой сессии, пока не истекал интервал TOTP. На бэкендах с нестрогим сравнением типов, например == в PHP, проверку могут пройти особые строки вроде "0e12345".
Что проверить: пустое значение, массив вместо строки и JSON null получают явный отказ. Код помечается использованным сразу после проверки и привязан к конкретной попытке входа.
Гонки в многошаговых процессах
Включение, отключение и подтверждение MFA состоят из нескольких шагов, и между ними возникают гонки. Публичный writeup описывает такой случай на самом HackerOne. Отключение 2FA порождало несколько подтверждающих ссылок. При параллельных запросах на отключение отмена одной ссылки не трогала остальные: потоки подтверждения не синхронизировались между собой.
Что проверить: параллельные запросы на verify, disable и re-enable через Turbo Intruder или single-packet attack. Кроме логина, на гонки стоит прогнать смену email и восстановление доступа. Критичные шаги должны идти через атомарные транзакции и блокировки.
Разобрать эти техники вместе с действующим пентестером на открытом эфире 29 октября
Второй фактор стоит только на парадном входе
MFA часто навешивают на логин по паролю и забывают про остальные маршруты: сброс пароля без повторного запроса второго фактора, «Войти через Google или Apple», SSO-токены сторонних провайдеров.
В отчёте #178293 пользователи Shopify с включённым Google Authenticator могли войти в аккаунт через Google Apps Login, и второй фактор там не требовался. Если приложение доверяет токену внешнего провайдера и не сверяет локальные настройки безопасности, 2FA превращается в опциональный шаг.
К тем же боковым входам относятся app-specific пароли для IMAP, SMTP и интеграций. Их создают один раз через защищённый интерфейс, дальше они работают без OTP.
Что проверить: все альтернативные логины сверяются с тем же флагом mfa_enabled, что и основной вход. Генерация app-password требует текущего OTP, а права такого токена ограничены.
Отключить MFA проще, чем войти
Endpoint, который выключает защиту, нередко защищён слабее самого логина. В отчёте #587910 при отключении 2FA не проверялся пароль пользователя. Если endpoint ещё и принимает user_id без проверки владения сессией, получается IDOR: атакующий с любым валидным токеном выключает 2FA в чужом аккаунте. Без anti-CSRF токена хватает автосабмит-формы на стороннем сайте, чтобы снять защиту у залогиненной жертвы.
Отдельная проблема начинается после сброса MFA. Если старые сессии и refresh-токены остаются действительными, атакующий, который успел закрепиться в аккаунте, остаётся внутри и после перенастройки второго фактора.
Что проверить: отключение и сброс MFA, смена привязанного номера или email требуют пароль или действующий OTP, защищены от CSRF и завершают все сессии, кроме текущей.
Веб защищён, мобильное API нет
Мобильное приложение и legacy API нередко реализуют вход отдельно от веба, и требование второго фактора туда не переносят. На HackerOne был репорт о том, что 2FA, обязательная в основном интерфейсе, не проверялась в отдельном флоу через отдельный endpoint.
Что проверить: пройти логин через официальный клиент, мобильное API, GraphQL и старую версию REST. Сверить, где второй фактор требует сервер, а где его только показывает интерфейс. Состояние OTP хранится в одном месте для всех клиентов.
Короткий чек-лист для проверки MFA
- Подменить ответ:
falseнаtrue,4xxна200, убрать поляmfa_requiredиotp_verified. - Проверить лимит попыток на шаге кода: по аккаунту, по сессии, внутри batch-запросов.
- Отправить пустой, тестовый и уже использованный код, код из другой сессии и после logout.
- Запустить параллельные запросы на verify, disable и re-enable.
- Пройти сброс пароля, OAuth, SSO, «remember me» и мобильное API.
- Проверить отключение и сброс MFA на CSRF, IDOR и повторную аутентификацию.
- Убедиться, что прохождение MFA привязано к конкретной сессии.
Разобрать обход MFA на практике
Чек-лист задаёт направления. Чтобы находить такие ошибки на реальных системах, нужно видеть, как каждая из них выглядит в запросах и ответах.
29 октября в 19:00 МСК на открытом эфире эксперт пройдёт по всем классам уязвимостей из этой статьи. Он разберёт примеры из публичных bug bounty-отчётов, методологию проверки и базовые принципы защиты.
Инженерам и разработчикам практикум даст карту уязвимых мест вокруг второго фактора, пентестерам и bug bounty-исследователям систематизированный порядок проверки.