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