web1

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 requests
import time
import hashlib
import io
from PIL import Image

# --- 全局配置 ---
HOST = "http://challenge.sky233.top:32923"

# --- 挑战1: Cookie持久化请求 ---
def solve_challenge1(session):
"""使用同一个会话发送100次请求以获取Flag。"""
print("--- 开始挑战 1: Cookie持久化请求 ---")
url = f'{HOST}/api/challenge1'
try:
for i in range(100):
response = session.get(url)
response.raise_for_status()
if (i + 1) % 10 == 0:
print(f" 已发送 {i+1}/100 次请求...")

flag = response.json().get('flag', 'Flag not found')
print(f"[+] 挑战 1 成功! Flag: {flag}\n")
return flag
except requests.RequestException as e:
print(f"[-] 挑战 1 失败: {e}\n")
return None

# --- 挑战2: 时间戳Token验证 ---
def solve_challenge2(session):
"""根据每分钟变化的时间戳生成Token并提交。"""
print("--- 开始挑战 2: 时间戳Token验证 ---")
url = f'{HOST}/api/challenge2'
try:
minute_timestamp = int(time.time()) // 60
token = hashlib.md5(str(minute_timestamp).encode('utf-8')).hexdigest()

print(f" 使用分钟时间戳 {minute_timestamp} 生成的 Token: {token}")

payload = {"token": token}
response = session.post(url, json=payload)
response.raise_for_status()

flag = response.json().get('flag', 'Flag not found')
print(f"[+] 挑战 2 成功! Flag: {flag}\n")
return flag
except requests.RequestException as e:
print(f"[-] 挑战 2 失败: {e}\n")
return None

# --- 挑战3: 重定向链处理 ---
def solve_challenge3(session):
"""自动跟随JSON响应中的'next'参数,直到找到Flag。"""
print("--- 开始挑战 3: 重定向链处理 ---")
base_url = f"{HOST}/api/challenge3"
next_step = "step1"

while next_step:
try:
params = {"next": next_step}
response = session.get(base_url, params=params)
response.raise_for_status()
data = response.json()

print(f" 访问 next='{next_step}', 响应: {data}")

if data.get('success'):
flag = data.get('flag', 'Flag not found')
print(f"[+] 挑战 3 成功! Flag: {flag}\n")
return flag

next_step = data.get('next')
if not next_step:
print("[-] 响应中未找到 'next' 参数,链条中断。\n")
return None
time.sleep(0.1)

except requests.RequestException as e:
print(f"[-] 挑战 3 请求失败: {e}\n")
return None
except ValueError:
print(f"[-] 挑战 3 失败: 无法解析JSON响应。\n")
return None

# --- 挑战4: 文件上传验证 ---
def solve_challenge4(session):
"""创建一个包含特定内容的文件并上传。"""
print("--- 开始挑战 4: 文件上传验证 ---")
url = f'{HOST}/api/challenge4'
content = "CTF_FILE_UPLOAD_SUCCESS"

try:
with io.BytesIO(content.encode('utf-8')) as file_in_memory:
files = {'file': ('challenge.txt', file_in_memory, 'text/plain')}
print(f" 正在上传包含 '{content}' 的文件...")
response = session.post(url, files=files)
response.raise_for_status()

data = response.json()
if data.get('success'):
flag = data.get('flag', 'Flag not found')
print(f"[+] 挑战 4 成功! Flag: {flag}\n")
return flag
else:
print(f"[-] 文件上传失败: {data.get('error', '未知错误')}\n")
return None

except requests.RequestException as e:
print(f"[-] challenge 4 失败: {e}\n")
return None

# --- 挑战5: 人工识别验证码 ---
def solve_challenge5_manual(session):
"""下载验证码图片,由用户手动输入识别结果。"""
print("--- 开始挑战 5: 人工识别验证码 ---")
captcha_url = f'{HOST}/api/captcha'
challenge_url = f'{HOST}/api/challenge5'

try:
# 1. 获取并展示验证码图片
print(" 正在下载验证码图片...")
response_img = session.get(captcha_url)
response_img.raise_for_status()

img = Image.open(io.BytesIO(response_img.content))
print(" 验证码图片已打开,请查看图片并输入内容。")
img.show() # 这会调用系统默认的图片查看器打开图片

# 2. 获取用户输入
captcha_text = input(" 请输入您看到的验证码: ").strip()

if not captcha_text:
print("[-] 未输入任何内容,跳过挑战 5。\n")
return None

# 3. 提交识别结果
print(f" 正在提交您的输入: '{captcha_text}'")
payload = {"captcha": captcha_text}
response = session.post(challenge_url, json=payload)
response.raise_for_status()

data = response.json()
# 检查服务器返回的成功状态
if data.get('success'):
flag = data.get('flag', 'Flag not found')
print(f"[+] 挑战 5 成功! Flag: {flag}\n")
return flag
else:
error_message = data.get('error', '验证失败,但未提供原因。')
print(f"[-] 挑战 5 失败: {error_message}\n")
return None

except requests.RequestException as e:
print(f"[-] 挑战 5 失败: {e}\n")
return None
except Exception as e:
print(f"[-] 挑战 5 发生未知错误: {e}\n")
return None

# --- 主执行函数 ---
if __name__ == "__main__":
with requests.Session() as s:
s.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})

flag1 = solve_challenge1(s)
flag2 = solve_challenge2(s)
flag3 = solve_challenge3(s)
flag4 = solve_challenge4(s)
flag5 = solve_challenge5_manual(s)

print("="*30)
print("所有挑战已完成,结果如下:")
print(f"Challenge 1 Flag: {flag1}")
print(f"Challenge 2 Flag: {flag2}")
print(f"Challenge 3 Flag: {flag3}")
print(f"Challenge 4 Flag: {flag4}")
print(f"Challenge 5 Flag: {flag5}")
print("="*30)

image-20250830211546123

神秘的图片

image-20250830220049796

image-20250830220539746

对照出来如下,/为未知的

1
inZgc/tzEBrwY/LDNMQEcmJB

加上图片的名字base32解码,脚本跑盲猜

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
import base64
import string

def decode_with_unknowns(encoded_str):
# Base32编码中允许的字符 (A-Z, 2-7)
base32_chars = string.ascii_uppercase + '234567'

# 找到未知字符的位置
unknown_positions = [i for i, c in enumerate(encoded_str) if c == '/']

if len(unknown_positions) != 2:
print(f"错误:需要正好2个未知字符,实际找到{len(unknown_positions)}个")
return

# 尝试所有可能的字符组合
count = 0
valid_results = []

for c1 in base32_chars:
for c2 in base32_chars:
# 替换未知字符
temp = list(encoded_str)
temp[unknown_positions[0]] = c1
temp[unknown_positions[1]] = c2
candidate = ''.join(temp)

try:
# 尝试解码Base32
decoded = base64.b32decode(candidate)
# 尝试将解码结果转换为字符串
decoded_str = decoded.decode('utf-8', errors='ignore')
# 简单过滤掉大部分无意义的结果
if len(decoded_str.strip()) > 0 and any(c.isprintable() for c in decoded_str):
valid_results.append((c1, c2, decoded_str))
print(f"找到可能结果: {c1}, {c2} -> {decoded_str[:50]}...")
except:
pass

count += 1
if count % 100 == 0:
print(f"已尝试{count}/{len(base32_chars)**2}种组合")

print(f"\n总共找到{len(valid_results)}个可能的有效结果")
return valid_results

# 待解码的字符串
encoded_string = "inZgc/tzEBrwY/LDNMQEcmJB5C7ZTZUYV7SLRKXIUGUONIF44WM2ELROF3SL3IHIV62OJOEN5CB33ZF3UV4GY43Y42EZHZN4QA76TAVDFYXC4==="

# 注意:Base32通常使用大写字母,这里先转换为大写
encoded_string = encoded_string.upper()

# 执行解码
results = decode_with_unknowns(encoded_string)

# 打印前几个结果
if results:
print("\n前5个可能的结果:")
for i, (c1, c2, result) in enumerate(results[:5]):
print(f"{i+1}. 替换字符: {c1}{c2} -> 结果: {result}")

image-20250830220154744

010打开,里面有段字符

image-20250830220852202

修改后缀xlsm打开

image-20250830220916790

把key填上image-20250830221001042

双击A1

image-20250830221047623

在表格中发现了一个链接和一个图片,LSB隐写

image-20250830221218832

image-20250830222659828

壁纸

左上角

image-20250830223825710

左上角第一个像素为26,17

竖方向每个点宽距为35

横方向每个点宽距为53

(这宽距竟然有误差)

竖方向共有29个,横方向共有29个

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
from PIL import Image, ImageDraw

def find_best_pixel_on_line(pixels, start_x, start_y, end_x, end_y):
"""
在一条直线(水平或垂直)上搜索最接近黑色或白色的像素点。
返回该点的精确坐标。
"""
min_dist_sq = float('inf')
best_coord = (start_x, start_y) # 默认使用起始点

# 确定迭代范围
x_range = range(min(start_x, end_x), max(start_x, end_x) + 1)
y_range = range(min(start_y, end_y), max(start_y, end_y) + 1)

for x in x_range:
for y in y_range:
try:
r, g, b = pixels[x, y][:3]

# 计算到纯黑和纯白的颜色距离平方
dist_to_black_sq = r**2 + g**2 + b**2
dist_to_white_sq = (255 - r)**2 + (255 - g)**2 + (255 - b)**2
current_min_dist_sq = min(dist_to_black_sq, dist_to_white_sq)

if current_min_dist_sq < min_dist_sq:
min_dist_sq = current_min_dist_sq
best_coord = (x, y)
except IndexError:
continue

return best_coord

def extract_calibrated_and_amplify(input_image_path, output_image_path):
"""
校准行列位置,提取像素点,并放大合成新图片。
"""
# --- 基础参数 ---
start_x, start_y = 26, 17
h_spacing, v_spacing = 53, 35
num_points = 29

# --- 可调整参数 ---
# 校准时搜索的范围(半径)
CALIBRATION_SEARCH_RADIUS = 15
# 新图片中每个点放大后的方块尺寸(像素)
BLOCK_SIZE = 10
# 新图片边缘的留白(像素)
PADDING = 20

try:
original_image = Image.open(input_image_path).convert('RGB')
original_pixels = original_image.load()
print(f"成功加载原始图片: {input_image_path}")
except FileNotFoundError:
print(f"错误:找不到文件 '{input_image_path}'。")
return
except Exception as e:
print(f"打开图片时发生错误: {e}")
return

# --- 步骤1: 网格校准 ---
calibrated_x_coords = [0] * num_points
calibrated_y_coords = [0] * num_points

print("开始校准网格位置...")
# 校准每一行的Y坐标
for j in range(num_points):
theoretical_y = start_y + j * v_spacing
# 在第一个点的理论位置附近进行垂直搜索
best_coord = find_best_pixel_on_line(
original_pixels,
start_x, theoretical_y - CALIBRATION_SEARCH_RADIUS,
start_x, theoretical_y + CALIBRATION_SEARCH_RADIUS
)
calibrated_y_coords[j] = best_coord[1] # 保存找到的精确y坐标
print(f" 行 {j+1}/{num_points} 校准: 理论 Y={theoretical_y}, 实际 Y={calibrated_y_coords[j]}")

# 校准每一列的X坐标
for i in range(num_points):
theoretical_x = start_x + i * h_spacing
# 在第一个点的理论位置附近进行水平搜索
best_coord = find_best_pixel_on_line(
original_pixels,
theoretical_x - CALIBRATION_SEARCH_RADIUS, start_y,
theoretical_x + CALIBRATION_SEARCH_RADIUS, start_y
)
calibrated_x_coords[i] = best_coord[0] # 保存找到的精确x坐标
print(f" 列 {i+1}/{num_points} 校准: 理论 X={theoretical_x}, 实际 X={calibrated_x_coords[i]}")

print("网格校准完成!")

# --- 步骤2: 图像合成与放大 ---
# 计算新图片的尺寸
output_size = num_points * BLOCK_SIZE + 2 * PADDING
# 创建一个白色背景的新图片
new_image = Image.new('RGB', (output_size, output_size), 'white')
draw = ImageDraw.Draw(new_image)

print("开始提取像素并合成新图片...")
# 遍历29x29网格
for j in range(num_points):
for i in range(num_points):
# 使用校准后的精确坐标
extract_x = calibrated_x_coords[i]
extract_y = calibrated_y_coords[j]

# 获取像素颜色并判断是黑是白
r, g, b = original_pixels[extract_x, extract_y]
dist_to_black_sq = r**2 + g**2 + b**2
dist_to_white_sq = (255 - r)**2 + (255 - g)**2 + (255 - b)**2

final_color = "black" if dist_to_black_sq < dist_to_white_sq else "white"

# 在新图片上画一个放大的色块
# 计算色块的左上角和右下角坐标
top_left_x = PADDING + i * BLOCK_SIZE
top_left_y = PADDING + j * BLOCK_SIZE
bottom_right_x = top_left_x + BLOCK_SIZE
bottom_right_y = top_left_y + BLOCK_SIZE

draw.rectangle(
[ (top_left_x, top_left_y), (bottom_right_x, bottom_right_y) ],
fill=final_color,
outline=None # 不需要边框
)

try:
new_image.save(output_image_path)
print(f"提取与合成完成!放大后的图片已保存为: {output_image_path}")
except Exception as e:
print(f"保存新图片时发生错误: {e}")


# --- 使用示例 ---
if __name__ == "__main__":
# 1. 将 'your_original_image.png' 替换成您的图片文件名
input_file = "wallpaper.png"

# 2. 这是最终生成的放大后的图片文件
output_file = "final_amplified_image.png"

extract_calibrated_and_amplify(input_file, output_file)

image-20250831103930683

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
行 1/29 校准: 理论 Y=17, 实际 Y=17
行 2/29 校准: 理论 Y=52, 实际 Y=52
行 3/29 校准: 理论 Y=87, 实际 Y=88
行 4/29 校准: 理论 Y=122, 实际 Y=123
行 5/29 校准: 理论 Y=157, 实际 Y=158
行 6/29 校准: 理论 Y=192, 实际 Y=194
行 7/29 校准: 理论 Y=227, 实际 Y=229
行 8/29 校准: 理论 Y=262, 实际 Y=264
行 9/29 校准: 理论 Y=297, 实际 Y=300
行 10/29 校准: 理论 Y=332, 实际 Y=335
行 11/29 校准: 理论 Y=367, 实际 Y=370
行 12/29 校准: 理论 Y=402, 实际 Y=406
行 13/29 校准: 理论 Y=437, 实际 Y=441
行 14/29 校准: 理论 Y=472, 实际 Y=476
行 15/29 校准: 理论 Y=507, 实际 Y=512
行 16/29 校准: 理论 Y=542, 实际 Y=547
行 17/29 校准: 理论 Y=577, 实际 Y=582
行 18/29 校准: 理论 Y=612, 实际 Y=617
行 19/29 校准: 理论 Y=647, 实际 Y=653
行 20/29 校准: 理论 Y=682, 实际 Y=688
行 21/29 校准: 理论 Y=717, 实际 Y=723
行 22/29 校准: 理论 Y=752, 实际 Y=759
行 23/29 校准: 理论 Y=787, 实际 Y=794
行 24/29 校准: 理论 Y=822, 实际 Y=829
行 25/29 校准: 理论 Y=857, 实际 Y=865
行 26/29 校准: 理论 Y=892, 实际 Y=900
行 27/29 校准: 理论 Y=927, 实际 Y=935
行 28/29 校准: 理论 Y=962, 实际 Y=971
行 29/29 校准: 理论 Y=997, 实际 Y=1006
列 1/29 校准: 理论 X=26, 实际 X=26
列 2/29 校准: 理论 X=79, 实际 X=79
列 3/29 校准: 理论 X=132, 实际 X=132
列 4/29 校准: 理论 X=185, 实际 X=185
列 5/29 校准: 理论 X=238, 实际 X=238
列 6/29 校准: 理论 X=291, 实际 X=291
列 7/29 校准: 理论 X=344, 实际 X=344
列 8/29 校准: 理论 X=397, 实际 X=397
列 9/29 校准: 理论 X=450, 实际 X=450
列 10/29 校准: 理论 X=503, 实际 X=503
列 11/29 校准: 理论 X=556, 实际 X=556
列 12/29 校准: 理论 X=609, 实际 X=609
列 13/29 校准: 理论 X=662, 实际 X=662
列 14/29 校准: 理论 X=715, 实际 X=715
列 15/29 校准: 理论 X=768, 实际 X=768
列 16/29 校准: 理论 X=821, 实际 X=820
列 17/29 校准: 理论 X=874, 实际 X=873
列 18/29 校准: 理论 X=927, 实际 X=926
列 19/29 校准: 理论 X=980, 实际 X=979
列 20/29 校准: 理论 X=1033, 实际 X=1032
列 21/29 校准: 理论 X=1086, 实际 X=1085
列 22/29 校准: 理论 X=1139, 实际 X=1138
列 23/29 校准: 理论 X=1192, 实际 X=1191
列 24/29 校准: 理论 X=1245, 实际 X=1244
列 25/29 校准: 理论 X=1298, 实际 X=1297
列 26/29 校准: 理论 X=1351, 实际 X=1350
列 27/29 校准: 理论 X=1404, 实际 X=1403
列 28/29 校准: 理论 X=1457, 实际 X=1456
列 29/29 校准: 理论 X=1510, 实际 X=1509
对照点看着都对呀,就是扫不出来码o(╥﹏╥)o