资讯详情

资讯详情

行业资讯

DVWA靶场DOM型XSS全等级通关:原理、绕过与防御实战

1. 先搞清楚DOM型XSS和反射型、存储型不是一回事前阵子有朋友在群里问DVWA里的DOM型XSS到底怎么打为什么照着反射型XSS的payload直接抄过去却经常不弹窗。DVWADamn Vulnerable Web Application里的XSS模块有三个入口分别是反射型、存储型和DOM型其中DOM型最容易被当成“改参数、看回显”来理解结果新手经常在这个模块卡住。这篇我把DVWA的xss_d模块从Low到Impossible四个难度完整过一遍把每一步的payload、源码和绕过思路都摊开讲顺便把DOM型XSS的防御方法也聊透。先花点时间把这三种XSS的本质区别说清楚。反射型XSS和存储型XSS有一个共同特征恶意脚本会出现在服务端的响应里。反射型是服务端把URL参数原样拼进页面返回存储型是服务端把恶意代码存进数据库之后每次渲染都带出来。这两种XSS的检测思路往往是看响应包、看服务端输出所以很多人习惯拿着“看回显”的思维来做题。DOM型XSS完全不一样。它的恶意代码从头到尾根本不进服务端服务端返回的就是一个正常的静态页面。真正出问题的是页面里的JavaScript它读取了浏览器URL中的某一段数据然后直接把它当成HTML写了进去。恶意脚本是在浏览器端、由前端代码自己“亲手”执行的。如果说反射型和存储型是快递员把危险包裹原样送到你家门口那DOM型就是快递员只把门牌号报给你你自己把门打开了。这也是为什么DOM型XSS在学习时很容易产生认知偏差你在浏览器里明明看到了弹窗打开网络面板却发现服务端响应里干干净净没有任何可疑标签。只有去读前端JavaScript源码才能看到漏洞点在哪里。DVWA这个靶场之所以适合练这个是因为它把XSS分成了三种场景又把每种场景拆成四个安全等级从完全没有防护到彻底修复都有对应代码可以直接读源码观察防御是怎么一步步变强的。在真实项目里做前端安全审计时你同样需要按这个思路去追数据从哪里来最后写到了哪个DOM接口中间有没有校验和编码。整篇文章的路线是这样的先把DVWA跑起来认准xss_d模块的入口然后分别走一遍Low、Medium、High、Impossible四个等级的通关步骤每步都会配上payload和源码片段最后再回到DOM型XSS的原理和防御层面做总结。2. 环境准备与入口先把DVWA跑起来2.1 部署、登录与安全等级切换学习DVWA之前先把环境准备利索。你不需要用特别复杂的架构一个本地PHP集成环境就够了把DVWA源码丢到网站根目录启动MySQL和PHP然后访问安装页面完成初始化。也可以用容器方式拉起一个现成的靶场镜像省去手动配置数据库的时间。两种方式我都试过集成环境更适合观察源码和中间件配置因为所有文件都在本地想改代码、加日志、打断点都很方便容器方式胜在干净快速五分钟就能进到登录页。DVWA的默认登录账密是admin/password登录成功后先不要急着进漏洞模块去DVWA Security页面把安全等级切成Low。这一步容易被忽略但非常关键DVWA的很多漏洞在High和Impossible等级下防护逻辑差别很大如果你默认等级是Impossible直接访问xss_d模块会发现怎么打都不弹窗容易产生“是不是环境坏了”的错觉。通关时按Low → Medium → High → Impossible的顺序逐级切换每切一次就重新访问一次模块页面确保当前等级的源码生效。有一个细节要注意切换安全等级之后DVWA不会自动刷新你的会话。如果浏览器里开了很久的页面或者登录状态已经过期切完等级后访问模块可能直接被弹回登录页。这时候重新登录一次就好不需要重新初始化数据库。2.2 先观察xss_d模块的正常行为进入xss_d模块后你会看到一个语言选择下拉框。正常页面URL里带着?defaultEnglish这样的参数下拉框默认选中English。试着手动把URL改成?defaultFrench刷新页面你会看到下拉框中多了一个French选项。此时打开浏览器的开发者工具看Network面板里这次请求的响应内容。你会发现一个很有意思的现象服务端返回的HTML里根本没有French这个词。也就是说下拉框里的French不是服务端帮你渲染的而是页面里的JavaScript读取了URL里的default值用DOM操作动态写进去的。这个观察就是理解DOM型XSS的钥匙。当URL参数被前端JavaScript读取并且用document.write、innerHTML这类接口拼进页面时参数内容就不再是简单的“字符串”而是会被浏览器当成HTML解析的“代码”。一旦这个拼接过程没有任何过滤XSS就成立了。2.3 给DOM型XSS画一条数据流在我开始逐个等级打靶之前建议你先在脑子里建立一个数据流模型。DOM型XSS的数据流是这样的浏览器URL的某一段比如location.search或者location.hash作为输入源被前端JavaScript用location.href、location.search、location.hash等方式读取然后经过字符串截取、解码、拼接最终传到了document.write、innerHTML、eval等危险接口。在这个链路里服务端只有两种姿态一种是不管直接把页面返回前端自己惹祸另一种是尝试在入口处做过滤但因为过滤只作用在服务端能看到的字符串上而DOM型XSS的真正触发位置在浏览器端所以绕过空间经常比反射型XSS大得多。后面谈Medium和High的绕过方法时你会反复用到这个数据流模型。尤其是“服务端能不能看到这段payload”这个判断会直接决定一条payload是否可用。3. Low等级通关从改语言到弹窗3.1 先看Low级别的源码漏洞一目了然Low等级通关逻辑很简单但源码值得细读。打开DVWA的xss_d模块对应源码文件核心JavaScript长这样var lang English; if (document.location.href.indexOf(default) 0) { var lang document.location.href.substring(document.location.href.indexOf(default)8); } document.write(option value lang decodeURIComponent(lang) /option);这段代码做了什么它先默认lang是English然后检查当前URL的完整字符串里有没有default。如果有就从default后面把所有内容截出来当作lang的值最后用document.write把lang拼进一个option标签里写入页面。这里有两个容易被忽略的细节。第一document.location.href拿到的是浏览器地址栏的完整URL包括参数和编码后的内容第二写入前调用了decodeURIComponent也就是URL编码会被还原。这两个细节叠加在一起意味着攻击者完全可以通过URL编码把带有尖括号的HTML标签安全地送进document.write浏览器在解析时看到的又是还原后的原始标签。3.2 正式通关用script标签弹窗在地址栏访问下面这个URLhttp://127.0.0.1/dvwa/vulnerabilities/xss_d/?defaultscriptalert(1)/script浏览器会自动把尖括号做URL编码实际请求会变成类似%3Cscript%3Ealert(1)%3C%2Fscript%3E的形态。前端JS用substring截取到编码后的字符串再经decodeURIComponent还原成scriptalert(1)/script最后document.write把它写进页面。浏览器解析这段HTML时遇到script标签就会执行里面的JavaScript弹窗出现。为了稳妥你也可以手动把payload编码后放进URLhttp://127.0.0.1/dvwa/vulnerabilities/xss_d/?default%3Cscript%3Ealert(1)%3C%2Fscript%3E两种方式效果一样。如果页面弹出了alert(1)说明Low等级已经通关。3.3 Low等级为什么完全拦不住Low等级没有任何服务端过滤前端也完全没有校验。整个漏洞的根源就一句话外界可控的数据被当成HTML代码写入了DOM。document.write这个接口本身就不是用来写普通文本的它直接把字符串交给HTML解析器。只要字符串里含有标签就会按照标签处理。这也解释了为什么要避免在真实项目里用document.write拼接任何用户可控数据。哪怕你对参数做了一次性过滤只要某个环节漏了一个payload变体DOM型XSS就可能复活。Low等级的教训本质上是在说不能用黑名单思想去修DOM型XSS。4. Medium等级通关被拦了别慌换条路走4.1 先踩坑script标签直接被拦切到Medium等级后先用Low等级的payload测试。访问?defaultscriptalert(1)/script页面没有弹窗URL被重定向成了?defaultEnglish。这说明服务端已经开始插手过滤了。打开源码文件Medium等级的服务端处理大概长这样if (array_key_exists(default, $_GET) !is_null($_GET[default])) { $default $_GET[default]; // 过滤script标签 if (stripos($default, script) ! false) { header(location: ?defaultEnglish); exit; } }stripos是不区分大小写的字符串查找所以script、SCRIPT、Script都会被拦下来。过滤方式简单粗暴只要参数里含有script这六个字符的开头组合就强制重定向回English。但注意这里只过滤了script。这个漏洞到了Medium等级依然很容易打因为XSS并不是只靠script标签才能触发。4.2 用非script标签绕过实测三个可用payloadHTML里能被浏览器解析并执行JavaScript的标签远不止script一个。img、svg、body、iframe、input、details等标签配合事件属性都能触发脚本。我在Medium等级下实测有效的payload有这几种img srcx onerroralert(1)svg onloadalert(1)body onloadalert(1)这些payload的共同点是不包含script子串所以能骗过服务端的stripos检查。当document.write把它们写入页面后HTML解析器会创建对应元素事件属性里的alert会在特定时机执行。比如img标签加载src失败就会触发onerror弹窗出现。完整URL之一http://127.0.0.1/dvwa/vulnerabilities/xss_d/?defaultimg srcx onerroralert(1)刷新后弹窗成功Medium通关。从这里开始你会感受到DOM型XSS的绕过规律过滤规则只会盯着某几个特征串HTML标签和事件属性的组合多到数不清黑名单很难防全。4.3 更隐蔽的玩法把payload藏进URL的#号后面除了换标签Medium还有一个更巧妙的绕过思路把payload放在URL的hash片段里。HTTP请求中浏览器只会把#前面的部分发给服务端#后面的内容只存在浏览器端服务端根本看不到。DVWA的JavaScript读取的是document.location.href这个属性包含完整的URL包括hash部分。所以你可以这样构造http://127.0.0.1/dvwa/vulnerabilities/xss_d/#defaultimg srcx onerroralert(1)服务端拿到的URL路径里没有default参数连检查都不会触发前端JavaScript从location.href里找到了default截取后面的payloaddecodeURIComponent还原后照样写入DOM执行。这个技巧在真实场景里很有价值WAF、服务端日志、IDS看到的都是干净请求恶意内容只存在于浏览器的地址栏里安全设备很难感知。做防御时需要意识到DOM型XSS的检测不能只看网络层还要看前端代码的输入源和危险接口使用情况。4.4 Medium的教训黑名单过滤挡不住DOM执行链Medium等级让我最感慨的一点是它确实出了力在服务端做了检测但这个检测只拦了一个特征而且完全没有意识到DOM型XSS的执行链是“前端读取URL → 前端拼接HTML”服务端对URL参数做再多过滤只要前端JS依然信任location.href绕过就是时间问题。所以在实际项目里如果你遇到一个DOM型XSS先在代码里找两个东西输入源是哪个接口输出点是哪个DOM API。这两个点之间的路径才是修复的重点。服务端加过滤即使能挡住一部分攻击也只能算临时缓解不能叫修复。5. High等级通关过滤更猛但DOM还是那个DOM5.1 High比Medium严在哪里切到High等级后先按Medium的payload试一下img srcx onerroralert(1)。某些版本的DVWA在High等级会把事件属性也拉黑此时onerror会被识别并拦截URL被跳回English。再看源码可能会看到类似这样的规则// 过滤script标签 if (stripos($default, script) ! false) { header(location: ?defaultEnglish); exit; } // 过滤常见事件属性 if (preg_match(/on[a-zA-Z]\s*/i, $default)) { header(location: ?defaultEnglish); exit; }这个正则会把onerror、onload、onclick这一类事件属性全部拦下。Medium里顺手可用的img onerror、svg onload在High等级下直接失效。看起来防护更强了但DOM型XSS真正的问题并没有解决前端JavaScript还是老代码还在读location.href还在用document.write。这个阶段最需要的是冷静梳理还有哪些标签和属性可以携带并执行JavaScript。黑名单管得住已知的坏字符但管不住组合变形。5.2 换一个执行通道iframe配合伪协议如果事件属性被拦了可以试着用javascript:伪协议。iframe标签的src属性可以指向一段javascript:协议的代码浏览器在加载iframe时会执行这段JavaScript。payload长这样iframe srcjavascript:alert(1)这个payload里没有script没有on开头的事件属性可以绕过上面那种关注黑名单的过滤规则。在High等级下构造完整URL访问http://127.0.0.1/dvwa/vulnerabilities/xss_d/?defaultiframe srcjavascript:alert(1)刷新页面后iframe被写入DOM浏览器尝试加载src指定的内容JavaScript执行弹窗出现。5.3 hash绕过依然是杀手锏High等级下最省事的通关方式依然是把payload放到#后面。因为服务端的正则再强也只能检查它自己收到的HTTP请求参数而hash部分压根不会出现在请求里。构造下面这个URLhttp://127.0.0.1/dvwa/vulnerabilities/xss_d/#defaultimg srcx onerroralert(1)页面刷新后前端JS从location.href中截取payloaddocument.write触发img加载失败onerror弹窗。即使DVWA在High等级下已经把事件属性写进了黑名单这条payload依然有效因为服务端连等于都没看到正则根本没有机会执行。这给了我们一个重要启示服务端过滤对DOM型XSS只有在“前端读取的数据恰好是服务端能验证的数据”时才有点用。一旦前端数据源包含location.hash、window.name、document.referrer这类服务端看不到的入口服务端过滤就会完全失效。5.4 各等级下payload存活情况速查payloadLowMediumHigh说明scriptalert(1)/script弹窗被拦被拦黑名单重点关照对象img srcx onerroralert(1)弹窗弹窗视版本可能被拦事件属性被拉黑后失效svg onloadalert(1)弹窗弹窗视版本可能被拦同上iframe srcjavascript:alert(1)弹窗弹窗弹窗不含script、不含on事件#defaultimg srcx onerroralert(1)弹窗弹窗弹窗服务端看不到hash绕过滤这张表不一定在所有DVWA版本里完全一致因为不同版本High等级过滤规则有小差异但核心结论是稳定的payload解法越多越能理解DOM型XSS的绕过本质。6. Impossible等级看看什么叫做“治本”6.1 白名单 服务端渲染才是真正的修复切到Impossible等级后正常访问xss_d模块再试前面那些payload。你会发现无论把payload放在query里还是hash里页面都不再执行脚本连URL都会被重置回?defaultEnglish。查看Impossible源码根因就很清楚了if (array_key_exists(default, $_GET) !is_null($_GET[default])) { $default $_GET[default]; // 白名单只允许预定义的语言 switch ($default) { case English: case French: case German: case Spanish: case Portuguese: case Chinese: break; default: header(location: ?defaultEnglish); exit; } }这一版不再试图识别坏payload而是直接规定什么值是合法的。不是English、French、German等预设语言之一就强制跳回English。黑名单和正则再长也有穷举不完的变形白名单直接缩小到几个固定值攻击者输入的字符串从一开始就不在允许名单里注入无从谈起。6.2 前端代码彻底放弃了location更重要的是Impossible等级的前端代码也不再从document.location.href里读取语言值了。select下拉框的选项是服务端写死的静态HTMLJavaScript最多做一下UI交互不会把URL参数当成HTML写入页面。危险接口document.write被移除用户可控数据没有一个输入源能流向它。我在本地试过各种绕过思路改成#default、URL编码变体、大小写变形、用iframe伪协议全部无效。原因很简单——输入源被白名单卡死在服务端DOM写入接口的危险调用被移除。两层防御叠加DOM型XSS的执行链彻底断掉。6.3 Impossible的防御思路值得抄进真实项目看完了Impossible等级的写法再回头看前面三个等级的源码可以总结出一个非常清晰的修复套路服务端做白名单校验客户端避免将不可信数据写入HTML接口。两件事里只要做到任何一件这种漏洞就基本堵住了两件都做才能算真正的治本。另外Impossible等级还体现了“默认拒绝”的安全思想。它不试图预测所有攻击payload而是把默认行为设为拒绝只放行明确允许的值。这个思路在处理其他安全问题时同样适用与其穷举黑名单不如用白名单把风险边界画出来。7. 从一通到底追溯DOM XSS的本质7.1 为什么服务端过滤对DOM XSS只能“碰运气”把四个等级全部通关之后我最大的体会是DOM型XSS的核心矛盾在于攻击代码在客户端执行但很多人习惯在服务端做防御。服务端能看到的只是HTTP请求的一部分而前端JavaScript能读到的数据范围更广包括URL的hash、window.name、document.referrer等。只要前端代码存在“读取不可信数据 → 写入DOM”的链路服务端不管做多少过滤都只是在入口处设关卡。可攻击者可以换一条服务端看不到的入口进来也可以在出口处换一种标签、属性、编码方式躲过特征检查。这也是为什么DOM型XSS在真实项目中看起来很“轻”调起门槛不高但检测和修复却经常踩坑。7.2 常见输入源与危险输出接口做DOM型XSS审计时我习惯先列一张表把输入源和输出接口分别找出来再沿着数据流看中间有没有净化。常见的输入源包括输入源说明location.href完整URL包含query和hashlocation.searchURL的query部分location.hashURL的hash部分服务端不可见document.referrer来源页面URLwindow.name浏览器窗口名可跨页面传递postMessage父页面与iframe之间的消息传递常见危险输出接口包括输出接口风险说明document.write/writeln直接把字符串交给HTML解析器element.innerHTML/outerHTML解析并生成DOM元素insertAdjacentHTML类似innerHTML的解析行为eval把字符串当JavaScript代码执行setTimeout/setInterval传字符串同样会执行字符串代码location/location.href赋值可触发javascript:伪协议如果一条数据流里输入源可控并且没有可靠编码或白名单校验输出接口恰好是这些危险API那DOM型XSS基本可以判定存在。7.3 修复建议白名单、编码、避免拼接从DVWA的Impossible等级可以推导出一套完整修复方案。服务端入口处对参数做严格白名单校验拒绝预期格式之外的所有值。前端代码里不要用document.write或innerHTML拼接用户可控数据需要动态渲染文本时用textContent赋值或者使用createElement、appendChild这类不会解析HTML的DOM操作。如果业务上必须使用innerHTML也需要先对数据做HTML实体编码。前端框架在默认情况下会对插值表达式做转义所以很多使用现代框架的项目能把XSS风险压得很低但一旦项目里出现v-html、dangerouslySetInnerHTML这类直接插入HTML的API就需要格外小心。7.4 把思路从靶场延伸到实战审计通关DVWA后我建议你用同样的思路去审一审自己参与过的前端项目。全局搜索项目代码里的document.write、innerHTML、insertAdjacentHTML、eval对每个命中点追一下数据来源这个变量来自网络请求吗来自URL参数吗中间有过滤或编码吗有没有经过白名单校验有一次我在一个内部系统的前端代码里看到location.hash被直接拼进innerHTML当时所有人都在关注服务端接口的输入校验没人注意到前端还有这样一条数据流。后来用DVWA里学到的hash绕过思路去验证果然是DOM型XSS。这种场景在真实项目里并不少见因为服务端接口容易统一治理前端散落的DOM操作很容易被遗漏。8. 通关过程中最常见的坑与排查思路8.1 常见问题速查表在我反复打DVWA DOM XSS模块的过程中遇到过不少看起来“灵异”的现象整理成下面的速查表供你排查时参考现象可能原因解决办法改了default参数下拉框没变化安全等级还在Impossible去DVWA Security页面切到对应等级弹窗没出现URL被跳回English服务端黑名单拦截检查当前等级换绕过payload或改用hashscript标签在Medium被跳转服务端用stripos过滤了script改用img onerror、svg onload等引号、尖括号粘贴到地址栏后显示混乱浏览器自动转码导致URL不直观直接粘贴原始payload让浏览器处理或预先用encodeURIComponent编码hash绕过时页面不执行hash里的payload编码不正确确保payload在URL里是合法编码不要出现未编码空格登录后等级设置不生效会话过期或页面缓存重新登录重新进入模块明明用img标签却弹不出某些版本High把on事件拉黑换iframe伪协议或hash绕过8.2 提升效率的调试思路第一个建议是直接用浏览器开发者工具控制台调试payload。在Console里输入document.location.href http://127.0.0.1/dvwa/vulnerabilities/xss_d/?default...回车即跳转比手动复制粘贴地址栏更快。想反复测试不同payload时可以在Console里维护一个payload列表逐个赋值给location.href观察页面行为。第二个建议是善用源码审计。DVWA源码文件每个等级是独立的切到对应等级后打开对应源码文件用最快速度找到JavaScript里读location和调用document.write的位置就能判断当前等级的过滤范围和可绕过方向。与其盲试payload不如先看代码再猜解法。第三个建议是注意浏览器的编码行为。浏览器地址栏会自动把非URL合法字符转成百分号编码这是正常现象。前端JS里的decodeURIComponent会把编码还原所以很多看起来乱码的URL实际执行时依然有效。关键是不要因为你“看不懂”地址栏里的payload就以为它失效了。8.3 为什么会漏判DOM型XSS最后我想单独说一个现象很多人在做安全测试时习惯只看服务端响应包发现响应的HTML里没有注入点就判断没有XSS结果漏掉了DOM型XSS。判断DOM型XSS的正确姿势是先看页面加载后的实际DOM结构再看前端JavaScript是否把URL里的数据动态写入了页面。浏览器开发者工具的Elements面板能直接看到document.write生成的内容Network面板反而看不到。如果你在做某次测试时发现页面出现了预期外的DOM节点但响应包里没有对应字符串第一反应就应该是“这里有DOM型XSS”。反过来如果你在某个页面里发现了DOM型XSS再去看服务端日志日志里也未必有攻击痕迹因为攻击可能一直藏在hash里。这个经验在做安全监控和应急响应时尤其重要。最后再分享一点心得体会通关完DVWA的DOM XSS模块之后我自己的收获并不只是几个payload而是重新理解了“数据与代码区分”这个安全原则。DOM型XSS最典型的特征就是太多人把它当成“字符串处理问题”而不是“代码执行问题”。当你看到前端代码把一个字符串拼进HTML时你就应该意识到这个字符串不再只是数据它马上就要变成页面代码了。以我个人的习惯拿到一个前端安全任务时我一般会先从危险输出接口反向追数据流找到所有可能进入这些接口的输入源服务端能做校验的地方尽量用白名单做限制前端代码能避免拼接HTML的地方就优先用textContent这类安全接口。DVWA只是把这些攻防逻辑压缩在一个小靶场里让你用最直观的方式体验一遍。如果你现在已经把Low到Impossible全部打通我建议你回到源码文件自己改一版防御代码比如在Level High的基础上再增加一层过滤或者在Medium的基础上移除document.write改用安全的DOM接口然后重新测试原来的payload能不能绕过。这个过程比单纯通关节奏更有价值因为你在练习的是攻防双方都需要的思考方式永远比对方多想一步。

准备好开启您的数字化征程了吗?

专业团队为您量身定制,让您的企业网站脱颖而出

立即咨询