CMCTF 2025

Misc

流量分析 - 1

Challenge

我需要你流量分析😡

首次发起端口扫描的 IP 是?

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

以下是附件链接:

通过网盘分享的文件:抓取流量.pcapng 链接: https://pan.baidu.com/s/1ye0KLzXGqyCYec2kGSlPMw?pwd=CM66 提取码: CM66 -- 来自百度网盘超级会员 v5 的分享

Solution

CMCTF2025-1
192.168.37.3

1
CM{d28ee9d60772acbcd4eca38e1a3c94b8}

流量分析 - 2

Challenge

扫描次数最多的 IP?

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

Solution

CMCTF2025-2

192.168.37.3

1
CM{d28ee9d60772acbcd4eca38e1a3c94b8}

流量分析 - 3

Challenge

扫描次数第二的 IP?

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

Solution

图同上题

192.168.37.1

1
CM{1edaa78b26c43a0cf438b4437f6ceeb3}

流量分析 - 4

Challenge

哪个 IP 用了 AWVS?

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

Solution

IP 数量很少,试一下就出来了

192.168.37.1

1
CM{1edaa78b26c43a0cf438b4437f6ceeb3}

流量分析 - 6

Challenge

有 IP 进行了 WEB 登录爆破😲,提交其 IP?

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

hint: 登录页面为 login.php

Solution

直接搜索字符串 POST /login.php,一条条看发现 192.168.37.87 多次连续出现

1
CM{83779b479698b76581244f6ac8acd8a6}

流量分析 - 7

Challenge

有 IP 进行了 WEB 登录爆破,提交其爆破次数

(将爆破次数比如 55, 进行加密)

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

Solution

CMCTF2025-3

1
ip.src == 192.168.37.87 && http contains "login.php"

筛选导出得到 107 条数据,除去第 1 条 GET 请求,剩下的 106 条全都是

1
CM{f0935e4cd5920aa6c7c996a5ee53a70f}

流量分析 - 8

Challenge

提交攻击者登录成功 admin 用户的 IP 和密码,以 & 连接

(示例:答案为 192.168.92.111&CM666, 将上述内容进行加密)

将答案经过 md5 32 位加密后小写形式放入 CM {} 中

Solution

CMCTF2025-4

192.168.37.200&zhoudi123

1
CM{3ca6dd54928fcfe47289ae62439116dd}

段涵涵学姐最爱的音乐

Challenge

王振宇从学姐闺蜜那边了解到学姐最爱的歌手是 Taylor Swift,猜猜这个音频有什么秘密吧

flag 格式为 CM

Solution

CMCTF2025-5

1
CM{U_Kn0w_TaYLOR}

OSINT

杜浩学姐の朋友圈

Challenge

王阵雨辗转反侧,突然刷到了杜浩学姐朋友圈的一张图,猜猜这是在哪里呢

flag 格式为 CM

例:CM

Solution

发现玻璃反射泄露信息,将图片水平镜像后放大发现这两处关键信息

CMCTF2025-6

首先是右边的 City花园城,搜索发现它改名为招商花园城了,因此后续的搜索中使用关键词招商花园城

然后是左边盒马的广告,说明这附近有盒马的店,并且极大概率就开设在招商花园城

因此搜索盒马 招商花园城

CMCTF2025-7

发现这篇文章盒马鲜生南京新店开业 市民多了新消费地标_腾讯新闻

文中提到的是南京招商花园城

使用手机高德地图搜索南京招商花园城后搜索其附近的地铁站

CMCTF2025-8

1
CM{Nanjing-万寿}

Web

小猿口算签到重生版

Challenge

考验手速和脑速的挑战

Solution

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

def main():
# 定义目标URL
generate_url = "http://27.25.151.40:32926/generate"
verify_url = "http://27.25.151.40:32926/verify"

# 创建一个Session对象,确保所有请求在同一个session内
with requests.Session() as session:
try:
# 发送GET请求获取表达式
response = session.get(generate_url)
response.raise_for_status() # 检查请求是否成功

# 解析返回的JSON数据
data = response.json()
expression = data.get("expression")
if not expression:
print("未获取到有效的表达式")
return

print(f"获取到的表达式: {expression}")

# 去掉等号并计算表达式的值
expression_to_eval = expression.replace("=?", "")
try:
result = eval(expression_to_eval) # 计算表达式结果
except Exception as e:
print(f"表达式计算失败: {e}")
return

print(f"计算结果: {result}")

# 准备POST请求的数据
payload = {
"user_input": str(result)
}

# 发送POST请求提交结果
verify_response = session.post(verify_url, json=payload)
verify_response.raise_for_status() # 检查请求是否成功

# 输出服务器返回的验证结果
verify_data = verify_response.json()
print(f"验证结果: {verify_data}")

except requests.exceptions.RequestException as e:
print(f"网络请求失败: {e}")

if __name__ == "__main__":
main()
1
flag{CAD709DE7E0B803D8BA72A55C4EB8C50}

lottery 签到重生版

Challenge

抽抽抽

flag 格式为 CM {xxxxxx}!

Solution

找了一圈都没线索,所以就试着爆破一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import requests
import time


URL = "http://27.25.151.40:33411/spin"

# 爆破次数上限
MAX_ATTEMPTS = 200

def spin_the_wheel():
"""
发送POST请求到 /spin 接口
"""
try:
# 参照前端代码,发送一个空的POST请求
headers = {'Content-Type': 'application/json'}
response = requests.post(URL, headers=headers, data='{}', timeout=10)

if response.status_code == 200:
return response.json()
else:
print(f"请求失败,状态码: {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"请求发生错误: {e}")
return None

def main():
print(f"[*] 开始爆破,目标URL: {URL}")
print(f"[*] 最大尝试次数: {MAX_ATTEMPTS}")
print("-" * 30)

for i in range(1, MAX_ATTEMPTS + 1):
print(f"[*] 正在进行第 {i} 次尝试...")

result = spin_the_wheel()

if result:
# 打印每次的结果,方便调试
print(f" [+] 收到结果: {result}")

# 检查是否存在 'flagContent' 字段
if 'flagContent' in result:
print("\n" + "=" * 40)
print(result['flagContent'])
print("=" * 40 + "\n")
return

time.sleep(0.1)

if __name__ == "__main__":
main()

结果到了第 178 次就爆出来了

1
flag{B4F8EC958F70E3EE2245F97068D00109}

Reverse

IDA

Challenge

flag 格式为 CM {xxxxxx}!

Solution

CMCTF2025-9

1
CM{W3lc0me_2_R3ver5e_h@v3_fun!}

Xor

Challenge

flag 格式为 CM {xxxxxx}!

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 提取 flag 数组内容
flag = [
0x5F, 0x55, 0x58, 0x5E, 0x42, 0x61, 0x09, 0x6B, 0x66, 0x08, 0x4A, 0x66,
0x0F, 0x79, 0x4A, 0x08, 0x5A, 0x66, 0x5F, 0x09, 0x4B, 0x66, 0x6B, 0x0A,
0x4F, 0x5C, 0x4B, 0x0C, 0x5C, 0x18, 0x44
]

# 异或密钥
key = 57

# 逆向计算原始输入字符串
original_input = ''.join([chr(byte ^ key) for byte in flag])

# 输出结果
print("原始输入字符串为:", original_input)
1
CM{X0R_1s_6@s1c_f0r_R3ver5e!}

maze

Challenge

点击。。。?就。。。?送。。。?诶这些 01 仿佛组成了一条路

Solution

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
import sys

# 增加递归深度限制,以防迷宫过大导致栈溢出
sys.setrecursionlimit(2000)

# 从C++代码中复制的地图数据
MAP_STRING = "$11111111100111111111010000111001011011101101101110000110111111110011111111011111111101111111110000#"
WIDTH = 10
HEIGHT = len(MAP_STRING) // WIDTH

def solve_maze():
"""
解析并解决迷宫问题
"""
# 1. 将一维字符串地图转换为二维列表
maze = []
for i in range(HEIGHT):
row = list(MAP_STRING[i * WIDTH : (i + 1) * WIDTH])
maze.append(row)

print("--- Maze Layout ---")
for row in maze:
print("".join(row))
print("--------------------")

# 起点是 (x=0, y=0)
start_pos = (0, 0)

# 2. 定义深度优先搜索 (DFS) 函数
# path: 记录移动指令 (e.g., "SSDDW...")
# visited: 记录访问过的坐标 (x, y),防止走回头路或无限循环
def find_path(x, y, path, visited):
# --- 检查边界条件和失败条件 ---

# 检查是否越界
if not (0 <= x < WIDTH and 0 <= y < HEIGHT):
return None

# 检查是否撞墙 ('1')
if maze[y][x] == '1':
return None

# 检查是否访问过 (防止循环)
if (x, y) in visited:
return None

# --- 检查胜利条件 ---

# 如果当前位置是终点
if maze[y][x] == '#':
# 并且路径长度正好是28
if len(path) == 28:
print(f"[*] Path found with length {len(path)}!")
return path
else:
# 找到了终点,但路径长度不对,这条路是错的
return None

# 如果路径已经超过28步,没必要继续了
if len(path) > 28:
return None

# --- 递归探索 ---

# 标记当前点为已访问
new_visited = visited.copy()
new_visited.add((x, y))

# 尝试四个方向: S(下), D(右), A(左), W(上)
# 这个顺序可以随便定,但会影响找到的第一条解

# Move Down (S)
solution = find_path(x, y + 1, path + 'S', new_visited)
if solution: return solution

# Move Right (D)
solution = find_path(x + 1, y, path + 'D', new_visited)
if solution: return solution

# Move Left (A)
solution = find_path(x - 1, y, path + 'A', new_visited)
if solution: return solution

# Move Up (W)
solution = find_path(x, y - 1, path + 'W', new_visited)
if solution: return solution

# 所有方向都走不通
return None

# 3. 从起点开始搜索
print("[*] Searching for a path of length 28...")
# 初始路径为空,访问过的集合只包含起点
solution_path = find_path(start_pos[0], start_pos[1], "", set())

# 4. 输出结果
if solution_path:
print("\n[+] Success! Found the correct input:")
print(solution_path)
else:
print("\n[-] Failed to find a valid path of length 28.")


if __name__ == "__main__":
solve_maze()
1
CM{SDSSASSDDDWWWDDDSSSSASSSDDDD}

sw1f7’s TEA

Challenge

相传 sw1f7 学姐喜欢做甜点,我猜她应该也喜欢泡茶,只有她认可的人才能喝到茶

flag 格式为 CM {xxxxxx}!

Solution

把 checkdebug 给 nop 掉,然后在第 19 行下断点

CMCTF2025-10

动调拿到密文

1
2
3
4
.data:0000000000404020 flag            dd 5B5C5F08h, 2766AE05h, 8C4D477Dh, 554F7F8Dh, 0E20BD674h
.data:0000000000404020 ; DATA XREF: sub_114514(void)+1D↑w
.data:0000000000404020 ; sub_114514(void)+57↑o ...
.data:0000000000404034 dd 0BE678AAh, 0F44B5224h, 0CA619F04h

然后 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
101
import struct

def decrypt(v, k):
"""
这是提供的 encrypt 函数的逆函数。
它将一个 8 字节的数据块 v (拆分为 v0, v1) 进行 32 轮解密。

参数:
v (tuple): 一个包含2个32位无符号整数的元组 (v0, v1)。
k (tuple): 一个包含4个32位无符号整数的元组,代表密钥 (k0, k1, k2, k3)。

返回:
tuple: 解密后的 (v0, v1) 元组。
"""
# 将元组解包到单独的变量中以便操作
v0, v1 = v
k0, k1, k2, k3 = k

# 这是C代码中的魔数: 1640531527 == 0x61C88647
delta = 1640531527

# ---- 逆向 sum 的计算 ----
# 在加密函数中,sum 的初始值为 0,然后循环 32 次 sum -= delta。
# 所以,在解密开始时,sum 的值应该是加密结束时的值,即 0 - (32 * delta)。
# 使用 & 0xFFFFFFFF 是为了模拟32位无符号整数的环绕溢出行为。
current_sum = (0 - (32 * delta)) & 0xFFFFFFFF

# ---- 解密循环 ----
# 加密是从 0 到 31 轮,解密则需要逆向这个过程。
for i in range(32):
# 1. 逆向 v1 的更新操作 (必须先做这一步)
# 原始公式: v1 += (v0 + sum) ^ (k[2] + 16 * v0) ^ ((v0 >> 5) + k[3]);
# 逆向公式: v1 -= (v0 + sum) ^ (k[2] + 16 * v0) ^ ((v0 >> 5) + k[3]);
# 注意: v0 << 4 等价于 16 * v0
term_v1 = (((v0 + current_sum) & 0xFFFFFFFF) ^ (k2 + (v0 << 4)) ^ (((v0 >> 5) & 0xFFFFFFFF) + k3)) & 0xFFFFFFFF
v1 = (v1 - term_v1) & 0xFFFFFFFF

# 2. 逆向 v0 的更新操作 (后做这一步)
# 原始公式: v0 += (v1 + sum) ^ (*k + 16 * v1) ^ ((v1 >> 5) + k[1]);
# 逆向公式: v0 -= (v1 + sum) ^ (*k + 16 * v1) ^ ((v1 >> 5) + k[1]);
term_v0 = (((v1 + current_sum) & 0xFFFFFFFF) ^ (k0 + (v1 << 4)) ^ (((v1 >> 5) & 0xFFFFFFFF) + k1)) & 0xFFFFFFFF
v0 = (v0 - term_v0) & 0xFFFFFFFF

# 3. 逆向 sum 的更新操作
# 加密时 sum -= delta,所以解密时 sum += delta
current_sum = (current_sum + delta) & 0xFFFFFFFF

return (v0, v1)

def solve():
"""
主求解函数,整合所有信息并执行解密。
"""
# 1. 从 main 函数中提取的密钥
# key[0] = 36; key[1] = 66; key[2] = 82; key[3] = 118;
key = (36, 66, 82, 118)
print(f"[*] 使用密钥: {key}")

# 2. 从 IDA .data 段中获取的加密后的 flag 数据 (8个32位整数)
encrypted_flag_words = [
0x5B5C5F08, 0x2766AE05, 0x8C4D477D, 0x554F7F8D,
0xE20BD674, 0x0BE678AA, 0xF44B5224, 0xCA619F04
]

# 3. 将32位整数列表转换为小端序(Little-Endian)的字节串,以匹配内存布局
# '<L' 表示小端序的无符号长整型 (4字节)
encrypted_flag_bytes = b''.join([struct.pack('<L', word) for word in encrypted_flag_words])
print(f"[*] 待解密的密文 (hex): {encrypted_flag_bytes.hex()}")

decrypted_result = b''

# 4. 将32字节的密文分成4个8字节的块,并对每个块进行解密
print("\n[+] 开始解密...")
num_blocks = len(encrypted_flag_bytes) // 8
for i in range(num_blocks):
# 提取当前块
block_start = i * 8
block_end = block_start + 8
encrypted_block = encrypted_flag_bytes[block_start:block_end]

# 将8字节块解包成两个32位无符号整数 (v0, v1)
# '<II' 表示两个小端序的无符号整型 (4字节 + 4字节)
v = struct.unpack('<II', encrypted_block)

# 调用解密函数
decrypted_v = decrypt(v, key)

# 将解密后的 (v0, v1) 打包回8字节的块
decrypted_block = struct.pack('<II', decrypted_v[0], decrypted_v[1])
decrypted_result += decrypted_block
print(f" - 块 {i+1} 解密完成,得到: {decrypted_block.decode('ascii', errors='ignore')}")

# 5. 打印最终的、完整的解密结果
# 使用 .decode('ascii') 将最终的字节串转换为人类可读的字符串
# .strip('\x00') 用于移除末尾可能存在的空字符填充
final_flag = decrypted_result.decode('ascii').strip('\x00')
print(f"\n{final_flag}")

# 当脚本被直接运行时,调用 solve() 函数
if __name__ == "__main__":
solve()

得到输出:

1
2
3
4
5
6
7
8
9
10
[*] 使用密钥: (36, 66, 82, 118)
[*] 待解密的密文 (hex): 085f5c5b05ae66277d474d8c8d7f4f5574d60be2aa78e60b24524bf4049f61ca

[+] 开始解密...
- 块 1 解密完成,得到: flag{sw1
- 块 2 解密完成,得到: f7's_Tea
- 块 3 解密完成,得到: _is_clas
- 块 4 解密完成,得到: sical!!}

flag{sw1f7's_Tea_is_classical!!}
1
CM{sw1f7's_Tea_is_classical!!}

sw1f7’s XXTEA

Challenge

sw1f7 学姐的茶被喝了,这次她决定泡一壶更浓厚的茶,在走之前放了个盖子

flag 格式为 CM {xxxxxx}!

Solution

AI 一把梭了

加密算法是 XXTEA

密文 (Ciphertext): 存储在 flag 地址的数据。

1
2
.data:0000000000403020 flag dd 19EA7A62h, 5BE6801h, 0D2AD8A17h, 1A1456A1h
.data:0000000000403030 dd 843B635Bh, 0E2369508h, 0BF552654h, 0FC87047Ch

这些是 32 位的 DWORD(双字),在小端序(Little-Endian)的 x86 架构中,内存中的字节序是反的。不过,当我们将它们作为 uint32_t 数组处理时,数值就是这些。
ciphertext = [0x19EA7A62, 0x05BE6801, 0xD2AD8A17, 0x1A1456A1, 0x843B635B, 0xE2369508, 0xBF552654, 0xFC87047C]

密钥 (Key):key =

参数:n = 8,rounds = 12

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
import struct

def decrypt(v, n, key):
"""
XXTEA解密函数
v: uint32_t 整数列表,代表密文数据
n: 数据块的数量
key: uint32_t 整数列表,代表密钥
"""
# 定义算法常量和参数
ROUNDS = 52 // n + 6
DELTA = 0x9E3779B9

# 解密的初始sum值
sum_val = (ROUNDS * DELTA) & 0xFFFFFFFF

# y用于传播上一个解密后的块,初始值为v[0]
y = v[0]

# 解密轮循环
for _ in range(ROUNDS):
e = (sum_val >> 2) & 3

# 内部循环,从p=n-1递减到1
# y持有v[p+1]解密后的值,z持有v[p-1]加密时的值
for p in range(n - 1, 0, -1):
z = v[p - 1]
term1 = (y ^ sum_val)
term2 = (z ^ key[(p & 3) ^ e])
term3 = ((y << 2) ^ (z >> 5)) & 0xFFFFFFFF
term4 = ((y >> 3) ^ (z << 4)) & 0xFFFFFFFF

# 逆运算:减法
v[p] = (v[p] - ((term1 + term2) ^ (term3 + term4))) & 0xFFFFFFFF
y = v[p]

# 处理第0个块(循环边界情况)
z = v[n - 1]
term1 = (y ^ sum_val)
term2 = (z ^ key[(0 & 3) ^ e])
term3 = ((y << 2) ^ (z >> 5)) & 0xFFFFFFFF
term4 = ((y >> 3) ^ (z << 4)) & 0xFFFFFFFF
v[0] = (v[0] - ((term1 + term2) ^ (term3 + term4))) & 0xFFFFFFFF
y = v[0]

# 更新sum
sum_val = (sum_val - DELTA) & 0xFFFFFFFF

return v

def main():
# 从.data段提取的密文数据 (8个DWORD)
ciphertext = [
0x19EA7A62, 0x05BE6801, 0xD2AD8A17, 0x1A1456A1,
0x843B635B, 0xE2369508, 0xBF552654, 0xFC87047C
]

# 程序中硬编码的密钥
key = [36, 66, 82, 118]

# 块数量
n = len(ciphertext)

# 执行解密
decrypted_data = decrypt(ciphertext, n, key)

print(f"解密后的整数数组: { [hex(x) for x in decrypted_data] }")

# 将解密后的uint32_t数组转换回字节串
# '<' 表示小端序, 'I' 表示32位无符号整数
plaintext_bytes = b''
for dword in decrypted_data:
plaintext_bytes += struct.pack('<I', dword)

# 打印最终结果
try:
flag = plaintext_bytes.decode('utf-8')
print(f"\n[+] 成功找到Flag: {flag}")
except UnicodeDecodeError:
print(f"\n[-] 解码失败,原始字节: {plaintext_bytes}")


if __name__ == '__main__':
main()

得到输出:

1
2
3
解密后的整数数组: ['0x67616c66', '0x3177737b', '0x73273766', '0x5458585f', '0x695f6165', '0x6f6d5f73', '0x79666964', '0x7d676e69']

[+] 成功找到Flag: flag{sw1f7's_XXTea_is_modifying}
1
CM{sw1f7's_XXTea_is_modifying}

Mobile

base_android

Challenge

flag 格式为 CM {xxxxxxx}!

Solution

逆向发现程序从 assets 文件夹中读取 timg_2.zip 文件的内容,然后将这些内容一字不差地写入到 /data/data/com.example.test.ctf02/databases/img.jpg 文件中

因此手动将 assets/timg_2.zip 提取出来,然后把.zip 后缀改为.jpg

CMCTF2025-11

1
CM{08067-wlecome}