Insistently Marketing Persistent XSS
The last few HTML injection articles here demonstrated the ephemeral variant of the attack, where the exploit appears within the immediate response to the request that contained the XSS payload. The exploit disappears once the victim browses away from the affected page. The page remains vulnerable, but the attack must be delivered anew for every subsequent visit.
A persistent HTML injection is usually more insidious. The site still reflects the payload, but not necessarily in the immediate response to the request that delivered it. This decoupling of the point of injection from the point of reflection is much like D&D’s delayed blast fireball – you know something bad is coming, you just don’t know when.
In the persistent case, you have to find the payload in some other area of the app as well as have a means of mapping it back to the injection point. The usual trick is to use a unique identifier for each injection point. This way you know that when you see a page generate a console message with 8675309
, it means you can look up the page and parameter where you originally submitted a payload that included console.log(8675309)
.
Typically the payload need only be delivered once because the app persists (stores) it such that any subsequent visit to the reflecting page re-delivers the exploit. This is dangerous when the page has a one-to-many relationship where an attacker infects a page that many users visit.
Persistence comes in many guises and durations. Here’s one that associates the persistence with a cookie.
This paricula app chose to track users for marketing and advertising purposes. There’s little reason to love user tracking (unless 95% of your revenue comes from it), but you might like it a little more if you could use it for HTML injection.
The hack starts off like any other reflected XSS test. Another day, another alert
:
https://web.site/page.aspx?om=alert(9)
But the response contains nothing interesting. It didn’t reflect any piece of the payload, not even in an HTML encoded or stripped version. And – spoiler alert – not in the following script block:
//<![CDATA[<!--/\* [ads in the cloud] Variables */
s.prop4="quote";
s.events="event2";
s.pageName="quote1";
if(s.products) s.products = s.products.replace(/,$/,'');
if(s.events) s.events = s.events.replace(/^,/,'');
/****** DO NOT ALTER ANYTHING BELOW THIS LINE ! ******/
var s_code=s.t();
if(s_code)document.write(s_code);
//-->//]]>
But we’re not at the point of nothing ventured, nothing gained. We’re at the point of nothing reflected, something might still be flawed.
So we poke around at more links. We visit them as any user might without injecting any new payloads, working under the assumption that the payload could have found a persistent lair to curl up in and wait for an unsuspecting victim.
Sure enough we find a reflection in an (apparently) unrelated link. Note that the payload has already been delivered. This request bears no payload:
https://web.site/wacky/archives/2012/cute_animal.aspx
Yet in the response we find the alert()
nested inside a JavaScript variable where, sadly, it remains innocuous and unexploited. For reasons we don’t care about, a comment warns us not to ALTER ANYTHING BELOW THIS LINE!
No need to shout – we’ll alter things above the line.
//<![CDATA[<!--/* [ads in the cloud] Variables */ s.prop17="alert(9)";
s.pageName="ar_2012_cute_animal";
if(s.products) s.products = s.products.replace(/,$/,'');
if(s.events) s.events = s.events.replace(/^,/,'');
/****** DO NOT ALTER ANYTHING BELOW THIS LINE ! ******/
var s_code=s.t();
if(s_code)document.write(s_code);
//-->//]]>
There are plenty of fun ways to inject into JavaScript string concatenation. We’ll stick with the most obvious plus (+
) operator. To do this we need to return to the original injection point and alter the payload. (Remember, don’t touch ANYTHING BELOW THIS LINE!).
https://web.site/page.aspx?om="%2balert(9)%2b"
We head back to the cute_animal.aspx
page to see how the payload fared. Before we can click to Show Page Source we’re greeted with that happy hacker greeting, the friendly alert()
window.
//<![CDATA[<!--/* [ads in the cloud] Variables */ s.prop17=""+alert(9)+"";
s.pageName="ar_2012_cute_animal";
if(s.products) s.products = s.products.replace(/,$/,'');
if(s.events) s.events = s.events.replace(/^,/,'');
/****** DO NOT ALTER ANYTHING BELOW THIS LINE ! ******/
var s_code=s.t();
if(s_code)document.write(s_code);
//-->//]]>
After experimenting with a few variations on the request to the reflection point (the cute_animal.aspx
page) we narrow the persistent carrier down to a cookie value. The cookie is a long string of hexadecimal digits whose length and content remain stable between requests. This is a good hint that it’s some sort of UUID that points to a record in a data store where value for om
variable comes from. Delete the cookie and the alert
no longer appears.
The cause appears to be string concatenation where the s.prop17
variable is assigned a value associated with the cookie. It’s a common, basic, insecure design pattern.
So, we have a persistent HTML injection tied to a user-tracking cookie. A mitigating factor in this vuln’s risk is that the impact is limited to individual visitors. It’d be nice if we could recommend getting rid of user tracking as the security solution, but the real issue is applying good software engineering practices when inserting client-side data into HTML.
We’re not done with user tracking yet. There’s this concept called privacy…
But that’s a story for another day.