1. 项目概述:一次不经意的越权漏洞挖掘
那天下午,我正像往常一样,对一个内部测试环境的后台管理系统进行常规的功能测试。我的任务很简单,就是验证几个新上线的用户权限管理功能是否正常。我登录了一个普通员工的测试账号,准备查看一下自己的工单列表。就在我点击“查看详情”按钮,浏览器地址栏的URL跳转时,我习惯性地多看了一眼——URL里包含了一个形如order_id=12345的参数。一个再普通不过的请求,但就在那一刻,我心里“咯噔”了一下。我隐约记得,昨天用管理员账号测试时,看到的某个重要订单的ID似乎也是这个数量级。一个大胆的念头冒了出来:如果我手动把这个order_id改成其他数字,会怎样?系统是严格校验了当前登录用户只能查看属于自己的订单,还是会“大方”地展示出所有数据?这次看似不经意的操作,最终演变成了一次完整的越权漏洞挖掘之旅。越权漏洞,这个在安全测试中老生常谈却又无处不在的问题,往往就隐藏在这些最不起眼的交互细节里。
越权漏洞,本质上是一种访问控制缺陷。它指的是应用程序未能对用户访问敏感数据或执行敏感操作进行恰当的授权验证,导致攻击者能够访问或操作本不属于其权限范围内的资源。根据绕过权限检查的方式,主要分为两类:水平越权和垂直越权。水平越权是指攻击者访问了与其同权限等级的其他用户的资源,比如A用户看到了B用户的订单信息;垂直越权则是指低权限用户执行了高权限用户才能执行的操作,比如普通用户删除了管理员的账号。我这次遇到的,正是一个典型的水平越权场景。对于开发、测试甚至运维人员来说,理解并能够挖掘这类漏洞,不仅是提升系统安全性的必要技能,更是构建安全开发意识(Security Mindset)的核心。无论你是前端、后端还是全栈开发者,在代码中不经意间埋下越权隐患的可能性都极高。接下来,我将完整复盘这次挖掘过程,拆解其中的思路、工具、技巧和深层原理,希望能为你点亮一盏灯。
2. 漏洞挖掘的核心思路与常见入口点
2.1 从“信任关系”破裂处入手
挖掘越权漏洞,首先得理解它的根源:服务器过度信任客户端提交的数据。在标准的Web交互中,服务器通过会话(Session)或令牌(Token)来识别“你是谁”,但在处理具体业务请求时,决定“你能干什么”的权限校验逻辑,却常常与“你是谁”的认证逻辑脱节。核心思路就是,寻找所有由客户端可控、且用于标识资源(ID)或操作(Action)的参数,然后尝试篡改它们,观察服务器的响应是否符合预期。
最常见的入口点包括:
- URL参数:这是最经典的入口。例如
/api/user/profile?user_id=1001,/admin/delete?id=55。任何在URL中看到的ID、编号、索引值,都是可疑对象。 - POST请求体(Body)参数:在表单提交或AJAX请求中,如
{"orderId": "2001", "action": "view"}。虽然不像URL那么直观,但通过浏览器开发者工具的网络(Network)面板可以轻易捕获和修改。 - HTTP请求头(Headers):某些应用会将用户身份或权限标识放在自定义头里,如
X-User-Id: 1001。修改这些头可能直接导致权限变更。 - 文件路径或名称:在文件上传、下载或预览功能中,如
/download?file=../config/password.txt(这同时可能涉及目录遍历),或者/api/document/2024_report.pdf,尝试修改文件名或路径指向其他用户的文件。 - 隐藏表单域(Hidden Fields)或Cookie:虽然现代开发较少直接将权限信息放在HTML隐藏域中,但在一些老系统中仍可能遇到。Cookie中的某些键值对也可能被错误地用于权限判断。
我的那次测试就是从URL参数开始的。关键在于,不要只测试“正常流程”,要时刻思考:“如果这个值变了,会发生什么?”这种思维模式需要主动培养。
2.2 水平越权与垂直越权的测试侧重点
虽然测试入口相似,但针对两类越权的测试策略略有不同。
对于水平越权,测试核心在于枚举资源标识符。你需要获取(或推测)两个同权限等级用户的资源ID。
- 操作:使用A用户的身份登录,尝试访问、修改或删除属于B用户的资源ID。
- 技巧:如何获取B用户的资源ID?有时在系统的其他功能处会泄露(如消息列表、公开评论),有时可以通过简单的ID递增(12345 -> 12346)或递减来爆破。在测试环境,你可以直接注册两个测试账号来创造测试条件。
对于垂直越权,测试核心在于直接访问高权限接口或功能点。
- 操作:使用低权限用户(如普通用户)身份登录,然后直接通过URL或请求访问仅高权限用户(如管理员)可见的页面或API接口。
- 技巧:手动拼接管理员后台的URL(如
/admin/user/list);通过爬虫或目录扫描工具发现隐藏的管理接口;观察普通用户页面中是否引用了只有管理员才能加载的JS或API资源(查看页面源码或网络请求)。
在我的案例中,我手头正好有两个测试账号:employee_a和employee_b。我先用employee_a正常创建一个工单,记下系统返回的工单ID,比如10001。然后,我退出登录,再用employee_b账号登录,直接尝试访问/order/detail?id=10001。这就是一个标准的水平越权测试流程。
3. 实战工具链与手动测试流程拆解
工欲善其事,必先利其器。完全依赖自动化扫描器很难发现逻辑复杂的越权漏洞,手动测试配合一些高效的工具,才是王道。
3.1 浏览器开发者工具:你的主战场
现代浏览器(Chrome/Firefox)的开发者工具是手动测试的瑞士军刀,关键面板如下:
网络(Network)面板:这是最重要的面板。确保勾选“保留日志(Preserve log)”。进行任何操作后,这里会记录所有HTTP请求。你需要:
- 找到触发目标功能的请求(通常是XHR/Fetch类型)。
- 右键点击该请求,选择“复制(Copy)”,可以复制为cURL命令、Node.js fetch请求等,方便在其它工具中重放。
- 直接修改“参数(Payload)”或“头(Headers)”,然后右键选择“重放XHR(Replay XHR)”进行测试。这是最快捷的修改重放方式。
控制台(Console)面板:可以执行JavaScript代码,有时用于快速生成或修改测试数据。
应用(Application)面板:查看和修改当前站点的Cookie、LocalStorage、SessionStorage。有时权限标识会存储在这里。
我的实操现场记录: 当我用employee_a查看自己的工单id=10001时,我在Network面板捕获到一个GET请求:GET /api/v1/order/10001,状态码200,返回了完整的工单JSON数据。然后,我右键这个请求,选择“Copy” -> “Copy as cURL”。接下来,我切换到employee_b账号的登录状态,在Console面板中,我新建了一个Fetch请求,将复制的cURL命令稍作修改(主要替换Cookie),直接请求GET /api/v1/order/10001。令人担忧的事情发生了:服务器同样返回了状态码200,并且返回的数据正是属于employee_a的工单详情。漏洞确认!
3.2 专业代理工具:Burp Suite / OWASP ZAP
对于更复杂、需要批量测试或深入拦截修改的場景,专业代理工具不可或缺。它们作为中间人(Man-in-the-Middle),可以拦截、查看、修改所有经过代理的HTTP/HTTPS流量。
- Burp Suite (Professional/Community):行业标准。Repeater模块用于手动修改和重放单个请求;Intruder模块用于参数爆破(例如,批量递增ID);Scanner模块可以进行自动化的主动扫描(但对逻辑漏洞发现能力有限)。
- OWASP ZAP:开源免费,功能强大,是Burp Suite的一个优秀替代品。
使用流程:
- 在浏览器中配置代理(如
127.0.0.1:8080)。 - 在Burp中开启拦截(Intercept is on)。
- 在浏览器中执行正常操作,请求会被Burp截获。
- 在Burp的Proxy -> Intercept标签页下,直接修改请求参数(如ID值),然后点击“Forward”发送。
- 切换到Repeater标签,可以将请求发送过去进行多次、反复的测试,无需在浏览器中重复操作。
注意:测试HTTPS网站时,需要在浏览器中安装并信任Burp Suite或ZAP生成的CA证书,否则会报SSL错误。这是一个关键的安全测试前置步骤。
3.3 漏洞验证与危害证明
发现疑似越权后,不能仅凭一次成功就下结论,需要严谨验证:
- 清除状态干扰:清理浏览器缓存、Cookie,或使用无痕模式,确保测试结果不受之前会话状态污染。
- 交叉验证:用A账号操作B的资源成功后,尝试反向操作(B操作A的资源),以确认漏洞的普遍性。
- 测试写操作:越权读取(信息泄露)固然严重,但越权修改、删除、创建(写操作)危害更大。尝试将请求方法从GET改为POST/PUT/DELETE,修改请求体中的参数,看是否能越权更新或删除他人数据。
- 构造PoC(概念验证):这是报告漏洞时的核心证据。一个清晰的PoC应该包括:
- 漏洞位置:完整的URL和HTTP方法。
- 必要凭证:测试使用的账号(通常是低权限账号)的认证信息(如Cookie、Token)。
- 攻击请求:修改后的恶意请求原始内容(Raw Request)。
- 正常响应与攻击响应:对比截图或响应体,清晰展示越权成功获取了本不应访问的数据或执行了操作。
在我的案例中,我进一步测试了写操作。我捕获了employee_a更新自己工单状态的请求:PATCH /api/v1/order/10001/status,Body为{"status": "processing"}。然后,我用employee_b的凭证,将这个请求中的ID改为10001并重放。服务器竟然也返回了成功!这意味着employee_b可以随意更改employee_a的工单状态,这是一个高危的水平越权漏洞。
4. 后端权限校验的典型缺陷与代码层面解析
漏洞出现在前端交互,但根因一定在后端。我们来深入看看,在代码层面,哪些常见的疏忽导致了越权。
4.1 缺失权限校验:最直接的错误
这是最低级但也最常出现的错误。后端接口只验证了用户是否登录(Authentication),但没有验证当前登录用户是否有权操作目标资源(Authorization)。
缺陷代码示例(Node.js/Express):
// 错误示范:只检查了登录态,未检查资源归属 app.get('/api/order/:id', authenticateToken, async (req, res) => { try { const order = await Order.findById(req.params.id); // 直接通过ID查找 if (!order) { return res.status(404).json({ message: 'Order not found' }); } // !!!缺少了关键一步:检查 order.userId 是否等于 req.user.id res.json(order); } catch (error) { res.status(500).json({ message: 'Server error' }); } });在这段代码中,authenticateToken中间件确保了req.user包含了登录用户信息。但后续查询订单时,直接从数据库用传入的id查找,找到后就返回。这意味着任何登录用户,只要知道订单ID,就能查看。修复方法很简单,在查询后增加一个归属判断:
if (order.userId.toString() !== req.user.id) { return res.status(403).json({ message: 'Forbidden' }); }4.2 基于前端状态的脆弱校验
另一种错误是,后端虽然做了校验,但依赖的是前端上传的、容易被篡改的用户标识。
缺陷代码示例:
// 错误示范:信任了客户端传来的用户ID app.put('/api/profile/update', authenticateToken, async (req, res) => { const { userId, name, email } = req.body; // userId 来自请求体 // 错误逻辑:如果body里的userId和token里的userId一致才更新(但攻击者可以修改body) if (userId !== req.user.id) { return res.status(403).json({ message: 'Forbidden' }); } // 更新用户信息... });这段代码的本意是防止用户修改他人资料,但它用来做比较的userId同样来自客户端请求体req.body。攻击者完全可以在修改自己资料时,将userId参数改为他人的ID,从而通过校验并更新他人资料。正确的做法是永远不要信任客户端传来的任何关于权限判定的标识。资源ID(如订单ID)可以作为查询条件,但最终做权限比对时,必须使用服务器会话中可靠的、不可篡改的用户标识(如req.user.id)与从数据库查出的资源所有者进行比对。
4.3 不安全的直接对象引用(IDOR)
这是越权漏洞的学术名称,指的就是上述这种通过修改参数(如ID)来直接访问数据库对象的行为。防御IDOR的核心原则是:
- 间接引用映射:不使用连续的、可预测的数字ID(如自增主键),而使用随机的、不可预测的UUID或哈希值作为资源标识符公开给前端。但这并非绝对安全,只是增加了攻击者的猜测难度。
- 服务端强制访问控制:这是根本解决方案。在每个涉及资源访问的接口中,必须加入“资源归属校验”逻辑。可以抽象为一个统一的中间件或数据查询方法。
- 基于角色的访问控制(RBAC):对于垂直越权,需要实现完善的RBAC。每个接口或操作都应关联所需的权限点,在请求处理前,校验当前用户角色是否拥有该权限点。
5. 进阶挖掘技巧与场景延伸
掌握了基础方法后,可以尝试一些更隐蔽的越权场景。
5.1 批量操作接口中的越权
很多系统提供批量操作接口,如批量删除消息、批量更新状态。这些接口通常接收一个ID数组。如果后端没有对数组中的每一个ID进行归属校验,就可能出现“部分越权”。
测试方法:构造一个请求,其中ids数组同时包含属于当前用户和不属于当前用户的ID。观察操作是全部成功、全部失败,还是只成功了属于当前用户的部分?如果属于他人的ID也被成功操作,那就是批量越权漏洞。
5.2 基于时间、状态等间接参数的越权
有时权限校验不是直接通过用户ID,而是通过一些间接参数。例如,一个“查看审核中文章”的接口,可能只校验了用户是“编辑”角色,但没有校验当前传入的“文章ID”是否处于“审核中”状态。攻击者可能通过传入一个已发布的文章ID,越权访问了本不该在“审核中列表”里出现的文章。
测试思路:关注所有可能影响业务逻辑的状态参数,如status、type、category、is_public等。尝试修改这些参数,看是否能绕过状态机约束,访问到不同状态下的资源。
5.3 GraphQL API中的越权
GraphQL接口通过单个端点接收查询,越权漏洞可能隐藏在复杂的查询嵌套中。攻击者可能通过精心构造的查询,在一次请求中不仅获取自己的数据,还通过关系字段“顺藤摸瓜”查询到其他用户的数据,如果后端在解析嵌套查询时没有层层施加权限校验的话。
测试工具:使用Altair GraphQL Client或GraphiQL等GraphQL专用客户端。尝试编写查询,遍历不同类型之间的关联关系,观察是否能获取到非授权数据。
6. 修复方案与安全开发建议
挖到漏洞不是终点,如何修复和预防才是关键。
6.1 立即修复方案
对于已发现的越权接口,修复必须遵循“服务端强制校验”原则:
- 统一数据访问层:创建通用的数据访问函数或服务类。例如,一个
getOrderByIdForUser(orderId, currentUserId)方法,它内部会先查询订单,然后比对订单所有者与currentUserId,如果不匹配则直接抛出权限异常或返回空。所有业务逻辑都通过这个安全的方法来获取数据。 - 中间件校验:对于RESTful API,可以为特定资源路由编写权限校验中间件。例如,在
/order/:id路由上挂载一个checkOrderOwnership中间件,它从数据库查出订单并校验归属,校验通过后将订单对象挂载到req.order上,后续的处理器直接使用即可,避免在每个控制器里重复校验。 - 使用安全的框架或库:许多现代Web框架提供了声明式的权限控制机制。例如,在Django中可以使用
@permission_required装饰器和基于对象的权限系统;在Spring Security中可以使用@PreAuthorize注解结合SpEL表达式。充分利用框架能力,比手动编写校验更可靠。
6.2 长期预防与安全开发流程
- 安全需求与设计评审:在项目设计阶段,就必须明确每个接口、每个功能的访问控制矩阵(谁、在什么条件下、能对什么资源、进行什么操作)。将权限校验作为功能需求的一部分写入文档。
- 代码审计与结对编程:将权限校验代码作为代码审查(Code Review)的重点检查项。鼓励开发者在编写数据查询代码后,互相审查是否遗漏了归属或角色校验。
- 自动化测试覆盖:编写专门的安全单元测试和集成测试。例如,为每个关键接口编写测试用例,分别用用户A和用户B的凭证去访问对方的数据,断言这些请求应该失败(返回403状态码)。将这些测试集成到CI/CD流水线中。
- 定期渗透测试与漏洞奖励:除了内部测试,可以引入外部的安全专家进行渗透测试,或者建立漏洞奖励计划,鼓励白帽子帮助发现潜在问题。
- 默认拒绝原则:在权限系统的设计上,采用“默认拒绝,显式允许”的策略。即除非明确配置了允许访问,否则一律拒绝。这比“默认允许,发现问题再修补”要安全得多。
7. 常见问题与排查技巧实录
在实际挖掘和修复过程中,你会遇到各种奇怪的现象。这里记录一些我踩过的坑和总结的技巧。
问题1:修改参数后,服务器返回了“数据不存在”(404),这是否意味着没有漏洞?不一定。这可能是一种“盲越权”。你需要对比操作自己资源(返回200)和操作他人资源(返回404)的响应时间、响应大小或细微的差异。有时,返回404是因为ID确实不存在于数据库,你可以尝试使用一个已知存在的他人资源ID(例如,从系统其他公开页面获取)。如果返回404,说明后端可能先做了存在性查询,发现不属于当前用户后,统一返回404以隐藏信息,这比直接返回他人数据要好,但仍可能通过时间差等侧信道泄露信息。
问题2:测试时,直接修改ID后请求,服务器返回了“无效参数”或校验错误。检查参数是否被签名或加密。有些应用会对关键参数(如ID)进行哈希(如HMAC)或加密处理,防止篡改。你需要分析前端生成请求的JavaScript代码,看参数是如何构造的。如果存在签名,你需要破解或绕过签名算法,这通常难度较大,但也并非不可能。
问题3:使用Burp Repeater重放请求时,总是返回“会话过期”或“未登录”。这通常是因为会话(Session)或令牌(Token)与原始请求绑定,而Repeater重放时没有携带正确的会话状态。确保你复制的是登录状态下的请求。在Burp的Repeater中,检查“Cookie”头是否正确携带。对于JWT等Token认证,检查“Authorization”头。你可以先浏览器正常操作,用Burp拦截到一个成功请求,然后将整个请求(包括所有Header)发送到Repeater,这样最可靠。
问题4:发现了越权漏洞,但不知道如何评估其危害等级。危害评估主要看两点:受影响的数据/操作的重要性和利用难度。
- 高危:能越权访问或操作核心敏感数据(如用户身份信息、支付数据、管理后台配置)、能导致资金损失、能获取系统控制权(垂直越权到管理员)。利用简单,无需复杂条件。
- 中危:能越权访问一般敏感数据(如订单记录、通讯录)、能执行敏感但非核心的操作。可能需要一定的条件(如知道目标ID)。
- 低危:越权访问的信息敏感性很低(如公开文章的草稿)、影响范围极小。或者利用难度极高。
问题5:开发人员说“我们用了RBAC框架,所以不会有越权”。这是一个常见的误解。RBAC(基于角色的访问控制)主要解决的是“垂直越权”问题,即控制不同角色能访问哪些菜单或功能模块。但它通常不解决“水平越权”问题,即同一角色下的用户之间的数据隔离。数据级别的权限校验(Data-Level Access Control)需要在业务代码中额外实现。所以,即使用了RBAC,每个涉及用户数据的接口,依然需要做资源归属校验。
最后,我想分享一点个人体会:挖掘越权漏洞,技术工具只是辅助,最重要的是一种“不信任”的思维习惯——不信任前端传来的任何用于权限判断的参数,不信任用户会按照你设计的流程走。每次你写下一行从数据库查询数据的代码时,都应该下意识地问自己:“我这里校验用户权限了吗?” 作为开发者,养成这个习惯,能从源头杜绝大量漏洞;作为测试者,拥有这个思维,能让你像黑客一样思考,发现更深层次的安全问题。安全之路,始于每一行代码的审慎,成于每一次测试的质疑。