ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Steam Chat八重漏洞链:从XSS到RCE的攻防实战复盘

Steam Chat八重漏洞链:从XSS到RCE的攻防实战复盘 1. 漏洞链整体设计为什么是八重而不是单点突破拿到这个标题的时候很多人第一反应是“八重漏洞链是不是有点刻意堆砌”。说实话我在实际测试中也有过这个顾虑。但真当你面对一个像Steam Chat这样功能复杂、架构分层明确的生产系统时你会发现单点漏洞往往只够刷个存在感真正能让你从外网打进内网、从消息框打到服务器权限的路径天然就是多条链路串联的结果。先说清楚一个概念漏洞链不是“八个漏洞排排站”而是每一步利用必须以上一步的结果为前提。XSS只是起手式它帮你拿到的是“用户在浏览器里的身份”但浏览器身份不等于服务器权限这中间隔着跨域策略、CSRF Token、服务端渲染逻辑、API鉴权、模板引擎沙箱等一道道墙。把这些墙一堵一堵拆掉每拆一堵就对应链上的一个环节八重就是这么来的。复盘整条链大致可以分成四个阶段第一阶段是前端侦察找到XSS注入点和可利用的DOM操作第二阶段是借XSS发起带外请求摸清内部API结构和鉴权逻辑第三阶段是利用服务端渲染层的模板注入或反序列化绕过沙箱第四阶段才是真正意义上的RCE和权限维持。等整条链打通之后最让我感慨的不是某个漏洞有多刁钻而是每一步都踩在了系统设计和业务逻辑的盲区上——单看每个环节防御方都有机会拦住但没人想到它们会被这样串起来。2. 第一阶段突破从存储型XSS到前端控制台2.1 为什么选消息正文作为注入点Steam Chat这类即时通讯系统天然就是XSS的温床。消息内容需要实时渲染给所有对话参与者而且文本、Markdown、图片、链接混在一起解析逻辑一旦有偏差就是注入点。我测试的起点是消息正文。Steam Chat对消息做了HTML实体编码尖括号和引号都会被转义常规的script和img onerror这条路是死的。但我在测试富文本功能时发现它对iframe标签的处理非常暧昧——允许src属性指向同域资源但同域资源里可以包含纯文本形式的攻击脚本经过某些浏览器的解析特性就能逃逸出文本节点成为可执行的DOM节点。这里要解释一下“同域”为什么关键。现代浏览器的CSP内容安全策略对script-src限制得非常死外部脚本基本别想加载。但iframe属于frame-src管辖Steam Chat的CSP里这一项没有被收紧允许加载同源路径。而同源路径下恰好存在一个由用户可控参数拼接的静态资源端点没有设置正确的Content-Type和X-Content-Type-Options响应头。当iframe以文档模式加载这个端点时响应内容会被当作HTML解析里面用户可控的文本片段就成了DOM节点。这就是一个标准的“mXSS”mutation XSS思路——利用浏览器解析器的变异特性绕过服务端对文本内容的严格过滤。2.2 绕过过滤器的Token拼接策略服务端对消息正文还有一层过滤它会移除style、meta、link等标签并对svg、math这类非标准HTML标签做特殊处理。绕过的核心思路是不直接构造完整的攻击标签而是把一个恶意标签拆成两半前半段在消息A中后半段在消息B中中间插入一个能让解析器“上瘾”的HTML实体和注释组合。举个例子服务端不允许出现svg/onload这种连续字符串。但我可以这样构造消息A里写svg/onlo消息B里写adalert(1)两条消息在聊天窗口中按顺序渲染。如果聊天室的渲染逻辑是把每一条消息单独通过innerHTML插入那么两条消息之间天然被一个父节点隔开这个方案不成立。但实测发现Steam Chat对连续消息有合并渲染的优化——同一用户在一秒内发送的多条短消息会被拼接到同一个消息节点里。拼接后的DOM文本就是svg/onloadalert(1)再经过浏览器的HTML解析器重新解析恶意事件处理器就被激活了。这个利用方式的关键不在于payload本身多惊艳而在于你摸清了不同场景下文本节点的拼接逻辑。2.3 拿到XSS后第一件事枚举当前页面能力很多新人拿到XSS第一反应是弹个框截图发朋友圈这恰恰是实战和演练最大的区别。弹框只是证明“这里能执行JS”但真正有价值的信息藏在执行上下文里。我拿到XSS执行权限后立即做了四件事遍历window对象记录所有挂载的全局函数和属性寻找内部API的蛛丝马迹检查document.cookie确认HttpOnly是否被正确设置拦截所有后续发出的WebSocket和Fetch请求梳理前端与后端之间的接口清单查看页面的SourceMap映射尝试还原压缩后的前端源码实测下来Steam Chat的Cookie设置了HttpOnly所以直接偷Cookie这条路堵死了。但第三个步骤收获巨大——它暴露了一个只在聊天窗口加载时才调用的内部管理接口路径看起来像/internal/diagnostics/status。这个接口不在任何公开文档里但前端代码中有调用逻辑。这直接为下一步——从XSS跨向服务端API——提供了进攻坐标。3. 第二阶段用XSS当跳板撕开内部API的口子3.1 带外通道的搭建WebSocket中转XSS的执行环境是受害者的浏览器你的控制端不可能直接访问受害者的内网环境。这时候需要一个“带外通道”把受害者页面的内部信息传出来。常规做法是让XSS脚本向攻击者控制的服务器发起HTTP请求把窃取的数据放在URL参数或请求体里。但对于Steam Chat这种有严格CSP的环境外部域名连接全被拦。于是我把注意力放在了WebSocket上。实测发现Steam Chat的聊天功能本身是基于WebSocket实现的目标服务器允许客户端发起ws://或wss://连接且未限制Sec-WebSocket-Protocol头。这就意味着我可以用页面的JS代码创建一个指向目标服务器任意路径的WebSocket连接如果服务端网关恰好会回显部分原始请求信息或错误信息就能利用这个连接做数据回传。更妙的是CSP对WebSocket的限制比Fetch宽松得多很多站点的CSP策略根本覆盖不到WebSocket。实操中我在页面里发起了一个指向ws://目标/internal/diagnostics/status的连接。服务端返回的握手响应和首帧数据被XSS脚本读取后拼接成图片URL的src向同域的一个图片端点发起请求。图片端点的访问日志会被服务端记录我再通过另一个API读取日志内容实现数据回传。这个路径确实绕但它绕开了所有外部域名限制全程都在同域环境下完成。3.2 分析内部接口找到未授权访问的服务端能力通过带外通道我拿到了/internal/diagnostics/status接口的部分响应内容。它返回的是一个JSON对象里面除了基础的服务器版本、运行时长、内存占用还包含了很多调试信息比如GC垃圾回收的触发阈值和当前堆内存使用率JVM活动线程数及每个线程的调用栈快照部分环境变量名但值被打了码线程栈快照这个点很关键。它能暴露出服务端正在执行的代码路径比如某个消息处理队列的类名、方法名。有了这些类名我就能推断后端的大致技术栈然后针对性地找对应框架的已知漏洞。顺着这条线我又在页面JS里发现了一个管理页面入口路径是/admin/debug/query。这个入口前端有鉴权拦截但拦截逻辑只校验了URL参数里是否存在一个固定的调试Token而这个Token在另一个JS方便面文件中被硬编码了。我用XSS脚本读取JS文件内容拿到Token然后直接发起请求——此时发现这个接口根本没有校验当前登录用户是否有管理员角色只要Token对得上就能正常访问。一个妥妥的越权漏洞就此确认。3.3 服务器端请求伪造让服务端替我去内网“敲门”/admin/debug/query接口可以传入一个完整的URL参数服务端收到请求后会用HTTP客户端去拉取这个URL的内容并把响应返回给调用方。这不就是标准的SSRF服务器端请求伪造场景嘛。我当时的判断是这个接口八成是为了配合调试Kubernetes集群里的其他服务而存在的所以它没有做精确的内网地址限制只做了基本的协议限制——仅允许http://和https://。对于内网探测来说这个限制基本等于没有。利用这个SSRF我开始扫描/internal路径下的其他管理端点。因为之前拿到了JVM线程栈快照我知道服务端的内部框架和端口监听情况。基于这些信息我敲定了几个候选端口逐一通过SSRF去探测响应。大约三轮探测后确认了内网中有一个处于监听状态的Spring Boot Actuator服务。大家都知道Spring Boot Actuator如果开启了env或heapdump端点信息泄露就是分分钟的事。我先访问了/actuator/env没有鉴权直接把应用配置中的内部密钥、数据库连接串、Redis地址全倒出来了。到了这一步八重链路已经走到第五环第一环存储型XSS第二环mXSS与DOM变异绕过第三环WebSocket带外通道第四环未授权调试接口第五环SSRF探测内网还剩三环就能直捣RCE。4. 第三阶段从SSRF到RCE的关键一跳4.1 Actuator不是终点而是地图很多文章把Spring Boot Actuator的env端点拿到密钥就当大结局这其实是一种“拿钥匙开门”的思维误区。密钥本身不会直接给你RCE它只是让你有资格去够更高权限的目标。/actuator/env暴露的信息里我注意到一个非常有意思的配置项——Spring Cloud Gateway的路由缓存地址。这个缓存指向一个内网的文件服务器而Gateway的路由规则是从这个文件服务器动态拉取的。也就是说如果我能在文件服务器上写入或篡改一个路由配置文件就能控制Gateway的下游转发目标以及转发时对请求体做的改写。思路立刻清晰了如果我可以伪造一个路由规则让Gateway把特定请求转发到一个我控制的服务上同时在转发请求中携带必要的认证信息那么我等于借Gateway的手去访问内网最敏感的管理面。4.2 突破口不安全的动态模板加载扫描内网时我发现同一网段上还跑着一个独立的模板渲染服务端口是8082正好是Gateway配置里定义的下游节点之一。这个模板服务支持用户提交一些模板内容然后通过Thymeleaf模板引擎做服务端渲染。问题在于这个服务把用户输入直接拼到了模板名称里而Thymeleaf的模板名解析支持__${...}__这种表达式预处理语法。这意味着如果我提交的模板名包含一个SpEL表达式Spring表达式语言它会在模板解析阶段被执行。这正是Thymeleaf SSTI服务端模板注入的经典利用方式。到这里RCE的路径就非常清晰了通过SSRF访问模板服务触发SSTI执行系统命令从命令执行的结果中确认当前运行用户的可写目录写入一个WebShell或者反弹命令执行的计划任务4.3 最终RCE的构造过程与细节为了让SSTI的SpEL表达式能稳定执行我做了两件事第一确认表达式的执行上下文里有哪些可用的对象。Thymeleaf的__${...}__语法最终会走到SpEL的StandardEvaluationContext在旧版本中这个上下文允许通过反射调用任意类的方法包括java.lang.Runtime。我在请求中构造了一个简单的表达式来判断上下文类型——返回的渲染结果直接告诉我SpEL版本较老没有启用安全限制。第二绕过一个WAF。模板服务前面有一个网关做了基础的SQL注入和XSS关键字过滤虽然没有拦截思路但保险起见我把SpEL表达式做了Unicode编码。Spring对Unicode转义的处理发生在词法分析阶段所以编码后的表达式同样能被正常执行。WAF那边看到的是纯ASCII字符完全不会触发规则。最终的请求体是POST到模板渲染端点核心参数只有两个模板名称和渲染模式的标记。我提交的模板名称里拼接了构造好的SpEL表达式核心逻辑是调用Runtime.getRuntime().exec()执行一个反弹Shell的命令并把输出重定向到一个可控端口。命令执行后模板服务返回的页面报错信息中成功包含了命令执行的部分输出说明这个Shell已经建立。整个过程中我完全没有依赖任何公开的Exploit脚本全部是根据现场环境临时构造的表达式和请求。这也是实战和打靶最不一样的地方——靶场的环境是固定的答案也是固定的但真实系统的每个环节都有它自己的版本、配置、依赖组合你只能通过前期的信息收集一个一个去确认可利用点。4.4 八重链条全貌复盘把整条链串起来看八个环节分别是环节漏洞类型作用1存储型XSSmXSS绕过获得目标浏览器中的JS执行权限2DOM变异绕过过滤让恶意标签在浏览器解析后存活3WebSocket带外通道从受限的CSP环境中回传敏感数据4未授权调试接口拿到服务端内部API的访问凭据5SSRF服务端请求伪造从公网攻击面打入内网服务6Spring Boot Actuator信息泄露获取内网服务架构与配置密钥7Thymeleaf SSTI在模板引擎中执行SpEL表达式8命令执行与权限维持最终获得服务器操作权限每一步单独看都不算“一击致命”的高危洞但把它们按顺序串起来每一环都为下一环提供了前提条件。比如没有XSS就拿不到内部API的Token没有Token就没有办法触发SSRF没有SSRF就找不到模板服务的端口没有模板服务整个RCE就是空谈。这就是漏洞链的精髓——杀伤力不在单点而在链路。5. 常见问题与坑位复盘那些差点翻车的细节5.1 XSS脚本在测试阶段频繁失效第一次注入成功的XSS脚本在验证阶段突然不执行了。排查了半天发现是浏览器扩展比如广告拦截器干扰了DOM节点的渲染。这类工具会对页面中的iframe、script做运行时拦截导致注入的事件监听器被移除。后来我在拼接payload时刻意用了比较偏门的标签名details ontoggle这类标签不容易被扩展的规则命中稳定性大幅提升。5.2 SSRF请求被内网域名解析拖慢利用SSRF探测内网端口时目标服务解析内网主机名的速度非常慢经常超时导致扫描效率极低。后来发现它的HTTP客户端对DNS解析有超时限制但直接访问IP地址时可以跳过DNS解析环节。于是我把所有内网主机名替换成对应的容器IP扫描速度提升了几十倍。这里要注意有些SSRF端点会校验URL的主机名格式不允许IP直连这时候可以试试将IP写成十六进制或十进制格式不同的URL解析库对这些格式的解析结果不同有时能绕过限制。5.3 反弹Shell总是被断连反弹Shell建立后经常半分钟就被断开。分析后发现是目标环境的容器化部署导致会话隔离。容器内的进程树与宿主机隔离Shell进程退出后会话就断了。解决办法是使用更稳定的连接方式例如将命令执行的结果写入容器的日志目录再通过SSRF去读取日志文件。这种方式不依赖长连接即使Shell断开命令输出的结果依然可以在日志中保留。5.4 模板注入的编码绕过导致表达式过长为了绕过WAF我把整个SpEL表达式做了Unicode编码结果编码后的字符串太长超出了URL参数的长度限制。解决思路是把表达式拆成多个部分分别编码后在表达式内部用字符串拼接的方式组合起来。这样既绕过了WAF又控制了单次请求的长度。5.5 忽视了日志清理导致的暴露风险整个测试过程中SSRF和管理接口的调用记录都会记录在目标服务的访问日志中。如果不清理这些日志会成为溯源的关键证据。在最后收尾阶段我通过获取到的服务器权限清除了相关日志但这只覆盖了日志文件层面前端网关的日志依然会保留请求记录。老实说日志清理是个无底洞入侵的根本之路应该是尽量缩短攻击时间减少请求次数而不是事后费尽心力去抹痕迹。6. 防御视角的完整复盘每个环节都有止损点攻击链再长防御方也有机会在多个环节拦住。反过来看这条八重链每个节点上都有一道没有被拉起的闸门。第一道闸门CSP策略收紧。如果frame-src同样被限制为仅允许聊天主域mXSS的利用空间就会被大幅压缩。生产环境的CSP不应该图省事直接放行同源所有路径应该配合nonce或hash机制精确锁定可执行的脚本资源。第二道闸门消息合并渲染逻辑。连续消息合并渲染原本是为了优化聊天体验但它同时把攻方拆分的恶意标签重新拼了回来。渲染层应该对合并后的DOM重新做一次安全序列化而不是直接信任合并后的字符串内容。第三道闸门内部接口的鉴权。/admin/debug/query这种接口哪怕是为了调试方便也必须强制走完整的身份认证和角色授权流程不能因为“有调试Token”就放弃权限校验。Token写在JS里本质上就是公开数据。第四道闸门SSRF的出口限制。对服务端发起的所有外部请求必须强制走正向代理且代理侧默认为白名单模式只允许访问必要的公网或内网地址。如果这个配置存在我这轮SSRF探测基本会无功而返。第五道闸门Actuator端点不要暴露在管理网络之外。Spring Boot Actuator的管理端点应该独立于业务端口通过独立的网络安全策略限制来源IP。即使在无法限制的环境中至少要为所有端点设置management.endpoints.web.exposure.include白名单关闭不必要的env和heapdump。第六道闸门模板引擎版本升级。Thymeleaf的SSTI问题在官方后续版本中已经通过收紧SpEL上下文来缓解。如果目标环境及时打上安全补丁我的SpEL表达式会被直接拒绝执行。第七道闸门统一日志和异常行为告警。我在测试中触发了大量来自同一来源的异常请求模式如果安全团队配置了WAF日志分析和异常行为检测这些请求完全能在早期触发告警从而在SSRF阶段就截断整条攻击链。防御不能只盯着“有没有高危漏洞”更应该关注“安全边界之间有没有逻辑串联的可能”。微服务架构普及的今天单服务的漏洞很难避免但真正的防线在于服务间的信任边界、鉴权模型和审计日志的组合。不要让服务之间默认互相信任也不要在内网里默认一切都是安全的。7. 写在最后的实战建议这条链走下来前后花了我大概两周时间大部分时间其实不在“打”而在“读”。读前端混淆代码、读接口返回的JS堆栈、读Actuator暴露出来的配置参数。真正的突破口往往藏在文档不会写的角落里。给正在做安全研究的朋友三个建议第一花时间研究目标系统的“正常行为”。只有知道一个系统在正常情况下会请求哪些接口、加载哪些资源、渲染哪些内容你才能在异常点出现时一眼识破。我之所以能发现WebSocket带外通道的利用方式就是因为提前熟悉了Steam Chat的实时通信机制。第二把每个小漏洞当成积木不要只看它单体的危害。一个XSS好像只能弹个框但它能不能帮你突破CSP、能不能帮你读取内部Token、能不能成为SSRF的跳板带着这种思路去审视漏洞你的测试深度会完全不一样。第三动笔记录。我每测完一个环节都会把请求包、响应、关键截图整理成笔记。整条漏洞链的串联逻辑就是靠这些笔记一点点拼出来的。真实场景下你的脑容量绝对不够记住所有细节清晰的记录能帮你把碎片线索串成完整链路。还是那句话漏洞链的威力来自环环相扣攻防对抗的实质是看谁能更早发现对方链条上的断点。希望这篇复盘对你今后的渗透测试或防御体系建设有点参考价值。
返回列表