XSS-Labs第三关绕过单引号转义:双引号闭合属性注入详解
XSS-Labs第三关卡了我整整一下午。原因很简单我提交的单引号被页面转成了#039;于是平时用得顺手的script这类payload全部失效。后来我才反应过来问题不在“怎么绕过单引号转义”而在“我一开始就看错了注入点的上下文”。这篇就把第三关的完整解决思路讲清楚包括字符实体转换的原理、为什么双引号能成为突破口、如何构造一个不依赖单引号的payload以及一套能迁移到后续关卡的通用分析方法。如果你是刚接触XSS靶场、正卡在第三关出不来或者虽然能通关但没想明白背后原理这篇应该能帮你把“撞大运式过题”变成“按图索骥式解题”。1. 卡关现场先弄清楚“单引号被转成”到底意味着什么1.1 第三关的注入点长什么样先说结论这一关的注入点不是普通的“HTML标签中间”而是一个input标签的value属性内部。我本地跑的这套XSS-Labs第三关的核心逻辑大致可以简化成下面这段伪代码$str $_GET[keyword]; echo input namekeyword value . $str . ;也就是说我通过URL参数keyword提交的内容会被原样拼进一个双引号包裹的value属性里。页面渲染出来大概是这样input namekeyword value这里是我输入的内容这个细节非常关键我的输入处于“属性值”的位置而不是“标签正文”的位置。所以当我第一次提交scriptalert(1)/script时预期中的逻辑是用单引号闭合掉value属性用结束当前input标签新建一个script标签执行代码。但实际页面源码变成这样input namekeyword value#039;scriptalert(1)/script单引号不见了取而代之的是一坨#039;后面的script全部乖乖躺在value属性内部连标签结构都没破坏更别提弹窗。1.2#039;是什么HTML字符引用的一次快速补课#039;是单引号的十进制HTML字符引用。HTML里有一类写法叫“字符引用”用来在HTML文档中安全地表示那些有特殊含义的字符或者那些不容易直接输入的字符。常见的有这么几个写法含义对应字符lt;小于号gt;大于号amp;与符号quot;双引号#039;单引号十进制#x27;单引号十六进制#039;和#x27;是同一个字符的两种编码写法前者是十进制后者是十六进制。这里需要理解一个容易踩坑的点浏览器解析HTML时字符引用是在“标记化阶段”处理的。#039;在被解析时会被当作一个普通字符放进当前的属性值token中而不是先解码成、再重新执行一遍HTML语法分析。打个比方你把一段话用引号括起来引号里面如果再写一个引号这个“里面的引号”只是内容的一部分不会导致外层引号提前关闭。#039;在属性值里就是那个“里面的引号”它作为实体被转义后已经没有了“闭合属性”的语法身份。所以试图让#039;“变回”单引号去闭合value属性这条路从根上就走不通。1.3 为什么常规payload会集体失灵如果你也和我一样一开始满脑子都是 onmouseoveralert(1)或者 autofocus onfocusalert(1)这类经典属性注入payload那你会发现它们全部失效。原因很简单这些payload的第一步都是“用单引号闭合当前的value属性”。单引号被转义成#039;之后value属性根本没被闭合后面的onmouseover、autofocus、onfocus自然也就只是value字符串里的一小段“文本”不会成为真正的HTML属性。换句话讲不是payload本身有问题而是payload依赖的关键分隔符被服务端提前处理掉了。1.4 大部分人会陷入的思维误区很多人在这一步会陷入一个误区疯狂搜索“如何绕过HTML实体编码”“让变回单引号”之类的技巧试图找到一个能“还原”单引号语法身份的方法。但实操里绕过字符实体转换的正确姿势往往不是“把被转义的字符还原”而是“绕开这个被盯上的字符改用其他还没被过滤的语法入口”。想通这一点解题方向就清晰了单引号没了那什么还在答案通常摆在眼前——双引号。2. 真正的突破口单引号被转但双引号还活着2.1 先做一次“过滤总盘点”别急着搜payload第一步永远是先摸清服务端到底过滤了哪些字符。我当时的测试方法是直接在浏览器地址栏里提交一串特殊字符然后观察页面渲染结果。我提交了 /();页面渲染后的HTML源码变成了input namekeyword value#039; /();逐个字符比对结果一目了然输入字符渲染结果是否被处理#039;被转义原样保留原样保留原样保留//原样保留()()原样保留;;原样保留原样保留也就是说服务端只对单引号做了防XSS处理双引号和尖括号都没有被过滤。这一下思路就打开了既然value属性是用双引号包裹的而双引号没有被转义那我直接用一个双引号把当前value属性闭合掉不就行了2.2 注入上下文决定绕法属性闭合的基本原则很多新手会忽略“注入上下文”这个概念。同样是XSS注入点落在div标签正文中间、落在script代码里面、落在onclick属性值里对应的绕过策略完全不同。第三关的上下文是“双引号包裹的HTML属性值”。这种上下文的核心规律是当前属性由value开始要跳出属性值必须找到一个和包裹引号一致的引号本例中包裹引号是双引号而双引号刚好没被过滤。所以我构造了这样的输入 后面跟新属性比如 autofocus onfocusalert(1)页面渲染出来就是input namekeyword value autofocus onfocusalert(1)浏览器会这样解析第一个双引号闭合了value属性空格作为属性分隔符autofocus是一个独立属性onfocusalert(1)是一个完整的事件属性最后input标签正常闭合。2.3 为什么事件属性可以不用引号很多人看到onfocusalert(1)会问alert(1)里的括号没问题但事件属性的值不需要引号吗HTML规范里属性值可以用双引号、单引号包裹也可以在满足特定条件下完全不加引号。不加引号的属性值叫作“unquoted attribute value”它允许出现除空格、双引号、单引号、、、、反引号之外的任意字符。因为我传入的是alert(1)里面没有空格也没有特殊符号所以完全可以不带引号。这就直接绕过了“属性值内不能再用双引号”的潜在限制。而且这里有一个很讨巧的设计我使用的是autofocus配合onfocus。autofocus会让页面加载时自动把焦点聚到这个input输入框上焦点一聚集onfocus事件立刻触发。所以不需要用户做任何点击页面加载就会自动弹出alert(1)。这比onclick之类的事件要稳定得多因为onclick还得等着用户去点测试的时候很不方便。2.4 完整payload演化过程我把最终payload的构造过程一步步拆给你看。第一步闭合value属性页面变成input namekeyword value第二步加一个空格让后续内容从“属性值内部”变成“新属性”页面变成input namekeyword value 注意这里的空格是HTML属性分隔符它让后面跟着的autofocus不再属于value的值而是成为新的属性。第三步加入autofocus属性和onfocus事件 autofocus onfocusalert(1)页面变成input namekeyword value autofocus onfocusalert(1)第四步如果担心后续还有HTML结构上的干扰可以在末尾补一个x把多余内容收纳进一个新属性里保证整个标签结构完整 autofocus onfocusalert(1) x页面变成input namekeyword value autofocus onfocusalert(1) x最终payload就是 autofocus onfocusalert(1) x这串payload从头到尾没有用到单引号完美绕过了#039;这道坎。2.5 为什么用事件属性而不是script标签有人可能觉得既然双引号和尖括号都没被过滤那我直接提交scriptalert(1)/script不也能闭合属性、结束标签、注入新标签吗理论上是这么回事页面会渲染成input namekeyword valuescriptalert(1)/script但实际测试中这条路经常是堵死的。原因有两点第一很多XSS靶场和WAF都会针对script这类标签做专门的关键词过滤你输入script可能被替换或者删除甚至整段被截断。第三关的过滤虽然主要针对单引号但在不同版本、不同作者改造过的题目里script往往被额外关照过。第二从绕过思路上讲事件属性方案更通用。现实场景中WAF和过滤规则的重心通常放在“防止插入外部脚本”上对标签内部属性的事件绑定反而检查没那么严格。学会在属性内部构造事件属性比单纯记住script标签注入要实用得多。3. 实操记录一步步把第三关打通3.1 本地环境准备我是在本地搭的XSS-Labs环境浏览器用的是开发者工具比较顺手的一个Chromium内核浏览器另外又开了一个不同内核的浏览器做对照主要为了验证行为一致性。如果你也打算实操建议至少准备一套本地靶场环境一个带开发者工具的现代浏览器一个抓包工具方便看请求和响应的原始数据。这里提醒一句靶场练习请务必在本地环境或者合法授权的测试环境中进行不要拿真实站点练手。3.2 第一步确认源码输出位置我先在URL里提交一个正常的值?keywordhello打开开发者工具查看Elements面板确认渲染后的HTML结构input namekeyword valuehello这一步很重要确认包裹属性的是双引号还是单引号直接决定后续payload的闭合方式。第三关是双引号所以突破口是双引号如果哪一关是单引号包裹那单引号转义的问题就又不一样了。3.3 第二步字符过滤器探测接下来提交一批特殊字符?keyword ?keyword ?keyword ?keyword()逐个观察渲染后的HTML源码。我整理了一张表提交内容HTML源码显示结论#039;单引号被转义双引号未过滤尖括号未过滤()()括号未过滤alertalert无关键词过滤通过这张表可以明确判定单引号是唯一的障碍双引号和其他字符都可用。3.4 第三步构造并提交payload确定突破口后我构造payload autofocus onfocusalert(1) x由于URL里空格会被自动处理成%20双引号会被编码成%22所以我直接提交?keyword%22%20autofocus%20onfocusalert(1)%20x%22也可以用浏览器地址栏直接输入带空格的原始payload现代浏览器通常会自动编码。3.5 第四步触发与验证页面加载后input框自动获得焦点onfocus事件触发浏览器弹出alert(1)。如果你提交后没有弹窗先检查几个地方输入的payload是否被浏览器“智能修复”过比如双引号被转成了全角autofocus是否被页面里的其他元素抢了焦点onfocus事件是否拼写错误浏览器是否有弹窗拦截设置。我实测下来几个主流内核的浏览器都能正常触发没有被拦截。3.6 变体练习把payload做进一步扩展通关之后我顺手做了几个变体方便理解这套思路的延展性。变体一把alert(1)换成alert(document.cookie)演示窃取cookie的效应。但这里要注意document.cookie里没有单引号所以直接写没问题。如果事件属性值里需要字符串参数而双引号被过滤的话可以考虑用反引号或者String.fromCharCode等方式编码。变体二用onmouseover替换autofocus onfocus onmouseoveralert(1) x这个变体需要鼠标移入输入框才触发适合模拟用户交互场景。变体三在value内部塞一个img标签但实测不会触发因为这个位置的尖括号只是属性值的一部分浏览器不会解析成新标签。这也是初学者经常踩的坑——以为在属性值里写img就能执行其实不行。4. 常见卡点与一套通用的XSS绕过分析思路4.1 为什么我输入了“双引号autofocusonfocus”还是没弹窗这是群里朋友问得最多的问题。如果你提交了 autofocus onfocusalert(1)但没反应大概率是下面几个原因第一你看到的HTML源码可能显示value autofocus onfocusalert(1)结构完全正确说明payload已经注入成功问题出在触发条件上。autofocus自动聚焦在个别场景下可能被浏览器拦截比如页面里已经有其他元素获得焦点或者浏览器设置禁用了自动聚焦。这时候可以把autofocus onfocus替换成onmouseoveralert(1)手动移入鼠标测试。第二事件属性值里如果带了空格不带引号会截断。比如onfocusalert(1) x能正常解析但如果你写成onfocusalert(1 2)第二个数字就会被当成新的属性事件值变成alert(1JavaScript语法错误自然弹不出来。需要带空格时可以用alert(1)这种没有空格的写法或者用反引号包裹字符串onfocusalert(这里反引号本身又可能被过滤需要灵活处理。第三确认你有没有把整个payload放进URL里。有时候地址栏会自动把双引号转换成%22如果某些环节频繁跳转导致参数丢失也会失败。4.2 为什么scriptalert(1)/script这道题里不好使前文提过这条路在理论上是能闭合属性的但因为script关键词经常被过滤实际通过率不高。更关键的是这一关的过滤规则虽然只针对单引号但如果你的目标是把payload稳定跑通选择事件属性方案比依赖script标签要稳得多。即使某个环境过滤了onfocus你也可以换成onclick、onmouseover、onkeydown等一大堆事件属性绕过面非常广。4.3 如果双引号也被过滤了怎么办这是一道很好的延伸题。假设某道关卡的过滤规则把单引号和双引号全部转成实体那么“闭合属性”这条路似乎就断了。这种情况下通常有三个方向第一个方向是寻找没有被过滤的“第三引号”。HTML属性虽然最常见的是用双引号和单引号包裹但理论上反引号在某些旧浏览器或特定解析器中也可能被接受。不过现代浏览器并不把反引号当作属性包裹符这个方向实战意义有限。第二个方向是“无引号属性值注入”。如果当前上下文是input value 注入点 也就是属性值本身没有引号包裹那就可以直接加空格加新属性完全不需要引号。这种场景在真实站点里也很常见。第三个方向是利用JavaScript上下文的实体解码差异。如果注入点位于script标签内部那么#039;会被解码成这个单引号在JS语法层面是会参与字符串界定和语句闭合的。这就是常说的“HTML实体解码与JavaScript解析的时机差”。但前提是你的注入点真的在script标签内部第三关的value属性上下文不适用。4.4 五步分析法以后再遇到XSS关卡不慌经过第三关我总结出一套可以反复使用的分析流程分享给你。第一步定位输出上下文。打开开发者工具看注入点的内容被输出在什么位置div标签正文value属性内部script代码块里还是注释里每个上下文对应完全不同的绕过方式。第二步画出当前语法结构。比如input value[注入点]明确包裹符是什么正文在哪个标签里代码块又被什么闭合。第三步确定“分隔符”。属性上下文的分隔符是引号标签正文的分隔符是和script上下文的分隔符是引号和/script。逐个测试这些分隔符是否被过滤或转义。第四步选择保留下来的分隔符闭合当前结构再构造后续内容。第三关就是“双引号活着所以用双引号闭合”。第五步设计触发条件。onload、onerror、onfocus、onmouseover各有各的触发时机根据需要选择最稳定的那种。这套流程不只在XSS-Labs里有效凡是遇到“带过滤规则的注入点”都可以按这个顺序排查。4.5 工具与查看技巧最后补充几个实用查看技巧。我在排查时一般用开发者工具的Elements面板而不是“查看源代码”。因为Elements面板显示的是浏览器解析后的DOM树能直接看到value autofocus onfocusalert(1)这样被解析后的结构“查看源代码”显示的是服务器返回的原始HTML有时反而看不出字符实体的实际效果。调试payload时可以在开发者工具Console里用encodeURIComponent快速生成URL编码encodeURIComponent( autofocus onfocusalert(1) x)输出结果就是可以直接拼进URL的编码串。抓包工具也是一大助力它能让你看到浏览器实际发出的请求。有些时候地址栏输入的和URL编码后的不完全一致抓包能看到真实状态避免被浏览器“帮忙改错”。5. 写在最后给同样卡关的人几句真心话说实话第三关破解之后回头看它的难度并不高难的是打破“单引号被转义就一定要绕过单引号转义”这个思维定式。我卡了一下午就是因为一直在跟#039;较劲直到无意中提交了一个双引号发现它原样保留才一瞬间想明白原来这关根本不需要跟单引号硬碰硬。我自己在实际操作中的体会是XSS注入点的突破本质上是“语法结构”的突破而不是“过滤关键词”的对抗。先认清输出上下文再找可用的分隔符最后构造触发事件这个顺序比背几十个payload模板管用得多。最后再分享一个小技巧每过一关都把成功的payload和失败的payload记下来尤其是“为什么这个能成、那个不能成”。积累到第三关之后你会发现自己面对新关卡时不再全靠猜而是能快速判断注入点应该用哪种闭合方式。这种能力才是刷靶场最大的收获。