В этой статье я расскажу о том, как мне удалось найти уязвимость для захвата аккаунта (Account Takeover, ATO) через ошибки конфигурации OAuth и украсть токены авторизации.
История началась с обнаружения уязвимости XSS на поддомене sub1.target.com. Проблема заключалась в том, что за баги на этом поддомене платили мало, поэтому наша цель состояла в том, чтобы использовать XSS для эскалации до захвата аккаунта (ATO) на основном приложении app.target.com, где вознаграждения были значительно выше.
Изучив механизм аутентификации sub1.target.com, мы выяснили, что он использовал JWT-токен, основанный на куках, в то время как в app.target.com аутентификация осуществлялась через JWT-токен, передаваемый в заголовке Authorization. Значения JWT на этих двух поддоменах различались, однако, к нашему удивлению, JWT с sub1.target.com также работал для аутентификации на app.target.com!
Таким образом, если бы мы смогли скомпрометировать JWT токен аутентификации с sub1.target.com, мы могли бы использовать его для входа в app.target.com.
К сожалению, JWT-токен, основанный на куках на sub1.target.com, был защищён флагом HttpOnly, что делало невозможным его извлечение с помощью XSS. Однако, во время анализа OAuth-потока целевого ресурса, мы обнаружили другой способ утечки этих JWT-токенов!
Мы нашли запрос, в ответе которого содержался JWT-токен с другого поддомена — auth.target.com.
После детального анализа мы обнаружили интересное поведение в предыдущем запросе. Если изменить значение параметра response_mode с post на get, ответ сервера становится другим. Более того, мы могли установить любые значения для параметров state и nonce, и сервер все равно возвращал аутентификационный токен.
Теперь, имея XSS на sub1.target.com, мы можем создать iframe, загружающий конечную точку OAuth на auth.target.com. К счастью, на данном этапе не было установлено никаких заголовков X-Frame-Options или CSP, предотвращающих отображение этой конечной точки в фрейме. Тем не менее, политика одного источника (SOP) будет препятствовать доступу к объекту contentWindow iframe с другого источника.
Мы рассмотрели возможность того, что оба поддомена могут установить document.domain = 'target.com', что приведет к их состоянию «один источник». Эта техника использовалась для ослабления SOP между поддоменами. Если оба поддомена явно установят свойство document.domain на родительский домен target.com, они будут делить один и тот же источник, что позволит осуществлять Cross-origin доступ.
Однако эта техника больше не поддерживается современными браузерами, такими как Chrome.
Вместо этого мы обнаружили, что при переходе по адресу https://auth.target.com/oauth?client_id=redacted&redirect_uri=https://auth.target.com/endpoint.jsp&response_mode=get&response_type=code&scope=redacted&state=redacted&nonce=redacted, нас перенаправило на https://sub1.target.com#token=. В результате страница теперь считается имеющей один и тот же источник, что позволяет нам получить доступ к свойству location.href окна и извлечь токен из фрагмента URL.
var auth = document.createElement('iframe');
auth.setAttribute('src', 'https://auth.target.com/oauth?client_id=redacted&redirect_uri=https://auth.target.com/endpoint.jsp&response_mode=get&response_type=code&scope=redacted&state=redacted&nonce=redacted');
auth.setAttribute('id', 'lol');
token = new URLSearchParams(document.getElementById("lol").contentWindow.location.hash.split('#')[1]).get('token');
После инициирования запроса для получения токена из фрагмента URL мы можем отправить его на свой сервер.
Так как JWT-токен аутентификации был украден в ходе аутентификации на auth.target.com с использованием XSS на sub1.target.com, мы можем теперь включить украденный JWT-токен аутентификации в запросы для изменения адресов электронной почты жертв на app.target.com, что позволит добиться (ATO).
Полный ход атаки можно представить следующим образом: