上海浦东网站建设公司,手机网站 方案,国外网站做调查,免费网站建设编辑器背景
随着Web应用的发展与动态网页的普及#xff0c;越来越多的场景需要数据动态刷新功能。在早期时#xff0c;我们通常使用轮询的方式(即客户端每隔一段时间询问一次服务器)来实现#xff0c;但是这种实现方式缺点很明显: 大量请求实际上是无效的#xff0c;这导致了大量…背景
随着Web应用的发展与动态网页的普及越来越多的场景需要数据动态刷新功能。在早期时我们通常使用轮询的方式(即客户端每隔一段时间询问一次服务器)来实现但是这种实现方式缺点很明显: 大量请求实际上是无效的这导致了大量带宽的浪费。
这时候我们急需一个新的技术来解决这一痛点Websocket应运而生: WebSocket是一种网络传输协议可在单个TCP连接上进行 全双工通信 位于OSI模型的应用层。
Websocket的诞生也给我们带来了新的挑战我们能否对websocket的请求与响应进行劫持与修改呢要想做到这一点我们首先得了解websocket协议。
websocket协议细节
等等看到这个标题的时候先别急着划走实际上websocket协议比我们想象中的要简单他实际上 几乎 等同于原始的TCP socket只不过多出了额外的协议头以及一个升级的过程。
我们先来看websocket的升级过程先是客户端发起协议升级请求其采用标准的HTTP报文格式且必须使用 GET 请求方法:
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw这里我们需要关注的最后四行的特殊请求头: Connection: Upgrade表示要升级协议 Upgrade: websocket表示要升级到websocket协议 Sec-WebSocket-Version: 13表示websocket的版本。如果服务端不支持该版本需要返回一个Sec-WebSocket-Versionheader里面包含服务端支持的版本号 Sec-WebSocket-Key与后面服务端响应头Sec-WebSocket-Accept配套提供基本的校验。其本身是一个 bas64编码过的随机16字节
服务器返回101状态码的响应至此完成协议升级:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU这里我们需要的关注的是最后的Sec-WebSocket-Accept请求头其与前文的Sec-WebSocket-Key对应主要有以下两个目的: 确保服务器理解 WebSocket 协议 防止客户端意外请求 WebSocket 升级
Sec-WebSocket-Accept请求头是由Sec-WebSocket-Key计算而成的其伪代码如下:
toBase64(sha1(Sec-WebSocket-Key 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
协议升级后双方开始使用websocket协议进行通讯。我们来看看websocket的协议细节一个经典的概览图如下:
0 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1--------------------------------------------------------|F|R|R|R| opcode|M| Payload len | Extended payload length ||I|S|S|S| (4) |A| (7) | (16/64) ||N|V|V|V| |S| | (if payload len126/127) || |1|2|3| |K| | |------------------------- - - - - - - - - - - - - - - - | Extended payload length continued, if payload len 127 | - - - - - - - - - - - - - - - -------------------------------| |Masking-key, if MASK set to 1 |--------------------------------------------------------------| Masking-key (continued) | Payload Data |-------------------------------- - - - - - - - - - - - - - - - : Payload Data continued ... : - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Payload Data continued ... |---------------------------------------------------------------如果看不懂无所谓我们逐个字段进行讲解:
FIN 1 bit 如果是1表示这是消息的最后一个分片如果是0表示不是是消息的最后一个分片。 通常为1 。 RSV1, RSV2, RSV3 各占1 bit 一般情况下全为0 。当客户端、服务端协商采用WebSocket扩展时这三个标志位可以非0且值的含义由扩展进行定义。如果出现非零的值且并没有采用WebSocket扩展连接出错。 Opcode : 4 bit 操作代码Opcode的值决定了应该如何解析后续的数据 可以简单地理解为消息类型,一般通讯时为%x1或%x2 。可选值如下: %x0表示一个延续帧。当Opcode为0时表示本次数据传输采用了数据分片当前收到的数据帧为其中一个数据分片 %x1表示这是一个文本帧frame %x2表示这是一个二进制帧frame %x3-7保留的操作代码用于后续定义的非控制帧 %x8表示连接断开 %x9表示这是一个ping操作 %xA表示这是一个pong操作 %xB-F保留的操作代码用于后续定义的控制帧 Mask : 1 bit 表示是否要对数据进行掩码操作。 客户端向服务端发送数据时该bit为1否则为0 。掩码算法在后续Masking key提到。 Payload length : 数据的长度单位是字节。其可能为7/716/164 bit。 假设数据长度 x如果 0x125用这7个bit来代表数据长度。 126x655357个bit设置为126(1111110)。后续2个字节代表一个16位的无符号整数该无符号整数的值为数据的长度(大端序)。 65535x7个bit设置为127(1111111)。后续8个字节代表一个64位的无符号整数该无符号整数的值为数据的长度(大端序)。 Masking-key 0/32 bit 假如前文所述Mask为1则此Masking-key占32 bit(即四个字节)否则为0 bit。Masking- key用于将客户端传输给服务器的数据进行掩码操作。前文的Payload length不包括Masking-key的长度。 具体的掩码算法伪代码如下: 设原数据为bytesMasking-key为key则: for i in range(len(bytes)): bytes[i] ^ key[i3] Payload data (xy) byte 载荷数据包括了扩展数据、应用数据。其中扩展数据x字节应用数据y字节。 在前文的升级阶段没有协商使用扩展的话扩展数据数据为0字节。 剩下的应用数据就是传输的原始socket内容 因此也一般会结合其他压缩算法/协议使用如protobuf。 websocket劫持实现
在了解了websocket协议之后我们实现websocket劫持就变得很简单了用一张流程图来展示: 其中重点主要是原始数据与websocket帧之间的转换。
解析原始数据
前面说过websocket协议实际上几乎只是比原始socket多了一个头那么我们解析原始数据可以分为以下几步: 设初始n2即抛弃前两个websocket头字节 判断第2个byte的后7个bit(payload length)如果为126则n2如果其127则n8 判断第2个byte的第1个bit(mask位)是否为1如果为1则从n~n4位取出masking-key并将n4将n位后的数据进行掩码处理 返回n位后的数据即为原始数据
重新封装成websocket帧
可以分为以下几步: 第1个byte照抄(也可以根据需要修改后4位bit及opcode修改消息类型) 第2个byte第1个bit(mask位)照抄后7位bit根据修改后的数据长度进行处理 如果数据长度大于125则要写入uint16或uint64的数据长度字节(大端序) 如果mask位为1则生成并写入32位的随机masking-key再将数据进行掩码处理与写入此时即封装好了的websocket帧
websocket劫持实现时遇到的坑点
这里讲下在websocket劫持实现时遇到的坑点仅供参考
保持协议的完整性
实际上前文提到的劫持所使用的技术都是中间人技术这里我遇到的坑点就是没保持协议的完整性我在处理时从服务器端接收到了101状态码的响应但却没有将其写入回客户端导致客户端断开整个websocket的升级也就失败了所以需要提醒的就是在劫持时要保持协议的完整性该发送或接收到的内容都要到位。
实现FrameReader而非简单的Read
我之前的一个错误实例如下: 这里实际上犯了几个错误: reader.Read()是非阻塞的也就是说如果缓冲中没有数据的话它会不断地返回0和EOF但是我这里判断如果n0则会不断continue这会导致不断创建新的4096字节的bytes无法释放 后续我将b作为websocket帧来处理但是b的大小只有4096假如数据量超大这样写毫无疑问是错误的
后来其他师傅发现了这个bug并指出这几点错误我才意识到我应该抽象出一个FrameReader来去读取websocket帧根据读取到的前几个字节来判断最终要读取的长度。
新版yak的websocket尝鲜
websocket劫持尝鲜
经过一番努力之后终于实现了websocket劫持功能在yak的mitm标准库中新增了wscallback与wsforcetext两个函数我们来看一个简单的用例:
go fn{mitm.Start(8084, mitm.wsforcetext(true),mitm.wscallback(fn(data, isRequest){if isRequest {data Hijack request} else {data Hijack Response}return data}))
}for {time.sleep(1)
}wscallback参数接受一个函数作为参数该函数拥有2个参数: data([]byte类型)和isRequest(bool类型)并 接收一个返回值(必须存在返回值) 作为修改后的数据。
isRequest参数用于判断劫持到的是否为websocket请求(true即websocket请求false为websocket响应)data参数则为劫持到的原始数据。
接下来我们使用go来启动一个websocket的测试服务器这里需要安装依赖:“github.com/gorilla/websocket”:
package mainimport (fmtnet/httpostimegithub.com/gorilla/websocket
)func main() {var upgrader websocket.Upgrader{}f, err : os.CreateTemp(, test-*.html)if err ! nil {panic(err)}f.Write([]byte(!DOCTYPE html
html
headmeta charsetUTF-8/titleSample of websocket with golang/titlescript srchttp://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js/scriptscript$(function() {var ws new WebSocket(ws:// window.location.host /ws);ws.onmessage function(e) {$(li).text(event.data).appendTo($ul);ws.send({message:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa});};var $ul $(#msg-list);});/script
/head
body
ul idmsg-list/ul
/body
/html))index : http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {http.ServeFile(w, r, f.Name())})http.Handle(/, index)http.Handle(/index.html, index)http.HandleFunc(/ws, func(w http.ResponseWriter, r *http.Request) {// msg : RecvMessage{}ws, err : upgrader.Upgrade(w, r, nil)if err ! nil {panic(err)return}defer ws.Close()go func() {for {_, msg, err : ws.ReadMessage()if err ! nil {panic(err)return}fmt.Printf(server recv from client: %s\n, msg)}}()for {time.Sleep(time.Second)ws.WriteJSON(map[string]interface{}{message: fmt.Sprintf(Golang Websocket Message: %v, time.Now()),})}})err http.ListenAndServe(:8884, nil)if err ! nil {panic(err)}
}现在我们访问http://127.0.0.1:8884会发现屏幕会每秒输出一条json内容例如:
{message:Golang Websocket Message: 2022-09-05 15:17:22.497926 0800 CST m7.689153001}同时在终端中会每秒输出一条以下内容:
server recv from client: {message:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}这时候我们挂上代理http://127.0.0.1:8084/,重启websocket服务器进行访问然后会发现上述的内容都会发生改变屏幕输出的内容变为:
Hijack Response同时终端输出的内容变为:
server recv from client: Hijack request直接发起websocket请求
还是使用上述的websocket的测试服务器作为服务端启动。
yak中编写如下代码运行:
rsp, req, err poc.Websocket(GET /ws HTTP/1.1
Host: 127.0.0.1:8884
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q0.9
Connection: Upgrade
Sec-WebSocket-Key: LIb4UiyphoP4B2y6uoA
Sec-WebSocket-Version: 13
Upgrade: websocket, poc.websocketFromServer(func(data, cancel){dump(data)
}), poc.websocketOnClient(func(wsClient) {go fn {for {wsClient.WriteText({message: hello})time.Sleep(1)}}
}))
die(err)解释一下上述代码poc.Websocket指定了这个请求需要去对websocket请求进行收发处理其实际上是poc.Http(…,poc.websocket(true))的简写。第一个参数是我们熟悉的websocket升级请求后面跟着的是可选参数函数: poc.websocketFromServer这个函数接受一个函数作为参数其中data为从服务端接收到的数据cancel是一个无参数函数用于直接中断websocket连接。 poc.websocketOnClient这个函数接受一个函数作为参数其中wsClient是一个结构体可以直接使用其的一些方法如:
1. c.Stop(),结束websocket连接2. c.Write([]byte),往websocket写入内容3. c.WriteText([]byte),同c.Write([]byte)4. ...通过程序输出可以看到我们正常建立了websocket连接并完成了收发。
新版yakit的websocket劫持尝鲜 Yak版本 1.1.2 Yakit版本 1.1.2 websocket劫持
正常启动yakit的MITM然后也启动上文提到的websocket服务器: 挂载代理访问http://127.0.0.1:8884/出现websocket升级的请求手动放行: 等待websocket协议升级完成后我们成功劫持到了websocket的请求按下劫持响应并修改请求内容最后按下提交数据: 可以看到服务器已经接收到修改过后的请求: 同时我们拦截到了服务器的响应修改响应内容然后按下提交数据:‘ 发现浏览器中显示我们修改过后的响应: websocket fuzzer
在MITM中的HTTP History找到websocket的升级响应按下FUZZ按钮: 跳转到websocket fuzzer页面我们尝试建立连接: 建立websocket连接完成后可以在右侧看到实时的服务器请求与响应: 我们尝试在下方发送数据框发送websocket请求: 可以看到成功发送websocket请求: websocket fuzzer
在MITM中的HTTP History找到websocket的升级响应按下FUZZ按钮:
[外链图片转存中…(img-WKp0XoFM-1676620638602)]
跳转到websocket fuzzer页面我们尝试建立连接:
[外链图片转存中…(img-AfHcTU1R-1676620638602)]
建立websocket连接完成后可以在右侧看到实时的服务器请求与响应:
[外链图片转存中…(img-hTxQJ96B-1676620638603)]
我们尝试在下方发送数据框发送websocket请求:
[外链图片转存中…(img-xogbIIIJ-1676620638603)]
可以看到成功发送websocket请求:
[外链图片转存中…(img-Ky0DLWRZ-1676620638603)]
[外链图片转存中…(img-glbr0Rp5-1676620638603)]
##最后 对于从来没有接触过网络安全的同学我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线大家跟着这个大的方向学习准没问题。 同时每个成长路线对应的板块都有配套的视频提供 当然除了有配套的视频同时也为大家整理了各种文档和书籍资料工具并且已经帮大家分好类了。 因篇幅有限仅展示部分资料有需要的小伙伴可以【扫下方二维码】免费领取