三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

XSS靶场实战:从原理到绕过技巧的深度解析

XSS靶场实战:从原理到绕过技巧的深度解析

1. 从靶场到实战:为什么XSS通关解析值得深挖

如果你正在学习Web安全,尤其是前端安全,那么“XSS Challenges”这个靶场你大概率听说过,甚至可能已经卡在了某个关卡。网上能找到的“通关攻略”不少,但很多只是贴出Payload,告诉你“这里输入这个就能过”。这就像只给了你一把万能钥匙,却没告诉你锁的内部结构,下次遇到稍微变形的锁,你还是会束手无策。我花了几天时间,从头到尾通关了多个经典的XSS靶场,包括XSS Challenges、XSS-Labs以及Pikachu里的相关模块,过程中记下了大量笔记。今天这篇文章,我不打算做简单的答案复读机,而是想和你一起,像侦探一样,拆解每一道关卡背后的出题人思路、浏览器的解析逻辑以及我们构造Payload的思考过程。我的目标是,当你读完这篇文章,不仅能复现通关,更能建立起一套遇到任何XSS过滤场景时的分析方法和绕过思路。无论你是刚入门的安全爱好者,还是想巩固基础的开发者,这篇深度解析都能让你有所收获。

2. 靶场环境搭建与核心工具准备

工欲善其事,必先利其器。在开始挑战之前,一个稳定、隔离的测试环境至关重要。我们不建议在线上任意网站进行测试,靶场为我们提供了合法的“沙盒”。

2.1 主流XSS靶场选择与部署

目前主流的XSS专项靶场主要有以下几个,各有侧重:

  1. XSS Challenges (例如经典的‘XSS Game’或‘alert(1) to win’): 这类靶场目标纯粹,通常要求执行alert(1)alert(document.domain)等函数即可过关。它们专注于考察对HTML、JavaScript语法以及浏览器解析特性的理解,是学习绕过技巧的绝佳起点。
  2. XSS-Labs (例如基于PHP的开源项目): 通常包含数十个关卡,过滤规则逐渐复杂,从简单的标签事件过滤,到正则表达式替换、编码解码混淆等,非常贴近真实的WAF(Web应用防火墙)防护场景。
  3. 综合性靶场中的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.hashdocument.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文档是顺序进行的,并且有不同的解析上下文:

  1. HTML解析器:首先工作,它识别标签(<...>)、属性、注释等,并构建DOM树。
  2. JavaScript解析器:当HTML解析器遇到<script>标签或包含JavaScript代码的事件处理器(如onclick)时,会调用JS解析器来执行其中的代码。
  3. URL解析器:在处理如<a href="..."><img src="...">等属性时生效。

解题的核心思路就是利用这些解析器之间的差异和切换。例如,一个常见的技巧是:先用一个方式“闭合”当前的解析上下文(比如用”>闭合前面的属性),然后“开启”一个新的我们想要的上下文(比如插入一个<script>标签或事件属性)。

3.2 四步通用分析法

面对任何一关,我都建议遵循以下四个步骤:

  1. 定位注入点:我的输入最终出现在页面的哪个位置?在Elements面板里仔细找。
  2. 识别过滤规则:尝试输入一些特殊字符,如< > “ ‘ & /,观察哪些被转义、删除或替换了。这是理解题目防御机制的关键。
  3. 构思上下文:根据注入点的位置,确定我们需要构造的Payload类型。
    • 在HTML标签内部(如<div> [注入点] </div>:我们需要构造新的标签或事件属性。例如:<script>alert(1)</script><img src=x onerror=alert(1)>
    • 在HTML标签的属性值内部(如<input value=”[注入点]”>:我们需要先闭合引号和标签,然后再构造新内容。例如:”><script>alert(1)</script>
    • 在JavaScript代码内部(如<script>var a = ‘[注入点]’; </script>:我们需要闭合字符串和语句,然后插入我们的代码。例如:’; alert(1);//
  4. 构造与绕过:根据过滤规则,利用编码、等价语法、冷门标签/事件等方式绕过防御。

3.3 Payload构造的“武器库”

你需要一个不断扩充的Payload清单,以下是一些最核心的:

  • 标签<script>,<img>,<svg>,<iframe>,<body>,<input>,<details>,<video>,<audio>等。当<script>被过滤时,<img>onerror事件是经典备选。
  • 事件处理器onclick,onerror,onload,onmouseover,onfocus,onblur等。onerror常用于<img>onload可用于<body><iframe>等。
  • 伪协议javascript:alert(1)常用于<a href><iframe src>等属性。
  • 无需闭合的Payload<svg/onload=alert(1)><img src=x onerror=alert(1)>。这类Payload自身结构完整,不易受周围标签影响。
  • 编码与混淆:HTML实体编码(&lt;)、JavaScript Unicode转义(\u003c)、Base64编码(结合data:协议)等,用于绕过基于黑名单字符串的过滤。

4. 经典关卡类型深度解析与实战绕过

下面,我将结合具体关卡类型,展示如何应用上述思路。为了避免直接提供答案导致思维惰性,我会重点讲解每一类关卡的设计意图和突破路径。

4.1 类型一:基础标签与事件注入

这是最简单的关卡,通常没有过滤或只有极弱的过滤。

场景模拟:一个搜索框,输入内容后直接显示在页面中,形如<div>您搜索的关键词是:[输入]</div>

解题思路

  1. 定位与识别:输入test<>”‘,发现尖括号和引号未被转义。
  2. 构思上下文:输入点直接在HTML标签内部,我们可以直接插入新标签。
  3. 构造Payload:最简单的<script>alert(1)</script>即可。如果<script>被拦截,可以尝试<img src=x onerror=alert(1)>。这里src=x指向一个不存在的图片,必然会触发onerror事件。

深度技巧

  • 为什么<img>src可以是一个不存在的路径?因为浏览器会尝试加载,加载失败就会触发onerror,这正是我们需要的。
  • 可以省略属性值的引号,只要属性值不包含空格或特殊字符。例如<img src=x onerror=alert(1)>是有效的。这有时可以绕过对引号的检查。

4.2 类型二:属性值内的注入与闭合

这类关卡非常常见,你的输入被放在了某个HTML标签的属性值里。

场景模拟<input type=”text” value=”[用户输入]”>

解题思路

  1. 定位与识别:输入test”,在Elements面板看到变成了test&quot;,说明双引号被HTML实体编码了,无法用于闭合。但输入test>,发现>没有被编码。这说明过滤可能不完整。
  2. 构思上下文:我们需要先闭合value属性的双引号,然后闭合<input>标签本身,最后在后面添加新内容。
  3. 构造Payload:由于双引号被编码,我们不能用它。但我们可以用>来闭合<input>标签!Payload为:><script>alert(1)</script>。当它被放入value属性后,完整的HTML变为:<input type=”text” value=””><script>alert(1)</script>”>。浏览器解析时,遇到value=””后,紧接着的>闭合了input标签,然后开始解析我们插入的<script>标签,成功执行。
  4. 另一种思路:如果>也被过滤了怎么办?我们可以尝试不闭合标签,而是利用事件属性本身。例如,输入点在一个<a href=”[输入]”>里。我们可以构造伪协议Payload:javascript:alert(1)。这样,点击这个链接时就会执行JS。

4.3 类型三:绕过简单的关键词过滤

靶场开始引入黑名单,过滤<script>onerroralert等关键词。

场景模拟:输入<script>后,页面显示为空或被替换成其他字符。

解题思路

  1. 大小写绕过:早期的黑名单可能只检查小写。尝试<ScRiPt><sCript>
  2. 双写绕过:如果过滤方式是删除匹配到的关键词,可以尝试<scr<script>ipt>。当中间的<script>被删除后,剩下的字符正好又组合成一个新的<script>
  3. 使用非<script>标签:这是更可靠的方法。如前所述的<img><svg><iframe>标签配合事件属性。
    • <svg/onload=alert(1)><svg>是HTML5标签,/在HTML解析器中可以起到类似>的作用(在某些上下文中),onload事件在SVG加载时触发。
    • <body onload=alert(1)>:如果输入点能出现在<body>标签内或能覆盖原有body标签。
  4. 编码部分关键字:如果过滤发生在HTML解析之后(例如在JavaScript执行时过滤),可以对部分字符进行编码。例如,<img src=x onerror=alert(1)>,可以将alert编码为al\u0065rt。浏览器在JS解析时会自动解码。

4.4 类型四:利用JavaScript上下文与闭合

这是难度较大的一类,你的输入出现在<script>标签内部的字符串变量里。

场景模拟<script> var message = ‘[用户输入]’; </script>

解题思路

  1. 定位与识别:输入test’;,观察单引号是否被转义(变成&#x27;&apos;)。如果没有,那么我们就有了闭合字符串的机会。
  2. 构思上下文:我们需要先闭合字符串,然后终止当前语句(加分号),再插入我们的恶意代码,最后可能还需要注释掉后面的原有代码。
  3. 构造Payload:’; alert(1);//
    • 闭合前面的字符串。
    • ;结束var message = ‘’这个赋值语句。
    • alert(1);我们插入的恶意代码。
    • //单行注释,注释掉后面可能存在的’;,避免语法错误。 最终代码变为:<script> var message = ‘’; alert(1);//’; </script>,成功执行。
  4. 高级绕过:如果引号被严格转义,无法闭合字符串怎么办?我们可以尝试不闭合,而是利用JavaScript语法“逃逸”出字符串。例如,如果输入出现在eval(‘[输入]’)中,我们可以构造’);alert(1);//,来闭合eval的参数和括号。更复杂的情况可能需要研究toStringconstructor等原型链方法。

4.5 类型五:综合过滤与编码绕过

高级关卡会组合多种过滤:过滤空格、过滤括号、过滤特定关键字、进行HTML实体编码等。

场景模拟:输入<img src=x onerror=alert(1)>后,整个字符串被显示为文本,即所有字符都被HTML实体编码了。

解题思路

  1. 寻找未过滤的入口:检查是否所有插入点都被编码?也许有另一个参数(如URL中的#后的片段、document.referrer)没有被充分处理,这引入了DOM型XSS的可能。
  2. 利用解析优先级:如果输入先经过一次HTML实体解码,再被放入某个属性,我们可以尝试双重编码。例如,服务端可能只解码一次。我们提交&lt;img src=x onerror=alert(1)&gt;,服务端解码成<img src=x onerror=alert(1)>,然后这个字符串被放入onerror属性值中,此时它不再是文本,而是可执行的属性值。
  3. 使用非常规标签和事件:当onerroronload被过滤,可以尝试onmouseoveronfocusonblur等。标签可以尝试<details ontoggle=alert(1)>(需要用户点击),<video><source onerror=alert(1)>等。
  4. 无括号调用alert:如果过滤了括号(),在JavaScript中,可以使用反引号(模板字符串)调用函数,如alert`1。这相当于alert(1)。也可以利用location赋值或throw语句间接触发。

5. 实战难题记录:那些令人印象深刻的绕过技巧

在这一部分,我分享几个在实战和高端靶场中遇到的、需要巧妙思维的案例。

5.1 案例:基于正则替换的递归过滤绕过

场景:题目过滤<script></script>,并且是递归删除,直到字符串中不再包含这些子串。

初始尝试:输入<scr<script>ipt>,第一次删除中间的<script>后,剩下<script>,又会被第二次删除,最终失败。

绕过方法:利用递归删除的特性,构造一个“俄罗斯套娃”式的Payload。例如:<scr<scr</script>ipt>ipt>。我们手动推演一下过滤过程:

  1. 第一轮删除:删除</script>,字符串变为<scr<script>ipt>
  2. 第二轮删除:删除<script>,字符串变为<script>
  3. 第三轮删除:删除<script>,字符串变为空。 等等,这好像不对。正确的思路是让删除后产生新的组合,但新组合不能被再次匹配。一个经典的Payload是:<scr<script>ipt>如果被递归删除,确实会失败。但如果我们引入大小写呢?假设过滤是大小写敏感的,只删<script>。我们可以输入:<SCript<script>IPT>。第一次删除中间的<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获取数据,然后使用innerHTMLdocument.write()将其写入页面。

特征:在Network面板查看,服务器返回的HTML中看不到我们的Payload,但在Elements面板中能看到。这说明数据是在客户端被JavaScript动态写入的。

利用方法

  1. 找到数据源:如var userInput = location.hash.substring(1);
  2. 找到接收点:如document.getElementById(‘msg’).innerHTML = userInput;
  3. 构造Payload:直接访问http://target/page.html#<img src=x onerror=alert(1)>location.hash获取到#后的内容,赋值给innerHTML后,<img>标签被解析执行。关键点:DOM型XSS的过滤完全依赖于前端JavaScript代码,因此可以仔细分析前端JS逻辑,寻找过滤的盲点。有时前端会使用encodeURI或正则进行过滤,但可能不彻底。

6. 防御视角与安全编程习惯

通过攻击,我们更能理解如何防御。作为开发者,以下原则至关重要:

  1. 原则:绝不信任用户输入。这是铁律。
  2. 输出编码(Output Encoding):根据数据输出的上下文,采用不同的编码方式。
    • 输出到HTML正文:使用HTML实体编码(如将<转为&lt;)。
    • 输出到HTML属性值:除了编码<>&”外,属性值最好始终用引号包裹。
    • 输出到JavaScript:使用JavaScript编码(如\uXXXXUnicode转义),或更好的是,避免直接将用户输入放入<script>标签,而是通过安全的API(如textContent)操作DOM。
    • 输出到URL:进行URL编码。
  3. 使用安全框架和库:现代前端框架(如React, Vue, Angular)默认提供了良好的XSS防护,因为它们使用声明式绑定和虚拟DOM,通常不会直接操作innerHTML。如果必须使用,React有dangerouslySetInnerHTML,Vue有v-html,使用时必须确保内容是可信或已净化的。
  4. 内容安全策略(CSP):这是防御XSS的终极武器之一。通过HTTP头Content-Security-Policy,可以告诉浏览器只允许加载指定来源的脚本、样式、图片等,即使网站被注入恶意脚本,浏览器也不会执行。在靶场中,你可以通过浏览器控制台的错误信息来感知CSP的存在。
  5. 输入验证与过滤:虽然不能单独依赖,但作为辅助手段。使用白名单(只允许已知好的字符)优于黑名单(试图阻止已知坏的字符)。

通关XSS靶场不是终点,而是一个起点。它训练的是在面对各种限制和过滤时,那种层层递进、不断试探的思维方式。我个人的体会是,每当我卡在一个关卡时,放下Payload,回头仔细阅读页面源码和前端JavaScript,分析每一个字符的处理流程,往往就能发现之前忽略的细节。真正的Web安全高手,比拼的不仅仅是Payload库的丰富程度,更是这种细致入微的分析能力和对技术原理的深刻理解。希望这篇解析能成为你XSS学习路上的一块有用的垫脚石。下次遇到看似坚固的过滤时,不妨想想:浏览器的解析器,真的完全按照开发者的预期在工作吗?

← 返回列表