xss-labs靶场实战:从基础绕过到高级编码技巧全解析

📅 发布时间:2026/7/3 12:19:01 👁️ 浏览次数:
xss-labs靶场实战:从基础绕过到高级编码技巧全解析
1. 为什么你需要一个XSS实战靶场如果你刚开始学Web安全或者对XSS跨站脚本攻击这个名词既熟悉又陌生那你来对地方了。我刚开始接触安全的时候总觉得XSS不就是弹个框吗直到在真实项目里踩了坑才发现这里面门道深得很。理论看了一堆什么反射型、存储型、DOM型但真遇到一个过滤了script的输入框脑子就一片空白不知道从哪下手了。这就是为什么我强烈推荐每个想学好XSS的人都去亲手“通关”一个像xss-labs这样的靶场。它不是一个炫技的工具而是一个精心设计的“闯关游戏”。靶场把Web开发中那些常见的、用来防XSS的“盾牌”——比如htmlspecialchars转义、关键词替换、大小写转换——做成了一个个关卡。你的任务就是找到这些“盾牌”的缝隙把攻击代码“送”进去。这个过程就像在解一道道有趣的谜题。你会从最基础的“没有任何过滤”开始一步步面对越来越复杂的防御机制。每过一关你不仅学会了一种绕过技巧更重要的是你理解了防御者为什么会这么写代码以及他这么写之后还留下了哪些“后门”。这种攻防对抗的思维才是安全能力的核心。所以别再把XSS当成简单的弹窗游戏了。跟着我咱们一起把xss-labs靶场从第一关打到后面我会把每一关的过滤机制掰开揉碎再告诉你我是怎么绕过去的以及我当时是怎么想的。保证你学完就能用遇到真实场景也不发怵。2. 热身理解靶场与XSS的三种“面孔”在开始闯关之前咱们得先统一“语言”。xss-labs靶场主要模拟的是通过URL参数发起的攻击也就是反射型XSS。这种攻击的特点是“一次性”你的恶意脚本藏在URL里诱骗用户点击这个链接脚本就在用户的浏览器里执行了。它不会存储在网站上所以每次攻击都需要重新“钓鱼”。但XSS可不止这一种。为了让你有个全局观我简单说说另外两种你在其他靶场或实战中肯定会遇到存储型XSS这才是“大杀器”。攻击者把恶意脚本提交到网站的后台数据库里比如论坛发帖、用户留言。之后任何一个普通用户浏览到这条内容脚本都会自动执行。危害极大因为它可以持续影响所有访问者。DOM型XSS它比较特殊漏洞的根源不在服务器端而在前端的JavaScript代码里。比如一段JS代码从URL中获取参数location.hash然后不经过滤就直接用innerHTML写到了页面上这就可能被利用。它的攻击过程可能只在客户端完成服务器日志里都看不到攻击载荷。xss-labs靶场虽然聚焦反射型但它训练的过滤与绕过技巧是通用的。无论是服务器端过滤反射型、存储型还是客户端处理DOM型你面对的“消毒”逻辑都大同小异。工欲善其事必先利其器。实战中我主要用两个工具它们能极大提升效率浏览器开发者工具F12这是你最好的朋友。随时查看页面源码看你的输入被处理成了什么样观察HTML结构甚至可以直接在前端修改元素属性来测试这是最快的学习方式。Burp Suite专业抓包和改包工具。当你在浏览器里直接输入URL参数不方便比如参数太长或需要编码时或者需要修改HTTP请求头如Referer、User-Agent来攻击时Burp就是神器。用它拦截请求改完再放行非常直观。准备好工具咱们就正式开打。记住核心思路输入 - 过滤/处理 - 输出。我们的目标就是让精心构造的“输入”在经历“过滤”后在“输出”点仍然能被浏览器当成有效代码解析。3. 初阶关卡直面基础过滤与转义3.1 第一关毫无防备的“Hello World”第一关通常是个“见面礼”。你看到一个搜索框URL里有个name参数。按照最经典的思路输入scriptalert(1)/script回车弹窗成功。这一关的源码通常长这样$name $_GET[name]; echo h2Hello, .$name.!/h2;它直接把你输入的$name拼接进了HTML里没有任何处理。这在实际的古老网站或开发者的疏忽中仍可能见到。这一关的意义在于确认漏洞存在点并理解最基本的XSS payload结构一个完整的脚本标签。绕过技巧无需任何绕过。但你要养成习惯输入payload后立刻按F12查看“元素”标签看看你的代码被原封不动地放到了哪个位置。这里是直接放在了h2标签中间。这很重要因为后续很多关卡payload出现的位置决定了我们该用什么方法。3.2 第二关当输出被转义但属性值没有来到第二关你可能习惯性地再次输入scriptalert(1)/script发现没弹窗页面上显示的是被转义后的文本scriptalert(1)/script。查看源码你会发现类似这样的逻辑$keyword $_GET[keyword]; echo input typetext value.htmlspecialchars($keyword).;htmlspecialchars()函数是PHP防御XSS的基石之一。它会把特殊字符转换成HTML实体比如变成lt;变成gt;这样浏览器就不会把它们解析为标签了。所以直接插入到HTML正文Text Node里的内容被安全地转义了。但是注意看它只对直接输出到页面内容的部分用了htmlspecialchars。而下面这个value属性呢input namekeyword value你输入的内容这个value的值来自表单提交后的$_GET它被直接填入了HTML属性里没有经过htmlspecialchars处理这就是突破口。我们无法在页面内容里创建新标签但我们可以尝试“逃逸”出这个value属性然后紧接着构造新标签。绕过技巧闭合属性与标签。首先我们需要闭合value属性的双引号。输入一个双引号。然后闭合当前的input标签输入。最后写入我们的脚本标签scriptalert(1)/script。 完整的payload就是scriptalert(1)/script输入后页面源码会变成input namekeyword valuescriptalert(1)/script这样script标签就成功地被放置在了input标签之外被浏览器正常解析执行。这一关的关键是找到未经过滤的输出点并利用HTML语法进行闭合。3.3 第三关当尖括号和引号都被“没收”第三关你尝试第二关的payloadscriptalert(1)/script发现失败了。再试试单引号闭合scriptalert(1)/script也不行。F12一看好家伙你输入的单双引号、尖括号全被转成了实体quot;lt;gt;。源码显示这次不仅在内容输出用了htmlspecialchars连value属性值也用了而且默认配置下引号也被转义了。这意味着我们既不能插入新标签被转义也无法通过引号来闭合属性和被转义。绕过技巧利用HTML标签自身的“事件处理器”。 既然不能“造”新标签我们就“用”现有的标签。input标签还在那里。HTML允许我们在标签内部定义一些事件属性比如onclick、onmouseover、onerror等。当事件触发时就能执行JavaScript代码。 我们的目标是在不使用尖括号和引号的前提下给这个现有的input标签添加一个事件。首先value属性已经被赋值但我们可以尝试在它后面继续添加新的属性。由于htmlspecialchars默认不转义空格我们可以用空格分隔属性。构造payloadonclickalert(1)。注意onclick前面有个空格。输入后页面源码可能类似input value onclickalert(1)。但是alert(1)作为属性值可能需要引号包裹其实在HTML中如果属性值不包含空格或特殊字符引号是可以省略的。更稳妥的写法是我们利用value属性未闭合的引号虽然被转义了但语法上它还在然后注入事件。但这里引号被转义了我们直接注入事件浏览器可能会将onclickalert(1)整体当作value的值。这时我们需要用空格和另一个属性来“结束”value。但更简单的方法是利用单引号闭合。查看页面源码发现value是用单引号包裹的value...。而htmlspecialchars默认可能只转义了双引号我们需要测试。如果单引号可用payload可以是 onclickalert(1)。这样第一个单引号闭合了value然后添加onclick属性其值用第二个单引号开始但没结束浏览器可能会容错处理或者我们让alert(1)作为它的值。 实际上这一关常见的解法是 onclickalert(1)。输入后生成的HTML是input value onclickalert(1)。这样onclick事件就被成功添加。点击这个输入框就会触发弹窗。这一关教会我们当无法创建新标签时利用现有标签的事件属性是一条捷径。4. 中阶对抗面对字符串替换与过滤4.1 第四关简单的“删除”过滤第四关输入script标签发现页面上显示的是script尖括号不见了。查看源码发现用了str_replace函数$str $_GET[keyword]; $str2 str_replace(,,$str); $str3 str_replace(,,$str2);str_replace函数的作用是搜索并替换。这里它把和直接替换成了空字符串相当于“删除”。这比转义更粗暴但同样有效因为浏览器没有尖括号就无法识别标签。绕过技巧回归事件属性。 既然被删了我们自然无法构造新的标签。但别忘了第三关的思路——利用现有标签的事件。这一关的页面通常也会有一个input标签。我们故技重施尝试闭合它的属性并添加事件。 假设value属性是双引号包裹我们输入 onclickalert(1)。 处理过程我们的输入是 onclickalert(1)。代码先删除没有。再删除也没有。所以字符串原封不动。最终输出到HTML中input value onclickalert(1)。完美闭合了value并添加了onclick事件。点击输入框弹窗成功。这一关告诉我们过滤了往往意味着开发者只想到了防范标签却忽略了标签内属性的危险性。4.2 第五关关键词替换与伪协议第五关输入scriptalert(1)/script发现页面显示的是scr_iptalert(1)/scr_iptscript中间被插入了下划线。再试试onclick也变成了o_nclick。查看源码$str strtolower($_GET[keyword]); // 先转小写 $str2 str_replace(script, scr_ipt, $str); $str3 str_replace(on, o_n, $str2);逻辑清晰了先把所有输入变小写然后查找script和on这两个关键词找到就插入下划线破坏它。这样无论是script标签还是onclick等事件属性都被废掉了。绕过技巧引入新标签与JavaScript伪协议。思路转换既然script和on被盯上了我们就用别的标签和属性。a标签的href属性是一个常见选择。构造Payload首先我们还是要闭合前面的标签和属性。输入a hrefjavascript:alert(1)点击我/a。用于闭合前面的value属性和input标签。a hrefjavascript:alert(1)创建一个链接href属性的值是一个JavaScript伪协议。javascript:这个协议告诉浏览器后面跟的是要执行的JS代码。点击我是链接的文本。/a闭合链接标签。执行页面会显示一个“点击我”的链接。点击它就会执行javascript:alert(1)触发弹窗。 这里的关键点是javascript:伪协议。它让我们可以将JS代码直接写在URL协议的位置常用于a标签的href、iframe的src等属性。这一关的过滤没有针对href和javascript:因此被我们成功绕过。它揭示了过滤规则的不完整性防御列表永远可能遗漏攻击向量。4.3 第六关大小写变通的妙用第六关你输入第五关的payloada hrefjavascript:alert(1)点击我/a发现链接没有显示或者被破坏了。查看源码发现过滤列表加长了$str2str_replace(script,scr_ipt,$str); $str3str_replace(on,o_n,$str2); $str4str_replace(src,sr_c,$str3); $str5str_replace(data,da_ta,$str4); $str6str_replace(href,hr_ef,$str5);href也被加入了黑名单但是仔细看这一关没有strtolower()了这是一个巨大的提示。绕过技巧HTML标签与属性的大小写不敏感性。 在HTML中标签名和属性名是不区分大小写的。SCRIPT、Script和script对浏览器来说是一样的。onClick、ONCLICK和onclick也是一样的。 但是PHP的str_replace是区分大小写的。它只替换href不会替换HREF或Href。 所以我们的payload只需要稍作修改a HREFjavascript:alert(1)点击我/a。将href写成HREF就轻松绕过了替换。同样如果要用script标签可以用SCRIPT。这一关是典型的黑名单过滤大小写绕过案例提醒我们过滤时要么统一转小写要么使用正则表达式进行不区分大小写的匹配。5. 高阶编码与混淆艺术5.1 第七关双写绕过与“万能语句”第七关你尝试大小写混合发现不行了因为源码开头又加上了strtolower()。你输入script它被过滤成什么了输入一个测试字符串SCRscriptIPT看看结果。 你会发现页面显示的是SCRipt。原来这关的过滤逻辑是先转小写然后把script这个字符串替换成空。即str_replace(script, , $str)。 对于输入scrscriptipt处理过程是转小写scrscriptipt替换script为空查找script找到中间部分第4-9位字符s c r i p t将其删除字符串变成script。 看它把script删掉了但剩下的部分恰好又组合成了一个新的script这就是双写绕过。绕过技巧双写绕过。 要插入script我们就插入scrscriptipt。经过str_replace(script, , ...)处理后中间的script被删除两边的scr和ipt拼接起来正好是script。 所以payload为scrscriptiptalert(1)/scrscriptipt。同理如果on被过滤可以写成oonn。这一关展示了顺序替换的缺陷。如果代码是循环替换直到找不到script为止那么双写也会失效。但很多简单的实现只做一次替换。这里提一下“XSS万能测试语句”就像原始文章里用的SCRscriptIPT()Oonnjavascript。它的设计很巧妙同时包含了大小写、双写、事件、协议等多种可能一次性测试多个过滤点能快速探明过滤规则是实战中高效的信息收集手段。5.2 第八关编码的艺术与进制转换第八关双写和大小写似乎都失效了。页面可能是一个“添加友情链接”的功能要求你输入一个URL。你输入javascript:alert(1)发现被过滤或转义了。查看源码过滤规则可能类似第五、六关并且还对双引号进行了实体化转义替换为quot;这让我们很难闭合href属性的引号。绕过技巧HTML实体编码。 既然直接的关键词script,on,href,javascript都被过滤或破坏而引号也被处理我们能否让浏览器在解析时把我们输入的内容“还原”成可执行的代码呢HTML实体编码就是为此而生。 浏览器在解析HTML时会先将实体编码解码成对应的字符。例如lt;会被解码为gt;被解码为。同样我们可以对javascript:alert(1)这整个字符串进行编码。十进制HTML实体编码将每个字符转换为其Unicode码点的十进制数字前面加#后面加;。例如j-#106;a-#97;。十六进制HTML实体编码同理使用十六进制数字前面加#x。例如j-#x6A;。 我们可以将javascript:alert(1)整体编码。一个在线编码工具或Burp Suite的Decoder模块就能轻松完成。 编码后的payload类似#106;#97;#118;#97;#115;#99;#114;#105;#112;#116;#58;#97;#108;#101;#114;#116;#40;#49;#41;当你把这个字符串填入“友情链接”的输入框并提交后后端代码可能没有解码它而是直接放入了a href...中。当浏览器渲染这个a标签时它会自动将href属性值里的实体编码解码得到javascript:alert(1)点击链接时便会执行。这实现了绕过服务端过滤在客户端解码执行的效果。5.3 第九关协议检测与注释干扰第九关你兴奋地输入上一关的编码payload却提示“链接不合法”。看来它加了一个验证链接必须以http://开头。查看源码果然用了strpos($str, http://)来检查。绕过技巧利用注释符和编码组合。 我们的目标是既要包含http://以通过检查又要让最终的href值执行我们的JS代码。这里可以用一个小技巧在javascript:伪协议后面加上http://然后用JavaScript注释符将其注释掉。 JavaScript的单行注释是//多行注释是/* */。 构造payloadjavascript:alert(1);//http://或者javascript:alert(1);/*http://*/但是别忘了第八关的过滤可能还在。所以我们需要对这个payload进行HTML实体编码。编码后的字符串里包含了http://能通过后端检查。当浏览器解码并执行时//http://或/*http://*/是JavaScript注释不会影响alert(1)的执行。 更隐蔽的做法是将http://放在一个永远不会执行的代码分支里比如javascript:alert(1);if(0){http://}。这一关结合了协议白名单校验和编码绕过非常贴近真实场景中开发者对用户输入URL的验证逻辑。6. 深入实战隐藏字段与HTTP头注入6.1 第十关挖掘隐藏的输入点第十关页面上可能没有明显的输入框。但URL中可能存在多个参数。用“万能语句”测试每个参数观察哪个参数的值会反映在页面上且过滤最弱。 你发现t_sort这个参数的值被放入了一个typehidden的输入框的value属性里。虽然它是隐藏的但F12可以看到。源码显示它只过滤了尖括号。绕过技巧为隐藏输入框添加事件。 既然过滤了不能用新标签但输入框本身是标签。我们可以尝试为这个隐藏的输入框添加事件属性比如onclick。但是隐藏输入框用户点不到啊没关系我们可以把它变成可见的。 payload onclickalert(1) typetext。闭合value属性的前引号。onclickalert(1)添加点击事件。typetext覆盖其原有的typehidden将其变为一个可见的文本框。注意这里故意不闭合最后一个引号利用HTML的容错性。 提交后页面上就会多出一个文本框点击它即可触发弹窗。这一关教会我们不要忽视任何用户可控的输入点即使是隐藏字段并且展示了如何通过修改HTML属性来改变元素行为。6.2 第十一关Referer头的妙用第十一关页面上可能有多个隐藏输入框其中一个的值来自HTTP请求头中的Referer。Referer头告诉服务器当前请求是从哪个页面链接过来的。这个值通常后端代码会直接取用并且可能缺乏过滤。绕过技巧伪造HTTP请求头。 使用工具如HackBar插件或Burp Suite来修改你的HTTP请求。在Burp Suite中拦截访问第十一关的GET请求。在HTTP请求头部分找到或添加一行Referer: onclickalert(1) typetext放行请求。 后端代码可能会将这个Referer值直接填入某个隐藏字段的value属性中从而重现第十关的场景创建一个可点击的文本框。这属于HTTP头注入的一种它拓宽了XSS的攻击面提醒我们所有用户可控的HTTP头都可能成为输入源。6.3 第十二关与第十三关User-Agent与Cookie第十二关和第十三关是第十一关思路的延伸。第十二关攻击载荷可能通过User-Agent请求头注入。User-Agent是浏览器标识自己的字符串同样可以被篡改。第十三关攻击载荷可能通过Cookie中的某个字段注入。你需要使用浏览器插件或Burp来修改Cookie值然后刷新页面。这两关的绕过手法与第十一关完全一致只是注入的渠道不同。它们深刻地说明了在评估XSS风险时必须审查所有来自客户端的、可控的数据包括URL参数、POST体、HTTP头Referer, User-Agent等、Cookie。任何一处不经处理的直接输出都可能成为漏洞点。7. 拓展与高级技巧7.1 第十五关AngularJS Client-Side Template第十五关引入了前端框架的特性。页面中出现了ng-include指令这是AngularJS一个老牌但仍有市场的前端框架的指令用于动态包含外部HTML片段。它的值可能来自URL参数。攻击思路 如果ng-include的值完全用户可控并且后端没有过滤那么就可能造成XSS。但AngularJS从1.6版本开始默认对ng-include等指令的值进行了严格的上下文转义直接执行字符串代码比较困难。 一种经典的AngularJS Client-Side Template Injection (CSTI) 攻击方式是利用Angular表达式。例如如果可控点在一个Angular表达式解析的上下文中可以尝试{{constructor.constructor(alert(1))()}}。但在ng-include里通常需要包含一个实际的文件路径。 这一关的解法可能更取巧ng-include可以包含其他文件比如包含第一关的页面并通过参数传递payloadsrclevel1.php?nameimg src1 onerroralert(1)。这利用了靶场自身页面的漏洞属于一种“链式”利用。它展示了在复杂前端应用下XSS的利用场景可以非常灵活。7.2 第十六关空格过滤与换行符绕过第十六关你输入img src1 onerroralert(1)发现script被过滤成空空格也被转换成了nbsp;HTML中的不间断空格实体。这导致标签属性连在一起无法被正确解析。绕过技巧使用URL编码的换行符或制表符代替空格。 在HTML和JavaScript中标签属性之间的空格、换行符、制表符通常被视作相同的分隔符。当空格被过滤时我们可以用其URL编码%0A换行或%09制表来代替。 构造payloadimg%0Asrc1%0Aonerroralert(1)提交后%0A在URL中会被解码为换行符。当这个字符串被输出到HTML中时浏览器解析img标签遇到换行符会将其视为属性间的分隔符从而正确识别src和onerror属性。这招在过滤了空格但未过滤其他空白符的场景下非常有效。7.3 工具与自动化思维手动一关关测试是学习的基础但实战中效率太低。我们需要培养自动化思维模糊测试Fuzzing准备一个包含各种XSS payload的字典包含各种编码、混淆、标签、事件用工具如Burp Intruder, XSStrike等批量发送观察响应快速识别过滤规则和可能的绕过点。浏览器扩展除了HackBar还有像“XSS Helper”、“Max HackBar”等扩展可以快速编码、解码、生成常用payload。编码转换器熟练使用Burp Suite的Decoder/Encoder以及在线工具在URL编码、HTML实体编码、JavaScript Unicode编码等之间快速切换。通关xss-labs只是起点。真实世界的防御可能组合了多种技术WAFWeb应用防火墙、CSP内容安全策略、严格的输入输出编码库。攻击则需要更精巧的混淆、更隐蔽的触发方式如窃取Cookie、发起CSRF请求、键盘记录等。但万变不离其宗核心永远是理解数据流找到未经验证或充分编码的用户输入并让它最终在浏览器中被解析为代码执行。多练、多思考、多读别人的漏洞报告你会逐渐建立起对这种漏洞的敏锐直觉。