WEB缓存欺骗

文章发布时间:

最后更新时间:

WEB 缓存欺骗

Web cache

web 缓存是位于源服务器(origin server)和用户之间的系统,当用户请求一个静态资源的时候,请求会先到缓存,如果缓存里有资源,就直接返回给用户,如果缓存中没有资源(称为 cache miss),则将请求发送到源服务器,源服务器做出响应,响应在发送给与用户之前会先到缓存,根据规则决定是否存储,之后再给用户。

Cache key

缓存键。缓存接收到 http 时,他需要决定是直接从缓存中获取资源还是转发到源服务器,这个就是缓存键的作用,

缓存键从 http 请求中生成,通常包括请求的 url 和参数,也能包括请求头之类的。

Cache rule

缓存规则,决定缓存的内容和时间

缓存欺骗

欺骗 web 缓存存储动态资源,当用户访问提供的恶意 url 时,缓存将它的响应存储,攻击者再次访问该 url,获取缓存中的响应,从而获取敏感信息。

构建缓存欺骗攻击

1.确定返回包含敏感信息的动态响应的端点,重点关注支持 GETHEADOPTIONS 方法的端点

2.确定缓存和源服务器解析 URL 路径的方式中的差异

  • url 映射到 resource
  • 处理分割符
  • 规范化路径

3.构造恶意 url

Cache buster

burp 提供的插件会向发出的每个请求添加一个唯一的查询字符串

判断是否缓存

X-Cache

  • X-Cache: hit - 响应是从缓存提供
  • X-Cache: miss - 缓存不包含请求密钥的响应,因此是从源服务器获取的
  • X-Cache: dynamic - 源服务器动态生成内容
  • X-Cache: refresh - 缓存的内容已过时,需要刷新或重新验证

Cache-Control

Cache-Control 标头可能包含是否缓存内容,例如 max-age 高于 0 的 public 只表示可缓存,而不是一定缓存了

静态拓展缓存规则

我们知道了 cdn 会根据文件拓展名,例如.css .js 来判断是否缓存此时可能有漏洞可钻

  1. 服务器和缓存对 url 的解析差异

例如我们构造的 url 最后带着/settings.css

这时源服务器识别为 /profile/settings 路径,返回用户动态数据

CDN 看到 .css 扩展名,认为是静态文件将结果缓存下来

  1. 分隔符处理差异

例如我们构造请求 URL: /account;.js

源服务器 把;识别为分隔符,访问/account

CDN 看到.js 将其缓存

路径映射不一致

缓存与源服务器将资源的 URL 路径映射方式不一致,可能导致网络缓存欺骗漏洞

路径映射有传统 url 映射 以及 REST 风格映射两种,以 http://example.com/user/123/profile/1.css 为例

若源服务器使用 REST 风格映射,那么他会认为请求的是 profile 这个端点,而忽略后面的 1.css

而 CDN 使用传统 url 映射,他就会将返回数据当作静态内容缓存起来

除了上述的传统风格以及 REST,还有其他风格
RPC 风格 — 最直接的思路,把操作名放进路径,参数跟在后面(/getUser?id=42) 。早期 Web API 和 XML-RPC、JSON-RPC 都用这种方式,gRPC 的 HTTP 映射也属于此类。缺点是动词爆炸,难以统一管理。
GraphQL 单端点 — 所有请求都打到同一个路径(通常是 POST /graphql),路径本身不携带语义,查询结构完全在请求体里描述。适合复杂的前端数据获取场景,但对 HTTP 缓存不友好。
Action 风格 — Rails、Laravel 等 MVC 框架的经典做法,路径结构是 /controller/action/id ,比纯 RESTful 更灵活,允许自定义动词(/posts/publish/5),但规范性弱一些
文件系统路由 — Next.js、Nuxt、SvelteKit 等现代框架的做法,直接用文件目录结构映射 URL,pages/blog/[slug].tsx 就自动对应 /blog/:slug
通配符 / 兜底路由 — 用 * 或 ** 匹配任意剩余路径段,常见于静态文件服务和 SPA 中的客户端路由接管(/* → index.html)
Matrix 参数风格 — 参数直接嵌入路径段而非 query string,用分号分隔(/shop;category=books;sort=price)
内容寻址 / Hash 路由 — 分两种:一是传统 SPA 利用 URL # 哈希部分做客户端路由(不触发服务器请求);
二是 IPFS 等去中心化系统用内容哈希作为路径标识符,路径指向的是内容本身而非位置。

分隔符的差异

/profile;foo.css 为例,如我们的 spring 框架会将 ; 作为分割符,截断后面内容,于是返回 profile 的值,而其他框架可能将解析整个 path

Rails 的 . 分隔符机制

1
2
3
/profile        → 返回 HTML(默认)
/profile.json → 返回 JSON
/profile.xml → 返回 XML

三种请求的对比

/profile 正常请求,Rails 用默认 HTML 格式化器处理,返回用户的个人信息页面,一切正常

/profile.css Rails 看到 .css,认出这是一个格式标识符,但它没有 CSS 格式化器,于是直接拒绝请求,返回错误(通常是 406 Not Acceptable)。这反而是”安全”的,因为什么数据都没返回

/profile.ico 这里是关键。Rails 看到 .ico,不认识这个扩展名,不知道该映射到哪个格式化器,于是回退到默认行为——用 HTML 格式化器处理,把完整的用户配置信息返回出去

漏洞如何形成
Rails 的视角:/profile.ico → 不认识 .ico → 当普通 /profile 处理 → 返回用户数据
缓存的视角:/profile.ico → 看到 .ico 结尾 → 这是静态图标文件 → 缓存下来

两边的”理解”完全错位了。Rails 觉得自己在处理一个动态请求,缓存觉得自己在缓存一个静态图标文件。最终结果是:含有敏感用户数据的动态响应被当成静态资源缓存了

编码字符也能做为分割符

如 %00,例如 /profile%001.js

大多数框架如果 URL 中有 %00 就会报错,但有些框架会把后面也当作路径

总而言之,可以使用编码字符来尝试是否存在处理差异

静态目录缓存规则

CDN 通常会配置这样的规则:凡是 URL 以某些特定前缀开头,就认为是静态资源,直接缓存。比如:

1
2
3
4
5
/static/* → 缓存
/assets/* → 缓存
/images/* → 缓存
/scripts/* → 缓存
其他路径 → 不缓存,转发给源服务器

攻击者把动态 API 路径和静态目录前缀拼在一起

1
2
3
原始路径: /api/orders/123 
构造路径: /static/../api/orders/123
或者 /assets/..%2Fapi%2Forders%2F123

缓存看到路径以 /static/ 开头,触发缓存规则,打算缓存这个响应

但源服务器在处理路径时会做路径规范化.. 解析了/static,最终实际访问的是 /api/orders/123,返回真实的订单数据

防御

给动态资源加 Cache-Control 响应头

返回动态数据的 API 响应头里明确告诉缓存”不要存这个”:

Cache-Control: no-store, private

no-store 的意思是任何缓存(CDN、代理、浏览器)都不得存储这个响应。private 的意思是这个响应只属于当前用户,不能被共享缓存存储

确保 CDN 的缓存规则不会覆盖CDN 的配置规则优先级可能更高。CDN 里配置了所有 .js 结尾的请求缓存 24 小时,这条规则就会把源服务器返回的 no-store 覆盖掉。确保缓存规则的优先级低于源服务器的响应头,或者设置遵守源服务器的 Cache-Control

启用 CDN 的内容类型校验功能缓存在存储响应之前,对比两件事:URL 的扩展名: .ico响应的 Content-Type: text/html两者不匹配就拒绝缓存

消除源服务器和缓存之间的路径解析差异让两边行为保持一致:要么都解码 %2f 再解析 ..要么都不解码,按字面路径处理