ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Token验证绕过实战:JWT攻击手法与靶场演练全解析

Token验证绕过实战:JWT攻击手法与靶场演练全解析 1. Token验证机制与靶场环境准备Token验证绕过这个话题在Web安全测试里属于“看着简单、门道很深”的一类。很多朋友一开始接触Token以为就是个登录后返回的字符串带上就能访问接口等真正进了靶场实战才发现里面能玩的花样远比想象中多。先说清楚一件事我们聊的Token指的是当前Web应用里最常见的几种身份凭证形态——比如服务端随机生成、存Redis或数据库里的Opaque Token以及无状态、自带签名和过期时间的JWT。它们的共同点是客户端请求受保护接口时在Header、Cookie或请求体里带上Token服务端校验通过才放行。但校验逻辑到底怎么实现、Token里放了什么、过期策略怎么定、签名算法是否可预测每一点都可能成为绕过点。靶场练兵的意义就在这里。真站你不能乱打靶场却是专门用来踩坑的。通过靶场把各类Token绕过手法过一遍你才能在做授权测试、代码审计或者自研系统加固时一眼看出问题出在哪一层。1.1 准备靶场环境的基本思路常见的开源靶场有不少比如DVWA、Webgoat、Pikachu还有专门把漏洞场景做细的Sqli-labs、Upload-labs这类单点靶场。做Token绕过练习的话我个人更推荐组合使用轻量级场景用Pikachu里的Token相关题目练手界面友好适合第一次接触绕过思路的人。深度场景用Webgoat的JWT章节里面把JWT的各种典型配置缺陷安排得明明白白。自行搭建如果你想把某个具体业务场景复现出来比如“修改密码接口存在Token固定漏洞”这类用Flask或者Spring Boot自己搭一个最小Demo反而更快。靶场部署没什么难度Docker一键起服务即可。关键是你心里要清楚靶场里的每一个漏洞点都要对应到真实业务里的某类风险。带着这个视角去打靶每一发子弹都不会白费。1.2 理解Token校验时发生在服务端的逻辑绕过Token本质上绕过的是“服务端对凭证的信任判断”。很多初学者有一个误区以为Token在客户端生成、客户端校验其实不是。真正的校验一定发生在服务端客户端能做的最多是“保存”和“携带”Token。服务端收到请求后大致会走这几步从请求中取出Token可能在Authorization头里可能在Cookie里也可能在请求体自定义字段里。如果拿不到Token有的系统直接拒绝有的系统会走“可选校验”分支——这是绕过点。拿到后校验格式、签名、有效期、用户状态。校验通过后从Token里解析出用户身份继续处理业务。校验失败返回401或403。绕过思路就藏在这些步骤的每一步里。比如第2步的“取Token方式太死板”第3步的“签名算法可被篡改”第4步的“身份信息从哪取、取的是不是可信数据”都可能被利用。接下来的实战章节我们就挨个场景过一遍。2. 常见Token绕过思路与攻击面分析很多人拿到靶场上来就是一顿操作抓包改包乱试一气。我建议先冷静下来画一张Token验证链路的图标出哪里可以动手脚。做安全测试和写代码一样先有全局再谈细节。2.1 把Token验证链路拆开看一个典型的Token验证链路大概长这样客户端构造请求 → 携带Token → 服务端接收 → 提取Token → 校验格式/签名/有效期 → 解析身份 → 授权判断 → 返回数据每一个箭头之间都有文章可做。举个最简单的例子“服务端接收”这一步如果开发者图省事对Token做了URL解码之后再取那你可以尝试二次编码绕过。“提取Token”这一步如果系统支持多来源取值先看Header再看Cookie优先级和覆盖关系就可能产生逻辑绕过。我在实际测试里碰到过一个有意思的案例某系统同时接受Authorization头和Cookie里的Token但校验顺序是“先取Header如果Header为空才取Cookie”。我尝试直接删掉Header服务端就去读Cookie而Cookie是服务端自己种下的、过期的旧令牌结果因为过期时间校验和Header路径不是一套逻辑旧Cookie反而被放行了。这就是典型的“多来源提取”导致的校验不一致。2.2 按照攻击手法归类绕过场景靶场里常见的Token绕过按手法大致可以分成这几类手法分类原理典型场景篡改攻击修改Token内容但不破坏格式JWT中修改用户名、权限字段伪造攻击自行构造一个让服务端信任的Token签名算法可空、密钥强度弱、算法混淆重放攻击把合法的Token在另一个上下文里再使用一次Token未绑定IP、设备、会话上下文逻辑缺陷利用校验流程的顺序、优先级、默认值绕过删除Token反而被默认放行、大小写绕过固定攻击让受害者使用攻击者指定的Token登录前下发Token、登录后不刷新Token并发绕过利用多线程/并发请求绕过一次性Token校验一次性Token未标记已使用前并发提交这六类里面前四类在靶场里最常见后两类需要你对业务流程有更深的理解。建议练的时候按照这个表格逐一对照每个类型至少打一遍形成肌肉记忆。2.3 为什么“删除Token”也是一种思路很多新手不理解删除Token服务端不是更应该拒绝吗但现实里不少系统对“缺失凭证”的处理是“降级处理”而不是“拒绝处理”。举个典型的例子一个下载文件的接口代码写的是“如果能解析到用户信息就按用户权限校验如果解析不到默认按游客权限”。如果游客权限恰好也能下载某些不该公开的文件那删掉Token就直接绕过了。这种问题在真实业务中经常出现在两个地方一是导出类接口比如报表下载、数据导出二是图片/附件预览类接口。前者因为功能上线急后者因为历史遗留代码。靶场里专门设计了类似的场景打一次你就会有很深的记忆。3. 靶场实战从探测到绕过的完整过程下面我们进入正题用一场完整的靶场实战把Token绕过从探测到利用的流程走一遍。这个过程中你会看到一个成功的绕过往往不是一步到位的而是通过不断的试探、观察、调整逐步缩小范围。3.1 第一步抓包分析Token结构和校验行为拿到靶场环境之后我的习惯是先登录一次用Burp Suite把登录和后续访问的完整请求抓下来看Token出现在哪些位置、长什么样。假设我们登录后服务端返回了这样一个JSON{ code: 0, message: login success, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MTAwMDAwMDB9.abc123signature } }一眼就能看出来这是JWT格式的Token。三段式结构用点分隔。第一段是Header第二段是Payload第三段是签名。先用Base64解码看一下Header{alg:HS256,typ:JWT}Payload{user:admin,role:admin,exp:1710000000}这告诉我们几个关键信息算法是HS256对称密钥Payload里直接放了用户名和角色过期时间是Unix时间戳。带着这些信息我们开始第一轮绕过尝试改Payload里的角色看看服务端认不认。3.2 场景一删除Token后的默认放行绕过第一个要试的往往不是高深的技术而是“删掉会怎样”。把请求中的Authorization头整个删掉发送请求观察响应HTTP/1.1 200 OK Content-Type: application/json {code:0,message:success,data:{list:[confidential-data-1,confidential-data-2]}}居然放行了。这说明接口对Token的处理是“可选校验解析失败按游客处理”而游客权限在这个接口里没有被限制住。这是非常典型的运维类漏洞。解决方式不难对这类接口明确要求必须携带有效Token且游客身份无权访问如果解析失败直接返回401而不是降级处理。但在靶场里这个漏洞的存在就是为了让你体会“不要把异常处理写成宽松模式”的教训。3.3 场景二JWT签名算法混淆绕过刚才注意到JWT的Header里alg是HS256。这里有一个经典的攻击手法把alg改成none然后去掉签名部分。实际上JWT标准里定义了alg为none的情况表示不签名。有些库在实现时没有禁用none算法于是攻击者可以自己构造一个无签名的Token服务端照样信任。我直接构造一个这样的Token第一步把Header改成{alg:none,typ:JWT}Base64编码后是eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0第二步Payload改成{user:admin,role:admin,exp:9999999999}Base64编码后是eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTl9第三步把三者拼起来签名部分留空eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTl9.替换到请求里发送Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTl9.如果服务端用的是不校验alg为none的旧版库这次请求就直接通过了。靶场里这个场景屡试不爽因为很多教学环境故意保留了这个问题。除了none算法还有一类是“算法混淆攻击”把HS256改成RS256。原理是有些JWT库在验证RS256签名时用的是公钥但如果攻击者把算法改成HS256服务端可能错误地使用同一个公钥作为HMAC密钥来验证。而公钥通常是公开的攻击者拿到公钥后就能伪造任意Token。具体操作是获取服务的公钥通常在/jwks.json或其他公开端点能找到靶场会直接提供。将Header中的alg改为HS256。用公钥内容作为HMAC密钥对HeaderPayload计算签名。把生成的Token发过去。用Python脚本实现一下伪造过程import base64 import hmac import hashlib import json def b64url_encode(data): if isinstance(data, str): data data.encode() return base64.urlsafe_b64encode(data).rstrip(b).decode() def b64url_decode(data): padding * (4 - len(data) % 4) return base64.urlsafe_b64decode(data padding) # 构造Header和Payload header {alg: HS256, typ: JWT} payload {user: admin, role: admin, exp: 9999999999} # Base64编码 header_b64 b64url_encode(json.dumps(header)) payload_b64 b64url_encode(json.dumps(payload)) # 用公钥内容作为HMAC密钥签名 # 假设从/jwks.json拿到公钥的内容 public_key -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----\n signing_input f{header_b64}.{payload_b64} signature hmac.new(public_key.encode(), signing_input.encode(), hashlib.sha256).digest() signature_b64 b64url_encode(signature) # 拼出完整Token token f{signing_input}.{signature_b64} print(token)这种手法的杀伤力在于它不需要知道真正的私钥只需要拿到公开的公钥就能完成伪造。很多真实系统的JWT实现都有这个问题尤其是框架自动生成密钥对但校验逻辑写得不严谨的情况。3.4 场景四Token重放与上下文绕过还有一种不需要修改Token内容就能绕过的场景——重放攻击。靶场里这个场景经常这样设计你的账号权限低但系统里存在某个高权限Token比如管理员的一次操作被截获你要想办法“借用”这个Token。重放的关键在于服务端对Token的使用是否做了上下文绑定。比如Token是否绑定了来源IPToken是否绑定了User-AgentToken是否绑定了一次性会话Session IDToken是否限制了可访问的接口范围如果都没绑定那你拿到一个合法Token往请求头里一放就能以Token持有人的身份操作。靶场里常见的操作路径是用低权限账号登录抓取自己的Token。在靶场某个页面触发管理员操作用Burp抓包截获管理员的Token。把自己的Token替换成管理员的Token访问管理员接口。听起来很简单但实际测试时问题往往出在“Token在哪里存放”。有的系统把Token放在localStorage有的放在Cookie有的放在内存变量里。截获到Token之后你得搞清楚服务端从哪个位置取。如果同时支持多个位置那也得知道优先级。这些细节都会影响重放的成功率。做重放测试时我习惯用Burp的Repeater工具先确认Token在当前位置能通过然后逐步修改请求的上下文信息比如IP、UA、Referer看服务端校验的“颗粒度”到底有多细。3.5 场景五并发绕过一次性Token的限制某些高危操作比如支付、改密、领奖服务端会生成一次性TokenOne-Time Token服务器在处理完请求后标记该Token为已使用。逻辑上没问题但实现上有个典型的竞态条件如果两个请求同时携带同一个一次性Token到达服务端服务端先查“是否已使用”再执行业务再把Token标记为已使用——两个请求可能在“第一次标记”之前都通过了检查。靶场里这个场景通常在“优惠券领取”或“抽奖”这类功能上。利用方式就是用Burp的Intruder或Turbo Intruder插件把携带Token的请求并发发送多次比如一次性发20个请求看是否有超过1个请求成功。Turbo Intruder的脚本可以这么写def queueRequests(target, wordlists): engine RequestEngine(endpointtarget.endpoint, concurrentConnections20, requestsPerConnection10, pipelineFalse) for i in range(20): engine.queue(target.req) def handleResponse(req, interesting): if success in req.response: table.add(req)如果20个并发请求里有2个以上都返回成功说明一次性Token的校验存在竞态条件可以让攻击者重复使用Token。真实业务中这类问题常见的修复方式是把“校验Token”和“标记Token已使用”放在一个数据库事务里或者用Redis的原子操作比如SETNX来保证同一时间只有一个请求能“消费”这个Token。4. 常见问题与排查技巧实录实战过程中总有一些问题反复出现。我整理了一份高频问题速查表这些都是我在靶场和真实测试中踩过、填过的坑分享出来希望能帮你少走弯路。4.1 高频问题速查表现象可能原因排查思路改完Payload后请求返回401签名校验未通过检查是否同步更新了签名如果是HS256需要用正确密钥重新签名使用algnone仍然返回401JWT库已禁用none算法换算法混淆思路或检查服务端版本是否已修复此问题删除Token后返回401服务端强制要求Token确认是否存在“白名单接口”即部分接口不需要Token重放Token时提示失效Token绑定了上下文IP/UA/会话尝试同时替换IP、User-Agent、Cookie等上下文信息并发请求全部失败一次性Token在第一次请求时已被正常标记使用检查并发连接数和触发时机有些场景需要在业务逻辑执行前发送并发请求修改Token后返回500服务端解析Token时报错检查Base64编码是否规范注意URL-safe编码是否使用了-和_Token校验明明失败但请求还是成功校验逻辑存在“降级放行”分支检查服务端对“校验异常”的处理方式是否存在默认放行的catch分支4.2 几个容易忽略的细节坑第一JWT的Base64URL编码和普通的Base64不一样。普通Base64里会出现、/和但JWT使用的是URL-safe的Base64URL替换成-/替换成_末尾的去掉。很多工具自动做了转换但如果你自己写脚本很容易忽略这点导致生成的Token服务端解析不了。我之前带过一个新人他写了半天脚本Token看起来没问题但服务端一直报错。最后定位到就是编码问题——他用的Python标准库base64.b64encode()输出的和/直接放进了Header里服务端按URL-safe解码时全乱了。正确的做法是用base64.urlsafe_b64encode()然后手动去掉末尾的。上面的脚本里我已经写好了b64url_encode函数直接拿来用就行。第二注意Token中的过期时间字段。有的系统用exp有的用expire有的用expires_in还有的用expires_at。在做绕过测试时把这些字段全部改成超大值或者删掉多试几种组合。不同字段名对应不同的解析逻辑总会有一个是开发者忘记校验的。第三服务端可能支持同时从多个位置读取Token。比如先读Authorization头为空则读Cookie。这种时候如果你只删除Header请求可能会“意外通过”。同样是这个逻辑的反向利用某些系统支持从自定义Header读取Token比如X-Token、X-Auth-Token把这些Header值填成跟Authorization一样的值某些旧实现会因为“已存在有效Token”而跳过二次校验。第四如果靶场接口返回的是JSON格式错误信息仔细读错误信息本身。很多框架会把内部异常直接带回响应里比如“Cannot read property xxx of null”“JWT decode error: invalid signature”之类。这些信息能帮你快速判断服务端到底走到了哪一步校验是格式错了、签名错了、还是过期了。4.3 一次完整的失败到成功排查记录分享一下我在某个靶场场景里的真实排查过程这个过程本身比结果更有教学意义。当时的情况是JWT算法混淆攻击我从/jwks.json拿到了公钥按照理论构造了HS256签名的Token但发过去一直是401。理论上没毛病为什么不行我逐步排查先用原始Token发请求确认靶场本身是通的。通过。用jsonwebtoken库的decode方法把自己的Token解码确认三段式结构没问题。通过。在Python端用PyJWT库做本地验证用公钥作为HMAC密钥验证结果居然是“签名有效”。本地都通过了说明Token本身没问题。问题只能在服务端。我换了个思路用Burp对比“原始Token”和“伪造Token”的请求差异。发现原始Token里Header是{alg:RS256,typ:JWT}而我伪造的是{alg:HS256,typ:JWT}。看起来没问题。再仔细看靶场实例对算法有白名单校验只接受RS256任何非RS256的算法直接拒绝根本不走签名校验。这个场景的“坑”在于靶场虽然保留了公钥泄露问题但同时把算法白名单写死了。所以正确的攻击路径应该是先尝试把公钥泄露结合其他逻辑漏洞配合利用而不是无脑改算法。这个排查过程让我意识到靶场里的每个题目设计者都会埋至少一层防护逻辑。打靶的时候不要只盯着一个技术点要从“这个场景为什么要这么设计”的角度去思考。4.4 打靶时的三个实用习惯最后分享三个我一直沿用的习惯对做Token绕过方向的练习特别有帮助每一次修改请求都单独保存一个请求快照。Burp的保存功能很好用把每次绕过的尝试记录下来方便回溯“哪个版本是通的哪个版本是不通的”。对于JWT相关操作用脚本化工具而不是纯手工改。手工改Base64编码的Token特别容易出错一个字符错了就全白费。jwt.io这个在线工具适合快速查看内容但真正要发请求测试时脚本更靠谱。留意靶场提示和源码。很多靶场会故意留下注释、代码片段、提示信息。Webgoat里的JWT章节甚至有直接展示部分代码的情况。这些信息含量极高认真读一遍比盲目爆破十次都有效。顺带说一句很多朋友问打靶场的意义靶场练的是思路和手感真实业务千变万化但底层的逻辑缺陷就那么几类。把靶场里的每一种绕过方式都打熟你才能在真实代码审计时一眼揪出那个“看起来没什么问题实则处处是洞”的Token校验实现。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进