freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Yaklang websocket劫持教程
2022-09-22 15:17:25
所属地 四川省

背景

随着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                   3
  0 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 len==126/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/7+16/1+64 bit。

假设数据长度 = x,如果

  • 0<=x<=125:用这7个bit来代表数据长度。

  • 126<=x<=65535:7个bit设置为126(1111110)。后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度(大端序)。

  • 65535<x:7个bit设置为127(1111111)。后续8个字节代表一个64位的无符号整数,该无符号整数的值为数据的长度(大端序)。

Masking-key:0/32 bit

假如前文所述Mask为1,则此Masking-key占32 bit(即四个字节),否则为0 bit。Masking-key用于将客户端传输给服务器的数据进行掩码操作。前文的Payload length,不包括Masking-key的长度。

具体的掩码算法伪代码如下:

设原数据为bytes,Masking-key为key,则:

for i in range(len(bytes)):

bytes[i] ^= key[i&3]

Payload data:(x+y) byte

载荷数据包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。

在前文的升级阶段没有协商使用扩展的话,扩展数据数据为0字节。剩下的应用数据就是传输的原始socket内容,因此也一般会结合其他压缩算法/协议使用,如protobuf。

websocket劫持实现

在了解了websocket协议之后,我们实现websocket劫持就变得很简单了,用一张流程图来展示:

其中重点主要是原始数据与websocket帧之间的转换。

解析原始数据

前面说过,websocket协议实际上几乎只是比原始socket多了一个头,那么我们解析原始数据可以分为以下几步:

  1. 设初始n=2,即抛弃前两个websocket头字节

  1. 判断第2个byte的后7个bit(payload length),如果为126,则n+2,如果其127,则n+8

  1. 判断第2个byte的第1个bit(mask位)是否为1,如果为1,则从n~n+4位取出masking-key,并将n+4,将n位后的数据进行掩码处理

  1. 返回n位后的数据,即为原始数据

重新封装成websocket帧

可以分为以下几步:

  1. 第1个byte照抄(也可以根据需要修改后4位bit及opcode,修改消息类型)

  1. 第2个byte第1个bit(mask位)照抄,后7位bit根据修改后的数据长度进行处理

  1. 如果数据长度大于125,则要写入uint16或uint64的数据长度字节(大端序)

  1. 如果mask位为1,则生成并写入32位的随机masking-key,再将数据进行掩码处理与写入,此时即封装好了的websocket帧

websocket劫持实现时遇到的坑点

这里讲下在websocket劫持实现时遇到的坑点,仅供参考

保持协议的完整性

实际上前文提到的劫持所使用的技术都是中间人技术,这里我遇到的坑点就是没保持协议的完整性,我在处理时从服务器端接收到了101状态码的响应,但却没有将其写入回客户端,导致客户端断开,整个websocket的升级也就失败了,所以需要提醒的就是在劫持时要保持协议的完整性,该发送或接收到的内容都要到位。

实现FrameReader而非简单的Read

我之前的一个错误实例如下:

这里实际上犯了几个错误:

  1. reader.Read()是非阻塞的,也就是说如果缓冲中没有数据的话,它会不断地返回0和EOF,但是我这里判断如果n<=0则会不断continue,这会导致不断创建新的4096字节的bytes,无法释放

  1. 后续我将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 main

import (
        "fmt"
        "net/http"
        "os"
        "time"

        "github.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>
<head>
    <meta charset="UTF-8"/>
    <title>Sample of websocket with golang</title>
    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script>
        $(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 id="msg-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 m=+7.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;q=0.9
Connection: Upgrade
Sec-WebSocket-Key: LIb4U+i+y+phoP4B2y6uoA==
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升级请求,后面跟着的是可选参数函数:

  1. poc.websocketFromServer,这个函数接受一个函数作为参数,其中data为从服务端接收到的数据,cancel是一个无参数函数,用于直接中断websocket连接。

  1. 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请求:

# 网络安全 # 网络安全技术
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录