跳转到帖子

CTF 密码学实战:JWT 算法混淆的授权验证、权限边界与日志取证

精选回复

发布于

背景JWT 算法混淆是 CTF Web/密码学交叉题里常见的一类问题,现实中也会出现在自研鉴权网关、旧版 JWT 组件或错误封装的验签逻辑中。典型场景是服务端原本使用 RS256:私钥签名,公钥验签;但验证端直接信任令牌头部的 alg,并允许同一份公钥材料被当作 HS256 的 HMAC 密钥。攻击者一旦能获取公开的 RSA 公钥,就可能构造通过验签的高权限令牌。以下步骤只适用于 CTF、本地靶场或已取得书面授权的测试环境。验证目标应使用专用测试账号,不应把生成的令牌投向未授权系统。问题定位先确认问题是否真在密码算法,而不是普通的越权或会话复用。建议按以下顺序排查:拆分 JWT 的 header、payload、signature 三段,检查 header 中的 alg、kid、typ。确认服务端预期算法。RS256 与 HS256 名称相近,但密钥模型完全不同。检查验签 API 是否由令牌自行选择算法,或者是否显式固定 algorithms。确认密钥来源:静态 PEM、JWKS、数据库配置,还是由 kid 动态拼接文件路径。用低权限测试账号只修改一个无副作用声明,例如自建靶场中的 role,并观察返回码、业务权限与审计日志是否同步变化。JWT 的 header 和 payload 只是 Base64URL 编码,不是加密。离线解码不会验证签名,也不代表令牌可信。python3 - <<'PY'
import base64, json

token = '把测试环境JWT放在这里'
parts = token.split('.')
if len(parts) != 3:
raise SystemExit('不是三段式 JWT')

def dec(s):
s += '=' * (-len(s) % 4)
return json.loads(base64.urlsafe_b64decode(s))

print('header =', json.dumps(dec(parts[0]), ensure_ascii=False, indent=2))
print('payload =', json.dumps(dec(parts[1]), ensure_ascii=False, indent=2))
print('signature_bytes =', len(base64.urlsafe_b64decode(parts[2] + '=' * (-len(parts[2]) % 4))))
PY技术分析安全的 RS256 验签应由服务端配置决定算法,并只把 RSA 公钥交给 RSA 验签器。危险实现则读取 header.alg,再把同一个 key 参数传给通用 verify 函数。若组件允许 HS256,攻击者可把公开公钥字节当作 HMAC secret。此时服务端和攻击者计算出的 HMAC 一致,权限声明便可能被接受。需要注意,较新的 JWT 库通常会拒绝把 PEM 格式的非对称密钥用于 HMAC,因此“改 alg 就成功”并不是可靠结论。测试时应记录库名、版本、密钥实际字节格式和服务端允许的算法列表。DER 字节、去掉 PEM 包装后的内容以及完整 PEM 文本并不等价。检查点危险表现安全表现算法来源直接采用 header.alg服务端按 issuer 或路由固定 RS256允许列表HS256、RS256 混用仅允许业务实际使用的算法密钥类型同一 key 对象交给多种算法RSA 公钥只进入 RSA 验签流程权限声明直接信任 role、is_admin以 sub 查询服务端权限,关键操作二次校验失败日志只记 401记录 alg、kid、iss、sub、jti 与失败原因授权环境中的验证步骤建议先搭建本地副本或测试命名空间,不要直接在生产验证。验证目标不是扩大权限,而是证明“算法选择可被令牌控制”这一根因。获取一个专用低权限账号签发的正常令牌,保存原始请求、响应和时间。导出服务端用于验签的公开密钥,并确认其来源确实允许公开获取,例如标准 JWKS。在本地复刻验证依赖及版本,分别执行正常 RS256 验签、篡改 payload 后验签、算法切换后的负向测试。测试环境仅把声明改为预先创建的 canary_role,并访问无副作用的 /api/me 或专用验证接口。若返回结果显示 canary_role 生效,同时日志中的验签算法为 HS256,即可形成证据链;随后立即撤销测试令牌。下面的脚本用于离线检查 JWT 头部与关键声明,并拒绝 none、HS/RS 混用及超出预期的 issuer、audience。它不会生成伪造令牌,适合放进验证流程或 CI 回归测试。#!/usr/bin/env python3
import json, sys, base64
from datetime import datetime, timezone

EXPECTED_ALG = 'RS256'
EXPECTED_ISS = ' /> EXPECTED_AUD = 'ctf-lab-api'

def b64json(part):
part += '=' * (-len(part) % 4)
return json.loads(base64.urlsafe_b64decode(part))

token = sys.argv[1]
h, p, s = token.split('.')
header, payload = b64json(h), b64json(p)
errors = []

if header.get('alg') != EXPECTED_ALG:
errors.append(f\"unexpected alg: {header.get('alg')!r}\")
if header.get('alg', '').lower() == 'none':
errors.append('unsigned token is forbidden')
if payload.get('iss') != EXPECTED_ISS:
errors.append('issuer mismatch')
aud = payload.get('aud')
if aud != EXPECTED_AUD and not (isinstance(aud, list) and EXPECTED_AUD in aud):
errors.append('audience mismatch')
now = int(datetime.now(timezone.utc).timestamp())
if not isinstance(payload.get('exp'), int) or payload['exp'] <= now:
errors.append('token expired or exp missing')

print(json.dumps({'header': header, 'payload': payload, 'errors': errors}, ensure_ascii=False, indent=2))
sys.exit(1 if errors else 0)
该脚本只做结构与策略检查,不能替代密码学验签。服务端仍需使用成熟库验证签名、时间和声明。以 Python/PyJWT 为例,关键是固定 algorithms,并显式验证 issuer 与 audience:import jwt

claims = jwt.decode(
token,
rsa_public_key,
algorithms=['RS256'],
issuer=' /> audience='ctf-lab-api',
options={
'require': ['exp', 'iat', 'iss', 'aud', 'sub'],
'verify_signature': True,
},
)
不要使用 unverified header 的 alg 直接填入 algorithms,也不要为了兼容旧客户端同时放行 HS256 与 RS256。若业务确需多种签名算法,应按可信 issuer、固定租户配置或独立验证端点分流,不能由未验签的令牌头部决定。日志留痕与取证鉴权日志应能回答“谁在什么时间、用什么算法和密钥标识访问了什么资源、结果如何”。建议至少记录请求时间、源地址、路由、HTTP 方法、issuer、subject、audience、jti、alg、kid、验签结果和拒绝原因。不要记录完整 JWT,因为 payload 可能含个人信息,签名令牌本身也可被重放。可记录令牌的不可逆摘要用于关联同一令牌的访问轨迹:printf '%s' \"$JWT\" | sha256sum

# 例:筛查算法异常、kid 异常和短时间内连续验签失败
jq -r 'select(.event==\"jwt_verify\") |
[.time,.src_ip,.sub,.alg,.kid,.result,.reason,.token_sha256] | @tsv' auth.jsonl |
awk -F '\t' '$4 != \"RS256\" || $6 == \"deny\"'如果发现疑似利用,应同时检查同一 token_sha256、sub、jti 和源地址后续访问过的管理接口,并结合网关日志、应用审计日志和数据库变更日志建立时间线。仅看到一个 200 响应不足以证明权限已经实际生效,还要核对资源读取或状态变更。风险与修复建议在服务端硬编码算法允许列表;RS256 场景只允许 RS256,不接受 none 或 HS256。按算法区分密钥对象和代码路径,避免通用 key 参数同时兼容对称与非对称算法。校验 iss、aud、exp、nbf、iat,并对高风险操作校验 jti 或短时会话状态。kid 只能在预定义键集合中查找,不得拼接本地文件路径或任意 URL;JWKS 拉取应限制可信主机、超时、响应大小和缓存策略。不要只依赖令牌内的 role、is_admin。关键权限应根据 sub 在服务端查询,或使用短期令牌并配合权限版本号。升级 JWT 依赖后增加回归用例:alg=none、RS256 攏为 HS256、未知 kid、错误 issuer、错误 audience、过期令牌都必须返回 401 或 403。修复后轮换签名密钥,撤销可能受影响的会话,并保留旧 kid 的拒绝记录以观察重放尝试。总结JWT 算法混淆的核心不是 Base64 解码,也不是简单修改 role,而是验签端把“不可信的算法声明”和“可跨算法复用的密钥材料”组合在了一起。CTF 中应通过本地复刻、负向用例和最小权限 canary 接口验证;真实授权测试中则要同时关注权限是否落地、日志是否可追溯以及修复后是否完成密钥轮换。最终验收标准应是:算法固定、密钥类型隔离、声明完整校验、关键权限服务端确认,且异常令牌能在日志中被准确关联。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意
安全事件影响分析示意图
安全动态、影响面和处置建议示意

这类题建议把“验签通过”和“授权通过”拆开看。JWT 算法混淆的关键通常不是单纯修改 alg,而是服务端是否信任令牌头部来选择算法、是否把 RSA 公钥误当作 HMAC 密钥,以及验签后是否直接信任 role/scope。

复现时可以按三条路径分别验证:

  • alg=none:确认库是否明确拒绝无签名令牌,而不是只看签名字段是否为空。
  • RS256 -> HS256:检查 verifier 是否允许多个算法共用同一份 key material。安全实现应同时绑定“固定算法 + 固定密钥类型”。
  • 合法低权限令牌篡改声明:即使签名无法绕过,也要检查后端是否仅凭 JWT 内的 role=admin 做授权,还是会结合服务端权限数据和资源归属重新判断。

以 Node.js 的 jose 为例,验证端应固定算法、发行方和受众,不要根据 header.alg 动态切换密钥:

import { jwtVerify, importSPKI } from 'jose';

const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY, 'RS256');
const { payload, protectedHeader } = await jwtVerify(token, publicKey, {
  algorithms: ['RS256'],
  issuer: '
  audience: 'ctf-api'
});

if (protectedHeader.alg !== 'RS256') throw new Error('unexpected alg');
if (payload.exp * 1000 <= Date.now()) throw new Error('expired');

// 授权不要只信任 token 中可变的角色字段
const allowed = await acl.can(payload.sub, 'read', resourceId);
if (!allowed) throw new Error('forbidden');

排查代码时优先搜索“从 header 选算法/密钥”的位置,以及把解码当验证的调用:

grep -RInE "jwt\.(decode|verify)|algorithms|header\.alg|jwk|kid|role|scope" src/

日志取证方面,不建议记录完整 JWT,避免令牌二次泄露。至少记录请求时间、请求 ID、主体 sub、iss、aud、声明的 alg/kid、验签结果、拒绝原因、目标资源和最终授权结果;令牌本身只保留 SHA-256 指纹。这样能把同一枚异常令牌在不同接口上的尝试串起来。

token_fp=$(printf '%s' "$JWT" | sha256sum | cut -d' ' -f1)
jq -r 'select(.event=="jwt_verify" and
  (.reason=="alg_not_allowed" or .reason=="key_type_mismatch")) |
  [.ts,.src_ip,.request_id,.alg,.kid,.token_fp,.path] | @tsv' app.jsonl

如果题目同时考权限边界,还应专门对比 401 与 403:签名无效、过期、发行方不符属于认证失败;令牌有效但访问越权资源属于授权失败。两类事件分开记录,才能判断是算法混淆尝试,还是拿到正常低权限令牌后的横向/垂直越权。

最后修复后应加回归用例:拒绝 none、拒绝 HS/RS 互换、拒绝未知 kid、拒绝错误 iss/aud、拒绝过期令牌,并验证普通用户即使携带伪造的管理员声明也无法跨越服务端 ACL。

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

最近浏览 0

  • 没有会员查看此页面。