校园系统越权漏洞实战挖掘:从IDOR到垂直越权的完整攻防解析
1. 项目概述:一次典型的校园系统越权漏洞挖掘实录
最近在和一些刚入行的朋友交流时,发现大家对“越权漏洞”这个概念既熟悉又陌生。熟悉是因为这个词在各类安全报告里出现频率极高,陌生是因为很多人不知道在实际渗透测试中,如何系统性地去发现和利用它。正好,前段时间我参与了一次针对某中学内部管理系统的授权安全测试,整个过程堪称一次教科书式的越权漏洞挖掘案例。这个案例非常典型,涉及到的系统架构、业务逻辑和漏洞成因,在很多中小型企事业单位、教育机构的系统中都能找到影子。今天,我就把这个过程完整地复盘一遍,从踩点、信息收集、漏洞发现、利用链构造到最终的报告,希望能给想入门漏洞挖掘,特别是逻辑漏洞挖掘的朋友们提供一个清晰的实战思路。
这次的目标是一个中学的“智慧校园综合管理平台”。这类系统通常集成了学生信息管理、成绩查询、教职工办公、后勤报修等多个模块,用户角色复杂(学生、教师、班主任、教务处管理员、校领导等),数据敏感,一旦出现越权,后果可能非常严重。我们的任务就是模拟攻击者,在不具备高权限账户的情况下,尝试访问或操作本不该有权限的数据和功能。
2. 前期侦察与目标分析
2.1 目标系统画像勾勒
在开始任何测试之前,盲目地“戳端口、扫目录”效率很低。我们需要先给目标画个像。通过公开信息搜集,我们了解到这个平台是一个B/S架构的Web应用,采用常见的“前端+后端API”模式。前端是Vue.js,后端是Java(Spring Boot框架),数据库是MySQL。这些信息可以通过查看网页源代码中的注释、引入的JS/CSS文件、HTTP响应头(如X-Powered-By: Spring Boot)甚至一些报错信息获得。
更重要的是业务逻辑分析。我们以访客身份浏览了系统的登录页和找回密码等公开页面。从页面文字和功能描述中,我们梳理出系统可能存在的几类角色:
- 学生:查看个人信息、课表、成绩、提交作业。
- 教师:管理所教班级的学生、录入成绩、发布通知。
- 班主任:管理本班学生详细信息、审核请假条。
- 教务处管理员:管理全校学生、教师档案,设置课程,权限最高。 系统采用“用户ID+密码+验证码”登录,登录后跳转到统一的仪表盘,再通过侧边栏菜单进入各功能模块。
2.2 关键入口点探测
越权漏洞的核心在于“权限校验的缺失或失效”。因此,我们的侦察重点在于找出所有涉及“对象标识符”的接口。所谓对象标识符,就是URL或请求参数中,用于唯一指定某个数据对象的ID,例如:
/api/student/info?id=1001(查询ID为1001的学生信息)/api/grade/update?examId=205&studentId=1001(更新某次考试中某个学生的成绩)/api/leave/approve?leaveId=58(审批ID为58的请假条)
我们的策略是,在获得一个低权限账号(比如一个普通学生账号)后,系统地遍历这些接口,尝试修改这些ID参数,访问其他用户的数据。首先,我们需要找到一个可用的低权限账号。对于教育系统,有时会有公开的演示账号,或者通过“忘记密码”功能结合社工技巧(但需在授权范围内)来推测账号格式(如学号、工号)。在本案例中,我们通过测试方提供了一个测试用的学生账号:stu_20230001。
登录后,我们立即打开浏览器的开发者工具(F12),切换到Network(网络)标签页,并勾选“Preserve log”(保留日志)。然后,在页面上进行常规操作:点击查看“我的信息”、查看“我的成绩单”、申请“请假”等。所有前端发往后端的HTTP请求都会在这里一览无余。
注意:很多现代前端应用使用API接口,数据通过AJAX请求异步加载,页面的URL可能不会变化。因此,观察网络请求比看浏览器地址栏更重要。
3. 漏洞挖掘过程:水平越权与垂直越权的发现
3.1 水平越权(IDOR)的快速验证
水平越权(Insecure Direct Object References, IDOR)是最常见的越权类型,即用户A可以操作属于用户B的同类数据对象。我们首先从最直观的“个人信息查询”接口入手。
在“我的信息”页面,网络请求中捕获到这样一个请求:
GET /api/personal/v1/profile?userId=20230001 HTTP/1.1 Host: school.example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...响应返回了该学生的姓名、班级、学号、身份证号(脱敏)、联系方式等。
这里,userId=20230001就是关键的对象标识符。它很可能就是我的当前登录账号stu_20230001对应的用户ID。那么,如果我把它改成20230002(假设是同班同学),服务器会返回谁的数据?
我们使用Burp Suite的Repeater模块来重放这个请求。将请求中的userId参数值从20230001修改为20230002,其他头部(特别是Authorization令牌)保持不变,发送。
结果:服务器返回了状态码200 OK,并且响应体里完整包含了学号为20230002的学生的个人信息!
漏洞确认:系统后端在处理/api/personal/v1/profile接口时,只验证了用户是否登录(即Authorization令牌是否有效),但没有校验当前登录的用户ID是否与请求的userId参数一致。这就导致了任何登录用户都可以通过遍历userId来获取其他用户的敏感信息。这是一个典型的水平越权漏洞。
3.2 垂直越权与功能滥用挖掘
水平越权往往伴随着更危险的垂直越权可能。垂直越权是指低权限用户获得了高权限用户的功能。我们继续深挖。
在教师端,有一个“成绩录入”功能。我们当然没有教师账号,但我们可以观察学生账号相关的成绩查询接口。我们发现一个查询某次考试详细成绩的接口:
GET /api/grade/v1/detail?examId=101&classId=12 HTTP/1.1这个接口看起来是根据examId(考试ID)和classId(班级ID)来查询整个班级的成绩列表。那么,是否存在一个“更新/录入”单个学生成绩的接口?
通过爬取前端JS文件(或者通过模糊测试工具对/api/grade/目录进行探测),我们发现了疑似接口:
POST /api/grade/v1/update HTTP/1.1 Content-Type: application/json {"examId": 101, "studentId": 20230001, "course": "math", "score": 95, "comment": "优秀"}这是一个POST请求,用于更新成绩。我们用自己的学生账号尝试直接向这个接口发送请求(哪怕是把我的数学成绩从80改成95)。结果返回了403 Forbidden,提示“权限不足”。这说明接口有做权限校验,学生角色不能访问。
但是,校验是否彻底?我们注意到,成绩管理模块里,教师也只能修改自己所教班级的学生成绩。那么,如果有一个教师账号,他能否修改其他班级、其他教师名下的学生成绩呢?这属于教师角色内部的水平越权,也可能是一种“功能滥用”式的垂直越权(如果系统本意是严格隔离)。
我们转换思路。在“请假申请”模块,学生可以提交请假条,然后由班主任审批。流程如下:
- 学生提交请假申请:
POST /api/leave/v1/apply - 生成一条请假记录,状态为“待审批”,并有一个唯一的
leaveId,例如 58。 - 班主任审批接口可能是:
POST /api/leave/v1/approve?leaveId=58&action=approve
我们以学生身份,尝试调用审批接口(即使我们不知道确切的URL,可以尝试常见路径如/api/leave/approve,/api/leave/audit)。使用Burp Intruder对leaveId进行遍历,同时猜测action参数(approve,reject,pass,agree等)。
结果:我们发现了接口/api/leave/v1/audit。当我们将请求方法从GET改为POST,并携带参数leaveId=58&status=1(假设1代表通过)时,服务器返回了200 OK,并且前端显示该请假条状态变成了“已通过”!
漏洞分析:这是一个严重的垂直越权漏洞。系统在/api/leave/v1/audit接口上,只校验了用户是否登录,但没有校验登录用户的角色是否为“班主任”,更没有校验该班主任是否是请假学生所在班级的班主任。导致任何登录用户(包括学生)都可以审批任何请假条。这完全破坏了审批流程的业务逻辑。
3.3 参数污染与接口组合拳
单一的越权可能危害有限,但组合起来就能形成强大的攻击链。例如,我们利用之前的信息查询越权,可以遍历userId获取全校学生的学号和姓名列表。然后,利用请假审批越权,可以为他们批量“批准”请假条,造成管理混乱。
更进一步,我们发现了“消息通知”接口。教师可以向指定班级发送通知。接口如下:
POST /api/msg/v1/sendToClass HTTP/1.1 Content-Type: application/json {"classId": 12, "title": "考试通知", "content": "明天上午数学考试。"}我们尝试用学生账号调用此接口。返回403,权限校验生效。但是,我们注意到发送成功后,前端会跳转到消息列表页,列表页查询接口是:
GET /api/msg/v1/list?classId=12&page=1&size=20这个list接口,学生有权限访问吗?测试发现,可以!学生可以查看发送给班级12的所有通知。那么,如果classId参数存在水平越权呢?我们将classId改为其他班级的ID,例如11。成功返回了班级11的所有内部通知,其中可能包含教学安排、会议纪要等敏感信息。
这里暴露了两个问题:1)列表接口存在水平越权;2)更重要的是,权限校验的不一致性。系统禁止低权限用户“写”(发送通知),却允许其“读”(查看通知),而“读”的接口又未做好对象级权限校验。这种设计缺陷非常普遍。
4. 漏洞原理深度剖析与修复方案
4.1 为什么会出现越权漏洞?
根本原因在于“信任前端传递的参数”和“权限校验的粒度不足”。我们来看看后端伪代码可能是什么样子:
漏洞代码示例(查询个人信息):
@GetMapping("/api/personal/v1/profile") public ResponseEntity getUserProfile(@RequestParam Long userId) { // 1. 从JWT令牌中解析出当前登录用户的ID (currentUserId) Long currentUserId = getCurrentUserIdFromToken(); // 2. 直接根据前端传来的userId查询数据库 UserProfile profile = userService.getProfileById(userId); // 漏洞点! // 3. 返回结果 return ResponseEntity.ok(profile); }这段代码的问题在于,它从令牌中解析了currentUserId,但后续查询完全没有使用它!它盲目地相信了前端传来的userId参数。正确的做法应该是将查询条件与当前用户绑定,或者至少进行比对。
正确的代码应该如下:
@GetMapping("/api/personal/v1/profile") public ResponseEntity getUserProfile(@RequestParam Long userId) { Long currentUserId = getCurrentUserIdFromToken(); // 方案A:强制绑定当前用户(如果接口设计就是查自己) // userId参数甚至可以去掉,直接使用currentUserId UserProfile profile = userService.getProfileById(currentUserId); // 方案B:如果接口设计允许查他人(如老师查学生),则需进行角色和业务关系校验 if (!securityService.canViewUserProfile(currentUserId, userId)) { // 校验当前用户是否有权限查看目标用户的资料 // 例如,老师只能查看自己班级的学生 throw new AccessDeniedException("无权查看该用户信息"); } profile = userService.getProfileById(userId); return ResponseEntity.ok(profile); }对于审批接口的漏洞,问题类似:
@PostMapping("/api/leave/v1/audit") public ResponseEntity auditLeave(@RequestParam Long leaveId, @RequestParam Integer status) { // 缺少对当前用户角色的校验! LeaveRecord leave = leaveService.findById(leaveId); leave.setStatus(status); leaveService.update(leave); return ResponseEntity.ok("审批成功"); }缺少了关键的步骤:判断当前用户是否为班主任,并且是该请假条所属学生的班主任。
4.2 修复方案建议
给开发团队提供可操作的修复建议,比单纯报告漏洞更重要。我通常会从三个层面给出方案:
1. 代码层修复(立即执行):
- 强制会话绑定:对于查询、修改、删除等操作,后端必须从可信的会话(如JWT token)中获取主体标识(用户ID、角色),并将其作为数据库查询的必要条件。例如,
SELECT * FROM orders WHERE user_id = ? AND order_id = ?。 - 引入统一的权限校验框架:在Spring Boot中,可以使用Spring Security的
@PreAuthorize注解或自定义AOP切面,在方法执行前进行细粒度校验。
@PostMapping("/api/leave/v1/audit") @PreAuthorize("@permissionService.canAuditLeave(#leaveId)") public ResponseEntity auditLeave(@RequestParam Long leaveId, @RequestParam Integer status) { // 业务逻辑 }其中permissionService.canAuditLeave方法实现了复杂的业务规则校验。
2. 架构层优化(中期规划):
- 实施RBAC(基于角色的访问控制)与ABAC(基于属性的访问控制)结合:RBAC控制功能菜单访问(如“审批请假”按钮是否显示),ABAC控制数据级权限(如“只能审批本班学生的请假条”)。
- API网关统一鉴权:在请求到达业务服务之前,由网关对令牌有效性、接口黑白名单进行初步校验,减轻业务服务压力。
- 后端对前端隐藏真实ID:使用无意义的UUID或加密后的令牌代替数据库自增ID作为资源标识符,增加攻击者猜测和遍历的难度。但请注意,这并非银弹,核心还是服务端的校验。
3. 安全开发流程(长期建设):
- 将越权检查纳入代码审查清单:在CR环节,重点审查所有涉及资源ID操作的接口。
- 定期进行专项安全测试:在测试阶段,使用自动化工具(如Burp Suite的Autorize插件)配合手动测试,专门针对越权漏洞进行扫描。
- 安全意识培训:让开发人员深刻理解“永远不要信任客户端传来的任何用于权限判断的参数”。
5. 实战中的技巧与避坑指南
5.1 信息收集阶段的技巧
- 不要只看“显式”参数:除了URL中的
?id=,更要关注POST请求的JSON/XML body、Cookie甚至HTTP头部中可能存在的标识符。例如,X-User-Id: 1001这样的自定义头。 - 关注批量操作接口:像
/api/users/batchDelete、/api/data/export?ids=1,2,3这类接口,一旦越权,危害呈指数级放大。 - 利用JS文件映射API路由:前端Vue/React应用通常有
router.js或api.js文件,里面定义了所有接口路径,是宝贵的“地图”。
5.2 漏洞利用与验证的注意事项
- 遵守测试范围:绝对不要测试授权范围以外的系统或功能。本次测试仅限于指定的“智慧校园平台”。
- 使用无害的POC:验证越权时,尽量执行“读”操作,避免“增删改”。例如,验证信息查询越权,比验证删除用户越权更安全。如果必须“写”,也要使用测试账号或构造无实际影响的数据。
- 注意请求顺序和状态依赖:有些操作需要前置状态。例如,审批请假条需要请假条处于“待审批”状态。你需要先利用一个账号创建这个状态(如学生提交请假),再用另一个账号去测试越权审批。
- Burp Suite插件推荐:
- Autorize:自动化越权测试神器。配置好低权限和高权限账号的Cookie/Token,它能自动重放所有请求并对比响应,快速找出差异。
- Param Miner:可以自动发现那些不常见的、可能被遗漏的参数(如
uid,key,ref等)。
5.3 报告撰写要点
一份好的漏洞报告能帮助开发团队快速理解并修复问题。我的报告通常包含:
- 漏洞标题:清晰明了,如“智慧校园平台个人信息查询接口存在水平越权漏洞”。
- 风险等级:根据CVSS标准或内部规范评定(如高危、中危)。
- 漏洞详情:
- 请求URL:完整的HTTP请求。
- 请求参数:指出存在问题的参数(如
userId)。 - 攻击步骤:一步一步描述如何复现(1. 使用账号A登录;2. 捕获查询请求;3. 修改参数为B的ID;4. 成功获取B的信息)。
- 漏洞证明:附上成功利用的截图或响应包关键部分。
- 漏洞原理:简要分析后端代码可能的问题所在。
- 修复建议:提供具体的代码修改方案或安全配置建议。
- 影响范围:评估受影响的功能模块、用户群体和数据量。
6. 从SRC视角看教育系统漏洞挖掘
本次挖掘的中学系统,属于教育行业SRC(安全应急响应中心)关注的范畴,比如EDUSRC。教育系统的特点使得它成为漏洞挖掘的热点:
- 资产复杂:历史遗留系统多,新老系统并存,框架从ASP、PHP到Java、Python都有,安全水平参差不齐。
- 数据敏感:包含大量学生、家长、教师的个人身份信息、联系方式、成绩、档案等,数据价值高。
- 用户安全意识薄弱:师生群体庞大,普遍网络安全意识不强,弱口令、默认口令问题严重。
- 开发运维投入有限:很多学校系统由外包公司开发,追求功能实现而忽视安全,且后期维护不足。
针对教育系统的漏洞挖掘,除了常规的越权、注入、XSS,还可以特别关注:
- 统一身份认证系统(单点登录SSO):这是整个数字校园的入口,一旦突破,全线崩溃。
- 在线考试系统:是否存在时间绕过、答案泄露、分数篡改等逻辑漏洞。
- 家校通/移动APP:APP的API接口安全、数据传输加密、客户端反编译风险。
- 文件上传功能:成绩单、作业、通知附件上传处,往往是获取Webshell的突破口。
对于想入门SRC漏洞挖掘的朋友,我的建议是:从逻辑漏洞开始。相比于需要深厚经验的二进制漏洞或复杂的Web漏洞(如SSRF、反序列化),逻辑漏洞(尤其是越权)更依赖于对业务的理解和耐心的测试,门槛相对较低,但产出和价值非常高。找一个目标,像侦探一样去梳理它的业务流程,思考“如果我是开发者,我会在哪里做校验?”,然后去验证这些校验是否真的存在且有效。这个过程,本身就是一次极佳的学习和实战锻炼。
