跳转到帖子

CTF&密码学实战复盘:从社会工程邮件取证到弱 JWT 密钥验证与权限边界修复

精选回复

发布于

背景这是一道面向授权靶场的 CTF 组合题:参赛者拿到一封可疑的密码重置邮件、Web 访问日志和一个低权限测试账号,需要还原社会工程入口,判断重置链接是否被篡改,再验证 JWT 签名配置是否会导致越权。重点不在“骗取账号”,而在邮件情报提取、密码学验证、权限边界和日志证据链。以下操作只适用于题目附件、隔离靶场或已有书面授权的测试环境。不要向真实用户发送诱导邮件,也不要复用题目中的令牌或密钥。问题定位题目给出的 eml 文件中,显示文本指向 helpdesk.example,但实际链接带有外部域名;附件 access.log 则记录了重置接口和 API 请求。先把问题拆成三条线:邮件是否伪造、重置令牌如何生成、登录后的 JWT 是否正确校验。邮件线:检查 From、Return-Path、Received、Authentication-Results,以及 HTML 中可见文本与 href 的差异。令牌线:确认重置令牌是否随机、是否一次性、是否绑定用户与有效期,避免只看“像随机字符串”。权限线:确认服务端是否固定 JWT 算法、是否使用足够强的密钥,并在业务层重新校验角色。邮件与日志取证先解析原始邮件,不要只看邮件客户端渲染结果。Python 标准库可以保留头部并提取所有链接;域名应做规范化,重点留意 punycode、用户名段和非默认端口。python3 - <<'PY'
from email import policy
from email.parser import BytesParser
from html.parser import HTMLParser
from urllib.parse import urlsplit

class Links(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag.lower() == 'a':
d = dict(attrs)
if 'href' in d:
u = d['href']
print('href=', u, 'host=', urlsplit(u).hostname)

msg = BytesParser(policy=policy.default).parse(open('reset.eml','rb'))
for h in ('From','Return-Path','Received','Authentication-Results','Message-ID','Date'):
print(h + ':', msg.get(h))
for part in msg.walk():
if part.get_content_type() == 'text/html':
Links().feed(part.get_content())
PYSPF、DKIM、DMARC 的结果只能说明域名授权与对齐情况,不能单独证明邮件内容可信。若 Authentication-Results 显示 spf=pass,但 smtp.mailfrom 与 Header From 不对齐,仍可能是代发域名滥用。还要按 Received 头自下而上还原投递路径,并结合网关日志确认,不应相信攻击者可自行填写的最底层头部。对 Web 日志可先围绕重置接口、令牌摘要和 JWT 的 jti 建立时间线。生产环境不建议记录完整令牌;题目里若出现完整 token,本身也应算一个配置缺陷。grep -E 'POST /auth/reset|GET /reset|/api/admin' access.log | sort
jq -r 'select(.path==

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

这类题建议把“邮件取证—JWT 验证—权限修复”串成一条证据链,而不是只记录最后拿到 flag 的步骤。

邮件侧先保留原始 .eml,不要只看客户端展示的发件人。重点核对最底部可信邮件网关开始的 Received 链、Return-Path、Message-ID,以及 Authentication-Results 中 SPF、DKIM、DMARC 的结果。附件和正文中的链接应先做离线提取、哈希和域名归一化:

sha256sum sample.eml attachment.bin
python3 -m email < sample.eml
exiftool attachment.bin
7z l attachment.bin

如果需要自动提取头部,可直接用标准库,避免邮件客户端折叠字段造成误判:

from email import policy
from email.parser import BytesParser

with open("sample.eml", "rb") as f:
    msg = BytesParser(policy=policy.default).parse(f)

for name in ("From", "Return-Path", "Reply-To", "Message-ID",
             "Authentication-Results", "Received"):
    for value in msg.get_all(name, []):
        print(f"{name}: {value}")

JWT 部分,弱密钥验证应限定在授权环境中离线进行。先解码 header,确认实际算法和 kid,再用贴合题目语境的小字典验证;不要把“能改 payload”误当成“能伪造签名”。服务端修复的关键也不只是换长密钥,而是固定算法、校验签发方和受众,并拒绝由 token 自由选择验签方式:

# 仅对题目提供的 token 和授权字典做离线验证
hashcat -m 16500 jwt.txt candidate-keys.txt
const claims = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ["HS256"],
  issuer: "auth.example.internal",
  audience: "api.example.internal",
  clockTolerance: 5
});

还要单独检查 kid:不能直接拼接文件路径或 SQL 查询,也不要接受 alg=none,HS/RS 混用时尤其要防止把公钥当作 HMAC 密钥。密钥至少使用 CSPRNG 生成并支持轮换,例如 openssl rand -base64 32;泄露过的旧密钥应立即失效,而不是只改配置后继续兼容。

权限边界修复建议落到服务端对象级授权。即使 token 中有 role=admin,访问具体资源时仍应根据当前主体、租户和资源归属重新判定,不能只隐藏前端按钮:

if (req.user.tenantId !== record.tenantId) return res.sendStatus(403);
if (!policy.can(req.user, "read", record)) return res.sendStatus(403);

复盘时可以补三组回归用例:普通用户替换对象 ID 应返回 403;修改 payload 但签名不合法应返回 401;旧密钥签发的 token 在轮换窗口结束后必须失效。这样才能证明修复覆盖了签名校验、对象级授权和密钥生命周期,而不是只封住当时那条利用链。

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

最近浏览 0

  • 没有会员查看此页面。