照烟照烟
WP

PolarisCTF-2026

ez_login

题目说以 admin 身份登录,即可获得 flag

不管是不是真的,总得先试试吧,而且下面也验证了这句话是对的

1
return render_template('index.html', username=username, flag=FLAG if username == "admin" else None)

在未配置环境变量的情况下,ADMIN_PASS = os.getenv(“ADMIN_PASS”, “admin123”)

值得注意的是,代码里还有 AES 加密,但加密并不应用于首次登录。当用户登录成功时,网站会通过 create_session 函数加密 cookie,从而使得正常登录的用户保持登录态(即无需再次输入密码)并拦截试图篡改 cookie 以绕过密码的行为。想法很好,但关我正常用户什么事(

这道题并没有配置额外的环境变量,所以访问后输入 username = admin, password = admin123 即可

神秘学

鉴定为高中数学

代码中,get_poly 函数先给了一个函数 poly=x3ax2+bxckn ,用 randint 函数对参数赋随机值之后,返回它的一阶导数 poly=3x22ax+b

在最后, poly x 的值都被给出了。虽然没有 poly 的值,但最后的负数项通常表示这个函数是移项得到的,即 poly=0 。于是有

1
2
3
b = deriv1_num % x1
a = (3 * x1 * x1 + b - deriv1_num) // (2 * x1)
c = pow(x1, 3) - a * x1 * x1 + b * x1 - k * n

同时我们发现 k 很小,只有 8 位,允许爆破。

在主函数中, e=inverse(c,phi) c 充当了标准 RSA 中的 d

于是有脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import long_to_bytes
from sympy import isprime

n =
x1 =
deriv1_num =
cipher =

b = deriv1_num % x1
a = (3 * x1 * x1 + b - deriv1_num) // (2 * x1)
for k in range(2**8):
if isprime(k) == False:
continue
try:
c = pow(x1, 3) - a * x1 * x1 + b * x1 - k * n
m = pow(cipher, c, n)
flag = long_to_bytes(m)
print(flag.decode())
except:
pass

truck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for _ in range(10):
A, B, C = bytes.fromhex(input('A > ')), bytes.fromhex(input('B > ')), bytes.fromhex(input('C > '))
ha, hb, hc = _H(A), _H(B), _H(C)
assert ha == hb == hc

D, E, F = bytes.fromhex(input('D > ')), bytes.fromhex(input('E > ')), bytes.fromhex(input('F > '))
hd, he, hf = _H(ha + D), _H(hb + E), _H(hc + F)
assert hd == he == hf

G, H, I = bytes.fromhex(input('G > ')), bytes.fromhex(input('H > ')), bytes.fromhex(input('I > '))
assert _H(hd + G) == _H(he + H) == _H(hf + I)

cur = (A, B, C, D, E, F, G, H, I)
assert len(set(cur)) == 9
assert not any(x in S for x in cur)
S.update(cur)

在容器中,我们每次被要求输入 9 个数据,循环 10 次。在每轮中,输入的数据需要满足:

  • A, B, C 的 md5 相同
  • md5(A)+D, md5(B)+E, md5©+F 的 md5 相同
  • 上一步的 md5 拼接 G, H, I 后 md5 依然相同

并且 90 个原始数据不能相同。

在 github 上搜索 fastcoll,这个软件专门用于生成 md5 碰撞。

对于三个 md5 的碰撞,可以先生成两个,再用其中一个.bin 文件做前缀,继续生成两个:

1
2
fastcoll -p prefix.txt -o msg1 msg2
fastcoll -p msg1 -o msg3 msg4

最后取三个就行了。根据这个思想,用 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
def generate_three_collisions(prefix: bytes) -> list:
"""
给定一个二进制前缀 prefix,生成三个不同的后缀数据块,
使得 md5(prefix + suffix) 全部相等。
返回包含三个 bytes 对象的列表。
"""
with tempfile.NamedTemporaryFile(delete=False) as f:
prefix_file = f.name
f.write(prefix)

out1 = tempfile.NamedTemporaryFile(delete=False).name
out2 = tempfile.NamedTemporaryFile(delete=False).name
subprocess.run(
[FASTCOLL_PATH, "-p", prefix_file, "-o", out1, out2],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

with open(out1, "rb") as f:
data1 = f.read()
with open(out2, "rb") as f:
data2 = f.read()
suffix1 = data1[-128:]
suffix2 = data2[-128:]

out3 = tempfile.NamedTemporaryFile(delete=False).name
out4 = tempfile.NamedTemporaryFile(delete=False).name
subprocess.run(
[FASTCOLL_PATH, "-p", out1, "-o", out3, out4],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

with open(out3, "rb") as f:
data3 = f.read()
with open(out4, "rb") as f:
data4 = f.read()
suffix3 = data3[-128:]
suffix4 = data4[-128:]

result = [
suffix1 + suffix3,
suffix1 + suffix4,
suffix2 + suffix3,
]

for path in [prefix_file, out1, out2, out3, out4]:
os.remove(path)

return result

重复执行 10 轮就能得到所有数据。

sda

四个断言中:

1
2
3
4
z_abs_bound = ((pi - qi) / (3 * (pi + qi))) * (y**2) * (Ai ** (1 / 4))
assert abs(zs[i]) < z_abs_bound
assert xs[i] ** 2 < A**delta
assert y**2 < A**delta
xi2<Aδyi2<Aδ,|zi|<piqi3(pi+qi)y2Ai1/4.
1
assert Bs[i] * xs[i] ** 2 - y**2 * euler_phi(Ai) == zs[i]
Bixi2y2ϕ(Ai)=zi

值得注意的是, Ai 都很小,从而能被质因数分解得到 pi qi ,进而得到 ϕ(Ai)

Pi=ϕ(Ai) ,再设 Xi=xi2 Y=y2 ,就能得到三个等式:

B1X1P1Y=z1B2X2P2Y=z2B3X3P3Y=z3

在这个方程组里,已知量是 Bi Pi ,未知量是 X1,X2,X3,Y 以及误差项 z1,z2,z3 。使用格密码,基向量设为 V=(X1,X2,X3,Y) 。根据上面的等式,我们要让 zi 在目标向量上出现。设目标向量 T=(X1,X2,X3,Y,z1,z2,z3)

根据关系构造未配平的格基:

M=(1000B10001000B20001000B30001P1P2P3)

但根据三条规定了未知量大小的断言, zi 的量级大约是 A5/8 ,而 Xi Y 的量级大约只有 A3/8 。为了让目标向量的长度远小于其它杂向量,我们加入权重系数 W=A1/4 ,得到新格基:

M=(W000B1000W000B2000W000B3000WP1P2P3)

经过 LLL 约简,得到向量 (WX1,WX2,WX3,WY,z1,z2,z3)

进而得到 x12,x22,x32 y2 ,最后 AES 解密即可。

ecc

学了一段时间,来补一下这道题的 wp。

定义的三个函数是椭圆曲线上的加法(包括了相同和相异的点)以及倍加。尝试直接破解,sage 回显“defines a singular curve”,说明 Δ=0 ,这是一条奇异曲线。

调用奇异曲线脚本,得到私钥,进而得到 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
def singular_curve_attack(p, a1, a2, a3, a4, a6, P, Q):
p = p
F = GF(p)
R_x.<x> = F[]
a1 = F(a1);a2 = F(a2);a3 = F(a3);a4 = F(a4);a6 = F(a6)
Px = F(P[0]);Py = F(P[1])
Qx = F(Q[0]);Qy = F(Q[1])
# 利用对 y 的偏导得到 y 关于 x 的显函数
yx = - (a1*x + a3)/F(2)
# 代入对 x 的偏导式子
fx = a1*yx - F(3)*x*x - F(2)*a2*x - a4
# 对 fx 求根,得到可能是奇点的 x 坐标,之后回代进曲线方程验证
roots = fx.roots()
x0, y0 = None, None
# 调用 roots 方法后,返回的是(解,重数)
for root_couple in roots:
x_temp = root_couple[0]
y_temp = - (a1*x_temp + a3)/F(2)
# 一般方程移项后的值,验证 val 是否等于 0
val = y_temp^2 + a1*x_temp*y_temp + a3*y_temp - x_temp^3 - a2*x_temp^2 - a4*x_temp - a6
if val == F(0):
x0, y0 = x_temp, y_temp
print("得到奇点坐标")
break
if x0 is None:
print("未找到奇点")
exit()
# 根据上面的分析,引入切线方程,构建新环。这里的 c 是平移后方程的二次项系数,也是切线方程的常数项
c = F(3)*x0 + a2
R_t.<t> = F[]
ft = t^2 + a1*t - c
roots = ft.roots()
if len(roots) == 2:
print("奇点是结点")
m1 = roots[0][0]
m2 = roots[1][0]
# 计算平移后的坐标
PX = Px - x0;PY = Py - y0
QX = Qx - x0;QY = Qy - y0
# 计算映射
uP = (PY - m1*PX) / (PY - m2*PX)
uQ = (QY - m1*QX) / (QY - m2*QX)
k = discrete_log(uQ, uP)
print(f"得到k = {k}")
elif len(roots) == 1:
print("奇点是尖点")
m = roots[0][0]
# 计算平移后的坐标
PX = Px - x0;PY = Py - y0
QX = Qx - x0;QY = Qy - y0
# 计算映射
tauP = PX / (PY - m*PX)
tauQ = QX / (QY - m*QX)
k = tauQ / tauP
print(f"得到k = {k}")
else:
print("奇点是非分裂结点")
K.<a> = F.extension(2)
ft_ext = ft.change_ring(K)
roots = ft.ext.roots()
m1 = roots[0][0]
m2 = roots[1][0]
PX = K(Px - x0);PY = K(Py - y0)
QX = K(Qx - x0);QY = K(Qy - y0)
uP = (PY - m1*PX) / (PY - m2*PX)
uQ = (QY - m1*QX) / (QY - m2*QX)
k = discrete_log(uQ, uP, ord = p + 1)
print(f"得到k = {k}")