# 危险的文件夹上传框

- URL: https://wuming.si/blog/1032/
- Author: Aaron
- Published: 2015-08-27
- Language: zh-Hans
- Categories: Uncategorized
- Originally published at: http://blog.wo.ai/archives/1032

分享一篇老文章。前些年在 ASRC 上的看了一篇文章[「Html5新功能上的ui-redressing」](https://security.alibaba.com/blog/blog.htm?spm=0.0.0.0.IYip0H&id=3)后，写的一些改进方案。

## 文件对话框

文件上传对话框是一直以来就存在的网页控件。

到了 HTML5 时代，增加了更多的功能，例如支持文件多选。Chrome 甚至还支持「上传文件夹」这一私有特征：
    
    
    <input type="file" webkitdirectory />

在给用户方便的同时，其安全隐患也逐渐出现。用户平时在下载时，理所当然的弹出的是保存对话框，因此常常不仔细看就做出了选择。

这极有可能被攻击者所利用。一些恶意网站在用户点击下载时，故意弹出一个上传对话框。只要用户一疏忽，就把选中的文件夹给上传了！

**下载对话框**

![](http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/9e38b0544a_b254dc71jw1evgzrdrkqsj20ki0fn75p.jpg)

**上传对话框**

![](http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/c006f4c08e_b254dc71jw1evgzregccjj20nx0e6di1.jpg)

当然，仅仅依靠默认的上传，这种攻击方式仍有较大难度。因为整个文件夹可能非常大，上传需要很久的时间。

如果用户等了半天也没看见下载进度，或许就会刷新重试，甚至放弃了。

## 选择即授权

然而，HTML5 带来了一个新的规范 —— File API，允许脚本访问文件。

但由于沙箱限制，脚本无法访问任何一个本地文件，除非用户主动授权。如何授权？最常见的，就是「上传对话框」了。

事实上，如今的上传对话框，早已不是从前「选择哪个文件」的功能，而是「允许脚本访问哪个文件」的权限申请！只不过界面上没有提示罢了。

例如，一个传统的文件上传控件。当用户选中文件后，即可通过 File API 读取文件内容：
    
    
    <input id="dialog" type="file" />
    <script>
        dialog.onchange = function(e) {
            var reader = new FileReader();
            reader.onload = function() {
                console.log(this.result);
            };
            reader.readAsText(this.files[0]);
        };
    </script>

或许你已注意到，File 位于`files[]`而不是`file`，这正是给文件夹预留的！

在 Chrome 里，上传控件只要加上 `webkitdirectory` 属性，就变成文件夹选择框。这时一旦用户选中某个文件夹，瞬间就赐予脚本访问整个文件夹的权限！
    
    
    <input id="dialog" type="file" webkitdirectory />
    <script>
        dialog.onchange = function(e) {
            var files = this.files;
            var table = {};
    
            for (var i = 0; i < files.length; i++) {
                var f = files[i];
                var dt = new Date(f.lastModified);
    
                table[i] = {
                    path: f.webkitRelativePath,
                    size: f.size,
                    modified: dt.toLocaleString()
                };
            }
    
            console.table(table);
        };
    </script>

![](http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/4f1748a157_b254dc71jw1evgzrf7ankj20l20gwgpj.jpg)

[演示](http://www.etherdream.com/funnyscript/dialog_phishing/demo1.html)

于是，用户本想将文件保存在桌面上，结果却将桌面上的所有资料，被攻击者的脚本拿到！

## 优化上传

一旦脚本可主动访问，我们可以用更灵活的方式处理这些文件，无需再用传统落后的方式上传。我们可以直接在前端分析出「有价值」的文件，例如：

  * 备注文件、脚本、批处理、电子表格等，很可能存有一些敏感信息，而且体积小价值大，优先将其上传；
  * 图片则可通过 canvas 缩放，先传较小的缩略图。当接收端发现有意义时，再传输原文件。
  * 对于一些体积较大但意义不大的文件，则可以直接忽略。

由于 HTTP 上传是没有压缩的，因此在传输文本文件时效率很低。我们可以借助 Flash 内置的`LZMA`压缩算法，极大提升传输效率。如果不支持 Flash，也可以使用`asm.js`版的 LZMA 压缩器，配合`Worker`线程在后台压缩和传输。

同时，将多个小文件合并后再压缩，可进一步提高压缩率。再多开几个连接，上传速度即可大幅提升。

## 续点上传

不过即使再优化，仍有传不完的可能。因此，我们得将没传完的内容储存起来，当用户再次回来时，继续传输。

得益于 HTML5 的 Storage API，这不难实现。

事实上，当用户授权了某个文件夹时，我们首先要做的**不是发送，而是读出文件夹内容，立即备份到 Storage 里** 。毕竟，文件的读取需要用户主动配合，是非常珍贵的；而访问 Storage 则无需任何条件。

![](http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/4f1748a157_b254dc71jw1evgzrgin07j20fg052wet.jpg)

当备份完成后，再从 Storage 里读取、发送、删除。这样，即使中途页面刷新或关闭了，下次回来时，仍能从 Storage 中继续。

考虑到每个域的 Storage 容量有限，我们可以使用`iframe`嵌入多个不同域的页面，然后通过`postMessage`进行数据的分发和汇总，这样就不受容量限制了。

![](http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/98bf74f031_b254dc71jw1evgzrh4x3sj20b005f74h.jpg)

> 当然，能不能无限容量还得看浏览器策略，不然硬盘会被撑满

将数据存放在 Storage 里还有另一个好处，即使用户永不回来，但数据仍持久保存着。只要以后一旦进入其他的站点，只要是我们可控的，仍有机会继续上传。（例如将用户引到我们布置了 XSS 的站点上）

## 延长上传

在之前[《延长 XSS 生命期》](http://www.cnblogs.com/index-html/p/xss_long_live.html) 中介绍过，可以使用各种黑魔法来提升脚本有效期。

利用这个原理，即使当前页面关闭，其他关联的页面也能继续上传。事实上，除了文中提到的方法，如今还有一个新的 API —— [SharedWorker](https://developer.mozilla.org/zh-CN/docs/Web/API/SharedWorker)，它可以让 Worker 共享于多个页面，只要有一个存在，线程就不会停止。可以让续点时丢失的数据更少。

## 视觉欺骗

由于上传控件有着独特的界面，如何才能让用户主动去点呢？万能的方法是点击劫持（Clickjacking）。

不过本场景无需这么麻烦，只需简单的调用控件的`click`方法就可以了。
    
    
    <a href="ed2k://|file|xxxxxxxxxx.avi" id="download">高速下载</a>
    <script>
        var uploader = document.createElement('input');
        uploader.type = 'file';
        uploader.webkitdirectory = true;
    
        download.onclick = function(e) {
            uploader.click();       // 弹出授权对话框
            e.preventDefault();     // 屏蔽下载对话框
        };
    </script>

[演示](http://www.etherdream.com/funnyscript/dialog_phishing/demo2.html)

我们将屏蔽超链接的默认行为，将其传递给上传控件，即可召唤出「文件夹授权」对话框了！

同时，为了不让眼亮的人发现对话框上的破绽，我们使用一些第三方的下载方式，例如电驴、迅雷等等，让人们误以为就是这样的。

![](http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/98bf74f031_b254dc71jw1evgzrft7pcj20gj0aoaao.jpg)

当然，这只是一个小例子。只要页面做的真实，下载内容引人入胜，用户自然就会中招。

## 后记

本以为 `webkitdirectory` 的这个私有属性很快就会放弃，至少是更强的安全提示。不过至少现在也没更新，因此上网时还是要多加留心，看清楚了再做决定。

当然，这篇只是最早的「交互欺骗」探索。事实上，深入挖掘会发现可利用点远不仅此。

如今的浏览器在视觉、音频上的体验已经非常完善，攻击者甚至可以在网页里，高度模拟一个本地应用的交互效果。让用户误以为是浏览器之外的程序弹出的界面，从而进行钓鱼。

[危险的文件夹上传框](http://blog.jobbole.com/90059/)，首发于[博客 – 伯乐在线](http://blog.jobbole.com)。
