遵循自定义 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协议的序列化过程主要涉及以下步骤:

  1. 写入方法信息

    • 使用binary.Write写入2字节的方法长度(uint16)
    • 写入方法名称的字节数据
  2. 写入URI信息

    • 使用binary.Write写入2字节的URI长度(uint16)
    • 写入URI的字节数据
  3. 写入请求头

    • 写入2字节的请求头数量(uint16)
    • 对于每个请求头:
      • 写入2字节的键长度(uint16)
      • 写入键数据
      • 写入2字节的值长度(uint16)
      • 写入值数据
  4. 写入请求体

    • 写入2字节的请求体长度(uint16)
    • 写入请求体数据

3.2 反序列化过程

H111协议的反序列化过程与序列化相反:

  1. 读取方法信息

    • 使用binary.Read读取2字节的方法长度(uint16)
    • 使用io.ReadFull读取方法名称数据
  2. 读取URI信息

    • 读取2字节的URI长度(uint16)
    • 读取URI数据
  3. 读取请求头

    • 读取2字节的请求头数量(uint16)
    • 对于每个请求头:
      • 读取2字节的键长度(uint16)
      • 读取键数据
      • 读取2字节的值长度(uint16)
      • 读取值数据
  4. 读取请求体

    • 读取2字节的请求体长度(uint16)
    • 读取请求体数据

4. 安全漏洞分析

4.1 整数溢出漏洞

H111协议存在严重的安全漏洞,主要问题在于:

  1. 长度字段使用uint16

    • 所有长度字段(方法长度、URI长度、请求头数量、键值长度、请求体长度)都使用uint16类型
    • uint16的最大值为65535(0xFFFF)
  2. 缺乏溢出检查

    • 当实际长度超过65535时会发生整数溢出
    • 例如:长度为65536(0x10000)会被截断为0(0x0000)
  3. 数据截断问题

    • 当长度溢出为0时,协议解析器会读取0字节数据
    • 剩余数据会被"搁置",可能被误认为是下一个请求的一部分

4.2 请求走私攻击

利用上述整数溢出漏洞,可以构造请求走私攻击:

  1. 攻击原理

    • 构造一个GET请求,但包含大量数据(超过65535字节)
    • 由于请求体长度溢出为0,代理服务器会认为没有请求体
    • 后端服务器可能以不同方式解析,导致请求拆分
  2. 攻击步骤

    • 发送一个精心构造的GET请求,请求体恰好65536字节
    • 前65535字节会被正常处理
    • 第65536字节会被认为是下一个请求的开始
    • 可以隐藏恶意请求在溢出的数据中
  3. 实际利用

    // 示例攻击代码(概念验证)
    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协议的漏洞,建议采取以下防御措施:

  1. 长度字段升级

    • 将uint16改为uint32或uint64,支持更大长度
    • 或使用可变长度编码(如varint)
  2. 添加溢出检查

    • 在序列化和反序列化时检查长度是否超过最大值
    • 如果超过则返回错误,而不是静默截断
  3. 严格协议验证

    • 验证方法是否合法(如只允许GET、POST等)
    • 验证URI格式是否合法
    • 验证请求头数量是否合理
  4. 连接处理改进

    • 实现更严格的连接复用逻辑
    • 添加请求超时机制
    • 限制单个连接的最大请求数
  5. 输入净化

    • 对所有输入数据进行严格验证和净化
    • 拒绝任何不符合协议规范的请求

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协议变体,在追求性能的同时牺牲了安全性。其主要的漏洞源于:

  1. 使用固定长度的uint16表示所有长度字段
  2. 缺乏必要的边界检查和溢出处理
  3. 对异常情况的处理不够健壮

通过分析这个案例,我们可以得到以下安全开发经验:

  1. 在设计二进制协议时,长度字段的选择要谨慎
  2. 必须对所有输入进行严格的验证和边界检查
  3. 整数溢出是常见的安全问题,需要特别注意
  4. 协议设计要考虑各种异常情况和边界条件
  5. 性能优化不能以牺牲安全性为代价

对于使用类似协议的开发者,建议立即检查并修复相关实现,添加必要的安全措施,防止请求走私等攻击的发生。

自定义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字节会被认为是下一个请求的开始 可以隐藏恶意请求在溢出的数据中 实际利用 : 5. 防御措施 针对H111协议的漏洞,建议采取以下防御措施: 长度字段升级 : 将uint16改为uint32或uint64,支持更大长度 或使用可变长度编码(如varint) 添加溢出检查 : 在序列化和反序列化时检查长度是否超过最大值 如果超过则返回错误,而不是静默截断 严格协议验证 : 验证方法是否合法(如只允许GET、POST等) 验证URI格式是否合法 验证请求头数量是否合理 连接处理改进 : 实现更严格的连接复用逻辑 添加请求超时机制 限制单个连接的最大请求数 输入净化 : 对所有输入数据进行严格验证和净化 拒绝任何不符合协议规范的请求 6. 实现示例 以下是H111协议的安全实现示例: 7. 总结 H111协议作为一种自定义的HTTP协议变体,在追求性能的同时牺牲了安全性。其主要的漏洞源于: 使用固定长度的uint16表示所有长度字段 缺乏必要的边界检查和溢出处理 对异常情况的处理不够健壮 通过分析这个案例,我们可以得到以下安全开发经验: 在设计二进制协议时,长度字段的选择要谨慎 必须对所有输入进行严格的验证和边界检查 整数溢出是常见的安全问题,需要特别注意 协议设计要考虑各种异常情况和边界条件 性能优化不能以牺牲安全性为代价 对于使用类似协议的开发者,建议立即检查并修复相关实现,添加必要的安全措施,防止请求走私等攻击的发生。