目录
一、前言
二、知识梳理:IDOR与水平越权
三、漏洞复现
一、前言
在 Web 安全领域,访问控制失效(Broken Access Control)常年稳居 OWASP Top 10 的第一位。它不像 SQL 注入、命令执行那样"炫技",却是真实业务系统中最常见、也最容易被开发者忽略的一类问题——接口本身工作正常、功能也完全正常,唯独少了一行"这个资源到底是不是你的"的校验。
其中,水平越权(IDOR,Insecure Direct Object Reference)是最典型的一种:服务端直接用客户端传入的对象标识(自增 `id`、订单号、文件名……)去数据库里取数据,却不校验该对象是否归属于当前登录用户。攻击者只要把参数里的数字改一改,就能读到、甚至改掉别人的数据。
本文记录的是我在一次授权测试中发现的一个真实案例:某高校主办的中文科技期刊投稿采编系统,普通作者登录后,仅需篡改"撤稿"请求中的稿件 `id` 参数,就能查看并撤回(删除)其他作者的投稿稿件
二、知识梳理:IDOR与水平越权
1、认证 ≠ 授权
认证(Authentication):解决"你是谁",比如登录、Cookie、Token;
授权(Authorization):解决"你能动什么",比如"这篇稿子是不是你的"。
很多系统只做了认证,却默认"登录了就是自己人",于是在授权环节留下缺口。
2、垂直越权 vs 水平越权
垂直越权:低权限用户干了高权限用户才能干的事,如普通用户调用了管理员接口;
水平越权:同一权限级别的用户之间越界,A 能操作 B 的数据——本文要讲的就是这一种。
3、IDOR(不安全的直接对象引用)
当服务端接口直接使用"客户端可控的、可预测的对象标识"来定位数据,并且**不做归属校验**时,就构成 IDOR。典型特征就是:URL 或表单里出现 `id=123`、`orderNo=2025xxx`、`file=xxx.pdf` 这类参数,改一下就能访问到别人的资源。
三、漏洞复现
5.1 环境与工具准备
测试账号:自建两个普通作者账号(下文称账号 A、账号 B),各自投稿一篇文章,用于构造"跨用户"场景;
思路很简单:用账号 A 的身份,去操作账号 B 的稿件。如果成功,则越权成立。(一般测试情况下,不允许更改数据库中的任何数据,为了方便复现,尽量准备两个账号)
5.2 步骤一:两个账号,各自投稿
先登录账号 A 和账号 B,分别进入作者中心的"已投稿件"页面并各投一篇稿,用于后续对比与验证。
从图中可以看到,稿件列表中出现了两条稿件记录:稿号 `202502005`(投稿时间 2025/2/16 18:31:49)和稿号 `202502006`(标题 `qweqwe`,投稿时间 2025/2/16 18:39:45),状态均为"正在初审"。这两篇稿子分属两个不同的账号,正是我们要用来验证越权的"靶子"。
5.3 步骤二:定位越权点——撤稿功能里的 `id`
进入"已投稿件"后,我注意到列表里提供了"撤稿"功能。点开稿件详情 / 撤稿操作时,一个关键细节出现了:
稿件详情页地址为 `/author/ScriptInfoEditor.aspx?id=6154`
撤稿请求地址为 `/author/AuthorCG.aspx?id=6153&scriptname=qwe`
`id` 是稿件编号,直接以明文形式出现在 URL 中,而且很可能是自增的。
这就是 IDOR 的经典信号——对象的引用标识(`id`)由客户端直接提供。此时只需问一句:服务端有没有校验这个 `id` 属于当前登录用户?接下来就是验证。
5.4 步骤三:拦截并篡改 `id`
开启 Burp 的拦截功能,在账号 A 下点击"撤稿",可以看到两个请求包都携带了 `id` 参数。按照测试思路,我把两个请求包中的 `id` 分别修改为想要操作的目标稿件 id(即账号 B 的稿件 id)。
以其中一个请求为例(已脱敏,仅保留结构):
```http
POST /author/ScriptInfoEditor.aspx?id=6153 HTTP/1.1
Host: www.xxx.edu.cn
Cookie: ASP.NET_SessionId=******; CUSTOMASPXAUTH=******
Content-Type: application/x-www-form-urlencoded
Content-Length: 7378
Cache-Control: max-age=0
Origin: https://www.xxx.edu.cn
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.61
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Sec-Fetch-Site: same-origin
.......
```
要点只有两个:请求携带的是账号 A 的合法会话(`ASP.NET_SessionId` / `CUSTOMASPXAUTH`),而 URL 里的 `id` 被换成了账号 B 的稿件编号。如果服务端只看 `id` 不看归属,那么"用 A 的会话操作 B 的稿件"就会成功。
5.5 步骤四:第一次放行——先观察现象
放行第一个请求后,页面确实产生了变化,但只是列表里的标题发生了变化,URL 中的 `id` 并没有变成我们想操作的那个 id。
这说明:单次放行并没有让操作彻底生效,该功能实际上涉及多个请求的先后配合(一个负责回显/选中,一个负责真正提交)。于是继续对第二个请求包做同样处理。
复盘小记:越权测试时,不要因为"第一次没成功"就放弃。很多业务功能的写操作是"分步提交"的,需要把整条链路都改到位。观察响应差异(标题变了但 URL 没变),正是判断"还差一步"的依据。
5.6 步骤五:确认撤稿并二次拦截
接着点击"确定撤稿",在弹窗中填写撤稿原因,同时开启拦截。此时抓到的请求为:
```http
POST /author/AuthorCG.aspx?id=6154&scriptname=qwe HTTP/1.1
Host: www.xxx.edu.cn
Cookie: ASP.NET_SessionId=******; CUSTOMASPXAUTH=******
Content-Type: application/x-www-form-urlencoded
Content-Length: 3919
Origin: https://www.xxx.edu.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.61
```
可以看到,请求中的 `id` 已经是我们修改后的目标稿件 id。这一步是"临门一脚",确认了提交阶段同样没有再做归属校验。
5.7 步骤六:放行——越权撤稿成功
再次放行,页面提示发送信息成功,并且地址栏中的 `id` 已经变成了目标稿件的编号。
至此证明:账号 A 在只持有自己合法会话的情况下,成功撤回(删除)了账号 B 的稿件。水平越权成立。
整个利用链可以浓缩为一句话:
登录任意作者账号 → 发起撤稿 → 把请求里的 `id` 换成目标稿件 id → 放行 → 他人稿件被删除。
四、漏洞原理分析
结合抓包结果,可以从几个层面解释这个漏洞为什么成立。
1、服务端只做"认证",没做"授权"
系统使用 ASP.NET Forms 认证(`CUSTOMASPXAUTH` 票据)确认"请求来自一个已登录用户",但在涉及稿件操作的接口中,没有校验"这篇稿件是否属于当前会话对应的用户"。这是最根本的原因。
2、直接对象引用 + 可预测的主键
稿件 `id` 直接以明文形式出现在 URL 查询串中,且为连续/可预测的数字。攻击者无需猜测、无需爆破,只要把数字改一改,就能命中别人的稿件。这是 IDOR 的"便利条件"。