新手入门:史上最全Web端即时通讯技术原理详解

澳门新葡亰网址 9

WebSockets是一个能够给单TCP连接提供全双工信道的HTML5特性。它的持续性连接功能,使得构建B/S模式的实时应用成为可能。Websockets常常用在那些带有聊天功能的WEB应用上。

前言

有关IM(InstantMessaging)聊天应用(如:微信,QQ)、消息推送技术(如:现今移动端APP标配的消息推送模块)等即时通讯应用场景下,大多数都是桌面应用程序或者native应用较为流行,而网上关于原生IM(相关文章请参见:《IM架构篇》、《IM综合资料》、《IM/推送的通信格式、协议篇》、《IM心跳保活篇》、《IM安全篇》、《实时音视频开发》)、消息推送应用(参见:《推送技术好文》)的通信原理介绍也较多,此处不再赘述。

而web端的IM应用,由于浏览器的兼容性以及其固有的“客户端请求服务器处理并响应”的通信模型,造成了要在浏览器中实现一个兼容性较好的IM应用,其通信过程必然是诸多技术的组合,本文的目的就是要详细探讨这些技术并分析其原理和过程。

下面的图片就非常贴切地阐释了一个APT攻击用到的websockets:

更多资料整理

Web端即时通讯技术盘点请参见:

《Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE》

关于Ajax短轮询:
找这方面的资料没什么意义,除非忽悠客户,否则请考虑其它3种方案即可。

有关Comet技术的详细介绍请参见:
《Comet技术详解:基于HTTP长连接的Web端实时通信技术》
《WEB端即时通讯:HTTP长连接、长轮询(long
polling)详解》
《WEB端即时通讯:不用WebSocket也一样能搞定消息的即时性》
《开源Comet服务器iComet:支持百万并发的Web端即时通讯方案》

有关WebSocket的详细介绍请参见:
《WebSocket详解(一):初步认识WebSocket技术》
《WebSocket详解(二):技术原理、代码演示和应用案例》
《WebSocket详解(三):深入WebSocket通信协议细节》
《Socket.IO介绍:支持WebSocket、用于WEB端的即时通讯的框架》
《socket.io和websocket
之间是什么关系?有什么区别?》

有关SSE的详细介绍文章请参见:
《SSE技术详解:一种全新的HTML5服务器推送事件技术》

更多WEB端即时通讯文章请见:

澳门新葡亰网址 1

一、传统Web的通信原理

浏览器本身作为一个瘦客户端,不具备直接通过系统调用来达到和处于异地的另外一个客户端浏览器通信的功能。这和我们桌面应用的工作方式是不同的,通常桌面应用通过socket可以和远程主机上另外一端的一个进程建立TCP连接,从而达到全双工的即时通信。
浏览器从诞生开始一直走的是客户端请求服务器,服务器返回结果的模式,即使发展至今仍然没有任何改变。所以可以肯定的是,要想实现两个客户端的通信,必然要通过服务器进行信息的转发。例如A要和B通信,则应该是A先把信息发送给IM应用服务器,服务器根据A信息中携带的接收者将它再转发给B,同样B到A也是这种模式,如下所示:

澳门新葡亰网址 2 

科普:

同源策略(Same origin
policy):同源是指,域名,协议,端口相同,即浏览器会检查同一浏览器的不同选项卡中,来源相同的脚本才能跨选项卡执行。

Origin字段:浏览器在发送POST请求的时候可能会加上一个Origin字段,这个Origin字段主要是用来标识出最初请求是从哪里发起的。如果浏览器不能确定源在哪里,那么在发送的请求里面Origin字段的值就为空。

IronWASP:某开源WEB测试平台,用户可以自定义安全扫描,并且可以自己用python/ruby来定义插件系统。相关介绍见:

ZAP(Zed Attack
Proxy):是一款集成各种工具的渗透测试框架,可以发现在WEB应用程序中的漏洞,相关介绍见:

二、传统通信方式实现IM应用需要解决的问题

我们认识到基于web实现IM软件依然要走浏览器请求服务器的模式,这这种方式下,针对IM软件的开发需要解决如下三个问题:

  • 双全工通信:
    即达到浏览器拉取(pull)服务器数据,服务器推送(push)数据到浏览器;
  • 低延迟:
    即浏览器A发送给B的信息经过服务器要快速转发给B,同理B的信息也要快速交给A,实际上就是要求任何浏览器能够快速请求服务器的数据,服务器能够快速推送数据到浏览器;
  • 支持跨域:
    澳门新葡亰网址,通常客户端浏览器和服务器都是处于网络的不同位置,浏览器本身不允许通过脚本直接访问不同域名下的服务器,即使IP地址相同域名不同也不行,域名相同端口不同也不行,这方面主要是为了安全考虑。

即时通讯网注:关于浏览器跨域访问导致的安全问题,有一个被称为CSRF网络攻击方式,请看下面的摘录:

CSRF(Cross-site request
forgery),中文名称:跨站请求伪造,也被称为:one click attack/session
riding,缩写为:CSRF/XSRF。

style=”color: #888888;”>你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。

style=”color: #888888;”>CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

基于以上分析,下面针对这三个问题给出解决方案。

WebSocket的安全评估

最近,我们对一个拥有复杂菜单选项和功能的WEB应用做了安全评估。该应用中大部分操作都用到了web-sockets,这意味着其大多数行为都不会记录到http代理日志上。

首先,我们打开主页后,网站会加载一个带有JS脚本和CSS文件的静态网页。此后,整个通信交互会转为Websockets模式,浏览器和服务端之间会建立websocket连接,从而加载网站里所有可见的HTML资源。点击链接或者提交Form表单时,浏览器会向服务端发送一些WebSocket消息。服务器处理了这些消息后,会通过WebSocket进行反馈,而后客户端浏览器会展示新的HTML内容。

这时当websocket消息在进行交互时,通信数量是非常巨大的。每隔一秒它们之间会有心跳探测包的交互。然而现有的工具达不到我的要求,我不得不给IronWASP添加了一个Websocket消息分析装置和一个WebSocket客户端,这样它才能识别Websocket从而尝试fuzz其漏洞。你可以在在这里了解下相关知识。

在测试这个应用时,我发现它存在WebSocket跨站劫持(Cross-Site WebSocket
Hijacking)漏洞(由christian
schneider首创)。当然,我会在给大家介绍测试方法之前,解释下这个漏洞的影响。在测试相关Websockets应用之前,我们需要先做下准备。

三、全双工低延迟的解决办法

WebSocket跨站劫持漏洞实验准备

大家应该明白,同源策略(SOP)不会通过浏览器在websockets上强制执行(同一浏览器下,受SSL保护的页面,不会让非SSL的WebSocket通过),我们测试的应用采用了http
cookie作为session认证,WebSocket通过浏览器发送的消息不会有Session
ID或是随机参数。

这样一来,如果某用户登录了带有漏洞的WEB应用,然后在同一浏览器还打开了
ID的请求包。于是,这个由攻击者网站建立的WebSocket连接会和应用本身有相同的权限。

由于整个应用都是以websockets为基础运行的,劫持了WebSocket就相当于劫持了用户的session。所以这个漏洞在本质上和存储型跨站脚本漏洞是一样的。

如果你认为这就很糟了,那当你听到某些情况下WebSocket跨站脚本,甚至可以在用户系统上实现远程代码执行的时候,会不会更惊讶呢?事见IPython
Notebook的案例。

测试之前,你首先要做的就是确认该应用是否存在WebSockets。幸运的是,做这个非常简单,你只需要知道以下三点:

1.WebSocket的URL连接一般是以ws://或者wss://开头的。

2.需要检测建立连接的Origin头,该网页可能是通过Origin字段建立的WebSocket链接。

3.浏览器和服务端之间发送的消息中,我们可以从中检查出普通WebSocket连接的特征。

下面这张图会给你展示:如何通过IronWASP日志得到Origin字段的值和WebSocket的URL值。

澳门新葡亰网址 3

一旦你得到这些信息,你可以使用一些特殊方法,对跨站WebSocket劫持漏洞进行检测。我在这里例举三个简单的例子:

解决方案3.1:客户端浏览器轮询服务器(polling)

这是最简单的一种解决方案,其原理是在客户端通过Ajax的方式的方式每隔一小段时间就发送一个请求到服务器,服务器返回最新数据,然后客户端根据获得的数据来更新界面,这样就间接实现了即时通信。优点是简单,缺点是对服务器压力较大,浪费带宽流量(通常情况下数据都是没有发生改变的)。

客户端代码如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function createXHR(){
        if(typeof XMLHttpRequest !='undefined'){
            return new XMLHttpRequest();
        }else if(typeof ActiveXObject !='undefined' ){
            if(typeof arguments.callee.activeXString!="string"){
            var versions=["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0",
                    "MSXML2.XMLHttp"],
                    i,len;
            for(i=0,len=versions.length;i<len;i++){
                try{
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString=versions[i];
                    break;
                }catch(ex) {
 
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
       }else{
            throw new Error("no xhr object available");
        }
    }
    function polling(url,method,data){
       method=method ||'get';
       data=data || null;
       var xhr=createXHR();
        xhr.onreadystatechange=function(){
            if(xhr.readyState==4){
                if(xhr.status>=200&&xhr.status<300||xhr.status==304){
                    console.log(xhr.responseText);
                }else{
                    console.log("fail");
                }
            }
        };
        xhr.open(method,url,true);
        xhr.send(data);
    }
    setInterval(function(){
        polling('http://localhost:8088/time','get');
    },2000);

创建一个XHR对象,每2秒就请求服务器一次获取服务器时间并打印出来。

服务端代码(Node.js):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var http=require('http');
var fs = require("fs");
var server=http.createServer(function(req,res){
if(req.url=='/time'){
    //res.writeHead(200, {'Content-Type': 'text/plain','Access-Control-Allow-Origin':'http://localhost'});
    res.end(new Date().toLocaleString());
};
if(req.url=='/'){
    fs.readFile("./pollingClient.html", "binary", function(err, file) {
        if (!err) {
            res.writeHead(200, {'Content-Type': 'text/html'});
            res.write(file, "binary");
            res.end();
        }
});
}
}).listen(8088,'localhost');
server.on('connection',function(socket){
    console.log("客户端连接已经建立");
});
server.on('close',function(){
    console.log('服务器被关闭');
});

结果如下:
澳门新葡亰网址 4 

使用代理软件(如Burpsuite):

这里必须提到的是,burpsuite可以捕获和记录WebSockets的消息。而ZAP和IronWASP是我所知道的,可以重放websocket请求的软件。

在burpsuite里,我们不能重放websockets消息,但我们还是可以在有限条件下,检测WebSocket握手包是否成功。为了进行测试,我们需要分析websocket的升级请求包:它们会通过http或者https发送,因此可以被重放。

以下截图为burpsuite重放器(Repeat选项卡)的记录,其显示了websocket连接的有效请求和应答情况:

澳门新葡亰网址 5

为了测试这个漏洞,我们需要发送另一个带有重制后的Origin头的请求包。如果我们回应包里有“101
Web Socket Protocol Handshake”的标志,这就表示WebSocket已经建立成功了。

如果连接没有建立成功,那就意味着该应用不存在这个漏洞,因为它会拒绝外部的WebSocket连接。建立成功后,我们就可以开始下一步测试,看看该应用是否有WebSocket跨站劫持漏洞。这里需要说明一下:即使已经建立连接,也需要其如Origin的正常连接一般,确认得到服务端对WebSocket消息的应答之后,才能证明该应用存在漏洞。这是因为开发者可能会同时启用Origin检测与连接权限认证。因此我们的实验中也许会出下面这种情况:建立的连接可以一直保持,但拥有外部来源的Origins不会通过认证。

ZAP可以重放WebSocket消息,但据我了解,它并不能更改Origin头。下面介绍的方法可以给你科普下,如何通过CSWSH(WebSocket跨站劫持)来获得更多的东西。

解决方案3.2:长轮询(long-polling)

在上面的轮询解决方案中,由于每次都要发送一个请求,服务端不管数据是否发生变化都发送数据,请求完成后连接关闭。这中间经过的很多通信是不必要的,于是又出现了长轮询(long-polling)方式。这种方式是客户端发送一个请求到服务器,服务器查看客户端请求的数据是否发生了变化(是否有最新数据),如果发生变化则立即响应返回,否则保持这个连接并定期检查最新数据,直到发生了数据更新或连接超时。同时客户端连接一旦断开,则再次发出请求,这样在相同时间内大大减少了客户端请求服务器的次数。代码如下。(详细技术文章请参见《WEB端即时通讯:HTTP长连接、长轮询(long
polling)详解》)

客户端:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function createXHR(){
        if(typeof XMLHttpRequest !='undefined'){
            return new XMLHttpRequest();
        }else if(typeof ActiveXObject !='undefined' ){
            if(typeof arguments.callee.activeXString!="string"){
                var versions=["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0",
                            "MSXML2.XMLHttp"],
                        i,len;
                for(i=0,len=versions.length;i<len;i++){
                    try{
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString=versions[i];
                        break;
                    }catch(ex) {
 
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        }else{
            throw new Error("no xhr object available");
        }
    }
    function longPolling(url,method,data){
        method=method ||'get';
        data=data || null;
        var xhr=createXHR();
        xhr.onreadystatechange=function(){
            if(xhr.readyState==4){
                if(xhr.status>=200&&xhr.status<300||xhr.status==304){
                    console.log(xhr.responseText);
                }else{
                    console.log("fail");
                }
                longPolling(url,method,data);
            }
        };
        xhr.open(method,url,true);
        xhr.send(data);
    }
    longPolling('http://localhost:8088/time','get');

在XHR对象的readySate为4的时候,表示服务器已经返回数据,本次连接已断开,再次请求服务器建立连接。

服务端代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var http=require('http');
var fs = require("fs");
var server=http.createServer(function(req,res){
    if(req.url=='/time'){
        setInterval(function(){
            sendData(res);
        },20000);
    };
    if(req.url=='/'){
        fs.readFile("./lpc.html", "binary", function(err, file) {
            if (!err) {
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.write(file, "binary");
                res.end();
            }
        });
    }
}).listen(8088,'localhost');
//用随机数模拟数据是否变化
function sendData(res){
    var randomNum=Math.floor(10*Math.random());
    console.log(randomNum);
    if(randomNum>=0&&randomNum<=5){
        res.end(new Date().toLocaleString());
    }
}

在服务端通过生成一个在1到9之间的随机数来模拟判断数据是否发生了变化,当随机数在0到5之间表示数据发生了变化,直接返回,否则保持连接,每隔2秒再检测。

结果如下:
澳门新葡亰网址 6 
可以看到返回的时间是没有规律的,并且单位时间内返回的响应数相比polling方式较少。

使用WebSocket跨站劫持的在线测试工具

打开需要测试的WEB应用登入其中,然后在同一浏览器中开一个新选项卡,访问

如果服务端的回应与前面有效session发送的正常包相同,那就说明该应用可能存在WebSocket跨站劫持漏洞。

澳门新葡亰网址 7

澳门新葡亰网址 8

解决方案3.3:基于http-stream通信

上面的long-polling技术为了保持客户端与服务端的长连接采取的是服务端阻塞(保持响应不返回),客户端轮询的方式,在Comet技术中(详细技术文章请参见《Comet技术详解:基于HTTP长连接的Web端实时通信技术》),还存在一种基于http-stream流的通信方式。其原理是让客户端在一次请求中保持和服务端连接不断开,然后服务端源源不断传送数据给客户端,就好比数据流一样,并不是一次性将数据全部发给客户端。它与polling方式的区别在于整个通信过程客户端只发送一次请求,然后服务端保持与客户端的长连接,并利用这个连接在回送数据给客户端。

这种方案有分为几种不同的数据流传输方式。

使用IronWASP

IronWASP可以做到更多,即使是最基础的检测也能提供自动化脚本检查。

3.3.1 基于XHR对象的streaming方式

这种方式的思想是构造一个XHR对象,通过监听它的onreadystatechange事件,当它的readyState为3的时候,获取它的responseText然后进行处理,readyState为3表示数据传送中,整个通信过程还没有结束,所以它还在不断获取服务端发送过来的数据,直到readyState为4的时候才表示数据发送完毕,一次通信过程结束。在这个过程中,服务端传给客户端的数据是分多次以stream的形式发送给客户端,客户端也是通过stream形式来获取的,所以称作http-streaming数据流方式,代码如下。

客户端代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createStreamClient(url,progress,done){
        //received为接收到数据的计数器
        var xhr=new XMLHttpRequest(),received=0;
        xhr.open("get",url,true);
        xhr.onreadystatechange=function(){
            var result;
            if(xhr.readyState==3){
                //console.log(xhr.responseText);
                result=xhr.responseText.substring(received);
                received+=result.length;
                progress(result);
            }else if(xhr.readyState==4){
                done(xhr.responseText);
            }
        };
        xhr.send(null);
        return xhr;
    }
    var client=createStreamClient("http://localhost:8088/stream",function(data){
        console.log("Received:"+data);
    },function(data){
        console.log("Done,the last data is:"+data);
    })

这里由于客户端收到的数据是分段发过来的,所以最好定义一个游标received,来获取最新数据而舍弃之前已经接收到的数据,通过这个游标每次将接收到的最新数据打印出来,并且在通信结束后打印出整个responseText。

服务端代码:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var http=require('http');
var fs = require("fs");
var count=0;
var server=http.createServer(function(req,res){
    if(req.url=='/stream'){
        res.setHeader('content-type', 'multipart/octet-stream');
        var timer=setInterval(function(){
            sendRandomData(timer,res);
        },2000);
 
    };
    if(req.url=='/'){
        fs.readFile("./xhr-stream.html", "binary", function(err, file) {
            if (!err) {
                res.writeHead(200, {'Content-Type': 'text/html'});
                res.write(file, "binary");
                res.end();
            }
        });
    }
}).listen(8088,'localhost');
function sendRandomData(timer,res){
    var randomNum=Math.floor(10000*Math.random());
    console.log(randomNum);
    if(count++==10){
        clearInterval(timer);
        res.end(randomNum.toString());
    }
        res.write(randomNum.toString());
}

服务端通过计数器count将数据分十次发送,每次生成一个小于10000的随机数发送给客户端让它进行处理。

结果如下:
澳门新葡亰网址 9 
可以看到每次传过来的数据流都进行了处理,同时打印出了整个最终接收到的完整数据。这种方式间接实现了客户端请求,服务端及时推送数据给客户端。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

相关文章

网站地图xml地图