遵循自定义 H111 协议格式走私请求的go-web分析
字数 2574 2025-08-29 08:30:24
自定义H111协议分析与请求走私漏洞研究
1. H111协议概述
H111是一种自定义的HTTP协议变体,用于在代理服务器前后端之间传输HTTP请求和响应。该协议采用二进制格式进行序列化和反序列化,主要特点包括:
- 使用固定长度的字段表示可变长度数据
- 采用大端序(网络字节序)进行数据编码
- 支持连接复用和pipeline处理
- 设计上类似于HTTP但采用二进制格式
2. H111协议格式详解
2.1 请求格式
H111协议的请求报文由以下部分组成,按顺序排列:
| 字段 | 长度 | 描述 |
|---|---|---|
| 方法长度 | 2字节 (uint16) | 方法名称的长度(如GET、POST) |
| 方法名称 | N字节 | 方法名称的实际数据 |
| URI长度 | 2字节 (uint16) | URI的长度 |
| URI | M字节 | URI的实际数据(如/path) |
| 请求头数量 | 2字节 (uint16) | 请求头的总数 |
| 请求头 | 变长 | 每个请求头的键值对 |
| 请求体长度 | 2字节 (uint16) | 请求体的长度 |
| 请求体 | K字节 | 请求体的实际数据 |
2.2 请求头格式
每个请求头由以下部分组成:
| 字段 | 长度 | 描述 |
|---|---|---|
| 键长度 | 2字节 (uint16) | 键名的长度 |
| 键数据 | 变长 | 键名的实际数据 |
| 值长度 | 2字节 (uint16) | 值的长度 |
| 值数据 | 变长 | 值的实际数据 |
2.3 响应格式
H111协议的响应报文格式与请求类似,但包含状态码字段:
| 字段 | 长度 | 描述 |
|---|---|---|
| 状态码 | 2字节 (uint16) | HTTP状态码 |
| 状态文本长度 | 2字节 (uint16) | 状态文本的长度 |
| 状态文本 | N字节 | 状态文本的实际数据 |
| 响应头数量 | 2字节 (uint16) | 响应头的总数 |
| 响应头 | 变长 | 每个响应头的键值对 |
| 响应体长度 | 2字节 (uint16) | 响应体的长度 |
| 响应体 | K字节 | 响应体的实际数据 |
3. 协议实现细节
3.1 序列化过程
H111协议的序列化过程主要涉及以下步骤:
-
写入方法信息:
- 使用
binary.Write写入2字节的方法长度(uint16) - 写入方法名称的字节数据
- 使用
-
写入URI信息:
- 使用
binary.Write写入2字节的URI长度(uint16) - 写入URI的字节数据
- 使用
-
写入请求头:
- 写入2字节的请求头数量(uint16)
- 对于每个请求头:
- 写入2字节的键长度(uint16)
- 写入键数据
- 写入2字节的值长度(uint16)
- 写入值数据
-
写入请求体:
- 写入2字节的请求体长度(uint16)
- 写入请求体数据
3.2 反序列化过程
H111协议的反序列化过程与序列化相反:
-
读取方法信息:
- 使用
binary.Read读取2字节的方法长度(uint16) - 使用
io.ReadFull读取方法名称数据
- 使用
-
读取URI信息:
- 读取2字节的URI长度(uint16)
- 读取URI数据
-
读取请求头:
- 读取2字节的请求头数量(uint16)
- 对于每个请求头:
- 读取2字节的键长度(uint16)
- 读取键数据
- 读取2字节的值长度(uint16)
- 读取值数据
-
读取请求体:
- 读取2字节的请求体长度(uint16)
- 读取请求体数据
4. 安全漏洞分析
4.1 整数溢出漏洞
H111协议存在严重的安全漏洞,主要问题在于:
-
长度字段使用uint16:
- 所有长度字段(方法长度、URI长度、请求头数量、键值长度、请求体长度)都使用uint16类型
- uint16的最大值为65535(0xFFFF)
-
缺乏溢出检查:
- 当实际长度超过65535时会发生整数溢出
- 例如:长度为65536(0x10000)会被截断为0(0x0000)
-
数据截断问题:
- 当长度溢出为0时,协议解析器会读取0字节数据
- 剩余数据会被"搁置",可能被误认为是下一个请求的一部分
4.2 请求走私攻击
利用上述整数溢出漏洞,可以构造请求走私攻击:
-
攻击原理:
- 构造一个GET请求,但包含大量数据(超过65535字节)
- 由于请求体长度溢出为0,代理服务器会认为没有请求体
- 后端服务器可能以不同方式解析,导致请求拆分
-
攻击步骤:
- 发送一个精心构造的GET请求,请求体恰好65536字节
- 前65535字节会被正常处理
- 第65536字节会被认为是下一个请求的开始
- 可以隐藏恶意请求在溢出的数据中
-
实际利用:
// 示例攻击代码(概念验证) func sendMaliciousRequest(conn net.Conn) { // 构造恶意请求 method := "GET" uri := "/" body := make([]byte, 65536) // 刚好触发溢出 // 填充body前部为正常数据 copy(body[:100], []byte("normal data")) // 在body后部隐藏恶意请求 copy(body[65400:], []byte("POST /admin HTTP/1.1\r\n...")) // 序列化请求 binary.Write(conn, binary.BigEndian, uint16(len(method))) conn.Write([]byte(method)) binary.Write(conn, binary.BigEndian, uint16(len(uri))) conn.Write([]byte(uri)) binary.Write(conn, binary.BigEndian, uint16(0)) // 0个header binary.Write(conn, binary.BigEndian, uint16(len(body))) // 会溢出为0 conn.Write(body) }
5. 防御措施
针对H111协议的漏洞,建议采取以下防御措施:
-
长度字段升级:
- 将uint16改为uint32或uint64,支持更大长度
- 或使用可变长度编码(如varint)
-
添加溢出检查:
- 在序列化和反序列化时检查长度是否超过最大值
- 如果超过则返回错误,而不是静默截断
-
严格协议验证:
- 验证方法是否合法(如只允许GET、POST等)
- 验证URI格式是否合法
- 验证请求头数量是否合理
-
连接处理改进:
- 实现更严格的连接复用逻辑
- 添加请求超时机制
- 限制单个连接的最大请求数
-
输入净化:
- 对所有输入数据进行严格验证和净化
- 拒绝任何不符合协议规范的请求
6. 实现示例
以下是H111协议的安全实现示例:
// 安全读取长度字段
func readLength(r io.Reader) (int, error) {
var length uint16
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return 0, err
}
if length > MaxAllowedLength {
return 0, fmt.Errorf("length %d exceeds maximum allowed %d", length, MaxAllowedLength)
}
return int(length), nil
}
// 安全写入长度字段
func writeLength(w io.Writer, length int) error {
if length > MaxAllowedLength {
return fmt.Errorf("length %d exceeds maximum allowed %d", length, MaxAllowedLength)
}
return binary.Write(w, binary.BigEndian, uint16(length))
}
// 安全读取H111请求
func ReadH111Request(r io.Reader) (*H111Request, error) {
req := &H111Request{}
// 读取方法
methodLen, err := readLength(r)
if err != nil {
return nil, fmt.Errorf("read method length: %w", err)
}
method := make([]byte, methodLen)
if _, err := io.ReadFull(r, method); err != nil {
return nil, fmt.Errorf("read method: %w", err)
}
req.Method = string(method)
// 读取URI
uriLen, err := readLength(r)
if err != nil {
return nil, fmt.Errorf("read URI length: %w", err)
}
uri := make([]byte, uriLen)
if _, err := io.ReadFull(r, uri); err != nil {
return nil, fmt.Errorf("read URI: %w", err)
}
req.URI = string(uri)
// 读取请求头
headerCount, err := readLength(r)
if err != nil {
return nil, fmt.Errorf("read header count: %w", err)
}
if headerCount > MaxHeaderCount {
return nil, fmt.Errorf("header count %d exceeds maximum %d", headerCount, MaxHeaderCount)
}
req.Headers = make(map[string]string)
for i := 0; i < headerCount; i++ {
// 读取键
keyLen, err := readLength(r)
if err != nil {
return nil, fmt.Errorf("read header key length: %w", err)
}
key := make([]byte, keyLen)
if _, err := io.ReadFull(r, key); err != nil {
return nil, fmt.Errorf("read header key: %w", err)
}
// 读取值
valueLen, err := readLength(r)
if err != nil {
return nil, fmt.Errorf("read header value length: %w", err)
}
value := make([]byte, valueLen)
if _, err := io.ReadFull(r, value); err != nil {
return nil, fmt.Errorf("read header value: %w", err)
}
req.Headers[string(key)] = string(value)
}
// 读取请求体
bodyLen, err := readLength(r)
if err != nil {
return nil, fmt.Errorf("read body length: %w", err)
}
if bodyLen > 0 {
req.Body = make([]byte, bodyLen)
if _, err := io.ReadFull(r, req.Body); err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
}
return req, nil
}
7. 总结
H111协议作为一种自定义的HTTP协议变体,在追求性能的同时牺牲了安全性。其主要的漏洞源于:
- 使用固定长度的uint16表示所有长度字段
- 缺乏必要的边界检查和溢出处理
- 对异常情况的处理不够健壮
通过分析这个案例,我们可以得到以下安全开发经验:
- 在设计二进制协议时,长度字段的选择要谨慎
- 必须对所有输入进行严格的验证和边界检查
- 整数溢出是常见的安全问题,需要特别注意
- 协议设计要考虑各种异常情况和边界条件
- 性能优化不能以牺牲安全性为代价
对于使用类似协议的开发者,建议立即检查并修复相关实现,添加必要的安全措施,防止请求走私等攻击的发生。