stihl не предоставил(а) никакой дополнительной информации.
Небольшая форма восстановления пароля может стать большой головной болью. На каждом шаге разработчики допускают ошибки. В этой статье я покажу актуальные методы атаки на формы сброса пароля и дам рекомендации, которые будут полезны пентестерам и разработчикам.
Я занимаюсь анализом защищенности в Singleton Security. При проведении пентеста часто сталкиваюсь с формами сброса пароля. Кажется, что это простой и стандартный механизм. На деле же — уязвимости в восстановлении пароля приводят к полному захвату аккаунта, утечке конфиденциальных данных или перехвату управления сервером.
Уязвимость часто встречается, когда части веб‑приложения находятся на разных доменах и пользователи привязываются к точному хосту:
Допустим, у нас есть сайт, уязвимый к подмене HTTP-заголовков, который отправляет ссылку для восстановления пароля вида Для просмотра ссылки Войдиили Зарегистрируйся{token}. Мы можем создать такой запрос, чтобы получить токен пароля:
Жертве придет ссылка на восстановление пароля с вредоносным доменом. Когда пользователь перейдет по ней, токен попадет в руки злоумышленника.
Если атака не сработала, пробуй различные дублирующие хедеры наподобие X-Forwarded-Host, X-HTTP-Host-Override и так далее.
Один из возможных подходов — дублирование заголовка Host. Прокси‑серверы обрабатывают дублированные заголовки по‑разному. В зависимости от прокси на выходе может быть отброшен первый или дублирующий заголовок либо выполнится join с разделением через запятую. Иногда может оказаться, что разработчики не предусмотрели такой сценарий и мы можем внедрить наш домен в ссылку, добавив еще один заголовок Host.
Бывают случаи, что сервер проверяет хедеры, но недостаточно безопасно. Возникает возможность внедрения висячей разметки (Dangling markup) — это вид атаки с инъекцией HTML-кода, предполагающий использование незакрытого тега или атрибута. Предположим, что заголовок Host валидируется, но есть возможность написать порт приложения. Этим мы и воспользуемся, отправив запрос такого вида:
С помощью такой нагрузки мы можем создать фишинговую страницу с формой отправки данных на наш сервер. Пользователь, уверенный, что получил письмо от настоящей компании, перейдет по ссылке и введет данные от учетной записи. Тут остается сделать хорошую фишинговую страницу.
Но современные почтовые клиенты (такие как Gmail или Outlook) и антивирусные решения имеют мощные фильтры, которые находят и блокируют подобные письма, помещая их в спам или вообще не доставляя, так как HTML в заголовках выглядит подозрительно.
или Зарегистрируйся, Для просмотра ссылки Войди или Зарегистрируйся и Для просмотра ссылки Войди или Зарегистрируйся.
Перед тем как анализировать содержимое, один из фильтров приводит HTML-код письма к «плоскому» и читаемому виду, аналогично тому, как это делает браузер. Все эти висящие <div>, <span>, <font> с белым цветом шрифта на белом фоне, элементы с display: none или font-size: 0 просто вычищаются. Остается только видимое текстовое содержимое. Извлекаются все ссылки письма, проверяются репутации URL-адресов и многое другое. Эта атака может сработать в корпоративных сетях со своими почтовыми серверами, где фильтрация менее строгая.
Некоторые приложения используют управляемые пользователем параметры для создания ссылок сброса пароля. Такая реализация может позволить злоумышленникам выполнить подмену домена, изменив тело POST-запроса. Пример, в котором удалось отравить хост ссылки для сброса пароля с помощью JSON-параметра в запросе:
Уязвимый параметр baseurl позволяет нам подставить свой контролируемый домен, чтобы ссылка сброса пароля указывала на него.
WAF могут блокировать запросы с подменой адреса, но не всегда делают это безопасно. В моей практике бывали случаи, когда фильтр искал вхождение домена в строке вместо точного сравнения. Пример:
Тем самым без проблем можно подставить такой субдомен своего контролируемого домена. Тестируй разные варианты. Иногда фильтр ищет вхождение, бывает, проверяет совпадения начала или конца строки.
В данном случае приложение слепо доверяет заголовку Host, что позволяет атакующему подменить его в своих целях.
Пример хорошего кода, тоже на Laravel:
Здесь явно задан допустимый заголовок Host, который при его замене выдаст в ответ код 403.
Для просмотра ссылки Войдиили ЗарегистрируйсяЗдесь token — это строка в Base64, расшифровав которую мы получим введенную нами почту admininstrator@vulncorp.com. Хакеру остается узнать почту админа, закодировать в Base64 и установить свой пароль.
Другой вид предсказуемых токенов — небезопасные алгоритмы генерации. Например, первая версия UUID. Если генерировать токены в короткое время, алгоритм выдаст значения, отличающиеся на несколько байтов:
Такой тип атаки называется «сэндвич». Название дано из‑за особенности алгоритма: атакующий «оборачивает» сброс пароля жертвы в сбросы пароля для своих аккаунтов. Он регистрирует два аккаунта, например hacker1@vulncorp.com и hacker3@vulncorp.com. Затем инициирует запрос на сброс пароля в следующем порядке:
Легко заметить, что идентификаторы различаются всего несколькими символами. Если рассчитать разницу между отличающимися идентификаторами, то выяснится, что для поиска токена жертвы нужно будет чуть более 23 тысяч попыток. Запросов много, атака «шумная», но выполнимая. Функция Intruder в Burp Suite позволяет проверить все варианты в многопоточном режиме и определить действительный токен. При десяти потоках и интервале в 300 мс потребуется чуть больше семи минут на выполнение атаки.
Атака эффективна не только для UUIDv1, но и для всех генераций токенов, которые используют временную метку сервера в качестве случайного числа во время генерации.
На практике встречается не только предсказуемая генерация, но и небезопасная проверка сгенерированных токенов. В одном из проектов была некорректная реализация проверки токена сброса пароля. Токен проверялся только по первым двум символам! Именно это позволяло сделать возможным перебор этой строки, ведь алфавит составлял всего 36 символов.
Случай довольно специфичный и в какой‑то степени курьезный. Разработчик либо забыл исправить тестовый код и выкатил его в продакшен, либо совсем не собирался реализовывать проверку. Уязвимый код мог быть, например, в некорректном SQL-запросе:
SELECT token FROM recovery_tokens WHERE token LIKE 's%' AND LENGTH('s%') > 2;
Здесь s — это ввод пользователя.
Допустим, наш токен выглядит так: YwAdskd512jlki0owW. Для атаки достаточно было перейти по ссылке, содержащей часть этого токена: Для просмотра ссылки Войдиили Зарегистрируйся.
# ПЛОХО: использование времени для генерации токена
В этом примере за основу берется никнейм пользователя и время генерации токена. Злоумышленник может воспользоваться уязвимостью и сгенерировать возможные токены для кражи чужого аккаунта.
Пример безопасного кода на Python:
# ХОРОШО: использование криптографически стойкого генератора
Здесь применена библиотека secrets для генерации случайного токена. Алгоритм исключает использование предсказуемых данных и паттернов, каждый раз ты будешь получать надежный токен.
Предпосылки для успешного брутфорса: короткий код (4–6 цифр) и отсутствие лимита на количество попыток. Время жизни кода тоже влияет, но для полного перебора 10 тысяч вариантов (код из четырех цифр) хватит пяти минут. Если время жизни недостаточно для атаки, хакер может перебирать часть значений, запрашивая новый сброс пароля. Так за несколько попыток получится наткнуться на нужный код.
Вариант получше, с ограничением количества попыток ввода:
Пример атаки: пользователь получает токен в виде строки, но в коде нестрогое сравнение, что позволяет заменить значение сопоставимым типом:
Если проверка нестрогая (== вместо ===), есть шанс приведения true к Boolean и непредсказуемому выполнению. Чаще всего такие уязвимости возникают в Node.js и некоторых версиях PHP и Python.
Результат сравнения — true. PHP отбросит нечисловые символы и выполнит сравнение 123 == 123.
Замени сравнение строгим, и код заработает предсказуемо:
Иногда перед отправкой формы JavaScript обрабатывает значения и расширяет набор параметров. В любом случае, если есть скрытые параметры, хакер может их найти. Где‑то с задачей справится Burp, показав реальный запрос. В случае с API хакер может устроить перебор параметров по словарю и, увидев отличия в ответах сервера, вычислить скрытые поля.
Атака похожа на подмену параметров из начала статьи, с той разницей, что параметр нужно найти. Пример атаки:
Если параметр существует, жертва получит ссылку с вредоносным доменом.
// ХОРОШО: явное указание разрешенных полей
В Laravel рекомендую использовать именно $fillable вместо $guarded. Первый формирует белый список полей, второй — черный. Работая с $guarded, ты можешь забыть добавить новый столбец в массив $guarded, оставив его открытым для массового назначения. Лучше сначала определи белый список. Если потребуется, после него переходи к черному.
Пример уязвимого кода на Laravel:
В данном случае есть разрешенное поле reset_base_url, которое может быть использовано для формирования фишинговой ссылки.
Я занимаюсь анализом защищенности в Singleton Security. При проведении пентеста часто сталкиваюсь с формами сброса пароля. Кажется, что это простой и стандартный механизм. На деле же — уязвимости в восстановлении пароля приводят к полному захвату аккаунта, утечке конфиденциальных данных или перехвату управления сервером.
Атаки с использованием заголовка Host
Начнем с атак на подмену HTTP-заголовков. Такая атака возникает, когда заголовки генерируются на основе пользовательского ввода и сервер не проверяет эти данные должным образом. Хакер может внедрить свой контролируемый домен в ссылку для восстановления пароля.Уязвимость часто встречается, когда части веб‑приложения находятся на разных доменах и пользователи привязываются к точному хосту:
- отдельные поддомены для разных языков;
- разбивка пользователей из разных стран по доменным зонам (.ru, .fr, .de и так далее);
- множественные домены — например, языковая школа может использовать что‑то вроде eng-lang.com, de-lang.com, rus-lang.com.
Код:
<?php
// Небезопасное получение хоста из запроса
$host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'];
$resetLink = "https://{$host}/reset-password?token={$token}";
Допустим, у нас есть сайт, уязвимый к подмене HTTP-заголовков, который отправляет ссылку для восстановления пароля вида Для просмотра ссылки Войди
Код:
POST /recovery-pass HTTP/1.1
Host: evil.com
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
email=[EMAIL]admin@vulncorp.com[/EMAIL]Жертве придет ссылка на восстановление пароля с вредоносным доменом. Когда пользователь перейдет по ней, токен попадет в руки злоумышленника.
Если атака не сработала, пробуй различные дублирующие хедеры наподобие X-Forwarded-Host, X-HTTP-Host-Override и так далее.
Один из возможных подходов — дублирование заголовка Host. Прокси‑серверы обрабатывают дублированные заголовки по‑разному. В зависимости от прокси на выходе может быть отброшен первый или дублирующий заголовок либо выполнится join с разделением через запятую. Иногда может оказаться, что разработчики не предусмотрели такой сценарий и мы можем внедрить наш домен в ссылку, добавив еще один заголовок Host.
info
WAF или reverse-прокси могут блокировать запросы с дублированными заголовками или подозрительными значениями X-Forwarded-Host. Обход часто требует экспериментов с разными названиями заголовков (например, X-Original-Host, X-Rewrite-URL).Бывают случаи, что сервер проверяет хедеры, но недостаточно безопасно. Возникает возможность внедрения висячей разметки (Dangling markup) — это вид атаки с инъекцией HTML-кода, предполагающий использование незакрытого тега или атрибута. Предположим, что заголовок Host валидируется, но есть возможность написать порт приложения. Этим мы и воспользуемся, отправив запрос такого вида:
Код:
POST /forgot-password HTTP/2
Host: vulncorp.com:443'></a> <a href="https://evil.com/login"> go to link </a> <type="hidden
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
username=admin
С помощью такой нагрузки мы можем создать фишинговую страницу с формой отправки данных на наш сервер. Пользователь, уверенный, что получил письмо от настоящей компании, перейдет по ссылке и введет данные от учетной записи. Тут остается сделать хорошую фишинговую страницу.
Но современные почтовые клиенты (такие как Gmail или Outlook) и антивирусные решения имеют мощные фильтры, которые находят и блокируют подобные письма, помещая их в спам или вообще не доставляя, так как HTML в заголовках выглядит подозрительно.
www
Попрактиковаться в атаках на заголовок Host можешь в лабораторных академии PortSwigger: Для просмотра ссылки ВойдиПеред тем как анализировать содержимое, один из фильтров приводит HTML-код письма к «плоскому» и читаемому виду, аналогично тому, как это делает браузер. Все эти висящие <div>, <span>, <font> с белым цветом шрифта на белом фоне, элементы с display: none или font-size: 0 просто вычищаются. Остается только видимое текстовое содержимое. Извлекаются все ссылки письма, проверяются репутации URL-адресов и многое другое. Эта атака может сработать в корпоративных сетях со своими почтовыми серверами, где фильтрация менее строгая.
Некоторые приложения используют управляемые пользователем параметры для создания ссылок сброса пароля. Такая реализация может позволить злоумышленникам выполнить подмену домена, изменив тело POST-запроса. Пример, в котором удалось отравить хост ссылки для сброса пароля с помощью JSON-параметра в запросе:
Код:
POST /recovery-password HTTP/1.1
Host: vulncorp.com
Content-Type: application/json
Content-Length: 50
{"email":"user@vulncorp.com","baseurl":"evil.com"}
Уязвимый параметр baseurl позволяет нам подставить свой контролируемый домен, чтобы ссылка сброса пароля указывала на него.
WAF могут блокировать запросы с подменой адреса, но не всегда делают это безопасно. В моей практике бывали случаи, когда фильтр искал вхождение домена в строке вместо точного сравнения. Пример:
Код:
POST /recovery-password HTTP/1.1
Host: vulncorp.com
Content-Type: application/json
Content-Length: 63
{"email":"user@vulncorp.com","baseurl":"vulncorp.com.evil.com"}
Тем самым без проблем можно подставить такой субдомен своего контролируемого домена. Тестируй разные варианты. Иногда фильтр ищет вхождение, бывает, проверяет совпадения начала или конца строки.
Защита
Пять простых правил для защиты:- Откажись от абсолютных URL или защити их. В большинстве случаев относительные URL хорошо впишутся в логику приложения. Если ты не можешь обойтись без абсолютного URL, убедись, что текущий домен вручную указан в конфиге и ссылается на это значение вместо заголовка Host.
- Проверяй заголовок Host, если не можешь от него отказаться. Используй проверку по белому списку доменов и отклоняй или перенаправляй запросы с неопознанными хостами. Например, платформа Django предоставляет параметр ALLOWED_HOSTS в файле настроек.
- Не доверяй заголовкам переопределения Host. Убедись, что твой код не поддерживает дополнительные заголовки, которые могут использоваться при атаках. Например, X-Forwarded-Host. Помни, что они могут поддерживаться веб‑сервером по умолчанию.
- Создай белый список разрешенных доменов. Чтобы предотвратить атаки на основе маршрутизации на внутреннюю инфраструктуру, настрой балансировщик нагрузки или любые обратные прокси‑серверы для перенаправления запросов только списку разрешенных доменов.
- Будь осторожен с внутренними виртуальными хостами: не размещай на одном сервере публичные веб‑приложения и приложения для внутреннего пользования. В противном случае злоумышленники могут получить доступ к внутренним доменам через манипуляции с заголовком Host.
Код:
use Illuminate\Http\Request;
class ProfileController extends Controller
{
public function resetPassword(Request $request) {
$url = $request->header('Host') . '/reset-password';
return response()->json(['reset_url' => $url]);
}
}
В данном случае приложение слепо доверяет заголовку Host, что позволяет атакующему подменить его в своих целях.
Пример хорошего кода, тоже на Laravel:
Код:
public function boot()
{
$allowedHosts = ['corp-app.com'];
if (!in_array(request()->getHost(), $allowedHosts)) {
abort(403, 'Forbidden');
}
}
Опасные токены
Предсказуемые токены или отсутствие их проверки
Допустим, пользователь запрашивает сброс пароля и ему приходит ссылка такого вида:Для просмотра ссылки Войди
Другой вид предсказуемых токенов — небезопасные алгоритмы генерации. Например, первая версия UUID. Если генерировать токены в короткое время, алгоритм выдаст значения, отличающиеся на несколько байтов:
Код:
UUIDv1 #1: 6f5a1f30-b8fc-11ef-a3b4-0242ac120002
UUIDv1 #2: 6f5a20d0-b8fc-11ef-a3b4-0242ac120002
UUIDv1 #3: 6f5a2240-b8fc-11ef-a3b4-0242ac120002
UUIDv1 #4: 6f5a234a-b8fc-11ef-a3b4-0242ac120002
Такой тип атаки называется «сэндвич». Название дано из‑за особенности алгоритма: атакующий «оборачивает» сброс пароля жертвы в сбросы пароля для своих аккаунтов. Он регистрирует два аккаунта, например hacker1@vulncorp.com и hacker3@vulncorp.com. Затем инициирует запрос на сброс пароля в следующем порядке:
- аккаунт злоумышленника (hacker1@vulncorp.com);
- аккаунт жертвы (admin@vulncorp.com);
- аккаунт злоумышленника (hacker3@vulncorp.com).
Код:
hacker1: 6b894ab2-845d-11ee-8227-00155d4e2cec
hacker3: 6b89a4c6-845d-11ee-8227-00155d4e2cec
Атака эффективна не только для UUIDv1, но и для всех генераций токенов, которые используют временную метку сервера в качестве случайного числа во время генерации.
На практике встречается не только предсказуемая генерация, но и небезопасная проверка сгенерированных токенов. В одном из проектов была некорректная реализация проверки токена сброса пароля. Токен проверялся только по первым двум символам! Именно это позволяло сделать возможным перебор этой строки, ведь алфавит составлял всего 36 символов.
Случай довольно специфичный и в какой‑то степени курьезный. Разработчик либо забыл исправить тестовый код и выкатил его в продакшен, либо совсем не собирался реализовывать проверку. Уязвимый код мог быть, например, в некорректном SQL-запросе:
SELECT token FROM recovery_tokens WHERE token LIKE 's%' AND LENGTH('s%') > 2;
Здесь s — это ввод пользователя.
Допустим, наш токен выглядит так: YwAdskd512jlki0owW. Для атаки достаточно было перейти по ссылке, содержащей часть этого токена: Для просмотра ссылки Войди
Общие рекомендации по токенам
- Генерируй токены случайным образом с использованием безопасного криптографического метода. Это сделает невозможным перебор за разумное время.
- Выполняй строгую проверку токена, даже во время тестов. Так ты избежишь курьезных уязвимостей.
- Обеспечь надежное хранение, одноразовое использование и четкий срок жизни токенов.
# ПЛОХО: использование времени для генерации токена
Код:
import time
import base64
def generate_insecure_token(user):
full_token = f"{user}{int(time.time())}"
insecure_token = base64.b64encode(full_token.encode('utf-8'))
return insecure_token.decode('ascii')
Пример безопасного кода на Python:
# ХОРОШО: использование криптографически стойкого генератора
Код:
import secrets
def generate_strong_token():
return secrets.token_urlsafe(32) # Генерирует 32-байтный токен (43 символа в Base64)
Брут OTP
Брутфорс кодов подтверждения — тоже частая проблема. Разница с подбором токена в том, что пользователю отправляется не ссылка, а код подтверждения, который нужно ввести для смены пароля. Код может быть отправлен не на почту, а на мобильный телефон, как гарантия, что получит его пользователь. Но на выполнение атаки это не влияет.Предпосылки для успешного брутфорса: короткий код (4–6 цифр) и отсутствие лимита на количество попыток. Время жизни кода тоже влияет, но для полного перебора 10 тысяч вариантов (код из четырех цифр) хватит пяти минут. Если время жизни недостаточно для атаки, хакер может перебирать часть значений, запрашивая новый сброс пароля. Так за несколько попыток получится наткнуться на нужный код.
Рекомендации по защите OTP
- Используй непредсказуемые коды. Четыре цифры — это 10 тысяч вариантов, с латинским алфавитом — 2 176 782 336, с зависимостью от регистра — 56 800 235 584 варианта.
- Ограничь количество неудачных попыток ввода и время жизни кода.
- Ограничь количество попыток сброса в сутки.
- Внедри политику блокировки источника запросов.
- Используй CAPTCHA при множественных попытках входа.
Код:
app.post('/verify-otp', (req, res) => {
const { otp, email } = req.body;
if (otp === getStoredOtp(email)) {
res.send("Success");
} else {
res.status(400).send("Invalid OTP");
}
});
Вариант получше, с ограничением количества попыток ввода:
Код:
const attemptCount = new Map();
app.post('/verify-otp', (req, res) => {
const { otp, email } = req.body;
const attempts = attemptCount.get(email) || 0;
if (attempts > 5) {
return res.status(429).send("Too many attempts, try again later.");
}
if (otp === getStoredOtp(email) and attempts <= 5) {
attemptCount.delete(email);
res.send("Success");
} else {
attemptCount.set(email, attempts + 1);
res.status(400).send("Invalid OTP");
}
});
Type juggling
Уязвимости, связанные с подменой типов (type juggling), возникают, когда злоумышленник может заставить программу рассматривать объект одного типа как другой, вызывая непредсказуемое поведение.Пример атаки: пользователь получает токен в виде строки, но в коде нестрогое сравнение, что позволяет заменить значение сопоставимым типом:
Код:
POST /recovery-pass HTTP/1.1
Host: vulncorp.com
Content-Length: 47
Content-Type: application/json
{
"email": "user@vulncorp.com",
"token": true
}
Если проверка нестрогая (== вместо ===), есть шанс приведения true к Boolean и непредсказуемому выполнению. Чаще всего такие уязвимости возникают в Node.js и некоторых версиях PHP и Python.
Защита
- Используй строгие сравнения (===,!==).
- Явно преобразовывай типы перед операциями.
- Производи валидацию и санитизацию входных данных.
Код:
if ("123a" == 123) {
echo "This is true! (PHP converts '123abc' to 123)";
}
else {
echo "This is false";
}
Результат сравнения — true. PHP отбросит нечисловые символы и выполнит сравнение 123 == 123.
Замени сравнение строгим, и код заработает предсказуемо:
Код:
if ("123a" === 123) {
echo "This is true! (PHP converts '123abc' to 123)";
}
else {
echo "This is false";
}
Mass assignment
Напоследок упомяну уязвимость, связанную с внедрением дополнительных параметров запроса. Разработчик мог использовать скрытые поля формы, чтобы в нем хранить адрес хоста, на котором выполняется сброс пароля. Или это может быть эндпоинт API, который обрабатывает как запросы от пользователя, так и сервисные запросы с другим набором параметров.Иногда перед отправкой формы JavaScript обрабатывает значения и расширяет набор параметров. В любом случае, если есть скрытые параметры, хакер может их найти. Где‑то с задачей справится Burp, показав реальный запрос. В случае с API хакер может устроить перебор параметров по словарю и, увидев отличия в ответах сервера, вычислить скрытые поля.
Атака похожа на подмену параметров из начала статьи, с той разницей, что параметр нужно найти. Пример атаки:
Код:
POST /recovery-pass HTTP/1.1
Host: vulncorp.com
Content-Length: 43
Content-Type: application/x-www-form-urlencoded
email=user@vulncorp.com&return_url=evil.com
Если параметр существует, жертва получит ссылку с вредоносным доменом.
Защита
- Никогда не доверяй пользовательскому вводу. Используй белые списки параметров. Добавляй в белые списки только те свойства, которые могут быть изменены клиентом.
- Разделяй функции, доступные пользователям, и сервисные функции.
- Избегай функций, которые автоматически присваивают переменным и внутренним объектам соответствующие значения из пользовательского ввода.
- Используй черный список параметров, к которым у пользователя нет доступа.
// ХОРОШО: явное указание разрешенных полей
Код:
class RecoveryPass extends PasswordResetToken {
protected $fillable = ['email']; // Только это поле можно назначать
}
В Laravel рекомендую использовать именно $fillable вместо $guarded. Первый формирует белый список полей, второй — черный. Работая с $guarded, ты можешь забыть добавить новый столбец в массив $guarded, оставив его открытым для массового назначения. Лучше сначала определи белый список. Если потребуется, после него переходи к черному.
Пример уязвимого кода на Laravel:
Код:
class RecoveryPass extends PasswordResetToken
{
// Только разрешенные поля
protected $fillable = [
'email', 'reset_base_url'
];
}
