Web安全实战:从IDOR到JWT绕过,详解越权漏洞的挖掘与防御
1. 项目概述与核心价值最近在带新人做渗透测试的专项训练发现很多朋友对越权漏洞的理解还停留在“改个ID”的初级阶段实战中遇到稍微复杂点的场景就无从下手。正好玄域靶场新上线的越权漏洞系列关卡从易到难设计了三个典型的实战场景非常贴合企业真实业务。我花了点时间把这三个关卡的核心思路和绕过技巧梳理了一遍形成这篇教程。无论你是刚入门的安全测试工程师还是想巩固越权漏洞知识体系的老手这篇从实战出发的拆解都能帮你建立起清晰的攻击路径和防御思路。越权漏洞尤其是水平越权和垂直越权是Web应用中最常见、也最容易被忽视的高危漏洞之一。它不像SQL注入或XSS那样有直接的攻击载荷其危害性在于它直接破坏了业务最核心的权限校验逻辑能让攻击者访问或操作本不属于自己的数据或功能。玄域靶场的这1-3关分别模拟了基于ID的简单水平越权、基于JWT令牌的算法绕过以及结合业务逻辑的复合型越权几乎覆盖了日常渗透测试中80%的越权场景。接下来我会带你一步步拆解每个关卡不仅告诉你怎么做更重点分析背后的“为什么”以及在实际项目中如何发现和利用这类漏洞。2. 环境准备与靶场接入2.1 玄域靶场简介与访问玄域靶场是一个专注于Web安全实战演练的在线平台其最大特点是场景高度仿真复现了许多来自真实SRC安全应急响应中心和渗透测试项目的漏洞案例。要开始我们的越权漏洞实战首先需要访问其官方网站。通常这类靶场平台需要注册账号部分高级场景可能需要激活码或付费但对于基础的学习关卡一般提供免费体验。访问后在漏洞分类或实验列表中寻找“越权漏洞”或“权限绕过”相关的模块应该能很快找到编号为1到3的关卡。每个关卡都是一个独立的、带有前端界面的Web应用模拟了某个具体的业务功能比如用户信息查询、订单管理或者后台管理入口。在开始测试前建议你准备一个专门的测试浏览器或浏览器无痕模式并配置好Burp Suite或类似的HTTP代理工具因为所有的漏洞发现和利用过程都离不开对HTTP请求和响应的深度分析。2.2 测试工具链配置工欲善其事必先利其器。对于越权漏洞的测试以下几样工具是必不可少的浏览器与开发者工具推荐使用Chrome或Firefox。熟练使用其内置的开发者工具F12打开的“网络”Network标签页这是你观察所有前端请求的窗口。Burp Suite Professional/Community版这是Web安全测试的“瑞士军刀”。社区版对于学习完全够用。你需要将其配置为浏览器的代理通常监听127.0.0.1:8080并安装Burp的CA证书到浏览器以便拦截和解密HTTPS流量。JWT调试工具由于第二关涉及JWT有一个能方便编解码和修改JWT的工具会事半功倍。推荐jwt.io这个在线网站或者使用Burp Suite的扩展如“JWT Editor”。jwt.io界面直观左侧直接粘贴Token中间部分实时显示解码后的Header和Payload右侧可以手动修改并观察编码后的变化。笔记工具准备一个记事本或思维导图工具用于记录不同测试账号的Cookie、Session、用户ID、Token等信息。越权测试的核心就是对比不同权限用户之间的请求差异。配置好Burp后打开靶场第一关的页面。首先正常使用页面功能让Burp捕获到所有的业务请求。这一步的目的是理解应用的正常业务流程和请求参数格式为后续的篡改测试打下基础。3. 第一关基于ID参数的水平越权3.1 关卡场景与正常流程分析第一关通常设计得非常直接目的是让测试者理解水平越权最基本的形式。场景可能是一个“查看个人资料”或“查询订单详情”的功能。假设我们有两个测试账号用户A:user_a ID为1001用户B:user_b ID为1002以“查看个人信息”为例。正常流程是用户A登录后点击“我的资料”浏览器会发起一个请求例如GET /api/user/profile?id1001服务器接收到这个请求后会执行如下逻辑从会话Session或令牌Token中获取当前登录用户的身份假设是user_a对应ID1001。检查请求参数中的id1001是否与当前用户身份匹配。如果匹配则从数据库查询ID为1001的用户信息并返回。如果不匹配应返回错误如“无权访问”。漏洞点不安全的直接对象引用IDOR。如果服务器在步骤2缺少了权限校验或者校验逻辑存在缺陷那么当用户A将请求中的id参数改为1002时服务器可能依然会执行查询并返回用户B的信息。3.2 漏洞利用步骤详解登录与抓包首先使用用户A的账号 (user_a) 正常登录系统。打开Burp Suite的代理拦截功能Intercept is on然后在页面上点击查看自己的资料或触发目标功能。分析请求Burp会拦截到对应的HTTP请求。你会在请求URL或请求体Body中看到一个用于标识数据对象的参数常见的有id、user_id、uid、docid、order_no等。记下它的当前值例如id1001。篡改与重放这是关键一步。在Burp的拦截界面将这个参数的值修改为另一个用户的标识符比如id1002。然后点击“Forward”发送这个被篡改的请求。观察响应立即切换到Burp的“HTTP history”标签页找到刚才发送的请求查看服务器的响应。如果漏洞存在你可能会看到两种成功迹象响应内容变更返回的JSON数据或HTML页面中包含了用户B的姓名、邮箱、手机号等敏感信息。状态码异常虽然返回了“成功”的状态码如200但内容提示“未找到”或为空这可能意味着ID 1002不存在。此时可以尝试一个已知存在的其他ID如1003。注意不要只测试一个ID。应系统性地测试参数边界例如将ID改为0、-1、单引号测试注入、1000一个可能不存在的ID等以探查应用的其他潜在问题如SQL注入或逻辑错误。3.3 漏洞原理与深度挖掘这一关的漏洞根源在于“信任了客户端传来的参数”。服务器端代码可能如下所示伪代码# 错误示例缺乏权限校验 def get_profile(request): user_id request.GET.get(id) # 直接从请求中获取ID user_data db.query(SELECT * FROM users WHERE id %s, user_id) return json_response(user_data)正确的代码必须在查询前加入权限校验# 正确示例基于会话的校验 def get_profile(request): current_user_id request.session.get(user_id) # 从会话获取真实用户ID requested_id request.GET.get(id) if current_user_id ! requested_id: # 关键校验 return error_response(Unauthorized) user_data db.query(SELECT * FROM users WHERE id %s, requested_id) return json_response(user_data)深度挖掘技巧参数位置ID参数不一定在URL里也可能在POST请求的JSON体或表单中。参数名称除了id还有userId、uuid、key等变体。间接引用有时应用使用非数字ID如用户名、邮箱甚至经过编码或哈希的值。你需要先以正常用户身份获取这些值再尝试替换。批量越权如果接口支持传入数组ID如ids[]1001ids[]1002尝试在数组中加入他人的ID看是否都能返回数据。4. 第二关JWT令牌算法绕过垂直越权4.1 JWT基础与漏洞原理第二关难度提升引入了JWTJSON Web Token作为认证机制。JWT是一种紧凑的、自包含的令牌格式为Header.Payload.Signature。Header包含令牌类型和签名算法如{alg: HS256, typ: JWT}经过Base64Url编码。Payload包含声明Claims如用户ID、角色、过期时间等同样经过Base64Url编码。Signature对编码后的Header和Payload加上一个密钥Secret通过Header声明的算法如HS256计算出的签名用于验证令牌的完整性和来源。漏洞原理JWT标准支持一种名为none的算法表示令牌没有签名。如果服务器端的JWT验证库配置不当当收到一个算法为none的令牌时可能会跳过签名验证步骤。攻击者可以伪造一个将alg改为none并在Payload中提升自己权限如将role: user改为role: admin的令牌从而实现垂直越权。4.2 实战利用从发现到绕过识别JWT登录第二关使用Burp抓取任意一个授权后的API请求。在请求的Authorization头或Cookie中寻找形如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxxx.yyyyyy的长字符串这就是JWT。解码分析将整个JWT复制到jwt.io。网站会自动将其拆解并解码。仔细观察解码后的两部分Header确认当前的算法alg通常是HS256或RS256。Payload寻找标识用户身份和权限的字段如user_id、username、role、isAdmin等。记下普通用户的这些值。构造攻击令牌 a. 在jwt.io的右侧编辑区将Header中的alg值修改为none。 b. 在Payload中将权限字段修改为高权限值。例如将role: user改为role: admin或isAdmin: false改为true。注意也可能需要修改user_id为其他用户的ID来实现水平越权。 c. 修改后jwt.io左侧的编码令牌会自动更新。关键点来了由于算法是none签名部分应为空。但JWT格式要求有三部分由点分隔。因此你需要手动删除第三部分Signature但必须保留第二个点。即最终的令牌格式应为[EncodedHeader].[EncodedPayload].注意末尾的点。在某些实现中甚至需要将第三部分完全置空即[EncodedHeader].[EncodedPayload].的签名部分没有任何字符。替换与重放在Burp中将原请求中的JWT替换为你刚刚伪造的、算法为none的新令牌。发送请求。验证结果查看响应。如果返回了本应只有管理员才能看到的数据或操作成功说明漏洞利用成功。4.3 漏洞成因与安全开发建议此漏洞的成因在于服务器端代码使用了不安全的JWT库配置或自定义了有缺陷的验证逻辑。一个危险的伪代码示例如下# 错误示例未校验算法或接受none算法 def verify_jwt(token): parts token.split(.) header decode_base64(parts[0]) payload decode_base64(parts[1]) # 直接从payload中读取算法信任了客户端输入 alg header.get(alg) if alg none: # 如果算法是none竟然直接返回payload return payload else: # ... 其他验证逻辑安全的做法是服务器端必须明确指定期望的算法列表并拒绝none算法# 正确示例使用安全库并指定允许的算法 import jwt def verify_jwt_safe(token): try: # 使用固定的密钥和明确允许的算法列表进行解码验证 payload jwt.decode(token, your-secret-key, algorithms[HS256]) return payload except jwt.InvalidTokenError: return None给开发者的建议永远不要使用none算法。在使用JWT库时始终显式指定algorithms参数而不是依赖库的默认行为。密钥Secret必须足够复杂并安全存储绝不能硬编码在客户端代码中。5. 第三关业务逻辑复合型越权5.1 复杂场景引入与测试思路第三关通常模拟更真实的业务场景漏洞可能不那么直观需要结合多种参数或多个步骤才能发现。场景可能包括多阶段操作如“提交申请” - “管理员审核”。测试是否能在提交阶段篡改数据影响后续审核视图。状态依赖如订单状态从“待支付”到“已发货”。测试是否能在未支付状态下直接调用发货接口。接口参数污染一个请求中包含多个身份标识参数如userId和orderId服务器错误地只校验了其中一个。平行权限绕过用户属于多个角色或部门接口通过检查用户是否在“某个列表”中来授权但列表可被预测或枚举。测试思路需要从“黑盒”转向“灰盒”即不仅要测试参数还要理解业务逻辑流。首先用两个不同权限的账号如普通用户和管理员完整走一遍业务流程用Burp记录下所有请求。然后对比这两个序列的请求差异寻找可能被篡改的“逻辑开关”。5.2 分步测试与逻辑绕过实战假设一个场景普通用户可以“提交工单”管理员可以“审核工单”。工单详情接口为GET /api/ticket/{id}。信息收集用户A提交一个工单ID为T1001。访问详情接口返回数据包含{“id”: “T1001”, “status”: “open”, “creator”: “user_a”, …}。用户B假设是另一个普通用户尝试访问GET /api/ticket/T1001服务器返回403 Forbidden这是正常的水平权限控制。寻找逻辑缺陷观察发现工单列表接口GET /api/my_tickets返回了用户自己的所有工单ID。但审核界面假设只有管理员能访问的URL可能是GET /admin/review_ticket?ticket_idT1001。参数猜测与绕过虽然用户B不能直接访问/api/ticket/T1001但他可以尝试访问/admin/review_ticket?ticket_idT1001。关键点在于服务器端是否在审核接口上重复校验了当前用户既是管理员又拥有该工单的审核权限可能存在以下缺陷缺陷A接口/admin/review_ticket只检查了用户角色是否为“admin”但没有二次校验ticket_id是否属于当前管理员的管理范围。如果普通用户通过某种手段如CSRF、会话固定获取了一个管理员会话他就可以审核任意工单。缺陷B接口/api/ticket/{id}和/admin/review_ticket?ticket_id{id}背后是同一个数据处理函数。该函数可能根据调用来源API路径动态决定是否进行权限校验。如果普通用户直接调用数据处理函数通过参数污染或其他间接方式可能绕过路径层面的校验。实战测试在Burp中以普通用户身份尝试将请求GET /api/my_tickets的响应中的某个工单ID拼接到类似管理员功能的URL或参数中进行访问。同时注意查看请求中是否有隐藏的参数如is_admin0、modeview等尝试修改它们。5.3 漏洞挖掘方法论与防御策略复合型越权的核心是不一致的权限校验。挖掘这类漏洞的方法论可以概括为流程图绘制为关键业务流绘制权限校验点。接口对比横向对比同一数据在不同接口用户端vs管理端的访问方式。状态机测试测试对象在不同状态如草稿、提交、审核、完成下非授权状态变迁是否可能。参数遍历对请求中的所有参数进行增、删、改、测观察对权限校验的影响。防御策略需要从架构和代码层面入手统一权限校验中间件在Web框架的层面设计并强制使用一个统一的权限检查组件。所有业务接口在处理前必须通过该组件进行权限判定。判定依据应基于“用户-角色-资源-操作”的访问控制模型如RBAC。避免客户端控制逻辑诸如“是否管理员”、“是否有权限”这样的标志位必须由服务器端会话或令牌信息决定绝不能依靠客户端传来的参数。服务端状态权威业务对象的状态必须存储在服务端数据库任何状态变更都必须通过服务端的严格逻辑校验不能由前端直接指定目标状态。关键操作日志与审计对所有敏感数据访问和权限变更操作进行详细日志记录便于事后追溯和异常发现。6. 自动化测试思路与工具辅助6.1 使用Burp Suite插件提升效率手动测试越权漏洞虽然有效但效率较低尤其面对大量参数和接口时。Burp Suite的插件生态可以极大提升测试效率Autorize这是测试越权漏洞的“神器”。你需要先配置一个低权限账号如user_low和一个高权限账号如user_high的会话。插件会自动用低权限会话去重放高权限会话捕获到的所有请求并对比响应差异。如果低权限账号也能返回相同或类似的成功数据插件会标记出潜在的越权点。JWT Editor集成在Burp中的JWT工具可以直接在Burp里解码、编辑、重签名JWT令牌无需切换到外部网站支持多种算法对于测试JWT各类漏洞如弱密钥、算法混淆非常方便。Param Miner可以自动发现请求中的隐藏参数、HTTP头等这些参数有时就是权限控制的开关。使用这些插件你可以将测试流程半自动化先手动浏览完所有功能点让Burp记录下流量然后利用插件进行批量测试和扫描最后对插件标记出的可疑点进行人工深度验证。6.2 自定义脚本与模糊测试对于有特定规律或复杂逻辑的越权点可以编写自定义脚本进行测试。例如如果发现用户ID是顺序递增的可以用Python脚本快速遍历一批ID进行请求筛选出成功的响应。import requests session requests.Session() # 设置低权限用户的Cookie或Token session.headers.update({Authorization: Bearer eyJ...}) base_url https://target.com/api/user/ for user_id in range(1000, 1100): resp session.get(f{base_url}{user_id}/profile) if resp.status_code 200 and email in resp.text: print(f[] Potential IDOR found for user_id: {user_id}) print(resp.text[:200]) # 打印部分响应内容模糊测试Fuzzing也可以用于测试参数。将参数值替换为预定义的模糊测试字典字典中包含诸如0、-1、NULL、true、false、其他用户的ID、权限标识等观察服务器的异常响应。7. 漏洞修复方案与代码示例7.1 修复方案总结针对以上三关的漏洞修复的核心原则是服务端做决策不信任任何客户端输入。针对IDOR第一关方案所有对象级访问必须从服务端可信源如会话Session、认证令牌的Payload获取当前用户身份并将其与请求中的资源标识符进行强制比对。代码示例Python Flask:from flask import session, request, abort app.route(/api/profile/int:requested_id) def get_profile(requested_id): current_user_id session.get(user_id) if current_user_id ! requested_id: abort(403, descriptionForbidden: You can only view your own profile.) # ... 后续查询逻辑针对JWT算法绕过第二关方案使用安全的JWT库并显式、强制地指定允许使用的签名算法列表绝对排除none算法。代码示例Node.js, jsonwebtoken库:const jwt require(jsonwebtoken); const secret your-strong-secret-key; function verifyToken(token) { try { // 关键明确指定算法如[HS256] const decoded jwt.verify(token, secret, { algorithms: [HS256] }); return decoded; } catch (err) { console.error(JWT verification failed:, err); return null; } }针对业务逻辑越权第三关方案实现基于角色的访问控制RBAC或属性基访问控制ABAC。在业务逻辑的入口处集中进行权限校验。对于复杂操作使用状态机明确状态转换规则。代码示例权限校验中间件:# 伪代码一个简单的权限检查装饰器 def require_permission(resource_type, action): def decorator(func): def wrapper(*args, **kwargs): user get_current_user() resource_id kwargs.get(resource_id) if not permission_service.can(user, action, resource_type, resource_id): abort(403) return func(*args, **kwargs) return wrapper return decorator app.route(/admin/delete/int:post_id) require_permission(post, delete) # 使用装饰器 def delete_post(post_id): # 业务逻辑此时已通过权限校验 pass7.2 安全开发生命周期SDL融入修复单个漏洞是治标将安全融入开发流程才是治本。建议需求与设计阶段进行威胁建模识别如越权、注入等关键威胁。编码阶段推行安全编码规范使用安全的API和框架进行代码安全扫描。测试阶段将越权测试用例纳入自动化测试如单元测试、集成测试定期进行渗透测试和代码审计。部署与运维阶段配置安全的HTTP头开启详细的访问日志和监控告警。8. 总结与个人实战心得走完玄域靶场这三个关卡你应该对越权漏洞的常见形态、利用手法和根本原因有了更立体的认识。从我个人的经验来看越权漏洞的测试三分靠工具七分靠思维。工具能帮你拦截请求、修改参数、批量测试但发现那些藏在复杂业务逻辑深处的漏洞更需要你对应用功能的理解、对用户角色的揣摩以及一种“如果我是攻击者我会怎么想”的思维模式。在实际项目中我习惯先扮演一个“正常但好奇”的用户把应用的所有功能点都点一遍用Burp把流量录下来。然后重点分析那些带有“ID”、“Key”、“Action”、“Type”等参数的请求。接着准备两个以上的测试账号普通用户、VIP用户、管理员等用对比分析的方法去找差异。最后也是最重要的一步不要满足于找到一个漏洞点。比如你通过改id参数看到了别人的信息接下来要问这个接口是否还有其他参数控制返回字段是否可以通过这个接口影响到其他关联数据这个漏洞模式在其他类似功能的接口上是否也存在这种“顺藤摸瓜”的深度挖掘往往能从一个简单的IDOR发现一片严重的权限失控区域。防御方面给开发团队最务实的建议就一条在数据访问层之上抽象并统一实现一个权限检查网关。所有业务逻辑在触碰数据之前都必须通过这个网关的校验。同时在代码审查和自动化安全测试中把“客户端传入的ID是否与当前用户身份匹配”作为一项必查项。安全是一个持续的过程靶场的练习是为了让我们在真实战场上能更快地识别风险、更准地定位问题、更有效地推动修复。希望这篇结合实战的教程能成为你权限测试工具箱里一件称手的兵器。