当前位置: 首页 > news >正文

JWT安全攻防实战:从原理到算法混淆、弱密钥爆破与防御

1. 从一次渗透测试中的“意外”发现说起

最近在复盘一个内部靶场环境时,遇到了一个关于JWT(JSON Web Token)的典型场景,让我想起了之前“陇剑杯”网络安全竞赛中一道非常经典的题目。那道题没有复杂的漏洞链,也没有高深的免杀技巧,核心就是考察对JWT这一现代Web应用身份验证“基石”的深入理解。很多刚接触安全的朋友,包括当时的我,都曾以为JWT就是一个加密过的、不可篡改的字符串,拿到手只能干瞪眼。但事实恰恰相反,JWT的设计哲学是“签名”而非“加密”,这为安全测试人员打开了一扇充满可能性的窗户。今天,我就结合这道竞赛题,把JWT从原理到攻击面,掰开揉碎了讲清楚,让你下次再遇到时,能像老师傅一样,一眼看穿其中的门道。

简单来说,JWT就是一个用于在各方之间安全传输信息的“令牌”。它由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),中间用点号.分隔,形如xxxxx.yyyyy.zzzzz。它被广泛用于单点登录(SSO)、API鉴权和分布式会话管理。这道题的核心,就是利用我们对JWT各部分处理逻辑的误解或配置缺陷,去伪造一个拥有更高权限的令牌。这不仅仅是CTF的技巧,在真实的渗透测试和红队评估中,对JWT的审计和测试是Web应用安全中不可或缺的一环。

2. JWT的结构拆解:远不止“三个点”那么简单

要攻击一个东西,首先得彻底理解它。JWT的三个部分,每一部分都藏着玄机。

2.1 头部(Header):算法声明与格式把戏

头部通常是一个JSON对象,经过Base64Url编码后形成JWT的第一部分。它最关键的字段是alg,用于声明签名所使用的算法。常见的值有:

  • HS256:使用HMAC SHA-256的对称加密算法。这意味着签名和验证使用同一个密钥(secret)。这是最常用但也最需要保护密钥的场景。
  • RS256:使用RSA SHA-256的非对称加密算法。使用私钥(private key)签名,使用公钥(public key)验证。公钥可以安全分发,更适合分布式场景。
  • ES256:使用ECDSA的椭圆曲线数字签名算法,同样是非对称的。
  • none:一个特殊值,表示“无签名”。在某些早期的JWT库实现中,如果服务器配置不当,允许algnone的令牌通过验证,这将导致严重的安全漏洞。

除了alg,头部还可能包含typ(类型,通常为JWT)、kid(密钥ID,用于在多个密钥中指定一个)等字段。这里第一个攻击点就出现了:算法混淆攻击(Algorithm Confusion Attack)。如果服务器端代码在验证签名时,逻辑是“从令牌头部读取alg字段,然后用该算法去验证签名”,那么攻击者就可以将alg改为none,或者从RS256改为HS256,从而绕过验证。例如,服务器本应使用RS256(非对称),公钥是公开的。攻击者将头部改为{"alg":"HS256","typ":"JWT"},然后用服务器的公钥作为HMAC的密钥(secret)去伪造签名。如果服务器验证逻辑有缺陷,它会用公钥作为HMAC密钥去验证这个签名,从而误判令牌有效。

2.2 载荷(Payload):承载信息的“声明集”

载荷同样是一个JSON对象,包含所谓的“声明”(Claims)。声明分为三种:

  1. 注册声明(Registered Claims):预定义的一些有特定含义的声明,非强制但建议使用。例如:
    • iss:签发者
    • sub:主题
    • aud:接收方
    • exp:过期时间(Expiration Time),这是一个时间戳(Unix epoch)。
    • nbf:生效时间
    • iat:签发时间
  2. 公共声明(Public Claims):可以自定义,但为避免冲突,应定义在IANA JSON Web Token Registry或使用防冲突命名空间(如包含一个域名)。
  3. 私有声明(Private Claims):在提供方和消费方之间约定使用的自定义声明,用于传递业务信息,如usernameroleuserid等。

载荷部分经过Base64Url编码后成为JWT的第二部分。这里的关键在于,载荷本身是未加密的,仅做了编码。任何人都可以轻松地将eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9(Header)和eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ(Payload)解码,看到原始内容。因此,绝对不要在JWT的载荷中存放任何敏感信息,如密码、信用卡号等。攻击面也随之而来:如果服务器仅仅依赖JWT中的useridrole字段来判断权限,而没有在服务端进行二次校验,那么篡改载荷(并相应重签)就能直接实现越权。

2.3 签名(Signature):完整性的守护者

签名是JWT安全的核心。它的生成方式取决于头部声明的算法。对于HS256,签名是这样生成的:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)对于RS256,则是:RSASHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), private_key)

签名的目的是验证消息在传输过程中未被篡改。验证方使用头部声明的算法和相应的密钥(对称算法的secret或非对称算法的公钥)对“头部.载荷”部分重新计算签名,并与JWT中的第三部分进行比对。如果一致,则证明令牌有效且未被修改。

3. 实战攻击手法详解:以“[陇剑杯 2021]jwt”为例

理解了原理,我们来看实战。这类题目的常见解题路径,往往围绕以下几个关键攻击面展开。我们假设题目环境是一个Web应用,登录后获得一个JWT,目标是提升权限(如从普通用户user提升为管理员admin)。

3.1 第一步:信息收集与令牌解码

拿到题目,首先用浏览器开发者工具或Burp Suite抓取登录后的请求,找到Authorization: Bearer <your_jwt_token>这个Header,或者Cookie中的jwttoken字段,拿到JWT字符串。

接着,进行解码。虽然可以手动Base64Url解码,但更推荐使用工具,如:

  • 在线网站:jwt.io(注意不要在真实敏感令牌上使用不可信的在线工具)。
  • 命令行工具:jwt-tool
  • Burp Suite扩展:JSON Web Tokens

解码后,我们重点关注:

  • 头部alg是什么?有没有不常见的字段如jwkkid
  • 载荷:有哪些声明?特别是自定义的usernameroleisAdmin等。exp字段的值是多少(一个时间戳)?它过期了吗?

假设我们解码后得到:

Header: {"alg": "HS256", "typ": "JWT"} Payload: {"sub": "user123", "username": "guest", "role": "user", "exp": 1698765432}

显然,我们的目标是修改usernamerole为管理员身份。

3.2 攻击面一:弱密钥(Weak Secret)爆破

如果算法是HS256、HS384等对称算法,那么密钥(secret)的强度至关重要。许多开发者在测试或初期会使用弱密钥,如secretpassword123456,甚至是空字符串。jwt-tool工具内置了强大的爆破功能。

使用命令:

python3 jwt_tool.py <your_jwt_token> -C -d /path/to/wordlist.txt

-C代表“Crack”,-d指定字典文件。工具会尝试用字典中的每一个词作为secret去验证签名。如果爆破成功,它会直接输出正确的secret。拿到secret后,我们就可以用任何JWT库(或jwt.io网站)修改载荷,并用这个secret重新生成合法的签名。

实操心得:爆破字典的选择很重要。除了常见的弱口令字典,可以尝试结合目标应用名称、公司名、项目代号等生成专属字典。有时密钥就是devtestchangeme这类简单单词。

3.3 攻击面二:算法混淆攻击(CVE-2015-9235等)

这是历史悠久的经典漏洞。如果服务器端的JWT验证库存在逻辑缺陷,可能会接受alg: none的令牌。攻击步骤:

  1. 修改头部为{"alg": "none", "typ": "JWT"}
  2. 修改载荷,如将"role": "user"改为"role": "admin"
  3. 将签名部分(即第三个点号后的内容)直接删除,或者置空。
  4. 将修改后的Header.Payload.(注意最后有一个点号)提交给服务器。

另一种混淆是RS256HS256。如果服务器公钥可获取(有时通过/jwks.json端点或网页源码泄露),且服务器验证逻辑有缺陷,攻击步骤为:

  1. 获取服务器RSA公钥(通常为PEM格式)。
  2. 修改JWT头部,将algRS256改为HS256
  3. 修改载荷。
  4. 使用这个公钥作为HMAC的secret,对新的Header.Payload进行HS256签名。
  5. 发送伪造的令牌。

使用jwt-tool可以自动化尝试多种混淆攻击:

python3 jwt_tool.py <your_jwt_token> -X a

-X a代表“Exploit - All tests”,它会自动尝试none算法、混淆攻击等多种方式。

3.4 攻击面三:无效签名绕过(“None”漏洞的变种)

有些服务器端的验证逻辑可能只检查签名是否存在,或者解析JWT结构失败时默认通过。我们可以尝试签名格式错误,比如:

  • 签名部分不是Base64Url编码(包含+/等非法字符)。
  • 签名部分长度不对。
  • 在签名后附加额外的点号或字符,如Header.Payload.Signature.Header.Payload.Signature.extra

这些手法的成功率取决于后端使用的具体JWT库及其版本和配置。

3.5 攻击面四:密钥文件泄露与JKU/JWK/KID滥用

这是更高级的攻击面,常出现在配置不当或对JWT扩展特性理解不深的场景。

  • JKU:头部中的jku参数是一个URL,指向一个包含验证密钥的JSON密钥集(JWKS)。如果攻击者能控制这个URL(通过SSRF、域名劫持或上传功能),就可以指向自己控制的恶意JWKS,从而使用自己的私钥签发任意令牌。
  • JWK:头部中直接嵌入一个jwk参数,包含用于验证的公钥。攻击者可以替换成自己的公钥。
  • KIDkid是密钥标识符,用于在服务器的多个密钥中选择一个。攻击可能包括:
    1. 目录遍历:如果kid参数未经过滤,像../../../../etc/passwd这样的值可能导致服务器使用文件内容作为密钥,可能被预测或利用。
    2. SQL注入:如果kid用于从数据库查询密钥,可能引发SQL注入。
    3. 命令注入:极少数情况下,kid可能被用于动态加载密钥,导致命令注入。

对于“[陇剑杯 2021]jwt”这类题目,往往需要综合判断。例如,题目可能提示“密钥就在服务器上”,结合弱密钥爆破和kid路径遍历(如kid: "/proc/self/cmdline"泄露进程信息,进而找到密钥文件路径)来解题。

3.6 攻击面五:时间戳攻击(exp, nbf, iat)

载荷中的expnbfiat都是时间戳。服务器库通常会验证exp(是否过期)和nbf(是否已生效)。攻击可能包括:

  • 时钟偏移利用:如果服务器时间配置不同步,存在较大偏移,可能使一个本应过期的令牌仍然有效。
  • 篡改时间戳:直接修改exp为一个未来的时间戳。但这需要同时能绕过签名验证,通常需要结合其他漏洞(如弱密钥、算法混淆)一起使用。

4. 工具链与手动操作:不只是点按钮

虽然jwt-tool是瑞士军刀,但理解手动过程能加深认知。这里以“弱密钥爆破成功后的令牌伪造”为例,展示完整流程。

场景:我们通过爆破,发现目标的HS256密钥是supersecret

  1. 原始令牌eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwidXNlcm5hbWUiOiJndWVzdCIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNjk4NzY1NDMyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

  2. 解码分析(使用Pythonpyjwt库或在线工具):

    • Header:{"alg": "HS256", "typ": "JWT"}
    • Payload:{"sub": "user123", "username": "guest", "role": "user", "exp": 1698765432}
  3. 修改载荷:我们将"role": "user"改为"role": "admin"。注意,exp如果已过期,也需要改为一个未来的时间戳(如9999999999)。新的Payload JSON为:

    { "sub": "user123", "username": "guest", "role": "admin", "exp": 9999999999 }
  4. 手动生成新令牌(使用Python):

    import jwt import time # 已知密钥 secret = 'supersecret' # 构造新的载荷 payload = { 'sub': 'user123', 'username': 'guest', 'role': 'admin', 'exp': 9999999999 # 一个很远的未来时间 } # 生成新的JWT,使用HS256算法和已知密钥 new_token = jwt.encode(payload, secret, algorithm='HS256') print(new_token)

    运行后会得到一个新的、使用正确密钥签名的令牌。

  5. 替换与测试:在Burp Suite中,用这个新令牌替换原请求中的令牌,重放请求。观察响应,看是否成功获取了管理员权限的访问(例如,访问/admin页面,或响应中返回了更多数据)。

踩坑记录:在手动编码时,务必确保JSON格式完全正确,没有多余的逗号,字符串使用双引号。一个常见的错误是exp的值没有引号(在JSON中它是数字),但在某些库的encode函数中,载荷是字典类型,库会自动处理。如果手动拼接字符串进行Base64编码,则必须严格遵守JSON格式。

5. 防御视角:开发中如何避免这些坑

作为攻击者,我们寻找漏洞;作为开发者或安全工程师,我们则要堵上这些漏洞。以下是一些关键防御措施:

  1. 使用强算法和强密钥

    • 优先使用非对称算法(如RS256、ES256)。私钥妥善保存在服务器端,公钥可以安全分发。
    • 如果必须使用对称算法(HS256),密钥必须是高强度的随机字符串(如通过密码学安全随机数生成器生成),并像保护密码一样保护它,绝不能硬编码在客户端或前端代码中。
  2. 严格验证算法

    • 在服务器端验证签名时,不要依赖客户端提供的alg。应该有一个应用配置,明确指定期望接受的算法列表(如只接受RS256)。在验证时,使用配置中指定的算法和对应的密钥去验证,而不是读取令牌头中的alg值。这是防止算法混淆攻击的根本方法。
  3. 全面验证声明

    • 必须验证expnbfiat
    • 验证iss(签发者)是否可信。
    • 验证aud(受众)是否包含本服务。
    • 对于自定义声明如roleuserid必须在服务端进行二次校验。例如,根据userid从数据库或缓存中查询用户最新的权限信息,而不是完全信任JWT中的role字段。JWT应作为会话状态的“引用”,而非“权威数据源”。
  4. 安全处理JKU/JWK/KID

    • 如果使用jkujwk,必须严格验证URL是否来自可信的白名单域名,并对获取的密钥进行完整性验证。
    • kid参数进行严格的输入验证,防止路径遍历、SQL注入等攻击。
  5. 使用最新的、经过安全审计的库

    • 避免使用已过时或有已知漏洞的JWT库。关注社区安全公告,及时更新。
  6. 设置合理的令牌生命周期

    • 访问令牌(Access Token)有效期宜短(如15分钟),配合刷新令牌(Refresh Token)使用,减少令牌泄露后的风险窗口。

6. 拓展思考:JWT在真实红队评估中的位置

在真实的渗透测试中,JWT漏洞很少孤立存在。它往往是横向移动或权限提升链条中的一环。我们可能需要结合其他漏洞来获取攻击JWT的初始条件:

  • 信息泄露:通过源码泄露、目录遍历、错误配置的.git目录等,找到硬编码的JWT密钥或公钥/私钥文件。
  • SSRF:利用服务器端请求伪造,让服务器从内网或攻击者控制的地址获取JWKS(jku),从而注入恶意公钥。
  • 逻辑漏洞:例如,注册或密码重置功能处,可能允许我们设置自己的usernameadmin(如果JWT的username直接来自用户输入且未做过滤),然后再通过JWT重放获得管理员上下文。
  • 中间件配置问题:某些API网关或反向代理(如Kong, APISIX)在配置JWT验证插件时,如果配置不当,也可能引入类似alg: none的漏洞。

因此,拿到一个JWT后,不要仅仅盯着令牌本身。要思考:这个令牌从哪里来(登录接口、OAuth回调)?服务器用什么库验证?密钥可能存储在哪里?是否有其他接口或页面泄露了关键信息?这种关联性思维,才是将CTF技巧转化为实战能力的关键。

回到“[陇剑杯 2021]jwt”这道题,它像是一个精致的微缩景观,集中展示了JWT最常见的安全问题。通过手动或工具辅助的逐步测试——从解码观察、尝试none算法、爆破弱密钥、检查kid路径遍历,到最终伪造高权限令牌——我们完成了一次完整的JWT安全审计流程。掌握它,你不仅能在CTF中得分,更能在真实的Web应用安全评估中,多一双发现漏洞的锐利眼睛。记住,令牌只是表象,背后的验证逻辑和系统配置,才是真正的战场。

http://www.jsqmd.com/news/1385089/

相关文章:

  • PKC 第 121 个开关:模拟关注的位置、验证方法与风险边界
  • 卡关不再重开:植物大战僵尸修改器 PvZ Tools 从安装到布阵的完整实操指南
  • 阿里CoPaw进阶指南:从本地部署到生产力工具深度调优
  • 泰拉瑞亚地图编辑器TEdit:地图批量改造,究竟能帮你省下多少时间
  • 从零实现AI编程助手:基于本地大模型构建Claude Code核心引擎
  • 从零搭建AD域服务器:原理、实战与故障排查全指南
  • 2026年数学建模国赛B题算法(38):基于LKH与遗传算法融合的旅行商问题高效求解:一个面向2026年数学建模的集成框架
  • Android应用保活实战:十大方案解析与架构设计指南
  • 达梦到Oracle数据库迁移实战:从评估到上线的完整方案
  • VMware安装macOS Big Sur:解决CPU禁用错误与深度配置指南
  • 思维导图在文学深度阅读中的应用:以《童年》为例
  • Sketch MeaXure终极指南:如何用智能标注插件提升300%设计交付效率
  • DeepSeek-V4 Flash高效推理:从FlashAttention到vLLM部署全解析
  • 基于GEE与深度学习的全球海上风机自动化识别与数据集构建
  • XML核心技术解析:从语法规则到企业级应用实战指南
  • Qwen多模态工具层实战:从零构建能看图说话的AI智能体
  • DeepCFD 快速上手指南:为什么它能用 AI 把流场预测提速 3 个数量级
  • AI工程化实践:破解效率悖论,从Prompt工程到RAG架构的落地指南
  • AI 时代新工作流:全面构建、小范围交付,降低结构决策成本!
  • 免费歌词下载工具实战指南:3步让网易云与QQ音乐的歌词自动归位
  • 3 步免费解锁加密音乐格式:Unlock Music 浏览器极简上手
  • RAG 技术全景综述2026
  • C++静态代码分析工具clang-tidy:从原理到实战的完整指南
  • 离散系统核心:从Z变换到数字控制器实现
  • 英飞凌TLD5098EL V7汽车LED驱动开发板全流程评测与调试指南
  • DDrawCompat 完整实战指南:6 个步骤让 DirectX 老游戏在新系统流畅运行
  • 手把手搭建全平台电视直播系统:基于M3U与IPV6源的原理与实战
  • STM32 PWM与S.BUS2遥测同步处理:解决DMA缓存一致性与中断优先级冲突
  • 14-SOFA_使用 Python Controller 与正在运行的仿真交互_16-python3-particle-interactive.py
  • 2026跨端开发技术选型:Flutter、React Native与HarmonyOS对比