查询 25 次,耗时 0.2061 秒

授权本地靶场中的 Web API 越权测试:从对象级授权缺失到可验证修复

xiaobinz08

New member

xiaobinz08 Rep
0
0
0
Rep
0
xiaobinz08 Vouches
Vouches
0
1 MONTH
1 1 MONTH OF SERVICE
xiaobinz08 获得
0
0
0
获得
0
xiaobinz08 凭证
0
0
0
凭证
0
主题
1
点赞
0
HakB
1 月
在社区成长 1 月
LEVEL 1 1 XP
 
一、背景与适用范围

在 APP/Web 渗透测试中,越权(尤其是对象级授权缺失,常称 IDOR 或 BOLA)是非常常见、也最容易被误判的一类问题。它通常不是 SQL 注入或代码执行,而是服务端在处理“当前用户能否访问某个资源”时,仅校验了资源是否存在,却没有校验资源归属、租户边界或操作权限。移动 APP、管理后台、订单系统、工单系统、云资源控制台都可能受到影响。

本文只讨论授权环境:测试者应使用本地搭建的靶场、企业书面授权的测试环境或专用培训平台。示例中的地址固定为 127.0.0.1,不能将测试请求发送到未获授权的互联网目标,也不能读取、修改真实用户数据。文章重点是可复现的检测方法、证据留存、风险判断和修复验证。

二、漏洞原理

一个安全的对象访问请求,至少应同时满足三类条件:第一,访问者已经通过身份认证;第二,访问者拥有执行该动作的业务权限;第三,被访问对象属于当前用户、当前组织或当前租户,且对象状态允许该操作。

典型不安全逻辑如下:
1. 从请求头或 Cookie 解析用户身份;
2. 根据 URL 中的 order_id 查询订单;
3. 只要查询到订单就返回数据。

问题在于查询条件可能只是“WHERE id = ?”,而不是“WHERE id = ? AND owner_id = 当前用户”。如果攻击者仅改变一个对象编号,就能获得其他用户的订单、工单或个人信息,这就是对象级授权缺失。对于修改、删除、退款、导出等写操作,影响通常高于只读场景。

三、前置条件与本地实验环境

建议准备以下环境:
1. Python 3.10 或更高版本;
2. Flask、Werkzeug、SQLite,仅用于本地演示;
3. Burp Suite Community、OWASP ZAP 或浏览器开发者工具,用于观察请求;
4. 两个测试账号,例如 alice 和 bob,各自拥有一条测试订单;
5. 明确的测试记录,包括测试时间、账号、请求范围和清理方式。

可使用以下命令创建隔离环境:
python3 -m venv venv
. venv/bin/activate
pip install flask werkzeug

启动靶场后只监听本机:
flask --app app run --host 127.0.0.1 --port 5000

不要使用 0.0.0.0 暴露到局域网,也不要在生产数据库上运行本文示例。

四、构造一个最小化易错接口

下面是用于理解问题的伪代码。它故意展示不安全的查询逻辑,实际测试时可将其放在本地实验项目中:

@app.get('/api/orders/<int:eek:rder_id>')
@login_required
def get_order(order_id):
order = db.execute('SELECT id, owner_id, amount, address FROM orders WHERE id = ?', (order_id,)).fetchone()
if not order:
return {'error': 'not found'}, 404
return dict(order)

这里虽然有登录校验,但没有将当前用户与 owner_id 进行比较,因此“已登录”被错误地当成了“可以访问所有对象”。如果应用存在多租户,还必须校验 tenant_id,不能只判断用户 ID。

五、分步骤测试方法

第一步,确认测试边界。记录本次仅允许访问 /api/orders/、测试账号 alice 和 bob,以及本地数据库中的模拟数据。禁止扩大到其他接口、目录或真实数据。

第二步,分别登录两个测试账号,并保存各自的认证信息。示例采用本地开发接口,不代表真实系统一定使用相同方式:
curl -i -c alice.cookie -X POST http://127.0.0.1:5000/login -d 'username=alice&password=AlicePass!'
curl -i -c bob.cookie -X POST http://127.0.0.1:5000/login -d 'username=bob&password=BobPass!'

第三步,使用 alice 的身份访问 alice 自己的对象,先建立正常基线:
curl -i -b alice.cookie http://127.0.0.1:5000/api/orders/1001

应记录状态码、响应字段、响应长度和服务器返回的对象归属。正常情况下,alice 能看到属于自己的 1001 号订单。

第四步,在不改变认证信息的前提下,将对象编号替换为 bob 的测试订单 2001:
curl -i -b alice.cookie http://127.0.0.1:5000/api/orders/2001

如果返回 200,并且响应包含 bob 的地址、金额或其他只应由 bob 查看的信息,则可以初步判断存在对象级授权缺失。这里的关键不是“编号可预测”本身,而是服务端是否在对象查询和业务授权层阻止了跨用户访问。

第五步,验证不同响应的差异。分别测试不存在的编号、属于当前用户的编号和属于其他用户的编号,例如:
curl -i -b alice.cookie http://127.0.0.1:5000/api/orders/999999
curl -i -b alice.cookie http://127.0.0.1:5000/api/orders/1001
curl -i -b alice.cookie http://127.0.0.1:5000/api/orders/2001

对比 HTTP 状态码、错误信息和响应时间。即使应用对跨用户对象返回 404,也不能仅凭表面结果下结论,还应结合后端日志、数据库查询和业务规则确认是否真正阻断了访问。反之,若不同对象返回明显不同的错误,也可能产生资源枚举风险。

第六步,检查写操作,但必须使用可撤销的本地数据。对于 PUT、PATCH、DELETE、退款、审批等接口,应先在代码中增加 dry-run 或事务回滚机制,避免误删实验数据。例如仅测试接口是否返回“无权限”,不要提交真实状态变更:
curl -i -b alice.cookie -H 'Content-Type: application/json' -X PATCH http://127.0.0.1:5000/api/orders/2001 -d '{"remark":"authorization-test"}'

如果服务端返回 200 或 204,并且 bob 的对象确实被修改,则风险从越权读取升级为越权修改。测试报告应明确说明是否发生了状态变化,不能仅依据成功响应推断影响。

六、推荐的修复方案

最基本的修复是在数据访问层加入归属条件:
SELECT id, owner_id, amount, address
FROM orders
WHERE id = ? AND owner_id = ?;

参数中的 owner_id 必须来自服务端已验证的会话、令牌或服务间身份,不能从客户端提交的 owner_id 字段直接读取。对于管理员、客服和普通用户,应采用明确的权限策略:普通用户只能访问自身对象;客服只能访问授权组织内的对象;管理员也应受到租户和审计规则约束,而不是简单地绕过全部检查。

对于修改和删除操作,应在同一条更新语句中完成授权约束:
UPDATE orders SET remark = ? WHERE id = ? AND owner_id = ?;

然后检查受影响行数。若为 0,应返回统一的 403 或经过业务设计的 404,且不能继续执行后续操作。不要先查询对象、再在另一个不受保护的更新语句中修改,这会造成授权检查与状态修改之间的不一致。

建议将授权逻辑集中到策略层或服务层,并覆盖以下维度:用户身份、角色、对象所有者、组织或租户、对象状态、操作类型和时间限制。对于对象编号,使用不可枚举的随机标识只能降低枚举难度,不能替代服务端授权校验。

七、自动化回归测试示例

可在测试环境中编写最小化单元测试,验证 alice 访问 bob 的订单必须失败:

def test_user_cannot_read_other_users_order(client):
login_as(client, 'alice', 'AlicePass!')
response = client.get('/api/orders/2001')
assert response.status_code in (403, 404)


def test_user_cannot_modify_other_users_order(client):
login_as(client, 'alice', 'AlicePass!')
response = client.patch('/api/orders/2001', json={'remark': 'test'})
assert response.status_code in (403, 404)
assert order_remark(2001) != 'test'

在 CI 中执行该测试,并为每个关键资源类型补充“本人可访问、他人不可访问、跨租户不可访问、无认证不可访问、越权写入不生效”等用例。若使用 GraphQL、批量接口或导出接口,还要验证数组中的每个对象都执行授权,而不是只对第一个对象校验。

八、风险评估与证据要求

风险判断应结合数据敏感性、可执行操作、影响范围和业务可利用性。仅能访问公开字段的缺陷,与可读取身份证号、支付信息、内部合同的缺陷,不应使用同一等级。能跨租户修改订单、审批权限或绑定信息,通常属于高风险业务越权。

合格的报告证据应包括:测试环境和授权范围、使用的测试账号、脱敏后的请求与响应、对象归属关系、预期结果、实际结果、是否产生状态变化、复现时间以及修复后的验证结果。不要在报告中提交真实个人信息、有效令牌或不必要的完整响应。

九、常见问题

问题一:把对象 ID 改成另一个值就算漏洞吗?
不一定。编号可预测只是辅助现象,核心是未授权用户能否获得或改变不属于自己的对象。若服务端正确返回 403/404 且无侧信道泄露,应记录为未复现。

问题二:返回 404 是否一定安全?
不一定。需要确认数据库查询带有授权条件,且不存在缓存、批量接口、导出接口或错误日志泄露。还要检查是否能通过时间差、响应长度判断对象存在。

问题三:前端隐藏按钮能否作为授权?
不能。前端校验只改善交互,攻击者可以直接构造 HTTP 请求。最终授权必须在服务端完成。

问题四:只测试 GET 是否足够?
不够。POST、PUT、PATCH、DELETE、批量导入、文件下载、异步任务和 GraphQL mutation 往往包含更严重的越权路径。

问题五:使用随机 UUID 后是否不需要权限控制?
不需要改变结论。随机标识属于纵深防御措施,不能替代对象归属、租户隔离和角色授权。

十、总结

授权测试的重点不是盲目枚举编号,而是建立两个身份、两组对象和一组明确的业务规则,然后验证服务端是否在每个请求、每个对象、每种操作上执行一致的授权判断。实际工作中应优先采用本地或书面授权环境,控制请求范围,保留最小化证据,并通过代码审查、数据库查询审查和自动化回归测试形成闭环。修复完成后,至少重新验证本人访问、他人访问、跨租户访问、批量访问、写操作和缓存路径,只有这些结果都符合预期,才能得出可验证的安全结论。
 
顶部