DVWA High级别XSS绕过实战:HTML5标签与Unicode编码的攻防对抗
1. 项目概述:从“三板斧”到“迂回渗透”的思维跃迁
如果你玩过DVWA(Damn Vulnerable Web Application)这个经典的Web安全靶场,并且已经轻松拿下了Low和Medium级别的XSS(跨站脚本攻击)挑战,那么当你信心满满地进入High级别时,大概率会遭遇当头一棒。你会发现,那些在低级关卡里无往不利的<script>alert(1)</script>之类的“三板斧”招式,在这里通通失效了。页面平静如水,你的攻击载荷仿佛石沉大海。这恰恰是High级别XSS关卡设计的精妙之处——它强制你跳出对<script>标签的路径依赖,去探索更广阔、更隐蔽的攻击面。
这个项目,就是一次针对DVWA High级别XSS防护机制的深度实战演练。我们不再执着于正面强攻被严格过滤的<script>标签,而是将目光转向HTML5规范中那些同样能执行JavaScript但可能被忽略的新标签、新属性,并结合Unicode编码这种“文字魔术”来巧妙地绕过前端或后端的过滤逻辑。这不仅仅是一次通关教程,更是一次Web安全攻防思维的升级:从“有什么漏洞打什么”转变为“在限制条件下,如何创造性地组合利用现有资源达成目标”。无论是安全研究人员、渗透测试工程师,还是希望深入理解防御机制的前后端开发者,通过这个实战过程,你都能对XSS攻击的多样性和现代过滤规则的局限性有更立体的认识。
2. 核心思路拆解:为何<script>失效,以及我们的破局点
在DVWA的XSS High级别中,开发者显然吸取了低级关卡的教训,部署了更严格的过滤机制。通常,它会采用一个“黑名单”或“正则表达式替换”的策略,核心目标就是让<script>标签(无论大小写)无法完整地出现在最终的页面输出中。常见的过滤方式包括:
- 直接删除:将
<script>和</script>字符串直接从输入中移除。 - 正则替换:使用类似
preg_replace('/<script.*?>/i', '', $input)的代码,不区分大小写地匹配并移除脚本标签。 - 编码转换:可能将
<和>转换为HTML实体(如<和>),但这在High级别较少见,因为我们的目标是绕过。
所以,当你输入<script>alert('XSS')</script>,服务器端处理后返回给浏览器的可能就只剩下alert('XSS')这段孤零零的文本,它不会被解析为可执行的代码。
我们的破局思路基于两个层面:
2.1 利用HTML5的“非主流”执行点现代浏览器对HTML5的支持引入了大量新的标签和属性,其中一些在设计上就允许执行JavaScript,但它们的安全风险可能未被所有过滤规则充分考虑。例如:
- 事件处理器属性:除了经典的
onclick、onload,HTML5为许多元素新增了事件,如<img>的onerror,<svg>的onload,<details>的ontoggle等。如果过滤规则只盯着<script>而忽略了这些事件属性的过滤,它们就是绝佳的突破口。 - 可执行脚本的标签:
<svg>标签内可以嵌套<script>,但有时对<svg>本身的过滤较弱。<iframe>的srcdoc属性可以直接包含HTML,可能构成一个沙箱内的执行环境。 - JavaScript伪协议:
<a>标签的href属性,或者<iframe>、<embed>的src属性,可以使用javascript:伪协议。虽然很多过滤器会检查并拦截javascript:这个字符串,但我们可以通过编码来伪装它。
2.2 利用Unicode编码进行“视觉欺骗”Unicode编码允许我们用多种方式表示同一个字符。这对于绕过基于字符串匹配的过滤器至关重要。
- 十六进制编码:
<可以表示为\u003c或\x3c(在某些上下文如JavaScript字符串中)。 - HTML实体编码:
<是<,>是>。但浏览器在解析某些属性值时,会先进行HTML解码。 - 关键点:过滤器在检测
javascript:或onclick时,匹配的是明文字符串。如果我们在输入时,将其中关键字符用Unicode编码替换,过滤器可能认不出来。但浏览器在渲染页面、解析HTML或执行JavaScript前,会先进行解码,从而还原出可执行的代码。
我们的核心战术就是:将含有HTML5事件处理器或伪协议的Payload,对其中的关键字符进行Unicode编码,以绕过过滤器的字符串匹配,待浏览器解码后成功触发执行。
3. 实战环境准备与初步侦察
在开始构造复杂Payload之前,我们必须先摸清DVWA High级别XSS模块的具体过滤规则。盲目尝试效率低下。
3.1 环境搭建与配置
- 确保你的DVWA已正确安装并运行。将安全级别设置为“High”。(在DVWA Security页面调整)。
- 访问“XSS (Reflected)”模块。你会看到一个简单的输入框,提示你输入名字。
3.2 试探性输入与规则分析我们首先进行一些简单的测试,以推断其过滤行为:
测试1:基础脚本输入:
<script>alert(1)</script>输出:页面显示空白或仅显示你输入的其他文本。查看网页源代码(Ctrl+U),你会发现<script>和</script>标签消失了。这说明它采用了删除策略。测试2:大小写变异输入:
<ScRiPt>alert(1)</ScRiPt>输出:同样被删除。说明过滤是不区分大小写的。测试3:嵌套与混淆输入:
<scr<script>ipt>alert(1)</scr</script>ipt>输出:理想情况下,内部的<script>被删除,剩下外部的<script>和</script>,可能触发。但在High级别,这种简单的嵌套混淆通常也会被递归处理或整体删除。测试后通常失败。说明过滤逻辑可能比较“聪明”,会进行递归或一次性匹配删除。测试4:测试其他标签输入:
<img src=x onerror=alert(1)>输出:这是关键一步!查看页面输出和源代码。你会发现,整个<img>标签被完整地输出到了页面上!这意味着,过滤器可能只针对<script>这个特定的标签进行了删除,而对于其他标签(如<img>)及其属性,并没有进行严格的过滤或编码。这为我们利用HTML5事件处理器打开了大门。
通过以上侦察,我们基本确定了DVWA High级别XSS(Reflected)的过滤策略:它主要聚焦于删除<script>和</script>字符串,但对其他HTML标签和属性的过滤非常宽松。这符合很多初级防护措施的特点——堵住了最明显的漏洞,却留下了更隐蔽的后门。
4. 利用HTML5标签与事件构造初级Payload
既然<img>标签可以存活,我们就从它开始。onerror事件是一个经典载体,当图片加载失败(src指向一个不存在的资源)时就会触发。
4.1 基础事件处理器Payload尝试输入:<img src=\"https://invalid.url\" onerror=\"alert('XSS-High')\">
提交后,如果页面弹出了警告框,恭喜你,你已经绕过了对<script>的依赖。但现实往往没那么简单。High级别的防护有时会对事件处理器属性值中的引号或括号进行一些处理。我们查看输出源代码,可能会发现属性值被改变了。
4.2 应对属性值过滤如果发现onerror=\"alert('XSS-High')\"中的引号被转义或删除,我们可以尝试省略引号,利用HTML的属性解析特性(在某些情况下,属性值可以不用引号包裹,直到遇到空格或>)。 尝试:<img src=x onerror=alert(1)>这里src=x会加载失败,触发onerror。alert(1)作为属性值,虽然没有引号,但在浏览器解析时是有效的。
注意:省略引号时,确保你的JavaScript代码中没有空格。例如
alert(document.cookie)中的空格会提前终止属性值,导致错误。此时可以用反引号()包裹(如果浏览器支持),或者使用其他无空格写法,如alert(document['cookie'])`。
4.3 探索更多HTML5载体<img>只是开始。我们可以测试更多标签和事件组合,增加攻击的隐蔽性和成功率:
<svg>标签:SVG本质是XML,内联JavaScript能力很强。<svg onload=\"alert(1)\">或<svg><script>alert(1)</script></svg>(注意内部<script>可能被过滤,但<svg>标签和onload事件可能存活)。<body>事件:如果输入点出现在<body>标签内或附近,可以尝试闭合前一个标签并注入:</textarea><body onload=alert(1)>(需要结合上下文)。<input>标签的onfocus:需要配合autofocus属性,在页面加载时自动触发。<input autofocus onfocus=alert(1)><details>标签的ontoggle:用户点击展开/收缩时触发。<details ontoggle=alert(1)><summary>点击我</summary></details>
通过这一步,我们已经证明了绕过<script>过滤的可行性。但真正的High级别挑战,往往还有第二道防线:对onerror、onload这类明显的事件处理器名称进行过滤。这就需要祭出我们的“编码大法”了。
5. Unicode编码进阶:绕过事件处理器名称过滤
假设DVWA的High级别加强了过滤,它不仅删除<script>,还会查找并删除onerror、onload、onclick等常见事件属性。这时,明文的事件处理器名称就无法使用了。
5.1 Unicode编码原理浏览器在解析HTML时,会识别并解码HTML实体和(在某些上下文中的)Unicode转义序列。例如:
onclick中的o可以用其Unicode十六进制码点\u006f表示。- 在HTML属性值中,我们可以使用HTML实体:
o是o。 - 关键技巧在于,我们可以对事件处理器名称本身进行编码,但保证浏览器在解析阶段能正确解码它。
5.2 构造编码Payload我们的目标是让服务器端收到的字符串里没有明文的onerror,但浏览器渲染时却能看到它。
方法一:HTML实体编码整个属性名我们可以将onerror每个字符都转换成它的十进制或十六进制HTML实体。o->o或on->nr->re->er->rr->r(注意,第二个r重复了,onerror只有两个r,这里示例有误,正确应为o,n,e,r,r,o,r? 等等,onerror拼写为 o-n-e-r-r-o-r?不对,是 o-n-e-r-r-o-r? 更正:onerror是 o-n-e-r-r-o-r? 拼写检查:onerror正确拼写就是 o-n-e-r-r-o-r。所以字符是 o, n, e, r, r, o, r。但通常我们不需要全部编码,编码关键部分即可。)
一个更实用的方法是只编码部分字符,以绕过简单的字符串匹配。例如,编码on: 输入:<img src=x onerror=alert(1)>这里n是n的十六进制HTML实体。对于过滤器来说,它匹配的字符串是onerror,这并不等于onerror,因此可能绕过。浏览器则会将其解析为onerror。
方法二:利用JavaScript Unicode转义(在HTML属性值中)这种方法更巧妙。我们可以在事件处理器的值(即JavaScript代码)中,使用Unicode转义来“动态生成”事件处理器名称本身。但这通常需要结合其他技巧,如利用eval或setAttribute。 一个经典的Payload是:<img src=x id=\"xss\" style=\"display:none\"><script>xss.setAttribute('on'+'error', 'alert(1)'); // 这里用字符串拼接绕过对‘onerror’的检测xss.src=''; // 触发错误</script>但注意,High级别下<script>标签会被删除,所以这个方案行不通。我们需要在不依赖<script>标签的情况下,在HTML层面实现类似效果。这非常困难,因为HTML本身不支持这种动态拼接。
因此,更可靠的方法仍然是直接对HTML标签内的事件处理器名称进行HTML实体编码。
5.3 实战编码绕过测试让我们构造一个综合性的测试Payload:<img src=\"x\" onerror=\"alert('XSS-Encoded')\">这个Payload做了两处编码:
onerror中的n被编码为n。alert中的e被编码为e。
提交这个Payload,并仔细观察页面行为。如果成功弹窗,说明DVWA High级别的过滤规则没有解码检查,我们成功绕过了。如果失败,我们需要查看页面源代码,看输出变成了什么。可能过滤器对&#x;这种实体编码也进行了处理,或者对属性值中的括号、引号进行了转义。
实操心得:编码绕过不是盲目的,需要根据过滤器的响应进行迭代测试。从编码一个字符开始,逐步增加编码范围。优先编码那些在正则表达式中可能是关键字的字符,如
on中的o和n,script中的s、c、r、i、p、t。同时,注意编码的一致性,确保浏览器能正确解码回有效的语法。
6. 组合拳:HTML5标签+编码+伪协议及其他技巧
在单一技巧可能被防御的情况下,组合多种技术能大大提高成功率。
6.1 结合javascript:伪协议<a>标签的href属性是另一个常见的攻击向量。High级别肯定会过滤javascript:这个字符串。我们可以用编码来绕过。
- 尝试:
<a href=\"javascript:alert(1)\">Click</a>(大概率javascript:被过滤或置空)。 - 编码绕过:对
javascript:进行编码。注意,这里编码需要在能被浏览器URL解码的上下文中生效。 一种方法是使用HTML实体编码冒号或部分字母:javascript:alert(1)可能不行,因为:是协议分隔符,必须明文。 可以尝试对协议名整体进行UTF-16编码或其他形式的混淆,但现代浏览器对javascript:协议的处理非常严格,简单的编码可能无法正确识别。更可行的方法是结合事件处理器,而不是依赖伪协议。
6.2 利用<iframe>的srcdoc属性<iframe srcdoc=\"<img src=x onerror=alert(1)>\">srcdoc属性允许内联HTML。如果过滤器只检查了主文档的输入,而没有递归检查srcdoc内的内容,那么内层的Payload可能被执行。并且,我们可以对srcdoc值内的HTML进行二次编码。
6.3 利用HTML5表单属性formaction或form(此技巧更适用于存储型XSS或特定上下文)某些表单相关的标签,如<button>或<input type=\"submit\">,具有formaction属性,可以覆盖表单的提交地址为JavaScript伪协议。但这在反射型XSS且输入点不在表单内的场景下较难利用。
6.4 编码的嵌套与多重编码有时,一层编码可能被过滤器的解码层剥掉。我们可以尝试双重编码。 例如,<的HTML实体是<。如果我们输入<,服务器端过滤器如果只解码一次,可能得到<,然后被其<script>过滤规则删除。但如果过滤器不解码,<输出到浏览器,浏览器会将其解码为<。 对于事件处理器,我们可以尝试:<img src=x onerror=alert(1)>这里将onerror每个字符都编码为十六进制HTML实体。这串字符对于字符串匹配的过滤器来说是完全陌生的。
7. 常见问题、调试与高级绕过思路
在实际操作中,你可能会遇到各种意外情况。以下是一些常见问题及排查思路:
7.1 Payload提交后无任何反应
- 查看源代码:这是最重要的调试步骤(Ctrl+U)。看看你输入的Payload被服务器处理成了什么样子。是整体消失了?还是被截断了?属性值是否被加上了引号或转义了?
- 检查控制台:打开浏览器的开发者工具(F12),查看Console面板。是否有JavaScript语法错误?错误信息能精准定位Payload哪里出问题了。
- 使用更简单的Payload:先测试
<img src=x>看标签是否能存活。再测试<img src=x onerror=console.log(1)>,在控制台观察输出,这比alert更隐蔽且不受弹窗拦截影响。
7.2 事件被触发但代码未执行
- 语法错误:在编码时,可能破坏了JavaScript语法。确保编码解码后的字符串是合法的JS。例如,
alert(1)被编码成alert(1),浏览器解码后是alert(1),语法正确。但如果编码了括号,如alert(1),解码后是alert(1),也正确。关键在于整体语法在解码后必须正确。 - 作用域问题:在某些沙箱环境或
srcdoc中,alert函数可能不可用。尝试使用parent.alert或top.alert,或者使用console.log证明代码执行。
7.3 高级绕过思路(如果上述均失效)如果DVWA的High级别实现了非常严格的过滤(例如,使用DOM Purify、CSP或严格的HTML解析器白名单),那么上述基于标签和事件的方法可能都会失效。这时,你需要考虑:
- 基于DOM的XSS:检查页面是否存在客户端JavaScript,其不安全地操作了
innerHTML、document.write或eval,并且你的输入能影响其参数。这需要分析前端JS代码。 - 滥用合法的HTML属性:例如,某些标签的
style属性支持expression()(旧版IE)或现代浏览器的CSSurl()函数中执行JavaScript(限制很多)。或者<link>标签的href结合某些协议。 - 字符集与编码混淆:如果网站指定了特殊的字符集,或者存在编码转换问题,可能利用UTF-7等旧编码(如
+ADw-script+AD4-alert(1)+ADw-/script+AD4-),但现代浏览器默认已不易受此攻击。
注意事项:在进行XSS测试时,务必在授权环境下进行(如自己的DVWA靶场)。切勿对未授权的真实网站进行测试,这是违法行为。本实战的所有技巧仅用于安全学习与研究,旨在帮助开发者理解漏洞原理,从而构建更安全的应用程序。
8. 防御视角:从攻击中学习如何防护
通过这一系列的绕过尝试,我们站在攻击者的角度深刻体会了过滤的难点。那么,作为开发者,应该如何有效防御这类攻击呢?
输出编码(Output Encoding)是黄金法则:永远不要相信用户输入。根据数据输出的上下文(HTML体、HTML属性、JavaScript、CSS、URL),进行相应的编码。
- 输出到HTML正文:将
< > & \" '等转换为HTML实体(<,>,&,",')。 - 输出到HTML属性值:除了上述字符,空格和换行也可能有问题,建议使用引号包裹属性值,并对引号进行编码。
- 输出到JavaScript:需进行Unicode转义,如
<转为\u003c。 - 使用成熟的库(如OWASP ESAPI、Java的
StringEscapeUtils, Python的html.escape等)来完成这项工作,而不是自己写正则。
- 输出到HTML正文:将
使用内容安全策略(CSP):CSP是一个强大的深度防御工具。通过HTTP头
Content-Security-Policy,你可以告诉浏览器只允许执行来自特定来源的脚本,禁止内联脚本(unsafe-inline)和eval。这能从根本上杜绝大部分XSS攻击,包括我们上面使用的所有基于事件处理器和内联脚本的技巧。例如,一个严格的CSP头可能是:Content-Security-Policy: default-src 'self'; script-src 'self'。输入验证与规范化:在接收输入时,进行严格的类型、长度、格式检查。对于姓名框,可以限制只允许字母、数字和少量符号。但记住,输入验证是辅助,不能替代输出编码。
避免危险的DOM操作:前端JavaScript避免使用
.innerHTML、.outerHTML、document.write()直接插入未经验证的用户数据。优先使用.textContent或.setAttribute等安全的API。使用现代框架的安全特性:如React、Vue、Angular等主流前端框架,默认都会对渲染的数据进行转义,在很大程度上自动防护了XSS。但开发者仍需注意避免使用
dangerouslySetInnerHTML(React)或v-html(Vue)等绕过安全机制的方法。
回到DVWA High级别XSS,它的过滤策略之所以能被绕过,正是因为它只做了局部的、基于黑名单的删除,而没有进行彻底的上下文相关输出编码。这给我们上了生动的一课:安全防护必须全面、深入,并且要紧跟技术发展(如HTML5新特性带来的新攻击面)。通过这种攻防实战,我们不仅能学会“破”,更能深刻理解如何“立”,从而在开发中写出更安全的代码。
