news 2026/10/17 9:37:29

XSS-Labs第三关绕过单引号转义:双引号闭合属性注入详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
XSS-Labs第三关绕过单引号转义:双引号闭合属性注入详解

XSS-Labs第三关,卡了我整整一下午。原因很简单:我提交的单引号被页面转成了&#039;,于是平时用得顺手的'><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>时,预期中的逻辑是:

  1. 用单引号闭合掉value属性;
  2. 用>结束当前input标签;
  3. 新建一个script标签执行代码。

但实际页面源码变成这样:

<input name="keyword" value="&#039;><script>alert(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会集体失灵

如果你也和我一样,一开始满脑子都是' onmouseover='alert(1)或者' autofocus onfocus='alert(1)这类经典属性注入payload,那你会发现它们全部失效。

原因很简单:这些payload的第一步都是“用单引号闭合当前的value属性”。单引号被转义成&#039;之后,value属性根本没被闭合,后面的onmouseover、autofocus、onfocus自然也就只是value字符串里的一小段“文本”,不会成为真正的HTML属性。

换句话讲,不是payload本身有问题,而是payload依赖的关键分隔符被服务端提前处理掉了。

1.4 大部分人会陷入的思维误区

很多人在这一步会陷入一个误区:疯狂搜索“如何绕过HTML实体编码”“让'变回单引号”之类的技巧,试图找到一个能“还原”单引号语法身份的方法。

但实操里,绕过字符实体转换的正确姿势往往不是“把被转义的字符还原”,而是“绕开这个被盯上的字符,改用其他还没被过滤的语法入口”。

想通这一点,解题方向就清晰了:单引号没了,那什么还在?答案通常摆在眼前——双引号。

2. 真正的突破口:单引号被转,但双引号还活着

2.1 先做一次“过滤总盘点”

别急着搜payload,第一步永远是先摸清服务端到底过滤了哪些字符。我当时的测试方法是直接在浏览器地址栏里提交一串特殊字符,然后观察页面渲染结果。

我提交了:

'"< >/();=

页面渲染后的HTML源码变成了:

<input name="keyword" value="&#039;"< >/();=">

逐个字符比对,结果一目了然:

输入字符渲染结果是否被处理
'&#039;被转义
""原样保留
<<原样保留
>>原样保留
//原样保留
()()原样保留
;;原样保留
==原样保留

也就是说,服务端只对单引号做了防XSS处理,双引号和尖括号都没有被过滤。

这一下思路就打开了:既然value属性是用双引号包裹的,而双引号没有被转义,那我直接用一个双引号把当前value属性闭合掉,不就行了?

2.2 注入上下文决定绕法:属性闭合的基本原则

很多新手会忽略“注入上下文”这个概念。同样是XSS注入点,落在<div>标签正文中间、落在<script>代码里面、落在onclick属性值里,对应的绕过策略完全不同。

第三关的上下文是“双引号包裹的HTML属性值”。这种上下文的核心规律是:

  • 当前属性由value="开始;
  • 要跳出属性值,必须找到一个和包裹引号一致的引号;
  • 本例中包裹引号是双引号,而双引号刚好没被过滤。

所以我构造了这样的输入:

" 后面跟新属性

比如:

" autofocus onfocus=alert(1)

页面渲染出来就是:

<input name="keyword" value="" autofocus onfocus="alert(1)">

浏览器会这样解析:

  1. 第一个双引号闭合了value属性;
  2. 空格作为属性分隔符;
  3. autofocus是一个独立属性;
  4. onfocus=alert(1)是一个完整的事件属性;
  5. 最后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从头到尾没有用到单引号,完美绕过了&#039;这道坎。

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源码显示结论
'&#039;单引号被转义
""双引号未过滤
<><>尖括号未过滤
()()括号未过滤
alertalert无关键词过滤

通过这张表,可以明确判定:单引号是唯一的障碍,双引号和其他字符都可用。

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>标签内部,那么&#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 onfocus="alert(1)"这样被解析后的结构;“查看源代码”显示的是服务器返回的原始HTML,有时反而看不出字符实体的实际效果。

调试payload时,可以在开发者工具Console里用encodeURIComponent快速生成URL编码:

encodeURIComponent('" autofocus onfocus=alert(1) x="')

输出结果就是可以直接拼进URL的编码串。

抓包工具也是一大助力,它能让你看到浏览器实际发出的请求。有些时候地址栏输入的和URL编码后的不完全一致,抓包能看到真实状态,避免被浏览器“帮忙改错”。

5. 写在最后:给同样卡关的人几句真心话

说实话,第三关破解之后回头看,它的难度并不高,难的是打破“单引号被转义就一定要绕过单引号转义”这个思维定式。我卡了一下午,就是因为一直在跟&#039;较劲,直到无意中提交了一个双引号,发现它原样保留,才一瞬间想明白:原来这关根本不需要跟单引号硬碰硬。

我自己在实际操作中的体会是,XSS注入点的突破,本质上是“语法结构”的突破,而不是“过滤关键词”的对抗。先认清输出上下文,再找可用的分隔符,最后构造触发事件,这个顺序比背几十个payload模板管用得多。

最后再分享一个小技巧:每过一关,都把成功的payload和失败的payload记下来,尤其是“为什么这个能成、那个不能成”。积累到第三关之后,你会发现自己面对新关卡时不再全靠猜,而是能快速判断注入点应该用哪种闭合方式。这种能力才是刷靶场最大的收获。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 9:36:01

小白也能玩转多Agent协作开发:300行代码搭建月度总结系统

1. 为什么小白也能玩转多Agent协作开发先说个扎心的事实&#xff1a;单Agent能做的东西越来越窄了。早期我们玩大模型&#xff0c;无非是写个Prompt调API&#xff0c;让模型帮你总结文档、补全代码、算个Excel公式。可一旦任务稍微复杂点——比如“帮我做一份本周销售复盘并发给…

作者头像 李华
网站建设 2026/10/17 9:35:32

果园柑橘检测数据集实战:从VOC/YOLO标注到模型训练部署

做目标检测这几年&#xff0c;农业场景是我觉得最“折磨人”也最有意思的方向之一。最近拿到一批“果园柑橘树上柑橘检测数据集”&#xff0c;VOCYOLO两种标注格式&#xff0c;2511张图&#xff0c;1个类别&#xff0c;专盯果树上还没摘的柑橘。别看只有1个类别、两千多张图&am…

作者头像 李华
网站建设 2026/10/17 9:34:42

superpowers:让Codex CLI变身高效AI编程助手的配置方案

我刚把superpowers这个配置方案完整跑通了一遍&#xff0c;从安装到实际写 Java 项目&#xff0c;前后折腾了几个晚上。这玩意儿不是个独立软件&#xff0c;准确说是给 AI 编程助手&#xff08;尤其是 Codex CLI&#xff09;用的一套会话管理规则和提示词集合&#xff0c;让 AI…

作者头像 李华
网站建设 2026/10/17 9:34:14

端口、套接字与多路分解:用四元组看懂网络编程核心

先说我自己的体会&#xff1a;我做网络相关开发这些年&#xff0c;真正把“端口、套接字、多路分解/复用”这组概念串起来&#xff0c;不是因为啃完了哪本协议书&#xff0c;而是某次线上服务突然报“端口被占用”&#xff0c;我把监听端口、连接四元组、套接字状态翻了个底朝天…

作者头像 李华
网站建设 2026/10/17 9:31:37

MySQL体系结构拆解:从分层原理到典型故障排查

1. 体系结构不是拿来背的&#xff0c;是拿来定位问题的刚入行那阵子&#xff0c;我也背过MySQL体系结构图&#xff1a;连接层、服务层、存储引擎层、物理文件层&#xff0c;一层一层画得整整齐齐。但说实话&#xff0c;那时候背下来纯粹是为了面试&#xff0c;真正让我意识到这…

作者头像 李华
网站建设 2026/10/17 9:30:36

ARIMA时间序列预测:原理、建模流程与Python实战

1. “高效时间序列预测”为什么选ARIMA&#xff0c;而不是更炫的模型先说结论&#xff1a;ARIMA不是最潮的模型&#xff0c;但它是在“数据量不大、规律相对稳定、业务需要可解释性”这三重约束下&#xff0c;最不容易翻车的选择之一。我在实际做预测项目时&#xff0c;最开始也…

作者头像 李华