访问控制与越权
访问控制与越权
访问控制回答的是“谁能访问什么”。越权漏洞发生在用户能访问或操作本不属于自己的资源。它不一定需要复杂 payload,更多时候考的是权限边界和后端校验。
认证和授权
认证是确认你是谁。
授权是确认你能做什么。
登录成功只解决认证,不代表所有操作都安全。
很多越权题的关键是:用户已经登录,但后端没有检查这个用户是否有权访问目标资源。
水平越权
水平越权指同级用户之间互相访问资源。
例如普通用户 A 访问普通用户 B 的订单:
/order?id=1001
/order?id=1002如果只改 ID 就能看到别人的数据,就是水平越权。
垂直越权
垂直越权指低权限用户访问高权限功能。
例如普通用户访问管理员接口:
/admin/user/delete
/api/admin/config前端隐藏按钮没有意义,真正要看后端是否检查权限。
IDOR 是什么
IDOR 是 Insecure Direct Object Reference,不安全的直接对象引用。
应用直接把对象 ID 暴露给用户,并且后端没有校验这个对象是否属于当前用户,就可能出现 IDOR。
IDOR 常常是水平越权的一种表现。
三者的区别与联系
水平越权、垂直越权和 IDOR 经常被混用,但它们描述的是不同层面的问题。水平越权关注的是"同级用户之间的越界访问",攻击者和受害者权限等级相同,只是身份不同。垂直越权关注的是"低权限向高权限的跨越",攻击者试图获取管理员才能执行的操作能力。IDOR 则是一种技术成因,描述的是应用直接用用户可控的对象标识符(如数据库主键、文件名)去访问资源,而没有做归属校验。
三者的关系可以这样理解:IDOR 是导致越权的技术原因之一,而水平越权和垂直越权是越权的效果分类。一个 IDOR 漏洞可能表现为水平越权(改 ID 看别人订单),也可能表现为垂直越权(改 ID 访问管理员资源)。在 CTF 题目中,出题人通常不会告诉你这是哪种越权,需要自己通过测试判断。
常见越权测试方法
越权测试的核心思路是"用 A 的身份去访问 B 的资源"。具体操作包括:改 URL 中的 ID 参数(最直接)、改请求体 JSON 中的 userId 或 role 字段、改 Cookie 或 JWT 中的身份标识、直接访问从 JS 或响应中泄露的 API 端点、切换 HTTP 方法(POST 改 PUT/DELETE)看是否有不同的权限校验。
测试时最好准备两个不同权限的账号。先用高权限账号正常操作一遍,记录所有请求,再用低权限账号的凭据逐个重放这些请求。如果某个请求用低权限凭据也能成功返回数据或执行操作,就说明存在越权。Burp Suite 的 Autorize 插件可以自动化这个流程。
越权的根因分析
越权漏洞的根因通常有三类。第一类是"前端校验、后端缺失":前端隐藏了管理按钮或做了角色判断,但后端 API 没有重复校验,直接信任前端传来的参数。第二类是"参数未校验归属":后端只检查了用户是否登录,但没有检查请求的资源是否属于当前用户,典型表现就是直接用 URL 中的 ID 查询数据库而不加 WHERE user_id = ? 条件。第三类是"权限逻辑缺陷":后端有权限校验逻辑,但存在绕过方式,比如通过路径变体(双斜杠、大小写、编码)绕过中间件的权限拦截,或者通过 HTTP 方法差异绕过只针对 GET 请求的校验。
常见观察点
URL 中的 ID。
JSON 中的 userId、role、owner、groupId。
Cookie 或 JWT 中的身份字段。
响应中隐藏的接口地址。
前端 JS 里写死的管理接口。
不同用户账号之间的响应差异。
IDOR 详细示例
IDOR 是 CTF 中最常见的越权形式。以下是典型场景:
场景1: 用户订单
正常请求: GET /api/orders/1001 (返回自己的订单)
尝试修改: GET /api/orders/1002 (尝试访问别人的订单)
场景2: 用户资料
正常请求: GET /api/user/profile?id=1
尝试修改: GET /api/user/profile?id=2
场景3: 文件下载
正常请求: GET /download?file=report_001.pdf
尝试修改: GET /download?file=report_002.pdf
GET /download?file=../config/database.yml水平越权详细示例
水平越权是同级用户之间互相访问资源:
import requests
def test_horizontal_privilege_escalation(base_url, token_user_a, token_user_b):
"""测试水平越权"""
headers_a = {"Authorization": f"Bearer {token_user_a}"}
headers_b = {"Authorization": f"Bearer {token_user_b}"}
# 步骤1: 用用户A的token访问自己的资源
resp_a = requests.get(f"{base_url}/api/profile", headers=headers_a)
print(f"用户A访问自己: {resp_a.status_code}")
user_a_data = resp_a.json()
# 步骤2: 用用户A的token尝试访问用户B的资源
resp_cross = requests.get(f"{base_url}/api/profile/{user_b_id}", headers=headers_a)
print(f"用户A访问用户B: {resp_cross.status_code}")
if resp_cross.status_code == 200:
print("[!] 水平越权成功!")
print(f"泄露的数据: {resp_cross.json()}")
else:
print("[*] 访问被拒绝")
# 使用示例
# test_horizontal_privilege_escalation("http://target.com", "token_a", "token_b")垂直越权详细示例
垂直越权是低权限用户访问高权限功能:
import requests
def test_vertical_privilege_escalation(base_url, user_token, admin_endpoints):
"""测试垂直越权"""
headers = {"Authorization": f"Bearer {user_token}"}
# 常见管理员接口
admin_paths = [
"/api/admin/users",
"/api/admin/config",
"/api/admin/logs",
"/api/admin/settings",
"/admin/dashboard",
"/api/v1/admin/panel",
]
for path in admin_paths:
try:
resp = requests.get(f"{base_url}{path}", headers=headers)
print(f"{path}: {resp.status_code}")
if resp.status_code == 200:
print(f" [!] 垂直越权成功! 响应: {resp.text[:100]}")
elif resp.status_code == 403:
print(f" [*] 被拒绝 (Forbidden)")
elif resp.status_code == 401:
print(f" [*] 未授权 (Unauthorized)")
except Exception as e:
print(f" [-] 错误: {e}")
# 使用示例
# test_vertical_privilege_escalation("http://target.com", "normal_user_token", [])JWT 操纵实现越权
当权限信息存储在 JWT 中时,可以通过修改 JWT 实现越权:
import base64
import json
import hmac
import hashlib
def decode_jwt(token):
"""解码 JWT (不验证签名)"""
parts = token.split('.')
header = json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
return header, payload
def modify_jwt_payload(token, modifications):
"""修改 JWT payload (不签名)"""
parts = token.split('.')
header = parts[0]
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))
# 应用修改
for key, value in modifications.items():
payload[key] = value
# 重新编码
new_payload = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
return f"{header}.{new_payload}."
# 使用示例
token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJ1c2VyIn0.signature"
# 解码查看
header, payload = decode_jwt(token)
print(f"Header: {header}")
print(f"Payload: {payload}")
# 尝试修改 role
new_token = modify_jwt_payload(token, {"role": "admin", "userId": 1})
print(f"修改后的 token: {new_token}")
# 服务端如果不验证签名,可能接受这个 tokenAPI 授权绕过技巧
# API 越权常见测试方法
# 1. HTTP 方法绕过
methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
for method in methods:
resp = requests.request(method, f"{base_url}/api/admin/users", headers=headers)
print(f"{method}: {resp.status_code}")
# 2. 路径变体绕过
path_variants = [
"/api/admin/users",
"/api/Admin/users", # 大小写
"/api/admin/users/", # 尾部斜杠
"/api/admin/users%20", # URL编码空格
"/api/./admin/users", # 当前目录
"/api/admin//users", # 双斜杠
"/api/admin/users;", # 分号
"/api/admin/users?", # 问号
"/API/ADMIN/USERS", # 全大写
]
# 3. Header 绕过
bypass_headers = [
{"X-Original-URL": "/api/admin/users"},
{"X-Rewrite-URL": "/api/admin/users"},
{"X-Forwarded-For": "127.0.0.1"},
{"X-Custom-IP-Authorization": "127.0.0.1"},
{"X-Real-IP": "127.0.0.1"},
]
# 4. 参数污染
# /api/user/profile?id=1&id=2
# /api/user/profile?id=1%00admin用 Burp Suite 测试越权
1. 配置两个不同权限账号的 Cookie/Token
2. 使用 Repeater 手动测试单个接口
3. 使用 Comparer 对比两个账号的响应差异
4. 使用 Intruder 批量测试 ID 参数
5. 使用 Autorize 插件自动化越权检测
Autorize 插件使用步骤:
1. 安装 Autorize 插件
2. 配置低权限用户的 Cookie/Token
3. 用高权限账号正常浏览网站
4. Autorize 自动用低权限凭据重放请求
5. 查看标记为 "Bypassed" 的请求常见误区
- 以为登录了就一定安全。
- 只测页面按钮,不直接测接口。
- 只改 URL 参数,不改 JSON、Header、Cookie。
- 不准备两个不同权限账号。
- 把前端权限判断当成后端授权。
一句话判断
同一个功能、对象或接口,换用户、换 ID、换角色、换 HTTP 方法、换 Header 后仍能访问不属于自己的资源或高权限功能时,就按访问控制与越权分析。
越权题的核心不是 payload 花哨,而是证明“当前身份不应该做这件事,但后端允许了”。
题目中常见信号
- URL 或 JSON 中有
id、uid、userId、orderId、fileId。 - 前端隐藏管理员按钮,但接口路径在 JS 里可见。
- 普通用户访问
/admin、/api/admin返回不同错误。 - JWT、Cookie 或请求体里有
role、isAdmin。 - 同一个接口高权账号能访问,低权账号重放也成功。
- 修改 HTTP 方法或路径大小写后绕过。
- 响应中泄露其他用户的数据。
核心概念
访问控制要分清:
认证:你是谁
授权:你能访问什么
资源归属:这个对象是不是你的
操作权限:你能不能执行这个动作越权测试必须有对照:至少要有一个当前用户、一个目标资源,最好有两个不同账号。没有对照,就很难证明越权。
最小分析流程
- 准备两个普通用户账号 A/B,有条件再准备管理员账号。
- 用 A 正常访问自己的资源,保存请求。
- 把资源 ID 换成 B 的资源 ID。
- 把 A 的 token/Cookie 用来重放高权限账号请求。
- 修改 HTTP 方法和路径变体。
- 修改 JSON、Cookie、JWT 中的身份和角色字段。
- 检查响应是否真的包含目标资源或操作是否真的生效。
- 记录“应拒绝但成功”的最小请求。
最小验证示例
水平越权:
curl -i http://target/api/orders/1001 \
-H 'Authorization: Bearer TOKEN_A'
curl -i http://target/api/orders/1002 \
-H 'Authorization: Bearer TOKEN_A'垂直越权:
curl -i http://target/api/admin/config \
-H 'Authorization: Bearer USER_TOKEN'方法绕过:
for m in GET POST PUT DELETE PATCH; do
curl -i -X "$m" http://target/api/admin/users \
-H 'Authorization: Bearer USER_TOKEN'
done成功标准:低权限身份拿到了高权限数据,或操作在服务端真实生效。
常见利用 / 解题路线
路线总览:
路线一:水平越权
用户 A token + 用户 B 资源 ID -> 读取/修改 B 资源路线二:垂直越权
普通用户 token -> 直接访问 admin/internal/debug 接口路线三:IDOR
fileId/orderId/reportId 可枚举 -> 后端不校验归属路线四:方法和路径绕过
权限中间件只拦 GET /admin
POST/PUT/大小写/双斜杠/尾斜杠绕过路线五:客户端字段篡改
role=user -> role=admin
ownerId=A -> ownerId=B如果字段在 JWT 或 Cookie 中,回到 [[JWT基础]] 或 [[Cookie与Session]]。
常见失败原因
- 没有两个账号:无法区分公开数据和越权数据。
- 只看状态码:200 可能是错误页,403 也可能响应体泄露数据。
- 操作未确认生效:修改接口返回成功后要再查询状态。
- 混淆认证和授权:登录成功不代表有权访问所有对象。
- 只测 URL ID:JSON、Cookie、JWT、Header 里也可能有身份字段。
- 忽略批量接口:单个对象校验正确,批量查询可能漏校验。
- 前端拦截误导:必须直接请求后端接口。
迷你案例
用户 A 的订单:
curl -i http://target/api/orders/1001 \
-H 'Authorization: Bearer TOKEN_A'返回:
{"id":1001,"owner":"alice","amount":50}用户 B 的订单 ID 从页面或枚举中得到为 1002。用 A 的 token 访问:
curl -i http://target/api/orders/1002 \
-H 'Authorization: Bearer TOKEN_A'返回:
{"id":1002,"owner":"bob","amount":80}结论:
服务端只按订单 ID 查询,没有检查 order.owner 是否等于当前用户
漏洞类型:IDOR 导致水平越权WP 要说明 A/B 身份、资源归属和越权响应证据。