
1. 从靶场到实战为什么XSS通关解析值得深挖如果你正在学习Web安全尤其是前端安全那么“XSS Challenges”这个靶场你大概率听说过甚至可能已经卡在了某个关卡。网上能找到的“通关攻略”不少但很多只是贴出Payload告诉你“这里输入这个就能过”。这就像只给了你一把万能钥匙却没告诉你锁的内部结构下次遇到稍微变形的锁你还是会束手无策。我花了几天时间从头到尾通关了多个经典的XSS靶场包括XSS Challenges、XSS-Labs以及Pikachu里的相关模块过程中记下了大量笔记。今天这篇文章我不打算做简单的答案复读机而是想和你一起像侦探一样拆解每一道关卡背后的出题人思路、浏览器的解析逻辑以及我们构造Payload的思考过程。我的目标是当你读完这篇文章不仅能复现通关更能建立起一套遇到任何XSS过滤场景时的分析方法和绕过思路。无论你是刚入门的安全爱好者还是想巩固基础的开发者这篇深度解析都能让你有所收获。2. 靶场环境搭建与核心工具准备工欲善其事必先利其器。在开始挑战之前一个稳定、隔离的测试环境至关重要。我们不建议在线上任意网站进行测试靶场为我们提供了合法的“沙盒”。2.1 主流XSS靶场选择与部署目前主流的XSS专项靶场主要有以下几个各有侧重XSS Challenges (例如经典的‘XSS Game’或‘alert(1) to win’): 这类靶场目标纯粹通常要求执行alert(1)或alert(document.domain)等函数即可过关。它们专注于考察对HTML、JavaScript语法以及浏览器解析特性的理解是学习绕过技巧的绝佳起点。XSS-Labs (例如基于PHP的开源项目): 通常包含数十个关卡过滤规则逐渐复杂从简单的标签事件过滤到正则表达式替换、编码解码混淆等非常贴近真实的WAFWeb应用防火墙防护场景。综合性靶场中的XSS模块 (如Pikachu、DVWA、WebGoat): 这些靶场将XSS置于一个更完整的Web应用上下文中。你需要先找到存在漏洞的输入点如搜索框、留言板再实施攻击。这有助于理解XSS漏洞的挖掘过程而不仅仅是利用。部署建议对于新手我强烈推荐使用Docker进行一键化部署。例如对于XSS-Labs你可以搜索对应的Docker镜像通过一条命令docker run -d -p 80:80 [镜像名]即可在本地启动。这种方式避免了复杂的PHP环境配置聚焦于漏洞本身。Pikachu、DVWA也都有现成的Docker镜像或集成环境包如PHPStudy、XAMPP集成安装下载解压配置即可。2.2 浏览器开发者工具你的核心侦查武器现代浏览器的开发者工具F12是我们分析XSS漏洞的“眼睛”和“手术刀”。以下几个功能面板必须熟练掌握元素Elements面板这是重中之重。当你提交一个Payload后必须第一时间来这里查看它最终被渲染成了什么样子。重点关注你的输入被放在了HTML结构的哪个位置是标签内、属性值里还是纯文本节点中哪些字符被转义了、、、变成了lt;、gt;等是否产生了新的标签或属性关键技巧不要只看源码View Source因为那是服务器最初的响应。一定要看“Elements”面板它显示的是经过JavaScript动态修改后的实时DOM树很多基于DOM的XSS漏洞只能在这里被观察到。控制台Console面板用于执行JavaScript代码测试想法也是alert弹窗的输出地。你可以在这里快速测试一些语句是否被浏览器安全策略如CSP所阻止。源代码Sources面板可以查看前端JavaScript文件分析其中是否存在不安全的数据流如从location.hash或document.referrer获取数据并直接写入DOM。网络Network面板观察请求和响应有时过滤发生在后端查看原始响应有助于判断服务端对输入做了何种处理。2.3 必备的浏览器与插件多浏览器测试不同浏览器Chrome、Firefox、Edge及其不同版本对HTML和JavaScript的解析有细微差异这可能影响Payload的成功率。至少准备Chrome和Firefox进行交叉测试。编码/解码工具熟练使用浏览器的控制台进行编解码非常高效。encodeURIComponent(‘script’)// 对整个字符串进行URL编码decodeURIComponent(‘%3Cscript%3E’)btoa(‘alert(1)’)// Base64编码atob(‘YWxlcnQoMSk’)// Base64解码HackBar或类似插件这类浏览器插件可以方便地对请求参数进行各种编码、加密并快速重放请求提升测试效率。3. XSS核心原理与关卡通用解题思路拆解在具体闯关前我们必须统一思想XSS的本质是让浏览器将我们可控的数据误解为代码来执行。因此所有解题思路都围绕一个核心如何“欺骗”浏览器的解析器。3.1 理解浏览器的解析顺序关键中的关键这是很多初学者忽略的底层逻辑。浏览器解析HTML文档是顺序进行的并且有不同的解析上下文HTML解析器首先工作它识别标签...、属性、注释等并构建DOM树。JavaScript解析器当HTML解析器遇到script标签或包含JavaScript代码的事件处理器如onclick时会调用JS解析器来执行其中的代码。URL解析器在处理如a href...或img src...等属性时生效。解题的核心思路就是利用这些解析器之间的差异和切换。例如一个常见的技巧是先用一个方式“闭合”当前的解析上下文比如用”闭合前面的属性然后“开启”一个新的我们想要的上下文比如插入一个script标签或事件属性。3.2 四步通用分析法面对任何一关我都建议遵循以下四个步骤定位注入点我的输入最终出现在页面的哪个位置在Elements面板里仔细找。识别过滤规则尝试输入一些特殊字符如 “ ‘ /观察哪些被转义、删除或替换了。这是理解题目防御机制的关键。构思上下文根据注入点的位置确定我们需要构造的Payload类型。在HTML标签内部如div [注入点] /div我们需要构造新的标签或事件属性。例如scriptalert(1)/script或img srcx onerroralert(1)。在HTML标签的属性值内部如input value”[注入点]”我们需要先闭合引号和标签然后再构造新内容。例如”scriptalert(1)/script。在JavaScript代码内部如scriptvar a ‘[注入点]’; /script我们需要闭合字符串和语句然后插入我们的代码。例如’; alert(1);//。构造与绕过根据过滤规则利用编码、等价语法、冷门标签/事件等方式绕过防御。3.3 Payload构造的“武器库”你需要一个不断扩充的Payload清单以下是一些最核心的标签script,img,svg,iframe,body,input,details,video,audio等。当script被过滤时img的onerror事件是经典备选。事件处理器onclick,onerror,onload,onmouseover,onfocus,onblur等。onerror常用于imgonload可用于body、iframe等。伪协议javascript:alert(1)常用于a href或iframe src等属性。无需闭合的Payloadsvg/onloadalert(1)或img srcx onerroralert(1)。这类Payload自身结构完整不易受周围标签影响。编码与混淆HTML实体编码lt;、JavaScript Unicode转义\u003c、Base64编码结合data:协议等用于绕过基于黑名单字符串的过滤。4. 经典关卡类型深度解析与实战绕过下面我将结合具体关卡类型展示如何应用上述思路。为了避免直接提供答案导致思维惰性我会重点讲解每一类关卡的设计意图和突破路径。4.1 类型一基础标签与事件注入这是最简单的关卡通常没有过滤或只有极弱的过滤。场景模拟一个搜索框输入内容后直接显示在页面中形如div您搜索的关键词是[输入]/div。解题思路定位与识别输入test”‘发现尖括号和引号未被转义。构思上下文输入点直接在HTML标签内部我们可以直接插入新标签。构造Payload最简单的scriptalert(1)/script即可。如果script被拦截可以尝试img srcx onerroralert(1)。这里srcx指向一个不存在的图片必然会触发onerror事件。深度技巧为什么img的src可以是一个不存在的路径因为浏览器会尝试加载加载失败就会触发onerror这正是我们需要的。可以省略属性值的引号只要属性值不包含空格或特殊字符。例如img srcx onerroralert(1)是有效的。这有时可以绕过对引号的检查。4.2 类型二属性值内的注入与闭合这类关卡非常常见你的输入被放在了某个HTML标签的属性值里。场景模拟input type”text” value”[用户输入]”。解题思路定位与识别输入test”在Elements面板看到变成了testquot;说明双引号被HTML实体编码了无法用于闭合。但输入test发现没有被编码。这说明过滤可能不完整。构思上下文我们需要先闭合value属性的双引号然后闭合input标签本身最后在后面添加新内容。构造Payload由于双引号被编码我们不能用它。但我们可以用来闭合input标签Payload为scriptalert(1)/script。当它被放入value属性后完整的HTML变为input type”text” value””scriptalert(1)/script”。浏览器解析时遇到value””后紧接着的闭合了input标签然后开始解析我们插入的script标签成功执行。另一种思路如果也被过滤了怎么办我们可以尝试不闭合标签而是利用事件属性本身。例如输入点在一个a href”[输入]”里。我们可以构造伪协议Payloadjavascript:alert(1)。这样点击这个链接时就会执行JS。4.3 类型三绕过简单的关键词过滤靶场开始引入黑名单过滤script、onerror、alert等关键词。场景模拟输入script后页面显示为空或被替换成其他字符。解题思路大小写绕过早期的黑名单可能只检查小写。尝试ScRiPt、sCript。双写绕过如果过滤方式是删除匹配到的关键词可以尝试scrscriptipt。当中间的script被删除后剩下的字符正好又组合成一个新的script。使用非script标签这是更可靠的方法。如前所述的img、svg、iframe标签配合事件属性。svg/onloadalert(1)svg是HTML5标签/在HTML解析器中可以起到类似的作用在某些上下文中onload事件在SVG加载时触发。body onloadalert(1)如果输入点能出现在body标签内或能覆盖原有body标签。编码部分关键字如果过滤发生在HTML解析之后例如在JavaScript执行时过滤可以对部分字符进行编码。例如img srcx onerroralert(1)可以将alert编码为al\u0065rt。浏览器在JS解析时会自动解码。4.4 类型四利用JavaScript上下文与闭合这是难度较大的一类你的输入出现在script标签内部的字符串变量里。场景模拟script var message ‘[用户输入]’; /script。解题思路定位与识别输入test’;观察单引号是否被转义变成#x27;或apos;。如果没有那么我们就有了闭合字符串的机会。构思上下文我们需要先闭合字符串然后终止当前语句加分号再插入我们的恶意代码最后可能还需要注释掉后面的原有代码。构造Payload’; alert(1);//’闭合前面的字符串。;结束var message ‘’这个赋值语句。alert(1);我们插入的恶意代码。//单行注释注释掉后面可能存在的’;避免语法错误。 最终代码变为script var message ‘’; alert(1);//’; /script成功执行。高级绕过如果引号被严格转义无法闭合字符串怎么办我们可以尝试不闭合而是利用JavaScript语法“逃逸”出字符串。例如如果输入出现在eval(‘[输入]’)中我们可以构造’);alert(1);//来闭合eval的参数和括号。更复杂的情况可能需要研究toString、constructor等原型链方法。4.5 类型五综合过滤与编码绕过高级关卡会组合多种过滤过滤空格、过滤括号、过滤特定关键字、进行HTML实体编码等。场景模拟输入img srcx onerroralert(1)后整个字符串被显示为文本即所有字符都被HTML实体编码了。解题思路寻找未过滤的入口检查是否所有插入点都被编码也许有另一个参数如URL中的#后的片段、document.referrer没有被充分处理这引入了DOM型XSS的可能。利用解析优先级如果输入先经过一次HTML实体解码再被放入某个属性我们可以尝试双重编码。例如服务端可能只解码一次。我们提交lt;img srcx onerroralert(1)gt;服务端解码成img srcx onerroralert(1)然后这个字符串被放入onerror属性值中此时它不再是文本而是可执行的属性值。使用非常规标签和事件当onerror、onload被过滤可以尝试onmouseover、onfocus、onblur等。标签可以尝试details ontogglealert(1)需要用户点击videosource onerroralert(1)等。无括号调用alert如果过滤了括号()在JavaScript中可以使用反引号模板字符串调用函数如alert1。这相当于alert(1)。也可以利用location赋值或throw语句间接触发。5. 实战难题记录那些令人印象深刻的绕过技巧在这一部分我分享几个在实战和高端靶场中遇到的、需要巧妙思维的案例。5.1 案例基于正则替换的递归过滤绕过场景题目过滤script和/script并且是递归删除直到字符串中不再包含这些子串。初始尝试输入scrscriptipt第一次删除中间的script后剩下script又会被第二次删除最终失败。绕过方法利用递归删除的特性构造一个“俄罗斯套娃”式的Payload。例如scrscr/scriptiptipt。我们手动推演一下过滤过程第一轮删除删除/script字符串变为scrscriptipt。第二轮删除删除script字符串变为script。第三轮删除删除script字符串变为空。 等等这好像不对。正确的思路是让删除后产生新的组合但新组合不能被再次匹配。一个经典的Payload是scrscriptipt如果被递归删除确实会失败。但如果我们引入大小写呢假设过滤是大小写敏感的只删script。我们可以输入SCriptscriptIPT。第一次删除中间的script后剩下SCRIPT由于大小写不匹配它不会被删除从而保留下来。这需要精确了解过滤规则。5.2 案例极度受限字符集的利用场景输入点只允许数字、字母和少数几个符号如 - * /完全不允许 “ ‘等。思路这种场景常出现在JSONP回调函数名或某些算式计算中。如果输入被直接放入script标签的src属性或者作为回调函数名执行我们可能有机会。 例如script src”/api?callback[输入]”/script。后端可能返回[输入]({data: “test”})。如果我们控制[输入]为alert那么返回的就是alert({data: “test”})成功执行。更进一步如果只能输入字母我们可以尝试输入eval然后通过其他参数传递恶意代码的字符串形式如Base64编码但需要能调用atob解码。这通常需要结合其他漏洞。5.3 案例DOM型XSS与innerHTML的陷阱场景页面使用JavaScript从URL片段location.hash或document.referrer获取数据然后使用innerHTML或document.write()将其写入页面。特征在Network面板查看服务器返回的HTML中看不到我们的Payload但在Elements面板中能看到。这说明数据是在客户端被JavaScript动态写入的。利用方法找到数据源如var userInput location.hash.substring(1);。找到接收点如document.getElementById(‘msg’).innerHTML userInput;。构造Payload直接访问http://target/page.html#img srcx onerroralert(1)。location.hash获取到#后的内容赋值给innerHTML后img标签被解析执行。关键点DOM型XSS的过滤完全依赖于前端JavaScript代码因此可以仔细分析前端JS逻辑寻找过滤的盲点。有时前端会使用encodeURI或正则进行过滤但可能不彻底。6. 防御视角与安全编程习惯通过攻击我们更能理解如何防御。作为开发者以下原则至关重要原则绝不信任用户输入。这是铁律。输出编码Output Encoding根据数据输出的上下文采用不同的编码方式。输出到HTML正文使用HTML实体编码如将转为lt;。输出到HTML属性值除了编码”外属性值最好始终用引号包裹。输出到JavaScript使用JavaScript编码如\uXXXXUnicode转义或更好的是避免直接将用户输入放入script标签而是通过安全的API如textContent操作DOM。输出到URL进行URL编码。使用安全框架和库现代前端框架如React, Vue, Angular默认提供了良好的XSS防护因为它们使用声明式绑定和虚拟DOM通常不会直接操作innerHTML。如果必须使用React有dangerouslySetInnerHTMLVue有v-html使用时必须确保内容是可信或已净化的。内容安全策略CSP这是防御XSS的终极武器之一。通过HTTP头Content-Security-Policy可以告诉浏览器只允许加载指定来源的脚本、样式、图片等即使网站被注入恶意脚本浏览器也不会执行。在靶场中你可以通过浏览器控制台的错误信息来感知CSP的存在。输入验证与过滤虽然不能单独依赖但作为辅助手段。使用白名单只允许已知好的字符优于黑名单试图阻止已知坏的字符。通关XSS靶场不是终点而是一个起点。它训练的是在面对各种限制和过滤时那种层层递进、不断试探的思维方式。我个人的体会是每当我卡在一个关卡时放下Payload回头仔细阅读页面源码和前端JavaScript分析每一个字符的处理流程往往就能发现之前忽略的细节。真正的Web安全高手比拼的不仅仅是Payload库的丰富程度更是这种细致入微的分析能力和对技术原理的深刻理解。希望这篇解析能成为你XSS学习路上的一块有用的垫脚石。下次遇到看似坚固的过滤时不妨想想浏览器的解析器真的完全按照开发者的预期在工作吗