发布于10月10日10月10日 本文以一个隔离的企业内网 CTF 场景为例:测试人员在获得书面授权后,使用预先创建的测试账号参与社工演练,随后验证应用是否存在 JWT 签名算法约束不足、密钥管理不当或权限边界失效问题。重点不在制作钓鱼页面,而在于如何把社工环节产生的“低权限凭据线索”与密码学验证、访问控制、日志留痕串成一条可复核的测试链路。 一、测试背景与边界 目标系统为内网工单平台,测试域名为 ctf-lab.example.test,身份服务向浏览器签发 JWT。测试范围仅包括以下内容: 使用甲方提供的测试账号和模拟社工页面,不采集真实员工密码、个人信息或第三方凭据。 只验证测试账号能否越过应用权限边界,不访问真实工单附件、生产数据库或其他租户数据。 所有请求使用固定测试网段发起,并提前记录源 IP、时间窗口和测试账号。 禁止对真实邮件系统进行批量投递,禁止密码喷洒、撞库和持久化操作。 该场景对应常见 CTF 题型:题目先通过公开的组织信息或模拟社工线索获得普通账号,再要求参赛者判断令牌结构、验证签名和权限控制是否可靠。企业授权测试时,应将每一步限制在可回滚的测试对象内。 二、问题定位:先确认是认证问题还是授权问题 发现测试账号可以访问管理接口时,不要直接把结论写成“JWT 被破解”。应先区分三类问题: 认证问题:服务端没有正确验证令牌签名、过期时间、签发方或受众。 授权问题:令牌签名有效,但服务端只信任客户端提交的 role、tenant_id 等声明,没有在服务端重新查询权限。 密码学与密钥管理问题:使用过短的 HMAC 密钥、允许客户端影响算法选择,或多个环境共用同一签名密钥。 在授权测试中,先用普通测试账号访问其本应有权限的接口,再访问一个明确属于管理员或其他租户的资源。每次只改变一个变量,并保存请求时间、响应状态码、响应体摘要和服务端日志关联 ID。 三、令牌结构与签名校验分析 JWT 通常由 Header、Payload 和 Signature 三段组成。Header 中的 alg 只能作为输入的一部分,不能由客户端决定服务端最终采用哪种验证方式。Payload 中的 role、is_admin 和 tenant_id 也不能被视为天然可信,可信度来自服务端使用受保护密钥完成验证,并结合服务端权限数据进行授权。 在隔离环境中,可以只对测试令牌做结构检查,不把令牌上传到第三方网站: python3 - <<'PY' import base64, json, os, sys def decode_part(part): part += '=' * (-len(part) % 4) return json.loads(base64.urlsafe_b64decode(part.encode())) token = os.environ.get('TEST_JWT', '') parts = token.split('.') if len(parts) != 3: raise SystemExit('not a compact JWT') header = decode_part(parts[0]) payload = decode_part(parts[1]) print('header:', json.dumps(header, ensure_ascii=False)) print('claims:', json.dumps({k: payload.get(k) for k in ('iss','aud','sub','exp','iat','role','tenant_id')}, ensure_ascii=False)) if header.get('alg') not in ('RS256', 'ES256'): print('review: algorithm is outside the approved allowlist') if not payload.get('exp'): print('review: exp claim is missing') PY 上述代码只解析测试令牌,不执行签名伪造,也不改变服务器状态。正式验证时,应让服务端或独立验证工具使用明确的公钥完成验签,并检查 iss、aud、exp、nbf 和 iat。不能仅依赖 Base64 解码结果判断令牌可信。 四、一个典型的配置缺陷 审计应用配置时,重点检查是否存在类似以下逻辑: # 不推荐:算法和密钥类型由令牌 Header 间接决定 algorithm = jwt.get_unverified_header(token).get("alg") key = config.get("jwt_hmac_secret") if algorithm == "HS256" else config.get("jwt_public_key") claims = decode(token, key, algorithms=[algorithm]) 这类写法的问题不只是“允许了某个算法”,而是把不可信输入带入了密钥选择流程。即使当前没有直接形成可利用路径,也会造成审计困难,并增加算法混淆和配置漂移风险。 推荐使用固定算法、固定密钥来源和固定声明校验。以 Python 服务为例,验证逻辑可以写成: import jwt ALLOWED_ALGORITHMS = ["RS256"] ISSUER = " AUDIENCE = "ticket-api" # PUBLIC_KEY 应从受保护的密钥配置或 JWKS 缓存获得,不能来自请求参数 claims = jwt.decode( token, PUBLIC_KEY, algorithms=ALLOWED_ALGORITHMS, issuer=ISSUER, audience=AUDIENCE, options={ "require": ["exp", "iat", "iss", "aud", "sub"] } ) # 角色只用于缩小权限范围;高风险操作仍应查询服务端权限表 user_id = claims["sub"] if not permission_store.can(user_id, resource_id, "ticket:read"): raise PermissionError("forbidden") 如果业务必须使用 HS256,应使用随机生成的高强度密钥、按环境隔离,并通过密钥管理系统注入。不要把密钥写入前端代码、Git 仓库、容器镜像或异常堆栈。 五、从社工演练线索到权限边界验证 社工环节只负责模拟“用户可能泄露低权限凭据”这一前提,后续验证应严格限制在测试账号。建议按以下顺序执行: 登录测试账号,记录身份服务返回的 sub、iss、aud、exp 和角色信息。 访问普通用户允许的工单接口,确认基线状态码和审计日志。 访问一个预先创建的管理员专属测试工单,确认服务端返回 403,而不是仅依赖前端隐藏按钮。 在不修改签名的前提下,使用本地调试工具检查修改 role 或 tenant_id 后服务端是否仍然拒绝请求。该步骤只针对隔离环境和测试资源。 检查令牌过期后是否仍能访问接口,检查注销或密钥轮换后旧令牌是否按设计失效。 如果普通账号能够读取其他租户数据,即使 JWT 验签完全正确,也应按越权或多租户隔离缺陷处理,不能归类为单纯的密码学问题。 六、日志留痕与排查方法 认证和授权日志至少应包含请求时间、源 IP、用户标识、令牌签发方、受众、请求路径、结果状态、拒绝原因、关联 ID 和服务版本。不要记录完整 JWT、密码或 Cookie;如需关联令牌,可记录其哈希值或 jti。 以 Nginx、应用日志和身份服务日志集中到同一时间源为例,可以先按测试账号和时间窗口筛选: # 按测试账号、测试网段和敏感路径筛选,避免直接输出完整 Authorization 头 zgrep -hE 'sub=test-user-01|10\.20\.30\.|/admin|/api/tenant' /var/log/app/*.log \ | grep -vE 'Authorization:|access_token=|refresh_token=' \ | sort -k1,2 # 检查同一关联 ID 是否出现“认证成功后立即越权访问” awk '$0 ~ /auth_success|authorization_denied|tenant_mismatch/ {print}' /var/log/app/security.log \ | grep 'request_id=' \ | sort 重点观察以下信号: 同一测试账号在短时间内从多个不合理网段登录。 认证成功后连续访问多个租户或管理员路径,并出现大量 403。 令牌中的 iss、aud 与当前服务不匹配,但请求仍被接受。 大量使用过期令牌、重复的 jti 或密钥轮换后仍接受旧签发密钥。 应用日志中出现完整 Authorization 头、密码字段或社工演练提交内容。 七、修复与复测清单 服务端固定允许算法,例如仅允许 RS256 或经过评估的 ES256,拒绝客户端指定算法。 签名密钥使用 KMS、Vault 或云密钥服务管理,按环境、租户和用途隔离,并建立轮换和吊销流程。 严格校验 iss、aud、exp、nbf、iat,并设置合理时钟偏差窗口。 高风险操作采用服务端权限查询、短时效令牌和必要的二次认证,不把管理员权限完全放在可变声明中。 社工演练使用一次性测试账号、模拟数据和独立域名;演练结束后立即禁用账号、撤销令牌并清理收集内容。 对认证成功、授权拒绝、跨租户访问和密钥轮换事件建立告警,并确保日志不可被普通应用账号修改。 复测时应至少覆盖:篡改未签名字段、使用不在白名单内的算法、过期令牌、错误 iss/aud、跨租户资源、旧密钥令牌和注销后的会话。每项都应得到明确的拒绝结果,并在日志中留下可关联但不泄露敏感材料的记录。 总结 CTF 中的社工线索通常只是进入认证链路的起点,真正决定风险等级的是密码学验证是否可靠、权限边界是否由服务端 enforced,以及异常行为能否被日志还原。授权测试应把“测试账号—令牌验证—资源授权—日志证据—修复复测”串成闭环,避免把 Base64 解码、角色字段显示或一次 403 响应误判为完整安全结论。 服务器、云资产与权限边界梳理示意
10月10日10月10日 这个题目建议把“社工线索”和“JWT 权限边界”拆开验证,不要混在一起做结论。授权演练里常见的问题不是 JWT 本身被破解,而是社工拿到的弱线索帮助你定位了可复用凭据、泄露密钥或权限模型缺陷。 可以按下面几个点排查和复现: 1. **先确认 JWT 的信任边界** 重点看服务端是否只依赖前端传来的 `role`、`isAdmin`、`scope` 这类字段。抓包后把 token 解码,对比普通用户和高权限用户 payload: python3 - <<'PY' import jwt tokens = [ "普通用户JWT", "高权限JWT" ] for t in tokens: print(jwt.get_unverified_header(t)) print(jwt.decode(t, options={"verify_signature": False})) PY 如果只是 payload 中多了类似: { "uid": 1001, "role": "admin", "scope": ["user:read", "user:delete"] } 那就继续验证服务端是否真正校验签名和权限来源,而不是只读字段。 2. **检查常见 JWT 实现缺陷** 可以重点测这几类: - `alg: none` 是否被接受; - HS256 / RS256 混淆; - 弱密钥可爆破; - `kid` 参数是否存在路径读取、SQL 注入或远程 JWK 加载问题; - token 是否缺少 `exp`、`aud`、`iss` 校验。 例如先看 header: echo 'JWT第一段' | base64 -d 2>/dev/null | jq 如果看到: { "alg": "HS256", "typ": "JWT", "kid": "prod-key" } 就要检查服务端取 key 的逻辑。`kid` 最好只允许白名单映射,不能直接拼路径或拼 SQL。 3. **弱密钥验证要留证据但不要扩大化** CTF 和靶场里经常把密钥放成公司名、域名、项目名、员工生日、Git 泄露里的变量名。授权环境下可以用小字典验证: hashcat -m 16500 jwt.txt wordlist.txt --show 如果跑出密钥,复现时只构造一个最低必要权限变更的 token,比如把 `role:user` 改成 `role:admin` 后访问一个无破坏性的只读管理接口: GET /api/admin/profile Authorization: Bearer forged.jwt.here 不要直接打删除、导出、修改类接口,报告里用截图和响应码说明边界失效即可。 4. **社工线索部分建议只作为攻击路径证据** 比如通过公开简历、GitHub、群聊昵称、工单截图推测到: - 内部系统域名; - 默认账号命名规则; - 项目代号; - JWT secret 命名习惯; - 公开仓库中的 `.env`、CI 日志、接口文档。 这部分不要写成“社工成功所以系统有问题”,而是写成“公开信息降低了密钥/账号猜测成本,最终触发 JWT 权限绕过”。这样结论更稳。 5. **修复建议要落到代码和配置** 后端不要信任 token 里的高权限字段,权限应从服务端查询或缓存中读取。JWT 只放身份标识和必要声明。 Node.js 示例: const jwt = require("jsonwebtoken"); function verifyToken(token) { return jwt.verify(token, process.env.JWT_PUBLIC_KEY, { algorithms: ["RS256"], issuer: "auth.example.com", audience: "api.example.com", clockTolerance: 5 }); } // 鉴权时不要直接信任 decoded.role async function requireAdmin(req, res, next) { const decoded = verifyToken(req.headers.authorization.replace("Bearer ", "")); const user = await db.users.findById(decoded.sub); 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.user = user; next(); } 另外建议: - 禁用 `none`,固定允许算法,不从 token header 动态决定算法; - HS256 密钥至少 256 bit 随机生成,不使用项目名、公司名、生日等可猜内容; - 优先 RS256/EdDSA,私钥只在认证服务,业务服务只持有公钥; - 校验 `exp`、`nbf`、`iss`、`aud`; - `kid` 做白名单映射,例如 `kid -> keyId`,不要直接访问文件路径或 URL; - 高危权限接口增加二次校验和操作审计; - token 泄露后要支持吊销,比如 `jti` 黑名单或短期 access token + refresh token 轮换。 6. **报告里建议这样组织** 路径可以写成: 公开信息收集 -> 定位内部系统和项目命名 -> 推测/发现 JWT secret 或 key 配置 -> 构造低风险越权 token -> 访问只读管理接口验证权限边界失效 -> 影响范围与修复建议 这样既能体现 CTF/靶场思路,也不会把授权演练写成单纯“猜密码”或“社工故事”。关键证据是:服务端是否接受伪造 token、是否错误信任 JWT 内权限字段、是否缺少服务端权限二次校验。
创建帐户或登录后发表意见