
平时做授权测试最磨人的不是漏洞利用而是越权这类逻辑漏洞的批量化验证。一个接口在低权限账号里能正常访问另一个接口换了高权限账号看起来也正常可只要把两个身份互换一下问题就冒出来了。手动在Burp Suite里反复改Cookie、克隆请求、切换标签页一天下来真能让人怀疑人生。这也是我写这个Burp Suite越权检测辅助插件的起因与其每次都靠眼神对比响应不如把Burp自身的扩展接口利用起来让插件自动完成身份切换、请求重放和响应比对把分钟级的手工操作压缩到几秒钟。如果你也是做安全测试、接口测试或研发排查的人应该经常被“用户A能看到用户B的数据却没人觉得有问题”这种场景困住。这个插件不是一个全自动的越权扫描器更准确说它是一个辅助工具帮你把需要多个账号、多轮请求的对比工作流程固化下来让Burp从“抓包改包工具”升级为“能做权限矩阵验证的半自动平台”。下面我会把从设计思路到具体实现、再到实操中踩过的坑都摊开讲想自己写一个的话可以直接照着抄。1. 越权检测的场景与插件定位1.1 越权问题为什么会成为漏网之鱼越权漏洞在现在的Web应用中依然高发主要原因不是开发者不知道要校验权限而是校验逻辑往往放在了“看起来没问题”的地方。比如一个订单详情接口前端根据当前登录用户动态拼参数后端也做了登录校验但忘记判断“这个订单是否属于当前用户”。这种缺陷用常规的SQL注入、XSS扫描器根本探不出来因为请求本身是合法的钓鱼点在于“用了别人的身份发这个请求”。水平越权和垂直越权是两大类。水平越权最常见典型场景是普通用户之间互相访问订单、地址、聊天记录垂直越权则表现为低权限用户能调管理员接口或者普通用户能修改自己的角色。无论是哪一种手工验证都需要至少两个账号一个“受害身份”一个“攻击身份”。在Burp里最原始的做法是登录两个账号分别抓取Cookie然后把两个请求复制到两个Tab里手动替换参数再肉眼看响应。一次两次还行接口一多这个流程的失误率极高很容易把“已修复”和“未修复”搞混。另一个容易被忽略的原因是响应差异的判断。很多开发同学在修复越权时只是把接口返回改成“无权限”但没有使用标准403状态码而是包了一层“业务失败”的JSON。这时候只看状态码或者只看响应长度都可能误判。真正有效的检测必须把“身份切换前后响应中与对象唯一性相关的数据”提出来做语义比较这恰恰是普通扫描器不会做的事。1.2 辅助插件到底解决什么问题我的定位非常明确插件不是要替代渗透测试人员而是把“多身份多请求的重复劳动”自动化。它要解决三个具体问题第一会话管理。把多个账号的Cookie、Token、自定义Header集中管理避免测试时手忙脚乱地复制参数。第二请求改写。基于一个原始请求自动克隆并替换身份相关字段生成“A身份访问”“B身份访问”两个版本。第三响应比对。把两个响应做标准化处理提取关键差异输出是否疑似越权。插件不做的部分也很重要。它不做用户名密码爆破不主动去枚举对象ID也不负责判断业务上“该不该访问”。插件只负责回答一个问题把请求里的身份上下文换成另一个角色后服务端给出的响应和基准响应之间是否存在可感知的差异。最终的确认和利用仍然需要测试人员介入。这样设计的好处是误报可控不会因为自动化而丢失对业务语义的判断。2. 插件设计的三个核心决策2.1 会话矩阵与身份标识方案第一个要设计的是会话怎么组织。越权检测最少需要两个身份实际加上管理员、多租户、多用户可能有五六个身份。如果每个身份都硬编码成变量代码会变得不可维护。我采用了一个简单的“会话矩阵”插件启动时加载一个配置文件里面描述每个身份名称以及对应的认证信息。配置格式大致是这样[role.LOW] cookie: JSESSIONIDAAAA123 header.Authorization: Bearer token_low header.X-User-Id: 10086 [role.HIGH] cookie: JSESSIONIDBBBB456 header.Authorization: Bearer token_high header.X-User-Id: 1这个设计有几个好处。第一不局限于Cookie。很多系统现在用JWT或者自定义Header传递身份尤其在前后端分离的项目里Cookie反而不是主路径。第二可以灵活扩展。要测多账号时直接复制一个[role.LOW2]即可。第三配置和代码分离非开发人员也能维护。身份标识不能只认“登录态”。我见过很多系统在Cookie之外还额外校验一个X-User-Id或者请求体里的userId字段。这种“双重身份标识”如果只替换Cookie插件测出来全是越权因为服务端可能根据参数里的X-User-Id决定返回谁的数据。所以我在会话配置里留了一个“身份关联参数”列表插件在改写请求时不仅会替换认证头还会替换URL参数、POST参数中对应名称的值。2.2 请求改写与重放逻辑请求改写是插件的核心动作。Burp的IExtensionHelpers提供了完整的请求分析能力可以先解析出URL、Header、参数列表再按需修改。具体流程我分了五步从Burp的Scanner调用或右键菜单拿到一个被选中的IHttpRequestResponse对象。使用helpers.analyzeRequest()解析请求头结构。删除原请求中所有认证相关Header包括Cookie、Authorization以及配置中列出的标识Header。从对应身份配置里读取新的Header重新构建请求。如果配置中指定了“身份关联参数名”比如userId则还需要同时替换请求参数中的该字段。重放时必须注意请求体的Content-Type。如果是application/x-www-form-urlencoded或JSON参数在请求体里的位置不一样替换逻辑要区分。大多数项目我会优先用helpers.getRequestParameter()和updateRequestParameter()这套API它能统一处理URL参数和Body参数。如果请求是一个JSON文档字符串替换反而更稳因为JSON里的userId字段名一般是固定的。我写的重放逻辑会生成两个请求一个用“基准身份”访问一个用“目标身份”访问。但这里有一个容易被忽略的细节到底谁作为基准谁作为目标针对水平越权正确做法是“用目标用户身份去访问目标用户自己的资源再用当前用户身份去访问同一个对象ID”。也就是说先把请求中的资源ID换成B的然后用B的会话发送得到基准响应然后用A的会话发送同一个请求得到对比响应。如果A也能拿到B的订单数据说明存在越权。2.3 结果对比与误报收敛响应比对是决定插件可用性的关键。早期版本我直接用响应长度作为差异指标结果误报率接近60%。原因很简单很多接口返回的都是固定结构user_id变了但响应体长度不变或者错误提示和正常数据刚好一样长。后来我把比对引擎改成两层第一层是结构特征提取。先拿响应状态码、Location头、Content-Type然后提取响应体中的“关键对象标识”。比如订单接口的响应里如果有orderId字段插件会把这个字段的值提取出来而不是整段比较。提取规则使用正则或JSON路径配置。第二层是语义归一化。把响应体里的时间戳、随机串、CSRF Token、Session ID全部替换成占位符然后计算两个响应之间的相似度。我用的算法是比较两个字符串的Token重合率加上一个简化版的编辑距离。如果两个响应经过归一化之后高度重合但关键对象标识不同说明接口“透传”了用户数据如果状态码从200变为403那属于“有权限控制”不报告越权。只有当目标身份拿到了基准身份才对返回的关键业务数据时才判定为疑似越权。为了收敛误报我还会做一次“自校验”。把目标身份请求重发两次如果两次响应差异较小说明目标身份本身稳定再将目标身份请求中的对象ID换成一个不存在的ID如果响应仍然返回相同业务数据那这个接口可能压根没做对象校验问题更大但也要人工确认是不是全局缓存或伪静态页面。3. 开发实现基于Burp Extender API的关键代码3.1 工程骨架与菜单挂载我用Java写这个插件因为Burp对Java的原生支持最稳打包成Jar之后放进Extender即可。工程结构很简单只有一个主类实现IBurpExtender、IScannerCheck和IContextMenuFactory。最基础的骨架长这样package com.example.authcheck; import burp.IBurpExtender; import burp.IBurpExtenderCallbacks; import burp.IExtensionHelpers; import burp.IContextMenuFactory; import burp.IScannerCheck; public class BurpExtender implements IBurpExtender, IScannerCheck, IContextMenuFactory { private IBurpExtenderCallbacks callbacks; private IExtensionHelpers helpers; Override public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { this.callbacks callbacks; this.helpers callbacks.getHelpers(); callbacks.setExtensionName(越权检测辅助插件); callbacks.registerScannerCheck(this); callbacks.registerContextMenuFactory(this); callbacks.printOutput([越权检测辅助插件] 加载完成); } }registerScannerCheck注册之后Burp的Active Scanner和Passive Scanner都会调用插件。registerContextMenuFactory能让用户在某个请求上右键选择“发送到越权检测”这样比单纯依赖Scanner更可控。我实际使用中90%的情况下是通过右键菜单发起的因为Scanner自动扫描会同时覆盖大量非目标请求容易干扰判断。3.2 被动扫描监听与主动扫描入口IScannerCheck有两个关键方法doPassiveScan和doActiveScan。被动扫描适合插件盯住特定特征自动触发主动扫描适合对某个自定义插入点进行越权验证。我不想插件对所有流量都做被动重放否则会造成大量并发请求。所以在doPassiveScan里设置了过滤规则只处理包含/api/或/admin/路径、且请求中存在数字参数或userId/orderId/xxxId字段的请求。过滤条件放在配置文件中可随时调整。doPassiveScan的伪代码如下Override public ListIScanIssue doPassiveScan(IHttpRequestResponse baseRequestResponse) { String url helpers.analyzeRequest(baseRequestResponse).getUrl().toString(); ListIParameter params helpers.analyzeRequest(baseRequestResponse).getParameters(); if (!isTargetUrl(url, config.getUrlPatterns())) { return null; } // 只处理存在身份相关参数的请求 SetString identityParams config.getIdentityParamNames(); boolean hasIdentityParam params.stream() .anyMatch(p - identityParams.contains(p.getName())); if (!hasIdentityParam) { return null; } // 根据配置的会话对执行一次跨身份重放 ListIScanIssue issues AuthzReplayExecutor.replay(baseRequestResponse, config); return issues.isEmpty() ? null : issues; }要跑通这个流程还需要实现replay用低权限身份访问高权限对象用高权限身份访问高权限对象再把响应交给比较器。3.3 越权特征提取与响应差异算法响应比较器我单独抽了一个类里面核心是两个方法normalizeResponse(String body)和calcSimilarity(String a, String b)。归一化的作用很直接把响应里的动态内容替换成固定标记否则相同接口在不同时间返回会带上时间戳、随机盐、会话标识导致误判。我维护了一个“动态字段正则列表”可以匹配time: 2025-02-20 10:00:00timestamp: 173...csrfToken: [a-f0-9]{32}jsessionid[A-Z0-9]代码片段public String normalizeResponse(String body) { String normalized body; for (String pattern : config.getDynamicPatterns()) { normalized normalized.replaceAll(pattern, #DYNAMIC#); } return normalized; } public double calcSimilarity(String responseBase, String responseTarget) { String base normalizeResponse(responseBase); String target normalizeResponse(responseTarget); // Token重合度 编辑距离归一化 double overlap 0d; SetString baseTokens new HashSet(Arrays.asList(base.split([\\s,;\]))); SetString targetTokens new HashSet(Arrays.asList(target.split([\\s,;\]))); if (!baseTokens.isEmpty() !targetTokens.isEmpty()) { SetString common new HashSet(baseTokens); common.retainAll(targetTokens); overlap common.size() / (double) Math.max(baseTokens.size(), targetTokens.size()); } int editDistance computeEditDistance(base, target); double similarity overlap * 0.6 (1 - editDistance / (double) Math.max(base.length(), 5)) * 0.4; return similarity; }在实际测试中相似度超过0.8的请求基本可以认为没有越权因为响应内容几乎一致相似度低于0.6但目标身份返回中包含对象ID的请求需要重点排查。注意响应页面很大的时候编辑距离计算会很慢所以我限制最多只取前2000个字符做计算或者先按JSON键排序后再比较。3.4 配置文件的组织与加载配置文件我是用一个简单的键值映射实现的放在Burp插件目录下文件名authz_roles.properties。# 配置文件身份与会话 # 每个role前缀代表一个角色 role.low.cookieJSESSIONIDlow_session_value role.low.header.AuthorizationBearer low_token role.low.header.X-User-Id10086 role.low.ident.paramsuserId,user_id role.high.cookieJSESSIONIDhigh_session_value role.high.header.AuthorizationBearer high_token role.high.header.X-User-Id1 role.high.ident.paramsuserId,user_id # 插件行为控制 target.url.pattern/api/,/admin/ dynamic.patterntime:(.*?)timestamp(.*?)csrf match.param.namesorderId,userId,user_id,id加载配置的方法很简单用java.util.Properties按role.前缀分组。每次启动插件后如果测试环境账号换代只需更新配置文件再点击“重新加载配置”。我把加载入口做进了右键菜单这样不用重新编译Jar。4. 实操中的常见问题与排查技巧4.1 Cookie池与Session失效的连锁反应实测中遇到最多的坑是Session并发失效。插件在检测时会同时发送多个请求有些服务端的安全策略会把短时间内来自同一用户的大量不同来源请求判定为异常进而让Session直接失效。我一开始只配置一个低权限账号、一个高权限账号测到第三个接口后后面所有请求都被踢到登录页响应比对自然完全失真。解决方案是给每个角色维护一个“会话池”。比如低权限角色下配置3个账号插件在重放时从池里随机取一个降低单个Session被风控的概率。同时增加一个“失效检测”如果响应中出现登录跳转、或响应Body里出现“未登录”“登录已过期”等关键词插件会暂停当前角色测试并打印提示让你重新登录账号。限速也很关键。我默认把每个角色的并发请求数限制为2请求间隔至少500毫秒。虽然慢了一点但换来的是测试过程稳定不用一边跑一边盯抓包看session是否过期。4.2 响应对比失真不只是长度的事只对比响应长度这个教训我记得很清楚。某个接口在越权成功时返回的是一串手机号打码后的字符串未越权时返回的是“无权限操作”的固定文案两者的字数恰好一致。用长度对比这个明显越权被直接漏掉了。后来我把比对逻辑改成提取“业务对象特征”。先尝试从响应中提取配置过的参数名比如orderId、userId、mobile再比较提取值是否与请求中使用的对象ID一致。还存在一种情况响应虽然包含对象ID但该值只是请求参数的回显不代表返回了对象数据。这需要人工复核插件只能标记为“可疑”。我还在插件里专门处理了重定向。如果高权限身份访问时返回200数据而低权限身份访问时返回302且Location指向登录页那反而说明权限控制生效应该自动跳过不算越权。只有两个身份都拿到差不多的200数据但数据内容存在差异时才产生告警。4.3 多角色与多租户的配置复杂度很多后台系统不是简单的“用户/管理员”两级而是多租户、多层组织架构。直接上所有角色做笛卡尔积测试请求量会爆炸。举个例子如果系统里有10个租户每个租户有3种角色那就是30个身份两两组合会产生450种情况。我的建议是不要全量跑而是根据业务风险挑有代表性的组合同租户不同用户的水平越权跨租户同角色用户的水平越权低角色到高角色的垂直越权高角色到低角色是否存在信息泄露有些系统高权限会额外返回某些字段但低权限不能看。我整理过一个简单的测试矩阵供配置参考测试场景基准身份目标身份预期结果同租户水平越权租户A用户1租户A用户2目标身份应无法访问用户1资源跨租户水平越权租户A用户1租户B用户2目标身份应无法访问用户1资源垂直越权低到高普通用户管理员普通用户应无法访问管理接口垂直越权高到低管理员普通用户管理员接口是否返回更多敏感字段配置多个角色时插件的会话矩阵会生成一个组合列表并在UI上显示当前进度。这个进度条非常有用否则长跑时你根本不知道卡在哪个请求。5. 从插件到工作流的进一步扩展5.1 把主动验证升级成批量模糊测试当插件的基本检测稳定后我给它加了一个“身份字典”模式。这个模式不再局限于两个角色而是从一个用户列表中循环替换userId等参数逐个重放。这实际上变成了一个小型越权枚举器可以检测到更隐蔽的越权比如“虽然不会直接返回别人数据但会影响他人数据”的写操作越权。写操作越权检测比读操作复杂得多因为重放一个写请求可能产生脏数据。我给插件加了“写操作确认开关”默认关闭。开启后插件会先发送一个创建请求拿到新资源ID然后用其他身份尝试修改或查看这个资源测试完成后再调用删除接口清理数据。这个流程需要在配置里明确指定哪些接口属于创建、查看、修改、删除否则自动化容易把系统数据搞乱。5.2 一些经验沉淀我在实际试用这个插件过程中总结出几条值得记住的经验。第一会话配置一定要在测试环境重新登录获取不要拿一个长期未用的Cookie硬测否则结果几乎不可信。第二越权检测的“业务上下文”比技术实现更重要。比如某个接口返回了别人的手机号但手机号字段做了掩码只显示前三位后四位这种算不算越权要结合业务和数据合规要求判断插件只能帮你把差异摆出来。第三插件产生的告警要保留原始请求和响应快照。我习惯把每次重放的请求、响应完整导出成JSON文件这样即使当轮没有发现问题后续补测或写报告时也能追溯。如果你打算自己动手写建议不要一上来就追求功能完整。先把“两个身份、一个请求、一次重放、一个对比结果”跑通再逐步加配置管理、会话池、多角色矩阵、写操作保护。插件永远是一个辅助工具真正判断越权是否成立的仍然是人对业务逻辑的理解。到目前为止我个人的体会是别指望一个插件能覆盖所有越权场景它最大的价值是把“重复劳动”从你身上卸下来。等你不必再盯着几十对响应发呆时你会发现更多精力可以用来思考那些更复杂的逻辑漏洞比如基于状态机的越权、基于时间窗口的越权、基于异步回调的越权。把基础流程自动化才有空去碰更高级的玩法。