1. 项目概述为什么JWT安全检测是Web安全工程师的必修课在今天的Web应用开发中JSON Web TokenJWT几乎成了身份认证和授权的代名词。从单页应用到微服务架构你很难找到一个完全不使用JWT的现代系统。它结构清晰、自包含、易于跨域传递这些优点让它迅速普及。但硬币的另一面是JWT的广泛使用也带来了新的攻击面。很多开发者甚至是一些安全工程师对JWT的理解还停留在“一个三段式字符串”的层面对其内部机制和安全配置一知半解。这就导致了许多本可避免的漏洞被部署到了生产环境。我见过太多因为JWT配置不当导致的严重安全事件。最常见的就是Header注入漏洞——攻击者通过篡改JWT的头部Header诱导服务器使用不安全的算法如none或脆弱的密钥进行验证从而绕过认证直接获取高权限。这种漏洞的检测如果纯靠手动分析效率极低。你需要手动解码JWT、检查alg字段、尝试各种攻击向量过程繁琐且容易遗漏。这就是为什么我们需要工具化、自动化的手段。Burp Suite作为Web安全测试的“瑞士军刀”其强大的可扩展性为我们提供了可能。通过开发或使用一个专门的Burp插件我们可以将JWT安全检测特别是Header注入漏洞的检测变成一个一键式或被动扫描的流程。这个项目的核心就是教你如何从零开始理解JWT的安全风险并亲手打造或配置一个Burp插件让它成为你自动化武器库中的一件利器。无论你是刚入门的安全测试人员还是想深化自动化测试能力的资深工程师掌握这套方法都能让你的漏洞挖掘效率提升一个数量级。2. JWT安全基础与Header注入漏洞深度解析2.1 JWT结构三要素不仅仅是三段Base64很多人把JWT简单理解为由点号分隔的三段Base64字符串这远远不够。每一部分都承载着关键的安全语义。Header头部这部分包含了令牌的元数据最重要的是alg算法和typ类型字段。alg字段决定了签名或加密所使用的算法如HS256、RS256或ES256。这里就是Header注入攻击的主要战场。一个脆弱的服务器可能会信任客户端传来的alg值。例如如果服务器代码逻辑是“读取JWT头部的alg然后用对应的算法去验证”那么攻击者将alg改为none就可能绕过签名验证。Payload负载这里存放了声明Claims也就是我们需要传递的信息。常见的标准声明有iss签发者、sub主题、exp过期时间、iat签发时间等。除了标准声明还可以包含自定义的私有声明。Payload部分的安全问题通常集中在敏感信息泄露如将密码哈希放在里面、未校验exp导致令牌永久有效、或者声明注入但较少见因为JWT通常被签名保护。Signature签名这是JWT的防篡改保证。签名的生成方式依赖于头部声明的算法。对于HMAC类算法如HS256签名是使用一个密钥secret对“Base64Url编码的头部 “.” Base64Url编码的负载”进行哈希计算得到的。对于非对称算法如RS256签名是使用私钥对上述部分进行签名公钥用于验证。如果签名验证被绕过整个JWT的安全模型就崩塌了。注意Base64Url编码与标准Base64略有不同它用-和_替代了和/并且去掉填充符。很多在线解码器能自动处理但在自己编写代码时需要注意这个细节否则会导致编码解码错误。2.2 Header注入漏洞的四种攻击向量与原理Header注入漏洞的本质是服务器端JWT库的实现或开发者的使用方式存在缺陷过度信任或错误处理了客户端提供的JWT头部信息。1.alg: “none”攻击这是最经典的攻击方式。JWT规范中确实包含一个名为“none”的算法表示不进行签名验证用于特殊情况。如果服务器配置的JWT库过于老旧或者开发者错误地允许了none算法攻击者只需将Header中的alg字段改为none并将签名部分置空即第三部分为空服务器就可能接受这个未经验证的令牌。现代主流的库如java-jwt、pyjwt、auth0/node-jsonwebtoken默认都已禁用none算法但一些自定义实现或老旧系统仍可能中招。2. 算法混淆攻击Algorithm Confusion这种攻击更为隐蔽和危险。它利用了非对称算法RS256和对称算法HS256验证逻辑的差异。原理服务器预期使用RS256非对称。它用公钥验证签名。攻击者将alg改为HS256对称。攻击过程攻击者需要获取到服务器的公钥公钥有时会暴露在/.well-known/jwks.json等端点。然后他使用这个公钥作为HMAC的“密钥”secret按照HS256的规则伪造一个签名。服务器收到令牌后看到alg: HS256便会尝试用验证HS256的方式去验证即使用同一个“密钥”进行HMAC计算并比对。如果服务器错误地将公钥当作HMAC的密钥来使用那么攻击者伪造的签名就能通过验证。关键在于服务器端代码是否区分了“用于RS256的公钥”和“用于HS256的密钥”。3. 无效签名或篡改签名攻击这种攻击试探服务器是否真的执行了签名验证。攻击者可能会轻微修改签名部分的几个字符或者完全替换成一个随机字符串然后观察服务器的响应。如果服务器返回“签名无效”的错误说明验证逻辑是存在的如果服务器直接返回认证成功或未报错则可能意味着签名验证环节被完全跳过或存在逻辑漏洞。这通常不是标准的Header注入但常作为初步探测手段。4. 密钥/密钥ID操控攻击一些JWT实现支持通过头部的kidKey ID参数来指定使用哪个密钥进行验证。如果kid参数的值是用户可控的并且服务器未做严格过滤就可能产生漏洞。例如路径遍历kid参数可能被设置为类似../../../../etc/passwd的路径如果服务器使用该路径来读取密钥文件就会导致敏感文件读取。SQL注入如果kid被用于数据库查询如SELECT key FROM keys WHERE id ‘$kid’则可能引发SQL注入。命令注入在极少数情况下如果kid被传递给系统命令可能导致命令注入。理解这些攻击向量是编写有效检测插件的前提。我们的插件需要能够自动识别并尝试这些攻击。3. Burp插件开发环境搭建与核心API初探3.1 环境准备Java、Gradle与Burp Suite要开发Burp插件Java环境是基础。我推荐使用较新的LTS版本如Java 11或17以确保良好的兼容性和开发体验。你可以通过命令行输入java -version来确认。Burp插件本质上是一个.jar文件。为了高效地管理依赖和构建项目我们使用Gradle。相比于传统的MavenGradle的构建脚本build.gradle更简洁灵活。首先确保安装了Gradle。接下来是最关键的一步获取Burp的API文件。你不能直接从安装目录里复制一个JAR包因为那可能涉及版权问题。正确的方法是启动你的Burp Suite专业版或社区版均可。导航到Extender标签页 -APIs子标签页。在这里你可以看到Burp Suite提供的所有API接口文档。更重要的是你可以点击Save interface files按钮将一系列Java接口文件.java保存到本地。这些文件定义了插件可以调用的所有方法是我们开发的“圣经”。创建一个新的项目目录例如jwt-header-injector。在该目录下初始化一个Gradle项目gradle init选择basic类型的项目即可。然后将从Burp保存的API文件通常在一个burp包名下复制到你的项目源码目录src/main/java下。你的项目结构应该大致如下jwt-header-injector/ ├── build.gradle ├── gradlew ├── src/ │ └── main/ │ └── java/ │ ├── burp/ -- 从Burp保存的API文件 │ │ ├── IBurpExtender.java │ │ ├── IHttpListener.java │ │ └── ... │ └── com/ │ └── yourname/ │ └── JwtHeaderInjector.java -- 我们的主插件类 └── settings.gradle3.2 核心API详解IBurpExtender, IHttpListener, IScannerCheck我们的插件需要实现几个核心接口来与Burp交互。IBurpExtender这是所有Burp插件的入口接口只有一个方法registerExtenderCallbacks。当插件被加载时Burp会调用这个方法并传入一个IBurpExtenderCallbacks对象。这个对象是插件的“生命线”通过它插件可以访问Burp的所有功能。public class BurpExtender implements IBurpExtender { private IBurpExtenderCallbacks callbacks; private IExtensionHelpers helpers; Override public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { this.callbacks callbacks; this.helpers callbacks.getHelpers(); callbacks.setExtensionName(JWT Header Injector Scanner); // 在这里注册其他组件如扫描器检查 callbacks.registerScannerCheck(new JwtScanner(this)); } }IHttpListener实现这个接口可以让插件监听Burp中所有的HTTP请求和响应。这对于被动扫描在代理流量经过时自动检测非常有用。processHttpMessage方法会在每个HTTP消息请求或响应经过时被调用。public class HttpListener implements IHttpListener { Override public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { // toolFlag 标识了消息来源Proxy, Scanner, Repeater等 // messageIsRequest true为请求false为响应 // messageInfo 包含完整的请求/响应详情 if (toolFlag IBurpExtenderCallbacks.TOOL_PROXY messageIsRequest) { // 只处理来自代理的请求 analyzeRequest(messageInfo); } } }IScannerCheck这是实现主动扫描逻辑的核心接口。Burp的扫描器会调用它来执行检查。它有两个关键方法doPassiveScan执行被动扫描。我们的JWT Header检查主要在这里进行因为我们需要分析请求中的现有JWT。doActiveScan执行主动扫描。这通常用于发送修改后的请求进行攻击。对于JWT注入我们可以在这里实现主动的算法混淆攻击测试。public class JwtScanner implements IScannerCheck { Override public ListIScanIssue doPassiveScan(IHttpRequestResponse baseRequestResponse) { // 分析baseRequestResponse中的请求查找JWT并检查其头部 // 如果发现可疑点返回一个包含IScanIssue的List // 如果没问题返回null } Override public ListIScanIssue doActiveScan(IHttpRequestResponse baseRequestResponse, IScannerInsertionPoint insertionPoint) { // 基于插入点主动修改JWT的头部并重放请求观察响应差异 // 用于确认漏洞是否存在 } }理解并熟练运用这三个接口你就掌握了Burp插件开发的核心。我们的JWT检测插件将主要是一个IScannerCheck同时可能辅以IHttpListener来记录或预分析。4. 插件核心功能实现JWT识别、解析与攻击模拟4.1 如何从海量流量中精准识别JWT在Burp的流量中JWT通常出现在以下几个位置Authorization头最常见的是Authorization: Bearer JWT。Cookie例如Cookie: sessionJWT或authJWT。URL参数较少见但可能存在如?tokenJWT。请求体在POST请求的JSON或表单数据中。我们的插件需要高效地扫描每个请求的这些位置。使用IExtensionHelpers提供的方法可以方便地解析HTTP请求。public class JwtHelper { private IExtensionHelpers helpers; public JwtHelper(IExtensionHelpers helpers) { this.helpers helpers; } public ListJwtCandidate findJwtsInRequest(IHttpRequestResponse requestResponse) { ListJwtCandidate candidates new ArrayList(); IRequestInfo requestInfo helpers.analyzeRequest(requestResponse); ListString headers requestInfo.getHeaders(); // 1. 检查Authorization头 for (String header : headers) { if (header.toLowerCase().startsWith(authorization: bearer )) { String token header.substring(authorization: bearer .length()).trim(); if (isPotentialJwt(token)) { candidates.add(new JwtCandidate(token, Authorization Header, header)); } } } // 2. 检查Cookie for (String header : headers) { if (header.toLowerCase().startsWith(cookie: )) { String cookieHeader header.substring(8); // 简单分割Cookie实际应用需考虑更严谨的解析 String[] cookies cookieHeader.split(;); for (String cookie : cookies) { String[] kv cookie.trim().split(, 2); if (kv.length 2 isPotentialJwt(kv[1])) { candidates.add(new JwtCandidate(kv[1], Cookie: kv[0], header)); } } } } // 3. 检查请求体针对JSON byte[] requestBytes requestResponse.getRequest(); int bodyOffset requestInfo.getBodyOffset(); String body helpers.bytesToString(requestBytes).substring(bodyOffset); if (requestInfo.getContentType() IRequestInfo.CONTENT_TYPE_JSON) { // 使用简单的正则或JSON解析库来查找JWT格式的字符串值 // 这里是一个简化的正则示例实际需要更健壮 Pattern jwtPattern Pattern.compile(\([a-zA-Z0-9_-]\\.[a-zA-Z0-9_-]\\.[a-zA-Z0-9_-])\); Matcher matcher jwtPattern.matcher(body); while (matcher.find()) { candidates.add(new JwtCandidate(matcher.group(1), Request Body (JSON), null)); } } return candidates; } private boolean isPotentialJwt(String token) { // 快速检查是否由三部分组成且每部分都是Base64Url字符 String[] parts token.split(\\.); if (parts.length ! 3) return false; return parts[0].matches([A-Za-z0-9_-]) parts[1].matches([A-Za-z0-9_-]) parts[2].matches([A-Za-z0-9_-]*); // 签名部分可能为空none攻击 } }JwtCandidate是一个简单的数据类用于存储找到的JWT字符串、其位置和原始上下文方便后续处理。4.2 JWT解码与头部信息提取识别出JWT字符串后下一步是解码。我们不能依赖网络请求必须在插件内部完成。Java标准库中的java.util.Base64类可以用于Base64Url解码。我们需要自己处理JSON解析可以使用轻量级的库如org.json或com.google.code.gson通过Gradle引入依赖。import org.json.JSONObject; import java.util.Base64; public class JwtParser { public static JwtData parse(String jwt) { String[] parts jwt.split(\\.); if (parts.length 2) { throw new IllegalArgumentException(Invalid JWT format); } // Base64Url解码 Base64.Decoder decoder Base64.getUrlDecoder(); String headerJson new String(decoder.decode(parts[0])); String payloadJson new String(decoder.decode(parts[1])); JSONObject header new JSONObject(headerJson); JSONObject payload new JSONObject(payloadJson); return new JwtData(header, payload, parts.length 2 ? parts[2] : null); } } class JwtData { JSONObject header; JSONObject payload; String signature; // ... 构造函数和getter }解析后我们重点关注头部中的alg、kid、typ等字段。例如检查alg是否为none、HS256、RS256等并记录下来。4.3 实现四种Header注入攻击的检测逻辑在doPassiveScan方法中我们对识别出的每个JWT进行分析。1. 检测alg: “none”String alg jwtData.header.optString(“alg”, “”).toLowerCase(); if (“none”.equals(alg)) { // 发现none算法这是一个高危信号 // 创建并返回一个IScanIssue标记为高危 return Collections.singletonList(createIssue(baseRequestResponse, “JWT使用’none’算法” “令牌头部指定了’none’算法可能允许未签名令牌通过验证。” “High”)); }2. 检测算法混淆潜在风险 这需要结合上下文判断。一个简单的启发式规则是如果JWT使用HS256算法但令牌本身看起来是来自一个通常使用非对称加密的应用例如令牌是从一个OAuth端点获取的则可以标记为“潜在风险”建议手动确认。更主动的检测需要在doActiveScan中实现。3. 检测无效签名试探 在被动扫描中我们无法直接验证签名。但我们可以检查签名部分是否存在。如果签名部分为空字符串或非常短且算法不是none可以给出警告。if (jwtData.signature null || jwtData.signature.trim().isEmpty()) { if (!“none”.equals(alg)) { // 非none算法但签名缺失可疑 return Collections.singletonList(createIssue(…, “JWT签名缺失” …)); } }4. 检测kid参数注入风险 检查kid值是否包含可疑字符。String kid jwtData.header.optString(“kid”, “”); if (kid.contains(“..”) || kid.contains(“/”) || kid.contains(“\\”) || kid.contains(“‘”) || kid.contains(“\””)) { // kid包含路径遍历或注入可能性的字符 return Collections.singletonList(createIssue(…, “JWT Key ID (kid) 参数存在注入风险” …)); }createIssue是一个辅助方法用于构建符合Burp API要求的IScanIssue对象包含漏洞名称、详情、严重等级、修复建议等并关联到具体的HTTP请求/响应位置。5. 主动扫描与漏洞验证让插件“动”起来被动扫描可以发现明显的配置错误和可疑点但要确认漏洞是否存在尤其是算法混淆漏洞必须进行主动测试。这就是doActiveScan方法的用武之地。5.1 构建JWT插入点Burp的主动扫描器会为参数、Cookie、头部等位置生成IScannerInsertionPoint对象。对于JWT我们需要一个自定义的插入点因为它可能是一个长字符串中的一部分如Bearer令牌。我们可以继承IScannerInsertionPoint接口但更简单的方式是利用IBurpExtenderCallbacks.getHelpers().buildParameter(…)来创建一个参数对象或者直接在原始请求字节数组中进行查找和替换。在我们的插件中当在doPassiveScan中发现一个潜在的算法混淆风险点例如看到一个RS256的JWT我们可以在doActiveScan中尝试将其改为HS256并进行测试。5.2 实施算法混淆攻击测试假设我们怀疑目标使用RS256但可能混淆使用公钥作为HMAC密钥。攻击测试步骤如下获取公钥首先需要尝试从常见端点如/.well-known/jwks.json、/oauth/certs等获取服务器的公钥。这可以在插件初始化时或发现JWT后通过发送一个额外的HTTP请求来完成注意线程和性能问题。伪造令牌解码原始JWT的头部和负载。将头部中的alg从RS256改为HS256。使用获取到的公钥作为HMAC密钥按照HS256的规则计算新的签名。这里需要用到JWT生成库如jjwt。将新的头部、负载和签名用Base64Url编码并用点连接。发送测试请求用伪造的JWT替换原请求中的JWT并通过callbacks.makeHttpRequest发送这个修改后的请求。分析响应比较原始请求的响应和测试请求的响应。如果测试请求返回了成功的状态码如200而原始请求也是成功的我们需要更精细的对比。可以检查响应体长度、特定关键词如”user_id”、”success”是否存在差异。最理想的情况是测试请求返回了不同的用户数据或权限提升的证据。如果测试请求返回了401/403则可能意味着服务器正确拒绝了算法混淆攻击。Override public ListIScanIssue doActiveScan(IHttpRequestResponse baseRequestResponse, IScannerInsertionPoint insertionPoint) { // 1. 检查这个插入点是否是我们关心的JWT位置可以通过之前passive scan的上下文传递 // 2. 获取原始JWT和公钥 // 3. 伪造HS256令牌 String forgedJwt forgeHs256Jwt(originalJwt, publicKeyPem); // 4. 构建新请求 byte[] newRequest buildRequestWithNewJwt(baseRequestResponse.getRequest(), forgedJwt); // 5. 发送请求 IHttpRequestResponse testResponse callbacks.makeHttpRequest( baseRequestResponse.getHttpService(), newRequest); // 6. 分析差异 if (isPotentialVulnerability(testResponse, baseRequestResponse)) { return Collections.singletonList(createActiveIssue(baseRequestResponse, testResponse, …)); } return null; }实现主动扫描的关键在于精准判断漏洞是否存在。误报会浪费测试人员的时间漏报则会让漏洞溜走。通常需要结合多个信号状态码、响应时间、响应体内容差异、错误信息变化等。6. 插件优化、部署与实战技巧6.1 降低误报与性能优化一个在实战中可用的扫描插件必须考虑误报和性能。降低误报上下文感知不要对所有找到的JWT都进行主动攻击。例如如果请求的目标路径是/api/login这通常是提交凭证获取令牌的地方在这里测试JWT注入是无效的。应关注使用令牌的API端点如/api/profile、/api/admin。响应差异智能对比简单的状态码对比不够。可以计算原始响应和测试响应的哈希值如MD5如果完全相同通常说明请求被以相同方式处理可能漏洞存在也可能请求未被处理。如果测试响应包含特定的认证错误关键词如”invalid token”、”signature invalid”则可能说明服务器有正确的验证可以降低风险等级。白名单机制允许用户将某些域名或路径加入白名单避免对已知的安全系统进行扫描。性能优化缓存机制对同一个目标主机获取的公钥进行缓存避免重复请求/jwks.json。限制主动扫描频率在doActiveScan中如果短时间内对同一主机同一路径进行了多次测试可以考虑跳过或延迟。异步处理获取公钥等网络操作可以考虑使用异步方式避免阻塞扫描队列。但需要注意Burp插件API的线程安全性。6.2 插件打包、加载与使用使用Gradle的jar任务可以轻松打包插件。确保在build.gradle中正确设置了主类清单。jar { archiveBaseName ‘jwt-header-injector’ manifest { attributes ‘Main-Class’: ‘com.yourname.JwtHeaderInjector’, ‘Class-Path’: configurations.runtimeClasspath.files.collect { it.name }.join(‘ ‘) } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } duplicatesStrategy DuplicatesStrategy.EXCLUDE }打包后在Burp的Extender-Extensions-Add中选择Java类型然后加载生成的.jar文件。加载成功后你的插件名称会出现在列表中。此时Burp的Scanner就已经集成了你的检测逻辑。你可以在Target站点地图中右键选择Actively scan this host或者直接使用Intruder、Repeater手动测试时被动扫描也会在后台工作。6.3 实战排查与技巧记录在实际使用中你可能会遇到各种情况。以下是一些踩坑经验“插件加载失败NoClassDefFoundError”这几乎总是依赖冲突或缺失。确保你的jar是包含所有依赖的“胖jar”使用上述Gradle配置。检查Burp自带的Java版本与你编译使用的版本是否兼容。扫描速度慢检查你的doPassiveScan和doActiveScan逻辑避免复杂的循环和同步网络请求。被动扫描应尽可能轻量。漏报某些应用将JWT放在自定义的HTTP头中如X-Access-Token。你需要更新findJwtsInRequest方法添加对这些常见自定义头部的检查。也可以考虑让用户通过配置添加自定义的头部名称。误报一些应用虽然使用了JWT格式的字符串但并非用于认证可能是其他数据的编码。如果插件持续在某个非认证接口上报警可以手动将其加入白名单。更好的方式是让插件尝试解码Payload如果里面包含标准的JWT声明如exp,iat则认为是真JWT的概率更大。与Burp其他功能联动你的插件不仅可以报漏洞还可以增强其他工具。例如在Repeater中可以增加一个自定义的标签页自动解码当前请求中的JWT并显示其头部和负载甚至提供一键修改alg或kid的功能。这需要实现IMessageEditorTab接口。开发Burp插件是一个持续迭代的过程。从最简单的none算法检测开始逐步加入算法混淆、kid注入等检测逻辑再优化误报和性能最后增加方便测试的辅助功能。把这个插件打磨顺手它将成为你在Web应用渗透测试中针对JWT攻击面的一把精准手术刀。