核心内容摘要
“靠逼”APP:打破边界,点燃你的无限可能
分开篇明义 —— 定义、价值与目标定位与价值在Web应用安全的世界里越权漏洞Broken Access Control 长期位居OWASP Top 10的榜首。
它并非一种单一的、具体的技术缺陷而是一类因访问控制策略缺失或错误执行导致用户可以执行其本无权执行的操作的逻辑缺陷集合。
当我们将目光聚焦于现代复杂的业务交互——如购物下单、审批流程、数据管理——时会发现一个更具迷惑性和破坏性的子类多步流程中的越权漏洞。
此漏洞之所以危险且常被忽视是因为它巧妙地隐藏在多个看似独立的HTTP请求组成的“事务”之中。
开发者往往只在关键操作如最终的“提交”、“付款”、“确认”请求上进行权限校验而忽略了前置的、用于准备数据或状态的步骤如“选择商品”、“填写地址”、“加载草稿”。
攻击者通过拆解业务流程精确操控其中某一非终态步骤的请求即可非法读取、篡改或注入属于其他用户的数据最终污染整个业务流程的最终结果。
理解、识别并防御此类漏洞是从“功能测试”思维迈向“逻辑安全测试”思维的关键一步也是现代渗透测试工程师的核心竞争力。
学习目标读完本文你将能够阐述多步流程中越权漏洞的核心成因、业务场景与潜在危害。
识别与发现复杂业务流程中的潜在越权点使用系统化方法进行黑盒与灰盒测试。
利用手动与自动化工具完成从信息收集、漏洞探测到完整攻击链构建的实战演练。
分析并实施从架构设计、代码开发到运维监控的多层次纵深防御策略。
连接此漏洞与横向移动、权限提升等更深层次攻击路径构建体系化的访问控制安全认知。
前置知识· HTTP协议基础理解GET、POST等请求方法以及URL参数、请求体的概念。
· 会话与会话管理理解Cookie、Session、Token如JWT在识别用户身份中的作用。
· 基础的越权漏洞了解水平越权访问同权限等级的其他用户资源和垂直越权访问更高权限等级的功能的基本概念。
分原理深掘 —— 从“是什么”到“为什么”核心定义与类比多步流程中的越权漏洞是指在一个需要多个HTTP请求才能完成的业务操作序列如“创建订单”涉及
加载商品页
提交订单
支付中由于应用未在一个或多个非最终步骤上正确验证当前用户是否有权操作其所提交或请求的数据从而导致权限被滥用的安全缺陷。
生动比喻装配线与质检员想象一个手机定制装配线分为三步步骤A选择手机颜色、步骤B选择内存容量、步骤C最终封装并贴上客户地址标签。
· 安全的逻辑在步骤C质检员会核对该手机的完整定制单源自A和B与当前正在流水线上操作的工人用户是否有权处理该订单。
所有步骤的数据关联性被最终校验。
· 存在漏洞的逻辑质检员只在步骤C工作。
那么一个恶意工人可以在步骤A将原本属于客户张三的“深空灰”订单偷偷替换成客户李四订单中的“银色”部件。
由于步骤A和B无人检查这个“选择”动作是针对哪个订单的这个被污染的部件会一路流转。
尽管步骤C的质检员看到的是一个张三的订单但里面的部件颜色已经被非法篡改。
漏洞就发生在前置的、非最终的“选择”动作缺乏权限校验。
根本原因分析此漏洞的根源在于业务逻辑的复杂性与安全校验的滞后性/单一性之间的矛盾。
具体体现在以下几个层面设计层面状态管理与权限验证脱节· 开发者将多步流程视为一个“黑箱”只关注输入和输出认为只要最终提交时校验了权限整个流程就是安全的。
他们错误地假设流程中每一步的状态如“当前正在编辑的文档ID”、“已选中的商品列表”会天然且安全地与当前登录用户绑定。
· 问题根源HTTP协议是无状态的。
每一步请求中携带的标识如 document_id123 cart_item_id456是应用识别操作目标的唯一依据。
如果应用在接收到这些标识时不立即验证“当前用户是否有权操作123号文档或456号购物车商品”那么权限校验就出现了空窗期。
实现层面对象引用与权限校验分离· 代码结构上常见的反模式是一个负责“获取数据”的服务或控制器方法只根据传入的ID从数据库取出对象而权限检查被放在了另一个“处理业务”的方法中或者仅在最终的“保存/提交”动作中调用。
· 伪代码示例危险模式// 步骤1加载文档编辑页面存在越权GetMapping(“/editDocument”)publicDocumentloadDocument(RequestParamLongdocId){// 危险直接根据ID查找未校验用户权限DocumentdocdocumentRepository.findById(docId);returndoc;// 攻击者传入他人文档ID即可看到内容}// 步骤2提交文档修改有校验PostMapping(“/saveDocument”)publicResultsaveDocument(RequestBodyDocumentdoc){// 此处有校验if(!permissionService.canEdit(currentUser doc.getId())){return“无权限”;}// ... 保存逻辑}· 攻击者可以完全绕过步骤2只调用步骤1实现数据读取越权。
即使他调用步骤2也可能因为在步骤1加载了错误的数据而导致后续逻辑混乱。
协议与交互层面对客户端状态过度信任· 应用将关键的对象ID、状态标识存储在客户端如隐藏表单域、URL参数、JavaScript变量中并默认客户端会“老实”地按流程顺序提交这些值。
攻击者可以通过拦截代理如Burp Suite轻易地修改这些客户端状态将其指向其他资源。
可视化核心机制以下Mermaid序列图清晰地展示了一个典型的三步流程中安全与不安全两种设计下的数据流与权限校验点差异。
AS数据库服务端客户端(浏览器/代理)攻击者AS数据库服务端客户端(浏览器/代理)攻击者**场景存在漏洞的三步流程 (以编辑文档为例)**漏洞点未校验权限仅根据ID查询校验点终于校验权限但可能基于错误状态攻击者目标已达到在步骤1已完成越权信息读取。
**场景安全的三步流程 (权限校验前置)**安全点立即校验权限
开始编辑他人文档(IDVictimDoc)请求GET /load?docIdVictimDocSELECT * FROM docs WHERE idVictimDoc返回他人文档数据返回他人文档内容(已越权读取)显示他人文档
篡改内容并提交请求POST /save?docIdVictimDoc {content: “恶意内容”}检查: user can edit VictimDoc? → 通常失败返回: “权限不足”
尝试编辑他人文档(IDVictimDoc)请求GET /load?docIdVictimDoc检查: user can view/edit VictimDoc?返回: “权限不足” (请求被早期阻断)图释左侧漏洞流程中权限校验严重滞后只在最终保存时导致第一步“加载”就产生了越权读取。
右侧安全流程中每一步涉及资源引用的操作都立即进行权限校验实现了早期安全阻断。
分实战演练 —— 从“为什么”到“怎么做”环境与工具准备· 演示环境我们搭建一个名为 “SecureCollab” 的简易在线文档协作系统。
它包含用户、文档以及一个关键的多步功能创建文档副本 (Fork)。
· 步骤1用户A访问自己文档 Doc_A 的“创建副本”页面。
· 步骤2系统加载 Doc_A 的内容到编辑器中并展示一个表单让用户填写新文档标题。
· 步骤3用户提交表单系统创建新文档 Doc_A_Copy内容来自 Doc_A。
· 漏洞存在点步骤2加载文档内容时未验证当前用户是否有权读取传入的 source_doc_id。
· 靶场启动 (使用 Docker Compose)# docker-compose.ymlversion:‘
8’services:web:image:vulnapp/securecollab:
0# 这是一个模拟的漏洞镜像ports:-“8080:8080”environment:-SPRING_PROFILES_ACTIVEdev-DB_URLjdbc:mysql://db:3306/securecollab-DB_USERappuser-DB_PASSWORDapppassdb:image:mysql:
0environment:-MYSQL_ROOT_PASSWORDrootpass-MYSQL_DATABASEsecurecollab-MYSQL_USERappuser-MYSQL_PASSWORDapppassvolumes:-mysql_data:/var/lib/mysqlvolumes:mysql_data:启动命令docker-compose up -d。
访问 http://localhost:8080。
· 核心工具· Burp Suite Professional / Community EditionHTTP流量拦截、重放、篡改。
· 浏览器任意现代浏览器用于正常业务操作。
· 自定义Python脚本用于自动化探测。
标准操作流程发现/识别业务流建模与关键请求定位操作登录系统用户: alice 密码: alice123。
创建一个文档 AliceDoc 内容为”Alice’s Secret Plan”。
登录另一个账户用户: bob 密码: bob123。
创建一个文档 BobDoc 内容为”Bob’s Public Info”。
以 alice 身份对 AliceDoc 执行“创建副本”操作。
打开Burp Suite 开启代理拦截 完成整个Fork流程。
观察与分析在Burp Suite的Proxy - HTTP history中你会看到类似请求序列GET /document/fork?sourceDocId1001 // 步骤1进入副本创建页 GET /api/document/loadContent?docId1001 // 步骤2加载源文档内容 (关键请求!) POST /document/doFork // 步骤3提交创建副本 {“sourceDocId”: 1001 “newTitle”: “My Copy”}思考/api/document/loadContent?docId1001 这个请求是否在返回 AliceDoc (ID
的内容前验证了 alice 有读取权限我们需要测试。
利用/分析权限校验缺失验证与越权读取操作在Burp Suite中找到 GET /api/document/loadContent?docId1001 这个请求。
右键 - Send to Repeater。
切换到 Repeater 标签页。
现在我们篡改这个请求尝试读取 Bob 的文档。
我们需要知道 BobDoc 的ID。
可以通过以 bob 身份查看其文档URL或通过信息枚举猜测如 1002。
将请求中的 docId1001 修改为 docId1002。
点击 Send。
预期结果与解释· 如果返回成功HTTP 200并且响应体中包含了 ”Bob’s Public Info” 恭喜你发现了水平越权读取漏洞服务器在加载内容时只认docId没有关联当前会话用户 alice 进行权限检查。
· 如果返回失败HTTP 403/404 说明此处有权限校验是安全的。
但不要气馁继续检查流程中的其他请求如第一步的 /document/fork?sourceDocId 是否会泄露元数据。
在本环境中你将得到成功响应。
这意味着 alice 非法读取了 bob 的文档内容。
验证/深入构建完整攻击链与影响最大化仅仅读取内容可能不是最终目标。
让我们思考如何将此漏洞与后续步骤结合产生更大的破坏。
场景越权副本数据污染与窃取目标让 alice 创建一个副本但副本的内容不是来自她自己的文档而是来自 bob 的文档。
正常流程中断如果我们只修改步骤2的请求步骤3的 POST /document/doFork 请求中仍然包含 sourceDocId1001 服务器可能会在最终步骤校验导致失败。
完整链路攻击我们需要同时污染步骤2和步骤3。
· 在Burp Proxy拦截状态下重新走一遍 alice 对 AliceDoc (
的Fork流程。
· 当请求 GET /api/document/loadContent?docId1001 被拦截时 将其修改为 …docId1002。
放行。
· 当最终的 POST /document/doFork 请求被拦截时 将其中的JSON body {“sourceDocId”: 1001 …} 修改为 {“sourceDocId”: 1002 …}。
放行。
观察结果。
如果系统设计糟糕最终校验也只检查alice是否有权fork一个文档而fork权限可能对所有公开文档开放或者再次缺失校验那么 alice 将成功创建一个新文档其内容完全来自 bob 的 BobDoc 实现了数据的非法复制和状态污染。
自动化与脚本手动测试效率低。
以下Python脚本使用requests库自动化探测一个接口是否存在此类ID参数越权。
它通过替换不同的ID进行尝试。
#!/usr/bin/env python3 多步流程越权探测器 - 针对ID参数 警告仅用于授权测试环境。
importrequestsimportsysimportargparsefromurllib.parseimporturlparse parse_qs urlencodedeftest_idor(target_url session_cookie param_name test_ids method“GET” dataNone): 测试给定URL和参数是否存在越权访问。
:param target_url: 待测试的URL :param session_cookie: 有效的会话Cookie (e.g. ‘sessionabc123’) :param param_name: 要测试的参数名 (e.g. ‘docId’) :param test_ids: 要尝试的ID列表如 [1002 1003] :param method: HTTP方法 ‘GET’ 或 ‘POST’ :param data: 如果是POST 附加的固定表单数据 (dict) headers{‘Cookie’:session_cookie ‘User-Agent’:‘IDOR-AutoDetector/
0’}fortest_idintest_ids:try:ifmethod.upper()“GET”:# 解析URL 替换参数parsedurlparse(target_url)query_dictparse_qs(parsed.query)query_dict[param_name][str(test_id)]# 替换为目标IDnew_queryurlencode(query_dict doseqTrue)new_urlparsed._replace(querynew_query).geturl()resprequests.get(new_url headersheaders timeout
else:# POSTifdataisNone:data{}# 注意这里简单覆盖参数实际情况可能需处理嵌套JSONdata[param_name]test_id resprequests.post(target_url headersheaders datadata timeout
print(f“[{method}]Testing{param_name}{test_id}-Status:{resp.status_code} Length:{len(resp.text)}”)# 简单启发式判断状态码为200且长度与首次不同或包含特定内容可能成功# 实际应用中需要更精细的差异比较如使用 difflib。
ifresp.status_code200:# 这里可以添加更智能的内容分析逻辑print(f” Potential success! Check responsefordata leakage.”)# 可选将响应保存到文件# with open(f’response_{test_id}.html’ ‘w’) as f:# f.write(resp.text)elifresp.status_codein[403404]:print(f” Access deniedornotfound.”)else:print(f” Unexpected status.”)exceptrequests.exceptions.RequestExceptionase:print(f” Error testing ID{test_id}:{e}”)if__name__“__main__”:parserargparse.ArgumentParser(description“多步流程IDOR漏洞探测脚本”)parser.add_argument(“-u” “--url” requiredTruehelp“目标URL”)parser.add_argument(“-c” “--cookie” requiredTruehelp“会话Cookie字符串”)parser.add_argument(“-p” “--param” requiredTruehelp“要测试的参数名”)parser.add_argument(“-m” “--method” default“GET”help“HTTP方法 GET/POST”)parser.add_argument(“-i” “--ids” requiredTruehelp“测试ID列表用逗号分隔如 ‘100210031004’”)parser.add_argument(“-d” “--data”help“POST请求的原始数据字符串(需要URL编码格式)”)argsparser.parse_args()id_list[int(x.strip())forxinargs.ids.split(‘’)]post_dataNoneifargs.method.upper()“POST”andargs.data:post_data{k:v[0]fork vinparse_qs(args.data).items()}test_idor(args.url args.cookie args.param id_list args.method post_data)使用示例python idor_detector.py -u “http://localhost:8080/api/document/loadContent?docId1001” -c “JSESSIONIDABC123DEF456” -p “docId” -i “100210031005”对抗性思考绕过与进化现代应用可能会引入一些基础的防御例如· 使用不可预测的IDUUID防止简单的顺序枚举。
· 绕过如果ID在其他地方泄露如用户列表、公开分享链接攻击依然有效。
重点从“枚举”转向“信息收集”。
· 在最终步骤加强校验如我们例子中步骤3会校验权限。
· 绕过攻击者可能放弃污染最终状态转而追求中间状态的信息泄露如步骤2的读取或者寻找流程中其他完全缺失校验的步骤。
例如一个“保存草稿”的中间步骤可能没有校验。
· 使用CSRF Token或状态Token尝试将流程状态绑定。
· 绕过如果Token不与具体的资源ID强绑定如Token只表示“用户开始了某个流程”但不记录是哪个资源的流程攻击者仍可在同一会话中用合法Token请求非法资源ID。
需要测试Token的可复用性和范围。
高级攻击模式将此类漏洞作为横向移动的支点。
例如在一个支持“以文档内容创建工单”的系统中越权读取一个高权限用户的文档后利用文档内容中可能包含的敏感信息如密码、内部链接来进一步攻击。
分防御建设 —— 从“怎么做”到“怎么防”防御的核心原则是默认拒绝每次校验。
权限校验必须与数据操作紧密绑定并在最早的可能时机执行。
开发侧修复代码层面强制实施权限校验中间件/注解安全代码应该在数据访问层或服务层入口处进行校验确保任何通过ID获取数据的操作都经过权限过滤。
· 危险模式ServicepublicclassDocumentService{publicDocumentgetDocumentById(LongdocId){// 仅查找无校验returndocumentRepository.findById(docId).orElseThrow();}publicvoidforkDocument(LongsourceDocIdStringnewTitleUsercurrentUser){DocumentsourcegetDocumentById(sourceDocId);// 这里调用source可能来自他人// ... 后续可能有一些业务逻辑 ...// 最终保存前才校验if(!permissionChecker.check(currentUser source “fork”)){thrownewAccessDeniedException();}// 创建副本...}}· 安全模式ServicepublicclassDocumentService{// 安全的获取方法强制传入当前用户并进行校验publicDocumentgetDocumentByIdAndCheckRead(LongdocIdUsercurrentUser){DocumentdocdocumentRepository.findById(docId).orElseThrow(NotFoundException::new);// 立即校验读取权限if(!permissionChecker.check(currentUser doc “read”)){thrownewAccessDeniedException();}returndoc;}// 或者在Repository层面使用支持SPEL的注解如Spring Security的 PostAuthorizePostAuthorize(“hasPermission(returnObject ‘read’)”)publicDocumentfindById(Longid){returndocumentRepository.findById(id).orElseThrow();}publicvoidforkDocument(LongsourceDocIdStringnewTitleUsercurrentUser){// 调用安全的方法获取资源DocumentsourcegetDocumentByIdAndCheckRead(sourceDocId currentUser);// 注意此处仍需校验“fork”权限但读取权限已在前置步骤保证if(!permissionChecker.check(currentUser source “fork”)){thrownewAccessDeniedException();}// 创建副本...}}架构层面使用不可猜测的上下文标识符· 对于敏感的多步流程不要直接使用数据库主键ID在客户端传递。
· 为每个会话-资源对生成一个临时的、随机的流程令牌Process Token。
服务器端维护一个映射表 {token: (userId resourceId step)}。
· 客户端在所有后续请求中携带此 token。
服务器根据 token 查找对应的合法 (userId resourceId) 天然避免了用户指定任意 resourceId 的问题。
安全流程
POST /fork/init {docId: 1001} - Server: 校验权限生成tokenT0k3nAbC关联(alice
。
返回 {token: ‘T0k3nAbC’}
GET /fork/load?tokenT0k3nAbC - Server: 通过token查得(alice
返回1001的内容。
POST /fork/confirm {token: T0k3nAbC newTitle: “…”} - Server: 通过token查得(alice
执行fork。
攻击者无法伪造或猜出其他资源对应的有效token。
运维侧加固· API网关/ WAF规则可以部署规则对特定模式的请求如频繁变更docId、userId等参数进行告警或限速。
但这只是一种辅助手段无法修复根本逻辑缺陷。
· 安全的会话配置# Spring Security 配置示例security:session:fixation:none# 或 migrateSession 防止会话固定攻击cookie:http-only:truesecure:true# 生产环境启用same-site:lax# 或 strict检测与响应线索在应用日志和监控系统中应关注以下异常模式同一用户会话在短时间内对同一接口请求不同ID参数特别是顺序遍历模式。
用户请求的资源ID与其常见行为模式历史基线严重偏离例如一个普通员工突然请求CEO的文档ID。
关键业务接口如/api//load /api//get返回了403错误这可能是一次失败的越权尝试。
业务流程的完成率异常大量流程在第一步或第二步中止可能表明攻击者在进行自动化探测。
日志记录增强在权限校验失败时不仅要返回403还应记录结构化的审计日志{ “timestamp”: “
T10:00:00Z” “event”: “ACCESS_DENIED” “userId”: “alice” “userIp”: “
192.
168.
100” “requestedResourceType”: “Document” “requestedResourceId”: “1002” “action”: “read” “reason”: “User not owner or shared with” }
分
总结与脉络 —— 连接与展望核心要点复盘核心本质多步流程越权漏洞源于前置步骤的权限校验缺失与对客户端提供的关键标识如ID的盲目信任。
它拆解了“事务”的安全边界。
危害严重不仅导致数据泄露读取越权更可能导致数据被非法篡改、污染业务状态是逻辑漏洞的典型代表。
发现关键通过业务流建模识别出流程中每一个涉及资源引用的请求点尤其是非最终请求并系统性地测试其参数是否受权限控制。
防御基石贯彻 “默认拒绝” 与 “每次校验” 原则。
在代码架构上确保数据访问层与权限校验层紧密结合或采用间接的流程令牌机制替代直接的对象ID传递。
测试演进从简单的参数篡改发展到结合业务流程上下文、利用中间状态泄露、作为横向移动跳板的综合攻击链思维。
知识体系连接· 前序基础· OWASP Top 10: Broken Access Control本文是其深度和场景化的延伸。
· HTTP协议与会话管理理解请求/响应无状态性是理解为何需要每一步校验的基础。
· 业务逻辑漏洞概述本文是多步流程这类特定业务逻辑漏洞的专题剖析。
· 后继进阶· 不安全的直接对象引用 (IDOR) 的自动化挖掘与利用可以基于本文的脚本思路构建更智能的爬虫与模糊测试工具。
· 基于状态的测试 (Stateful Testing)如何建模复杂的用户状态机如购物车、审批流并进行安全测试。
· 微服务与API安全中的权限挑战在分布式系统中权限校验如何通过API网关、服务网格如Istio或零信任架构统一实施。
进阶方向指引纵向深入与身份联邦和OAuth的交互风险· 研究在多步流程中当系统涉及多个身份提供商如使用SAML或OAuth
0时权限上下文如何在步骤间传递和校验。
探讨“中途切换用户”或“令牌混淆”可能带来的新型越权风险。
横向扩展在GraphQL与gRPC中的表现形式· 现代API技术栈GraphQL gRPC同样面临多步逻辑问题。
探究在这些协议下如何通过单个复杂查询或多次RPC调用实现越权以及相应的防御模式如GraphQL的字段级权限控制。
自检清单· 是否明确定义了本主题的价值与学习目标 —— 开篇即阐明其在越权漏洞及逻辑安全中的核心地位并列出了5个可衡量的学习目标。
· 原理部分是否包含一张自解释的Mermaid核心机制图 —— 提供了对比安全与不安全流程的序列图清晰展示了漏洞发生点与校验点。
· 实战部分是否包含一个可运行的、注释详尽的代码片段 —— 提供了完整的Docker Compose环境描述、手动测试步骤以及一个功能完整的Python自动化探测脚本脚本包含详细注释和安全警告。
· 防御部分是否提供了至少一个具体的安全代码示例或配置方案 —— 通过“危险模式”与“安全模式”的代码对比展示了权限校验前置的编码方法并提出了流程令牌的架构级方案及日志记录规范。
· 是否建立了与知识大纲中其他文章的联系 —— 在
总结部分明确列出了前序的“基础越权”、“业务逻辑漏洞”和后继的“自动化挖掘”、“微服务API安全”等关联主题。
· 全文是否避免了未定义的术语和模糊表述 —— 关键术语如“越权漏洞”、“水平/垂直越权”、“流程令牌”均在首次出现时加粗并解释论述严谨逻辑链条清晰。