Аннотация

Один клик по вредоносной ссылке — и контроль над аккаунтом потерян. Без фишинга, без вредоносного ПО.

Я обнаружил уязвимость в механизме OAuth аутентификации популярного десктоп-приложения, позволяющую похитить учетную запись любого пользователя всего в один клик. Основные причины проблемы: отсутствие проверки состояния (state validation), хранение долгоживущих токенов после перенаправления (loopback redirect) и слепое доверие параметру remote_key. В данном посте я подробно расскажу обо всех этапах атаки, почему она сработала настолько эффективно, и каким образом разработчики могут устранить данную проблему.

Несколько месяцев назад я решил заняться поиском уязвимостей в десктоп-приложениях. Для ясности поясню: говоря о поиске ошибок в десктоп-программах, я имею в виду взаимодействие приложений с веб-сервисами (процесс аутентификации, обмен данными или любые точки интеграции). Целью моего исследования стала известная компания, и я не вправе раскрывать ее название, поэтому на протяжении всей статьи буду использовать redacted.com.

Подготовка

Я настроил сертификат и проксирование через Burp Suite. Запустив приложение и изучив захваченные запросы, я выяснил, что приложение не использует привязку сертификатов (certificate pinning). Это облегчило процесс перехвата и позволило обойтись без инструмента Frida.

Разведка

Нужно понимать работу приложения лучше самих разработчиков.

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

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

Процесс OAuth

Я попробовал войти в свою учетную запись через OAuth. Когда вы выбираете вариант входа, например, «Войти через Google» или «Войти через Facebook», открывается окно браузера с таким адресом:

https://oauth2.redacted.com/auth/google?

remote_key=[REMOTE_KEY]

&jwt=1&client_id=[CLIENT_ID]&lang=en&return_skip=1

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

Первым моим вопросом было: «Как осуществляется передача аутентификационных данных от браузера к десктоп-приложению?» Я проверил наличие deep links, однако ничего подобного не нашел. Тогда я обратился к секции WebSocket в Burp. Оказалось, что сразу после генерации remote_key десктоп-приложение инициирует WebSocket соединение с сервером и циклически запрашивает подтверждение успешной аутентификации пользователя. Если пользователь вошел через браузер, WebSocket возвращает доступный токен, подтверждающий успешную аутентификацию, и приложение автоматически входит в учетную запись пользователя.

Вот что происходило после успешного завершения процедуры аутентификации:

1) Пользователь запускает десктоп-приложение.

2) Десктоп-приложение отображает страницу входа с двумя вариантами: Вход по учетным данным и Вход через OAuth.

3) Пользователь выбирает Вход через OAuth.

4) Десктоп-приложение открывает стандартный браузер и загружает следующую ссылку:

https://oauth2.redacted.com/auth/google?

remote_key=[REMOTE_KEY]

&jwt=1&client_id=[CLIENT_ID]&lang=en&return_skip=1

5) Браузер показывает страницу провайдера OAuth. Пользователь завершает процесс входа через OAuth.

6) После успешного завершения аутентификации через OAuth браузер переадресует пользователя на страницу с сообщением: «Процесс входа завершен, просто закройте браузер.»

7) Тем временем, сразу же после открытия окна браузера, десктоп-приложение устанавливает WebSocket соединение.

8) Приложение непрерывно проверяет статус аутентификации, циклично отправляя параметр remote_key в WebSocket:

- Проверка №1 → «не аутентифицирован»

- Проверка №2 → «не аутентифицирован»

- Проверка №3 → «не аутентифицирован»

9) Как только аутентификация через браузер завершится, приложение получит токен доступа:

- Проверка №X → «Пользователь аутентифицирован! (токен доступа: xxxxxxxx)»

10) Получив токен доступа, десктоп-приложение помечает пользователя как прошедшего аутентификацию и позволяет ему войти в свою учётную запись.

11) Наконец, пользователь залогинен через OAuth.

Сценарий атаки

Первое, что приходит на ум, — проверить, связана ли переменная remote_key с конкретной сессией десктоп-приложения или уникальным идентификатором пользователя.

Возникает вопрос: «Что произойдет, если я запущу процесс входа через OAuth в своем десктоп-приложении, скопирую сформированный OAuth URL (например) и перешлю его жертве? Что случится, если жертва завершит процесс входа через OAuth? Аутентифицируется ли мое десктоп-приложение под именем жертвы?»

Эксперимент

Я вышел из своего аккаунта, повторил весь цикл аутентификации через OAuth и скопировал вновь созданный URL из браузера. Затем, через телефон (в роли жертвы) открыл эту ссылку. К моему удивлению, десктоп-приложение выполнило вход под моей учетной записью, фактически присвоив личность жертвы.

Соответственно, сценарий атаки можно представить следующим образом:

1) Нападающий открывает десктоп-приложение.

2) Приложение отображает две опции входа: через обычные учетные данные либо через OAuth.

3) Нападающий выбирает вход через OAuth.

4) Приложение открывает браузер и возвращает ссылку:

https://oauth2.redacted.com/auth/google?

remote_key=[ATTACKER_REMOTE_KEY]

&jwt=1&client_id=[CLIENT_ID]&lang=en&return_skip=1

5) Нападающий копирует URL и отправляет его жертве.

6) Жертва переходит по ссылке.

7) Браузер отображает страницу OAuth-провайдера. Жертва завершает вход через OAuth (например, через Google).

8) После успешного завершения OAuth браузер отобразит: «Вы успешно вошли, просто закройте браузер».

9) Параллельно приложение инициирует WebSocket-соединение.

10) Приложение периодически проверяет статус аутентификации, отправляя ключ remote_key в WebSocket:

- Проверка №1 → «не аутентифицирован»

- Проверка №2 → «не аутентифицирован»

- Проверка №3 → «не аутентифицирован»

11) После прохождения жертвой OAuth, сервер отправляет WebSocket-ответ с токеном доступа жертвы:

- Проверка №X → «Пользователь аутентифицирован! (access_token: xxxxxxxx)»

12) Приложение принимает токен и авторизуется в учетной записи жертвы.

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

Устранение уязвимости

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

Для исправления данной уязвимости рекомендуется внедрить следующие меры защиты:

1. Использование параметра состояния OAuth (State Parameter)

Приложение должно генерировать криптографически защищённый уникальный параметр состояния (state), который:

- Привязан к сессии десктоп-приложения,

- Проходит обязательную проверку,

- Является одноразовым и имеет ограниченный срок действия (например, 5 минут).

Это гарантирует, что завершить процесс сможет лишь тот субъект, который его инициировал.

2. Привязка ключа remote_key к устройству/сессии

Ключ remote_key должен быть криптографически связан с сессией десктоп-приложения или идентификатором устройства. При установлении соединения WebSocket сервер должен проверять:

- Запрос аутентификации осуществлен локально и с вашего устройства

- remote_key соответствует приложению, которое начало процесс аутентификации.

3. Добавление подтверждения пользователем

Должна отображаться подробная информация об устройстве, запрашивающем аутентификацию:

- Название и тип устройства,

- Приблизительное местоположение,

- Время запроса.

Необходимо требовать от пользователя явного подтверждения типа: «Подтверждаете ли вы попытку входа с устройства [Название устройства]?» до отправки токена доступа через WebSocket.

4. Ограничение срока действия ключа remote_key

Срок жизни ключа remote_key должен быть коротким (например, 2–3 минуты). Если процесс OAuth не завершён в течение этого периода, ключ аннулируется на стороне сервера, а десктоп-приложение выдаёт ошибку тайм-аута. Хотя эта мера была реализована в рассматриваемом примере, сама по себе она не обеспечивает безопасность OAuth.

5. Мониторинг аномалий

Следует реализовать механизмы мониторинга подозрительных действий:

- Несколько неудачных попыток аутентификации с разными значениями remote_key,

- Запросы на аутентификацию из географически отдалённых регионов в короткие промежутки времени,

- Нетипичные паттерны WebSocket соединений.

Реализация перечисленных мер позволит обеспечить безопасность процесса OAuth  аутентификации и гарантировать, что легитимный пользователь, инициирующий процесс входа, завершит его в своём приложении. Стоит отметить, что пункты 4 и 5 делают атаку сложнее, но не обеспечивают полной защиты OAuth.

Заключение

Не бойтесь тестировать кросс-платформенные приложения. Многие исследователи безопасности избегают настольных или мобильных приложений, потому что они кажутся сложными, расслабьтесь и расширяйте свое мышление. Вы хакер! Для настоящего хакера нет ограничений. Подходите к кросс-платформенным приложениям с любопытством и желанием исследовать более глубокие слои. Вам следует расширить поверхность атаки настолько, насколько возможно. Каждое приложение похоже на айсберг; то, что видно снаружи, — это лишь малая часть, поэтому исследуйте, что находится под поверхностью. После того, как я сообщил об этой проблеме, разработчик быстро исправил её. Когда первоначальный отчет был отмечен как разрешенный, я решил глубже изучить их новую реализацию и успешно обошел патч. В следующем отчете я подробно расскажу, как именно я сделал это и что пошло не так с их исправлением.

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

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


  1. Myateznik
    11.12.2025 17:54

    Я может быть чего-то не понимаю, но описанная схема входа буквально выглядит как OAuth Device Authorization Flow. Только пуллинг заменили на WebSocket. Нет тут конечно есть кастомизация и отхождение, но всё же атака вида "скинуть ссылку с user code жертве" буквально также работает и в классическом Device Flow. Ну и ряд рекомендаций плюс минус ровно то, что нужно соблюдать при опять же Device Flow.