• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Разбираем техники тестирования и защиты форм восстановления пароля

stihl

bot
Moderator
Регистрация
09.02.2012
Сообщения
1,440
Розыгрыши
0
Реакции
792
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Небольшая форма восстановления пароля может стать большой головной болью. На каждом шаге разработчики допускают ошибки. В этой статье я покажу актуальные методы атаки на формы сброса пароля и дам рекомендации, которые будут полезны пентестерам и разработчикам.
Я занимаюсь анализом защищенности в 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-заголовков, который отправляет ссылку для восстановления пароля вида Для просмотра ссылки Войди или Зарегистрируйся{token}. Мы можем создать такой запрос, чтобы получить токен пароля:

Код:
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"}

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


Защита​

Пять простых правил для защиты:

  1. Откажись от абсолютных URL или защити их. В большинстве случаев относительные URL хорошо впишутся в логику приложения. Если ты не можешь обойтись без абсолютного URL, убедись, что текущий домен вручную указан в конфиге и ссылается на это значение вместо заголовка Host.
  2. Проверяй заголовок Host, если не можешь от него отказаться. Используй проверку по белому списку доменов и отклоняй или перенаправляй запросы с неопознанными хостами. Например, платформа Django предоставляет параметр ALLOWED_HOSTS в файле настроек.
  3. Не доверяй заголовкам переопределения Host. Убедись, что твой код не поддерживает дополнительные заголовки, которые могут использоваться при атаках. Например, X-Forwarded-Host. Помни, что они могут поддерживаться веб‑сервером по умолчанию.
  4. Создай белый список разрешенных доменов. Чтобы предотвратить атаки на основе маршрутизации на внутреннюю инфраструктуру, настрой балансировщик нагрузки или любые обратные прокси‑серверы для перенаправления запросов только списку разрешенных доменов.
  5. Будь осторожен с внутренними виртуальными хостами: не размещай на одном сервере публичные веб‑приложения и приложения для внутреннего пользования. В противном случае злоумышленники могут получить доступ к внутренним доменам через манипуляции с заголовком Host.
Пример плохого кода с применением фреймворка Laravel:

Код:
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');
    }
}
Здесь явно задан допустимый заголовок Host, который при его замене выдаст в ответ код 403.


Опасные токены​


Предсказуемые токены или отсутствие их проверки​

Допустим, пользователь запрашивает сброс пароля и ему приходит ссылка такого вида:

Для просмотра ссылки Войди или ЗарегистрируйсяЗдесь token — это строка в Base64, расшифровав которую мы получим введенную нами почту admininstrator@vulncorp.com. Хакеру остается узнать почту админа, закодировать в Base64 и установить свой пароль.

Другой вид предсказуемых токенов — небезопасные алгоритмы генерации. Например, первая версия 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. Затем инициирует запрос на сброс пароля в следующем порядке:

Цель — минимизировать задержку между тремя запросами, чтобы снизить нагрузку на последующий перебор. После этих запросов злоумышленник получит следующие UUID:

Код:
hacker1: 6b894ab2-845d-11ee-8227-00155d4e2cec
hacker3: 6b89a4c6-845d-11ee-8227-00155d4e2cec
Легко заметить, что идентификаторы различаются всего несколькими символами. Если рассчитать разницу между отличающимися идентификаторами, то выяснится, что для поиска токена жертвы нужно будет чуть более 23 тысяч попыток. Запросов много, атака «шумная», но выполнимая. Функция Intruder в Burp Suite позволяет проверить все варианты в многопоточном режиме и определить действительный токен. При десяти потоках и интервале в 300 мс потребуется чуть больше семи минут на выполнение атаки.

Атака эффективна не только для UUIDv1, но и для всех генераций токенов, которые используют временную метку сервера в качестве случайного числа во время генерации.

На практике встречается не только предсказуемая генерация, но и небезопасная проверка сгенерированных токенов. В одном из проектов была некорректная реализация проверки токена сброса пароля. Токен проверялся только по первым двум символам! Именно это позволяло сделать возможным перебор этой строки, ведь алфавит составлял всего 36 символов.

Случай довольно специфичный и в какой‑то степени курьезный. Разработчик либо забыл исправить тестовый код и выкатил его в продакшен, либо совсем не собирался реализовывать проверку. Уязвимый код мог быть, например, в некорректном SQL-запросе:

SELECT token FROM recovery_tokens WHERE token LIKE 's%' AND LENGTH('s%') > 2;
Здесь s — это ввод пользователя.

Допустим, наш токен выглядит так: YwAdskd512jlki0owW. Для атаки достаточно было перейти по ссылке, содержащей часть этого токена: Для просмотра ссылки Войди или Зарегистрируйся.


Общие рекомендации по токенам​

  • Генерируй токены случайным образом с использованием безопасного криптографического метода. Это сделает невозможным перебор за разумное время.
  • Выполняй строгую проверку токена, даже во время тестов. Так ты избежишь курьезных уязвимостей.
  • Обеспечь надежное хранение, одноразовое использование и четкий срок жизни токенов.
Пример уязвимого кода на Python:

# ПЛОХО: использование времени для генерации токена
Код:
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)
Здесь применена библиотека secrets для генерации случайного токена. Алгоритм исключает использование предсказуемых данных и паттернов, каждый раз ты будешь получать надежный токен.


Брут 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

Если параметр существует, жертва получит ссылку с вредоносным доменом.


Защита​

  • Никогда не доверяй пользовательскому вводу. Используй белые списки параметров. Добавляй в белые списки только те свойства, которые могут быть изменены клиентом.
  • Разделяй функции, доступные пользователям, и сервисные функции.
  • Избегай функций, которые автоматически присваивают переменным и внутренним объектам соответствующие значения из пользовательского ввода.
  • Используй черный список параметров, к которым у пользователя нет доступа.
Пример хорошего кода для фреймворка Laravel:

// ХОРОШО: явное указание разрешенных полей
Код:
class RecoveryPass extends PasswordResetToken {
    protected $fillable = ['email']; // Только это поле можно назначать
}

В Laravel рекомендую использовать именно $fillable вместо $guarded. Первый формирует белый список полей, второй — черный. Работая с $guarded, ты можешь забыть добавить новый столбец в массив $guarded, оставив его открытым для массового назначения. Лучше сначала определи белый список. Если потребуется, после него переходи к черному.

Пример уязвимого кода на Laravel:

Код:
class RecoveryPass extends PasswordResetToken
{
    // Только разрешенные поля
    protected $fillable = [
        'email', 'reset_base_url'
    ];
}
В данном случае есть разрешенное поле reset_base_url, которое может быть использовано для формирования фишинговой ссылки.


Заключение​

В статье я разобрал актуальные уязвимости, которые встречаются в реальном тестировании. Формы сброса пароля — один из самых быстрых векторов атаки. Небезопасная проверка токенов приводит к моментальной краже аккаунта админа. Злоумышленнику останется найти способ загрузить шелл и полностью перехватить управление над сервером. Чтобы защититься, используй мои рекомендации из каждого блока уязвимостей. И никогда не доверяй вводу пользователя.
 
Activity
So far there's no one here
Сверху Снизу