Recently I was pentesting a web app that had an unauthenticated XSS vulnerability but there was some heavy filtering in place. Nonetheless I was able to achieve session fixation using a combination of a technique I previously explained and some fun filter workarounds – including using the application’s own defensive HTML encoding to create a working XSS payload!
The problem
The application used a bespoke session management cookie. I’ll call it MYSESSIONID. On login, it wasn’t renewed. I couldn’t push a session cookie onto the victim in a classic session fixation attack. However, I had XSS in an unauthenticated page – but not the login page. The filtering in place used a combination of removal and encoding. Characters that were stripped out included:
+ ; ( ) ? < >
Characters that were allowed included:
" ' = [ ] / , .
So even though MYSESSIONID wasn’t protected with the HttpOnly flag, I just couldn’t construct a payload to steal it. Instead I looked to set one of my own. Here’s a breakdown of the attack:
1. Get a valid cookie
The application did not accept arbitrary session management cookies so the attacker sends a request to get a valid one. In this case, simply having no MYSESSIONID wasn’t enough, the cookie had to be present but an invalid value did the trick:
Cookie: MYSESSIONID=aaaaaaaaaaaaaaaaaaa:xx01
returned
Set-Cookie: MYSESSIONID=NDnQrZ6JsMHyJTBCw8n:xx01; Path=/; Domain=.example.com
2. The XSS
The malicious link looked something like this (the highlighted bits are explained below):
https://www.example.com/app/folder/page?vuln=foo"%0adocument.cookie%3d"MYSESSIONID%3dNDnQrZ6JsMHyJTBCw8n:xx01:%0dpath%3d/app/
When clicked, the XSS flaw wrote the following to the return page inside a JavaScript code block:
var a = "foo"
document.cookie="MYSESSIONID=NDnQrZ6JsMHyJTBCw8n:xx01: path=/app/";
The %0a at the front of the XSS payload was used to start a new line and this was sufficient to act as a statement terminator after var a = "foo"
(semi-colons were being stripped). But in order to inject a path
attribute (discussed below) I did need a semi-colon in the cookie string. By running every character through a Burp Intruder attack, I saw which characters were allowed, which were stripped and which were returned encoded. By inserting :%0d into the XSS payload : was returned – yes, %0d was encoded but %0a (used above) came back fine! Being inside a string inside a JavaScript block wasn’t seen as an HTML entity by the browser and thus wasn’t interpreted. This provided the semi-colon needed to create a path
attribute.
The colon at the front was used because it looked like the session cookie was delimited in that way. That “xx01″ might refer, for example, to an internal server for load-balancing. Anyway, whatever it did, the application tolerated the unusual suffix to the session cookie. So that explains the :%0d appended to the cookie value in the XSS payload. Now for the path%3d/app/
…
3. The victim logins in
So, at this point, the attacker has set the MYSESSIONID cookie on the victim to be NDnQrZ6JsMHyJTBCw8n:xx01:
via a reflected XSS attack. Now the victim goes to the login page at https://www.example.com/app/login or is bounced there by navigating to a part of the site that enforces authentication. At login two MYSESSIONID cookies are passed up. This is because one had been set earlier in a Set-Cookie
response header the first time the victim hit the site, even if that was by visiting the XSS’ed page. The genuine MYSESSIONID has a path
of / and a domain
of .example.com. If I had set a cookie by XSS with no attributes my cookie would have had a path
of /app/folder/ (to match the path of the page which set the cookie) and a domain
of www.example.com (to match the domain of said page). This would mean my cookie would never be sent up to /app/login for authentication, hence the need to set a path
as part of the XSS.
Furthermore, when two MYSESSIONID values were sent up, the application took the first value so I had to make sure my cookie was first. By setting a path
of /app/, it trumped the real MYSESSIONID for having a better path match to /app/login. Thus it was listed first in the POST request with the credentials and became authenticated:
Cookie: MYSESSIONID=NDnQrZ6JsMHyJTBCw8n:xx01: MYSESSIONID=4GRc4jiKNeQIfsqh2:xx01
In contrast, the domain
of a cookie does not govern precedence in a standardised way, it varies between browser. From memory I think my cookie (with a more specific domain match) was sent up first by IE but second by Chrome and Firefox. It’s not something you want to rely on. Neither could I overwrite the cookie because for that to happen the name, path and domain must match. That would mean having to change both attributes from their defaults but in this case I could only change one. This is because I’d need a second semi-colon to set a second attribute and in doing so, using the encoding trick above, the first attribute would be spoilt, e.g. I’d get
var a = "foo"
document.cookie="MYSESSIONID=NDnQrZ6JsMHyJTBCw8n:xx01: path=/app/ domain=.example.com";
Developing this proof-of-concept for this specific injection point was quite fiddly and took some persistence but it was worth it. For all of their filtering – and because they did not change the session cookie after authentication – this was a nice practical attack using an unauthenticated XSS. One take-away thought then: be sure to probe the XSS defences in full because you never know what might come back and how it could be of help!
Hi.
Wow Very Nice Article! Thank you so much for sharing great post.