越权漏洞自动化检测:Burp扩展插件从原理到实战
凡是做过业务系统安全测试的应该都有这种体会越权漏洞在带用户体系的系统里几乎年年上榜无论是SaaS平台、后台管理系统还是对外API服务只要存在用户身份和资源归属的概念就绕不开这一类问题。我去年在某电商后台做安全评审时一个订单详情接口就同时出现了水平越权和垂直越权两种形态修复优先级直接拉满。但比发现问题更头疼的是测试过程本身准备两三个账号手工切换身份逐条替换Cookie重放请求再对着响应猜结果。接口一多这种手工操作根本撑不住所以我花了几个晚上写了一个越权检测辅助插件跑通之后把检测流程从“不停复制粘贴”变成了“录一段流量、按一下按钮、看一份报告”。这篇文章就把这个插件的完整设计思路、核心实现和踩坑过程拿出来聊聊。适合正在做Web安全测试、API安全评审或者想给团队配一套低成本越权检测方案的同学参考。1. 越权检测为什么难自动化问题本身比工具复杂1.1 水平越权和垂直越权本质上是同一件事很多人一上来就把越权分成水平、垂直两类去研究这当然没错但从自动化检测的角度看它们背后是同一种验证逻辑在身份A产生的请求中把身份凭证替换成身份B再观察响应结果是否还属于A能访问的数据范围。水平越权是两个同级别用户之间互相访问资源。比如普通用户A能查看自己的订单详情如果把请求里的用户身份换成普通用户B接口依然返回订单数据那这个漏洞就成立了。垂直越权则是低权限用户访问高权限接口。例如普通账号直接调用管理员的后台导出接口返回了敏感数据列表。把这两个场景抽象出来就得到一条非常简洁的检测公式基线请求 身份替换 响应对比 是否越权。所谓基线请求就是真实用户操作时发出的原始HTTP请求身份替换是把请求中与用户绑定的一切凭证换掉响应对比则是判断替换身份之后的响应与基线响应是否高度相似。相似度越高说明越权的可能性越大。插件自动化检测的核心就是把这个公式完整落地。1.2 人工测试的四个效率瓶颈我见过不少人做越权检测方式基本是浏览器登录账号A复制关键请求再登录账号B手动改一下Cookie或Authorization头粘贴重放最后肉眼对比响应页面。这套流程有几个非常明显的问题。身份切换是第一个痛点。改Cookie要小心翼翼多头Cookie容易漏改、错改如果系统用的是动态签名或短时Token切换到另一个账号之后签名已经失效请求直接被打到登录页白白浪费时间。请求量大是第二个痛点。一个后台系统几十个接口每个接口又有不同的参数组合手工一个个替换身份是巨大的体力活。更麻烦的是用户主动访问的请求仅仅是基线的一部分很多接口是页面异步加载的需要从抓包记录里人工筛选出所有带身份特征的请求稍微走神就会漏掉关键接口。动态参数是第三个痛点。CSRF Token、时间戳、随机数、签名串这些参数在每次请求中都会变化。直接用另一个身份去重放可能因为Token失效被网关拦截导致误判为“不存在越权”产生严重的漏报。响应结果是第四个痛点。有些系统对未授权访问返回统一的JSON错误比如{code:401,msg:login timeout}有些系统则直接返回登录页HTML。如果检测逻辑只是简单对比状态码和请求长度这类情况几乎全部会误判成“越权存在”。更麻烦的是有些接口正常响应里包含当前登录用户的昵称、手机号等信息身份替换后这些字段必然不同又不能简单认为“响应不同就不是越权”。这四个瓶颈其实就是越权检测插件要解决的核心问题。1.3 为什么做成Burp扩展而不是独立脚本做插件之前我也考虑过直接用Python写一个独立脚本加载抓包导出的HTTP记录再批量重放。这条路能走但有几个绕不过去的短板。Burp Suite本身是所有代理流量的集中点扩展直接通过IHttpListener接口监听经过代理的请求和响应不需要额外写抓包导出导入这套流程。也方便测试人员在真实操作过程中随时标记请求、补录接口。对比响应时如果要查看某个可疑请求的完整报文直接从Burp的Proxy History里打开即可调试体验顺畅很多。独立脚本则要自己维护数据格式、自己管理会话在测试高峰期反而增加切换成本。它还天然继承了Burp的断点、Match and Replace、Session Handling等能力比如某条请求需要单独修改时可以直接在Proxy里操作不用让插件去兼容各种边界情况。而且最终的检测报告可以直接从Burp UI面板导出测试人员截图贴工单都很方便。所以这个插件选择了Burp扩展这个形态本质上就是把“重复劳动”从人的手里拿掉同时保留安全测试本身需要的灵活判断空间。2. 插件设计思路三个模块凑成一条越权检测流水线2.1 功能拆解基线录制、身份替换、响应对比、报告输出插件最终被我拆成了四个核心功能基线录制、身份管理、批量重放与响应对比、报告导出。基线录制负责收集测试者在代理中产生的所有HTTP请求及其响应默认记录完整请求头、请求体、响应状态码和响应体摘要。身份管理维护一组“命名身份”每个身份对应若干账号登录态Cookie、Authorization头、额外Header等。测试者在代理中切换账号并正常浏览页面时插件可以自动抓取该账号的凭证并绑定到指定身份上省去手动粘贴Cookie的步骤。批量重放是插件的核心动作。选择一组基线请求和一个“替换身份”后插件会把每条请求中的身份凭证替换成目标身份发送到原目标服务器并把返回的响应存下来。响应对比模块则会对基线响应和重放响应做相似度计算输出每个接口的判定结果。报告导出部分会生成一张相对结构化的表格包括请求URL、状态码、响应长度、相似度、判定结果和建议关注程度选中即可复制到工单或消息里。2.2 执行流程从账号A浏览到账号B身份重放整个使用流程是一条清晰的单向流水线。第一步用账号A登录系统打开代理后正常浏览要测试的功能模块插件自动记录基线请求。第二步在插件面板中添加身份B随后在浏览器中切换到账号B并保持登录状态。第三步点击“开始检测”插件读取全部基线请求批量替换身份B凭证后重放。第四步在结果面板中按相似度和状态码排序人工确认重点接口导出报告。这个过程里有个容易被忽略的设计插件不要求用户“先录A再切B那么严格”但推荐按这个顺序执行。因为基线的响应是判定相似度的基准如果基线本身就不是A的真实响应比如A还没登录时的302跳转那后面所有对比都会失真。所以我给插件加了一个“录制状态”标记录制过程中遇到非200/201等异常响应会把该请求标灰提示测试者检查是否录到了登录页、验证码页等无效请求。2.3 关键设计决策用响应相似度而不是完全相等最早我用的判定逻辑是“响应完全相同则标记越权”上线后误报和漏报都很严重。原因很简单大部分业务接口的响应都带时间、随机数、登录用户名、会话标识等动态内容即便同一个人连续请求两次响应也不可能逐字节相同反过来携带登录态但被正常拦截时系统返回统一的401错误页面又恰巧和基线响应完全一致就会产生误报。所以我把判定逻辑改成了响应相似度。具体做法是取响应中的Body部分过滤掉明显动态的字段如时间戳、随机数、用户昵称等再计算两个响应的相似度结合状态码和响应长度做加权判断。这样只要替换身份后的响应与基线响应的结构、关键内容高度一致我默认阈值设在0.8以上就认为该接口存在越权风险再由测试者人工确认。这个“相似度比较而不是精确比较”的取舍是插件从“只能检测最简单场景”进化到“能在真实业务里用起来”的最重要一步。3. 核心实现这几个代码细节决定了检测好不好用3.1 扩展骨架监听代理流量是第一步我用Jython实现在Burp里通过Extender加载。整个扩展的骨架就是实现IBurpExtender和IHttpListener两个接口。registerExtenderCallbacks里注册回调processHttpMessage里对经过代理的请求和响应分别做处理。这段代码是最基础的框架核心是toolFlag判断只处理来自Proxy工具的流量避免把Scanner或其他工具的请求也录进基线。messageInfo对象可以同时取到请求和响应的原始字节配合helpers就能解析出头、Body、Cookie等信息。from burp import IBurpExtender, IHttpListener from java.util import List, ArrayList class BurpExtender(IBurpExtender, IHttpListener): def registerExtenderCallbacks(self, callbacks): self._callbacks callbacks self._helpers callbacks.getHelpers() callbacks.setExtensionName(越权检测辅助插件) callbacks.registerHttpListener(self) print([] AuthZ Checker loaded - 越权检测辅助插件 ready) self._monitoring False # 是否在录制基线 self._base_requests [] # 基线请求列表 def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): if toolFlag ! self._callbacks.TOOL_PROXY: return if messageIsRequest: if not self._monitoring: return request messageInfo.getRequest() if request is None: return analyzed self._helpers.analyzeRequest(messageInfo) url analyzed.getUrl() headers analyzed.getHeaders() body request[analyzed.getBodyOffset():] # 这里可以做过滤然后存入基线列表 self._base_requests.append({ host: url.getHost(), port: url.getPort(), https: str(url.getProtocol()).startswith(https), method: analyzed.getMethod(), path: url.getPath(), headers: list(headers), body: str(body), resp: None })Jython只支持Python 2.7语法所以写代码时不能用到f-string、海象运算符这些新特性。这是我最初踩过的一个坑写完导入之后直接报语法错误后来老老实实全部改成传统的格式化写法。3.2 基线采集哪些请求该留哪些该过滤基线采集不能眉毛胡子一把抓。真实代理流量里至少有一半是静态资源CSS、JS、图片、字体文件这些请求既不需要替换身份也不值得重放录进来只会拖慢检测速度并制造大量噪声。过滤器必须处理掉这一类请求。还需要过滤掉几个特殊接口登录接口、登出接口、验证码接口、密码重置接口。登录接口不涉及资源越权替换身份后反而会因为多因素校验导致一堆无关异常登出接口重放之后会直接注销测试账号的会话影响后续所有检测。所以在processHttpMessage里我加了一个简单的URL分类函数默认跳过静态资源和上述认证类接口同时允许用户在面板上配置自定义排除规则。另一个细节是对响应体存储的处理。有些接口响应体可能很大全部存下来既占内存又影响对比速度我默认只保留响应体和响应头丢弃二进制内容并且对超过2MB的响应体只保存前2MB。这个限制在大多数业务系统里都够用但如果有文件导出类接口建议把阈值调大否则相似度计算结果会失真。3.3 身份替换Cookie、Authorization和动态Token怎么处理身份替换是整个插件里最容易翻车的地方。最初版本我只做了一件事找到请求头里的Cookie字段整体替换成目标身份对应的Cookie。看起来很简单实际跑起来就露馅了。一个常见情况是系统同时支持Cookie和Authorization两种鉴权方式用户登录后请求里可能两者都带或某些接口只带Authorization。如果只替换Cookie重放后系统依然用的是旧Authorization头里的身份检测等于白做。所以替换逻辑必须同时覆盖Cookie、Authorization、以及X-Token这类常见自定义凭证头。第二个问题是CSRF Token。很多写操作接口的请求体或请求头里带_token、csrf_token字段这个Token和当前登录会话绑定。替换成账号B的身份后如果Token没跟着换请求大概率被网关以“CSRF校验失败”拦截。但如果目标是检测越权我们不能简单地把Token也替换掉因为越权检测的核心是“保持资源路径不变只换身份”Token改变会导致请求语义改变。我的处理策略是插件在录制阶段单独记录每个响应里出现的Token字段名和值检测阶段如果发现请求体里存在这类字段就尝试用目标身份对应响应中新获取的Token替换如果找不到目标身份的对应Token则把这条请求标记为“Token待人工处理”在结果里提示可能因为CSRF拦截导致误判。这是我认为对自动化检测影响最大的一步——如果不加这层处理大量写操作接口会被漏掉加了之后至少能把问题明确暴露出来。3.4 响应相似度计算阈值、长度差和特征字段响应相似度计算我选了difflib.SequenceMatcher。它自带对序列的匹配能力计算两个文本块的相似度非常方便而且Jython自带这个库不需要额外引入第三方依赖。代码大体这样import difflib def calc_similarity(body_a, body_b): if not body_a and not body_b: return 1.0 if not body_a or not body_b: return 0.0 return difflib.SequenceMatcher(None, body_a, body_b).ratio() def is_high_similar(ratio, len_a, len_b): if ratio 0.8: return True if ratio 0.5: # 长度接近的时候中高相似度也可以重点观察 max_len max(len_a, len_b) if max_len and abs(len_a - len_b) / float(max_len) 0.25: return True return False只用相似度还不够我把响应长度差和HTTP状态码也纳入判定。优先条件是基线状态码是200重放状态码是200且相似度超过0.8标记为“高危疑似”基线状态码是200重放状态码是302或401通常说明身份隔离生效标记为“正常”如果重放状态码是302但相似度依然很高就需要重点看一下是不是统一跳转页面造成的假象。这个算法对JSON接口的效果最好因为结构化数据里字段顺序和键名相对稳定即使数值不同也能匹配出高相似度。对HTML页面差一些因为页面包含大量导航、脚本、样式内容这些占了相似度的主要权重反而让真正变化的业务数据区被稀释。针对HTML接口我后续又加了一个可选的“关键区域”模式让用户用正则指定响应里需要关注的部分计算时只对这一区域做相似度。比如userId:\d、订单号字段这类模式对后台管理系统的检测特别实用。4. 实操过程半小时跑完一轮越权检测4.1 准备工作测试账号、代理配置、扩展加载实际检测前需要准备两样东西测试账号和Burp代理。测试账号至少准备两个。我习惯用一个“账号A”充当数据所有者用“账号B”充当越权访问者。如果系统还有角色权限之分建议再准备一个低权限普通账号。测试数据也要提前规划账号A名下要有真实订单、个人资料、消息通知这类可被观察的资源否则替换身份后接口返回空列表无法和基线响应做对比检测结果可信度会大幅下降。代理配置上浏览器直接挂Burp的代理即可确保代理启动并打开了HTTP/HTTPS拦截记录。然后加载插件Burp主界面点Extender - Extensions - Add语言选Python选择插件脚本文件加载成功后日志区会打印启动消息。注意Jython环境需要提前配好否则加载会直接报错。4.2 五步完成一轮检测从采集基线到导出报告第一步把插件面板里的录制开关打开然后用账号A登录系统按功能模块逐页点过去。这个阶段的核心原则是“覆盖比深度重要”宁可把所有菜单都过一遍也不要在一个功能上反复刷新。我一般会打开系统的网络面板确认主要接口都已经被代理记录而不仅仅是页面框架加载完成。第二步在插件面板中添加账号B身份。此时浏览器切到账号B登录登录成功后在面板上点击绑定当前会话插件会把当前代理中出现的身份凭证捕获下来绑定到该身份。如果系统有记住登录设备、多端互踢之类机制建议关闭这些设置否则账号B登录会把账号A的会话踢掉基线请求的身份凭证也跟着失效。第三步关闭录制点击“开始检测”。插件会读取之前保存的基线请求列表逐一替换成账号B身份再重放。这个过程可能会因为系统响应慢而持续一段时间我一般会让它跑完中途不要手动去代理里翻记录避免干扰。第四步等待检测结束后在结果面板里按相似度降序排列。重点看相似度大于0.8、并且结果码是200的条目这些大概率就是越权点。对相似度不高但请求路径长得像业务接口的也值得单独点开看响应内容。第五步勾选要汇报的接口点击导出。导出内容默认是一张Markdown表格包含请求路径、基线状态码、重放状态码、相似度、判定结论和“是否包含动态参数未处理”的标记。这张表可以直接贴给研发他们能看明白问题在哪沟通效率会明显提升。4.3 结果怎么读判定规则与报告示例我用一个简化的结果表格展示插件的输出样式请求路径基线状态码重放状态码相似度判定/api/order/detail?id1001user20012002000.96疑似水平越权/api/user/profile2003020.15身份隔离正常/api/admin/export/userlist4032000.87疑似垂直越权/api/message/read?msg_id88122002000.42动态字段过多需人工确认第一行是典型水平越权基线是账号A查自己订单替换成账号B后依然返回了订单详情相似度0.96基本可以确认。第三行是垂直越权基线请求被403拦截说明账号A本身没有管理员权限但替换成账号B低权限普通账号后竟然返回200这里账号B在测试里被我配置成的是另一个低权用户实际场景里通常还会用一个管理员身份去验证这行的关键点是“低权限账号访问高权限接口成功”。第四行相似度只有0.42因为响应里带用户昵称和未读数动态字段多插件没法自动判定需要打开报文确认。这个结果表的可读性相当关键。最初版本我直接把相似度数字丢给测试者对方根本不知道0.42意味着什么。后来我加了“判定结论”这一列用自然语言描述“疑似越权”还是“身份隔离正常”报告的可读性立刻上来了。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因解决办法插件加载后无任何请求被记录代理工具过滤未开启或监听端口不对确认浏览器代理端口和Burp一致且toolFlag判断的是代理流量重放时大量请求返回302到登录页目标身份Cookie未绑定成功或并发把对方会话踢下线检查身份绑定是否成功重新登录目标账号再绑定一次写操作接口全部被CSRF拦截请求中带Token但目标身份的Token未更新打开“Token自动提取”开关或把这些接口标记为人工确认响应相似度普遍偏低响应里包含大量动态字段时间戳、随机数、当前用户信息开启“关键区域”模式用正则限定对比范围检测结果全是高危但实际没有越权登录页或统一错误页内容相似度过高在排除规则中加入登录页、错误页、公共静态接口地址5.2 降低误报率的几条实战经验误报是越权检测插件最大的敌人。一旦误报多了测试者就会失去对结果的信任最终插件沦为摆设。我实测下来最有效的几条经验是排除规则一定要先写清楚。系统有统一的登录页缓存和错误提示页先手动访问一次各个路径把它们的URL特征提取出来加进排除列表。比如某系统的未授权响应是/login?redirect开头这类特征排除掉就不会进来了。不要只对比整个响应体。JSON接口按业务字段对比HTML接口按“主内容区域”对比效果远好于整体相似度。我上面提过的“关键区域”模式在这里价值最大设置正则匹配订单明细、用户列表等核心数据区之后检测准确率能从六成提到九成以上。关注响应里“只属于某个用户”的个人信息字段。很多系统的响应模板会渲染当前登录用户的手机号、姓名、头像地址替换身份后这些字段必然会变导致相似度被拉低。如果测试目标本身就是为了验证“用户B不能看用户A数据”那这部分动态变化其实恰恰是“隔离生效”的证据。处理办法是把这类字段提取出来单独对比如果除了用户信息字段外其余内容高度一致说明资源本身已经暴露属于越权风险如果整体结构都变了那大概率是隔离正常。5.3 一次典型的误报排查过程有一个具体案例让我印象很深。某后台管理系统接了一个附件下载功能插件检测报告里标记了十几个接口为“疑似越权”。我打开重放报文一看基线响应和替换身份后的响应几乎相同相似度确实都在0.85以上。但仔细观察发现这个系统对未授权访问会返回统一的提示页页面内容只有一行“您无权访问该资源”其余全是公共布局。重放请求虽然同样进了这个提示页但并非真的获取到了资源。这就是典型的错误页误报。解决办法是在响应对比前增加一道拦截层识别“拒绝访问”“无权访问”“未授权”这类关键词。如果替换身份后的响应包含这些关键词即使相似度很高也先标记为“疑似拦截待人工确认”而不是直接判定越权。之后我在检测逻辑里加入了负面关键词过滤这个附件下载模块的误报率瞬间从80%降到了10%以内。同样思路也适用于登录页误报只要重放响应里出现“登录”“请输入密码”“验证码”等特征词就先标为“可能被拦截”避免直接归到越权结果里。这个排查过程给我的启发是自动化检测的难点从来不在于发请求而在于怎么让程序理解响应的语义。越权检测插件本质上是把“无法理解语义”的部分通过规则、特征词、动态字段过滤逐步缩小剩下的判定再交给安全测试人员。插件可以帮你从1000条请求里筛选出20条重点关注对象但不能代替你判断那20条到底算不算漏洞。我个人在实际使用中始终把插件定位成“越权初筛器”而不是“越权自动判官”。碰上新系统、新框架第一轮跑出来的结果一定会有各种意外把误报特征整理好、加进规则库第二轮、第三轮跑起来就会越来越顺手。最后再分享一个小技巧检测完成后记得顺手把相似度特别低但存在高权限接口路径的请求也翻出来看看很多垂直越权并不会返回与基线相似的响应体而是直接输出一份全新的数据列表这种情况单靠相似度算法抓不到但测试人员扫一眼路径和状态码却能很快发现。安全测试终究是半自动化的活儿工具把重复劳动做掉剩下的判断力才是测试者真正的价值所在。