JWT漏洞实战解析:从BurpSuite靶场到真实渗透链

发布时间:2026/5/23 5:22:13

JWT漏洞实战解析:从BurpSuite靶场到真实渗透链 1. 这不是“破解”而是对JWT签名机制的一次诚实复盘你点开这个标题大概率是刚在CTF练习里被JWT卡住或是渗透测试报告里看到“存在未校验JWT”却不知从何下手。我第一次遇到类似问题是在给一家做SaaS后台的客户做代码审计时——他们用HS256算法签发token但后端校验逻辑里硬编码了密钥而前端JavaScript里居然明文拼接了/api/auth/verify?tokenxxx连基本的Referer检查都没有。当时没用BurpSuite只靠浏览器开发者工具手动改Base64URL就拿到了管理员权限。这件事让我意识到JWT漏洞从来不是“密码学失败”而是开发习惯、部署配置和安全意识断层的总和。所谓“5分钟搞定”指的是从打开BurpSuite到触发越权响应的实操耗时而真正有价值的是你能通过这次靶场操作建立起对JWT三段结构、签名验证流程、常见绕过路径的肌肉记忆。PortSwigger的JWT Lab之所以被全球安全团队反复使用正因为它不设障、不隐藏、不误导——所有漏洞都裸露在HTTP流量里只要你看懂Header.Payload.Signature这三段怎么被构造、怎么被校验、又怎么被篡改。它不考你密码学推导只考你是否真的理解“服务器信任的是什么”。关键词“JWT漏洞”“BurpSuite”“PortSwigger靶场”背后实际指向三个层次的问题第一层是协议层面的误用比如该用RS256却用了HS256第二层是实现层面的疏漏比如密钥硬编码、算法切换未禁用第三层是工程层面的盲区比如未校验kid字段、未限制alg为none。这篇文章不会讲JWT标准RFC 7519的每个字节定义也不会堆砌OpenSSL命令而是带你用BurpSuite这个最贴近真实渗透场景的工具把这三层漏洞像剥洋葱一样一层层拆开。适合刚学完HTTP协议想动手练手的新人也适合做了三年Web渗透但还没系统梳理过JWT攻击链的老手——因为绝大多数人其实连eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9这段Base64URL解码后到底写了啥都说不清楚。2. JWT结构与签名验证机制别再把token当黑盒2.1 三段式结构的本质不是加密而是可验证的声明封装JWT由三部分组成用英文句点.分隔Header.Payload.Signature。很多人误以为这是“加密token”其实它根本不是加密而是签名后的结构化声明signed claims。Header和Payload都是Base64URL编码的JSON对象任何人都可以解码查看Signature才是防篡改的关键。我们拿PortSwigger靶场第一个实验“Broken Algorithm”里的典型token举例eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJpYXQiOjE3MTQyNzg0MDAsImV4cCI6MTcxNDI4MjAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c解码Header第一段{ alg: HS256, typ: JWT }解码Payload第二段{ user: admin, iat: 1714278400, exp: 1714282000 }注意iat是issued at时间戳2024-04-27 16:26:40exp是expires at2024-04-27 17:26:40这两个字段直接决定token生命周期但它们本身不参与签名计算——签名只对HeaderPayload的原始字节做HMAC-SHA256运算。提示不要用普通Base64解码器JWT使用Base64URL编码RFC 4648 §5它把换成-/换成_并省略末尾。浏览器控制台里可以直接运行atob(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.replace(/-/g, ).replace(/_/g, /))来解码但更稳妥的是用BurpSuite内置的Decoder工具CtrlU它自动识别Base64URL格式。2.2 HS256与RS256的根本区别密钥所有权决定信任边界HS256HMAC-SHA256和RS256RSA-SHA256看似只是算法不同实则代表两种完全不同的信任模型HS256是共享密钥模型签发方Issuer和校验方Verifier必须持有同一份密钥secret。服务器用这个密钥对HeaderPayload做HMAC运算生成Signature校验时服务器用同一密钥重新计算HMAC比对结果是否一致。一旦密钥泄露整个认证体系崩塌——这正是PortSwigger“Secret Key Disclosure”实验的核心。RS256是公钥基础设施模型签发方用私钥签名校验方用对应的公钥验签。私钥永远不离开签发服务器公钥可公开分发。即使攻击者拿到公钥也无法伪造签名RSA非对称特性。但问题在于如果服务器错误地用公钥去“签名”即把公钥当HS256密钥用就等于把验签密钥暴露给了攻击者。PortSwigger靶场中“RS256 Public Key Disclosure”实验就模拟了这种经典误用服务器返回的.well-known/jwks.json里明文暴露了RSA公钥而校验逻辑却错误地将该公钥字符串作为HS256密钥进行HMAC计算。此时攻击者只需把Header中的alg:RS256改成alg:HS256再用拿到的公钥重算Signature就能生成合法token。2.3 签名验证流程的四个关键检查点服务器校验JWT时绝不是简单比对Signature。一个健壮的校验流程必须包含以下四步缺一不可语法解析检查确认token是标准三段式每段Base64URL可解码Header和Payload是合法JSON。若解析失败直接拒绝HTTP 400 Bad Request。算法一致性检查读取Header中alg字段确认该算法在白名单内如仅允许HS256、RS256且不允许none。很多老框架默认支持none算法即无签名攻击者只需把alg设为none清空Signature服务器就会跳过验签直接信任Payload。密钥匹配检查根据alg值选择对应密钥。HS256用共享密钥RS256需从JWKS或证书中获取公钥并确保该公钥属于可信Issuer检查kid字段是否匹配、证书链是否有效。业务逻辑检查验签通过后仍需检查Payload中exp过期时间、nbfnot before、iss签发者、aud受众等字段是否符合业务要求。例如exp已过期的token必须拒绝哪怕Signature正确。PortSwigger靶场的每个实验本质上都在攻击这四个检查点中的某一个。比如“None Algorithm”实验直击第2步“Kid Header Injection”实验则绕过第3步的密钥选择逻辑。3. BurpSuite实战四步法从拦截到越权的完整链路3.1 环境准备为什么必须用BurpSuite Professional而非Community版PortSwigger官方明确说明JWT Labs靶场需配合BurpSuite Professional使用核心原因在于Intruder模块的Payload Processing功能。Community版虽能拦截、改包、重放但无法在Intruder中对Payload做动态Base64URL编码/解码、字符串替换、哈希计算等操作——而这恰恰是JWT爆破密钥、篡改Payload、重算Signature的必备能力。安装步骤极简下载BurpSuite Professional官网提供30天试用启动Burp进入Proxy → Options → Proxy Listeners确认Running状态且监听127.0.0.1:8080浏览器配置代理Chrome设置→系统→打开计算机的代理设置→LAN设置→勾选“为LAN使用代理服务器”地址127.0.0.1端口8080访问http://portswigger.net/web-security/jwt/lab-jwt-authentication-broken-algorithm登录靶场账号靶场会自动生成唯一URL注意务必关闭浏览器自带的HTTPS拦截警告Chrome地址栏点击“不安全”→“继续前往...”否则Burp的CA证书无法生效导致HTTPS流量无法解密。Burp首次启动会提示安装CA证书按向导完成即可。3.2 第一步拦截并定位JWT位置——别只盯着Authorization头很多人一上来就抓Authorization: Bearer xxx但JWT可能藏在任何地方Cookie、URL参数、POST Body、甚至自定义Header如X-Auth-Token。PortSwigger靶场为教学清晰通常把JWT放在Cookie中。以“Broken Algorithm”实验为例登录后访问/my-account页面在BurpSuiteProxy → HTTP history中筛选my-account请求查看Request Headers找到Cookie: sessioneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...右键该请求→Send to Repeater方便后续修改此时你已在Repeater中看到完整token。但关键动作是右键token值→Decode as → Base64URL立刻看到Header和Payload明文。这是建立信任的第一步——确认你理解这个token在说什么。3.3 第二步算法降级攻击Algorithm Confusion——HS256变none的底层逻辑“Broken Algorithm”实验的漏洞本质是服务器校验时未校验alg字段且支持none算法。攻击步骤如下在Repeater中将Header解码后修改为{alg:none,typ:JWT}将Payload保持不变如{user:carlos}Signature清空即第三段留空但注意JWT标准要求三段必须存在所以填入一个空字符串Base64URL编码后是→实际就是空段但需保留.分隔符生成新tokeneyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiY2FybG9zIiwiaWF0IjoxNzE0Mjc4NDAwLCJleHAiOjE3MTQyODIwMDB9.发送请求若返回200且显示Carlos账户信息即成功。原理很简单alg:none表示“无需签名”服务器跳过验签直接信任Payload内容。实操心得我第一次做这个实验时在Repeater里手动拼接三段结果总返回401。排查半小时才发现——Payload解码后修改了user值但忘记重新Base64URL编码JWT对大小写、填充符极其敏感。正确做法是在Repeater中右键Payload原文→Encode/Decode → Encode as Base64URL再粘贴。Burp的Encoder工具会自动处理→-、/→_转换避免手工失误。3.4 第三步密钥爆破Secret Brute Force——Intruder的精准打击“Secret Key Disclosure”实验中服务器在错误页面泄露了密钥如Internal Server Error: Invalid signature. Expected signature: xxx。但更多时候密钥是未知的需爆破。HS256密钥强度取决于长度和字符集但开发常用弱密钥secret、mysecret、password123、jwt-key等。使用Intruder爆破步骤在Repeater中将原始token的Signature段选中第三段右键→Send to Intruder切换到Intruder →Positions标签页点击Clear §清空所有标记然后仅在Signature段开头插入§结尾插入§即§xxxx§这样Intruder只替换Signature切换到Payloads标签页Payload type选Simple listPayload list填入常见密钥字典如secret,jwt,123,password关键设置Payload Processing→Add→ 选择Base64-encode因Signature需Base64URL编码再Add→URL-encode确保/被转义Resource→Start attackIntruder会为每个密钥用HS256算法重新计算HeaderPayload的HMAC生成新Signature替换原token发送。观察Status列若出现200且Length明显大于401响应大概率爆破成功。经验技巧爆破效率取决于密钥字典质量。我整理了一份针对JWT的精简字典共127个条目覆盖80%以上CTF和靶场场景核心原则是优先短密钥8字符、优先英文单词组合、优先含jwt/secret/key的变体。避免用rockyou.txt这类千万级字典——HS256计算虽快但网络延迟才是瓶颈。实测用127行字典平均3秒内出结果。3.5 第四步Kid Header注入——绕过密钥选择的隐蔽通道“Kid Header Injection”实验利用kidKey ID字段做服务端路径遍历。服务器校验时会根据Header中kid值拼接文件路径读取密钥如/var/secrets/jwks/kid.pem。攻击者将kid设为../secrets/secret_key即可读取任意文件。操作步骤解码原始Header添加kid字段{alg:HS256,typ:JWT,kid:../secrets/secret_key}重新Base64URL编码HeaderPayload保持不变用已知密钥如从错误信息泄露的secret_key计算新Signature拼接三段发送此攻击成功的关键在于服务器未对kid值做白名单过滤或路径规范化。我在某电商后台审计中见过类似漏洞kid被用于数据库查询SELECT key FROM jwk_keys WHERE kid ?但未参数化导致SQL注入密钥读取双重危害。4. 靶场进阶实验拆解从单点突破到攻击链构建4.1 “Public Key Disclosure”实验RS256到HS256的算法欺骗该实验服务器返回的/.well-known/jwks.json包含RSA公钥{ keys: [ { kty: RSA, kid: 123, use: sig, n: t-S2..., e: AQAB } ] }但校验逻辑错误地将公钥的n字段模数作为HS256密钥使用。攻击链如下访问/.well-known/jwks.json提取n值长字符串如t-S2...构造新Headeralg改为HS256Payload设为{user:administrator}用n值作为HS256密钥计算新Signature发送token这里的关键认知是RSA公钥的n字段本质是一个大整数字符串它完全符合HS256密钥的格式要求任意字节序列。服务器拿到alg:HS256后直接用n字符串做HMAC而攻击者用同一字符串重算自然通过。踩坑记录我最初用Python的hmac.new()计算Signature但结果总不匹配。调试发现——BurpSuite的HMAC计算默认使用UTF-8编码输入字符串而某些n值含不可见Unicode字符。解决方案在Burp的IntruderPayload Processing中先Convert to UTF-8再HMAC-SHA256密钥填n值原文。这样保证编码一致性。4.2 “JWKS Host Header Injection”实验服务端请求伪造SSRF的JWT变体此实验更隐蔽服务器校验JWT时会向https://host/.well-known/jwks.json发起HTTP请求获取公钥。攻击者通过篡改Host头让服务器请求攻击者控制的域名返回恶意JWKS含攻击者指定的公钥。步骤构造恶意JWKS文件上传至自己的VPS如https://attacker.com/jwks.json内容为{keys:[{kty:RSA,kid:1,n:...攻击者可控的n值...,e:AQAB}]}在原始请求中将Host头改为attacker.com修改JWT Header的kid为1用对应私钥签名或直接用n值做HS256密钥同上一实验这本质上是SSRFJWT密钥劫持的组合攻击。我在某云平台渗透中遇到过类似设计其API网关校验JWT时会动态请求https://tenant-id.auth.example.com/.well-known/jwks.json而tenant-id来自请求头。通过Header注入tenant-id: attacker.com%00URL编码的空字符截断成功让网关请求我的恶意JWKS。4.3 “User ID Controlled by Request Parameter”实验业务逻辑与JWT的耦合风险此实验揭示一个常被忽视的事实JWT本身安全不等于业务逻辑安全。服务器校验JWT通过后用Payload中的user_id查询数据库但同时又接受URL参数?user_id123覆盖该值。攻击者只需拿到自己的合法tokenuser_id: 101请求/my-account?id123管理员ID服务器忽略JWT中的user_id直接查id123的数据这并非JWT漏洞而是业务层未遵循“单一数据源”原则。修复方案很简单校验JWT后所有用户标识必须来自Payload禁止从其他输入源URL、Body、Header二次读取。5. 从靶场到真实世界的防御实践开发者必须写的三行代码5.1 密钥管理永远不要硬编码更不要用secretHS256密钥必须满足长度≥32字节、随机生成、环境隔离。Node.js示例// ✅ 正确从环境变量读取启动时生成 const jwtSecret process.env.JWT_SECRET || crypto.randomBytes(32).toString(hex); // ❌ 错误硬编码、短密钥、可预测 const jwtSecret secret; // 危险 const jwtSecret my-super-secret-key; // 仍不够长Python Flask示例# ✅ 使用secrets模块生成 import secrets app.config[JWT_SECRET_KEY] secrets.token_urlsafe(32) # 生成56字符URL安全字符串经验之谈我在代码审计中发现70%的JWT密钥泄露源于Dockerfile或CI/CD脚本里明文写ENV JWT_SECRETxxx。正确做法是Kubernetes用Secret挂载AWS ECS用Parameter Store本地开发用.env文件加入.gitignore。5.2 算法强制与白名单堵死none和算法切换所有JWT库都提供算法白名单配置。Express-JWT示例// ✅ 强制仅允许RS256禁用HS256和none app.use(jwt({ secret: publicKey, // 公钥 algorithms: [RS256], // 严格白名单 issuer: https://auth.example.com, audience: https://api.example.com }));Java Spring Security示例// ✅ 在SecurityConfig中配置 JwtDecoder jwtDecoder NimbusJwtDecoder.withPublicKey(publicKey) .jwtProcessorCustomizer(jwtProcessor - { // 强制算法为RS256 jwtProcessor.setJwsAlgorithmConstraints( JwsAlgorithmConstraints.Builder .forAlgorithms(JWSAlgorithm.RS256) .build() ); }) .build();5.3 Kid字段安全绝不信任客户端输入若必须用kid则kid值必须是内部生成的UUID不暴露业务含义服务端维护kid到密钥的映射表不拼接文件路径或SQL查询对kid做长度和字符集校验如仅允许[a-zA-Z0-9_-]{8,32}Node.js安全校验示例// ✅ 安全的kid校验 const isValidKid /^[a-zA-Z0-9_-]{8,32}$/.test(kid); if (!isValidKid) throw new Error(Invalid kid format); const secret keyStore.get(kid); // 从内存Map中获取非文件读取6. 最后分享一个真实渗透中的“非标”绕过技巧去年帮一家金融客户做红队演练时遇到一个特殊场景他们的JWT使用ECDSA算法ES256且密钥轮换频繁常规爆破无效。但我在静态资源/static/config.js里发现一段注释// TODO: migrate to ES256, current dev key for testing: // const devKey -----BEGIN EC PRIVATE KEY-----\nMHQCAQEE...;这串PEM私钥虽标注“dev only”但生产环境API网关的校验逻辑未区分环境仍会加载该密钥。我用OpenSSL提取公钥openssl ec -in dev_key.pem -pubout -out dev_pub.pem再将公钥内容-----BEGIN PUBLIC KEY-----\n...Base64URL编码后填入JWKS的n字段构造恶意JWKS上传到VPS最后通过Host头注入完成攻击。这件事教会我JWT漏洞的入口往往不在JWT本身而在开发者的注释、配置文件、错误日志、甚至Git历史中。PortSwigger靶场的价值不在于教你“怎么赢”而在于训练你“怎么找”。当你熟练用BurpSuite解码、篡改、爆破、注入每一个JWT字段时你获得的不是某个靶场的通关钥匙而是面对任何Web应用时一眼看穿认证逻辑薄弱点的能力——这种能力不会过期也无法被补丁修复。

相关新闻