mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24: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
241 lines
9.4 KiB
Python
241 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Memory dump credential extraction agent using volatility3 subprocess and pypykatz."""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CLOUD_KEY_PATTERNS = [
|
|
(r"AKIA[A-Z0-9]{16}", "AWS Access Key"),
|
|
(r"ASIA[A-Z0-9]{16}", "AWS Temp Key"),
|
|
(r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+", "JWT/Azure Token"),
|
|
]
|
|
|
|
AUTH_STRING_PATTERNS = [
|
|
r"(?i)bearer\s+[A-Za-z0-9_\-\.]+",
|
|
r"(?i)authorization:\s*\S+",
|
|
r"(?i)api[_-]key[=:]\s*\S+",
|
|
r"(?i)password[=:]\s*\S+",
|
|
]
|
|
|
|
|
|
def verify_dump(dump_path: str) -> dict:
|
|
"""Verify memory dump exists and compute hash."""
|
|
if not os.path.isfile(dump_path):
|
|
logger.error("Memory dump not found: %s", dump_path)
|
|
return {"valid": False}
|
|
size = os.path.getsize(dump_path)
|
|
with open(dump_path, "rb") as f:
|
|
sha256 = hashlib.sha256(f.read(1024 * 1024)).hexdigest()
|
|
return {"valid": True, "size_bytes": size, "sha256_1mb": sha256}
|
|
|
|
|
|
def run_vol3(dump_path: str, plugin: str, extra_args: Optional[List[str]] = None) -> str:
|
|
"""Run a volatility3 plugin and return stdout."""
|
|
cmd = ["vol", "-f", dump_path, plugin]
|
|
if extra_args:
|
|
cmd.extend(extra_args)
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
if result.returncode != 0 and result.stderr:
|
|
logger.warning("vol3 %s stderr: %s", plugin, result.stderr[:200])
|
|
return result.stdout
|
|
except FileNotFoundError:
|
|
logger.error("volatility3 (vol) not found in PATH")
|
|
return ""
|
|
except subprocess.TimeoutExpired:
|
|
logger.error("vol3 %s timed out", plugin)
|
|
return ""
|
|
|
|
|
|
def get_os_info(dump_path: str) -> dict:
|
|
"""Identify OS version from memory dump."""
|
|
output = run_vol3(dump_path, "windows.info")
|
|
info = {}
|
|
for line in output.splitlines():
|
|
if "\t" in line:
|
|
parts = line.split("\t", 1)
|
|
if len(parts) == 2:
|
|
info[parts[0].strip()] = parts[1].strip()
|
|
return info
|
|
|
|
|
|
def find_lsass_pid(dump_path: str) -> Optional[int]:
|
|
"""Find LSASS process PID from process list."""
|
|
output = run_vol3(dump_path, "windows.pslist")
|
|
for line in output.splitlines():
|
|
if "lsass.exe" in line.lower():
|
|
parts = line.split()
|
|
for p in parts:
|
|
if p.isdigit():
|
|
return int(p)
|
|
return None
|
|
|
|
|
|
def extract_hashdump(dump_path: str) -> List[dict]:
|
|
"""Extract SAM hashes using windows.hashdump."""
|
|
output = run_vol3(dump_path, "windows.hashdump")
|
|
results = []
|
|
for line in output.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 4 and parts[1].isdigit():
|
|
results.append({
|
|
"user": parts[0], "rid": int(parts[1]),
|
|
"lm_hash": parts[2], "ntlm_hash": parts[3],
|
|
})
|
|
logger.info("Extracted %d SAM hashes", len(results))
|
|
return results
|
|
|
|
|
|
def extract_lsadump(dump_path: str) -> List[dict]:
|
|
"""Extract LSA secrets using windows.lsadump."""
|
|
output = run_vol3(dump_path, "windows.lsadump")
|
|
results = []
|
|
for line in output.splitlines():
|
|
line = line.strip()
|
|
if line and not line.startswith("Offset") and not line.startswith("-"):
|
|
results.append({"raw": line})
|
|
logger.info("Extracted %d LSA secret entries", len(results))
|
|
return results
|
|
|
|
|
|
def extract_cachedump(dump_path: str) -> List[dict]:
|
|
"""Extract cached domain credentials using windows.cachedump."""
|
|
output = run_vol3(dump_path, "windows.cachedump")
|
|
results = []
|
|
for line in output.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 3 and parts[0] not in ("User", "---"):
|
|
results.append({"user": parts[0], "domain": parts[1], "dcc2_hash": parts[2] if len(parts) > 2 else ""})
|
|
logger.info("Extracted %d cached domain credentials", len(results))
|
|
return results
|
|
|
|
|
|
def run_pypykatz(dump_path: str, output_dir: str) -> dict:
|
|
"""Run pypykatz against LSASS minidump or full memory for credential extraction."""
|
|
lsass_dmp = os.path.join(output_dir, "lsass.dmp")
|
|
target = lsass_dmp if os.path.isfile(lsass_dmp) else dump_path
|
|
mode = "minidump" if target == lsass_dmp else "rekall"
|
|
cmd = ["pypykatz", "lsa", mode, target, "--json"]
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
|
if result.stdout:
|
|
return json.loads(result.stdout)
|
|
except FileNotFoundError:
|
|
logger.warning("pypykatz not found; skipping LSASS credential extraction")
|
|
except (json.JSONDecodeError, subprocess.TimeoutExpired) as exc:
|
|
logger.warning("pypykatz error: %s", exc)
|
|
return {}
|
|
|
|
|
|
def parse_pypykatz_creds(pypykatz_data: dict) -> List[dict]:
|
|
"""Parse pypykatz JSON output into structured credential list."""
|
|
creds = []
|
|
for session_key, session in pypykatz_data.get("logon_sessions", {}).items():
|
|
username = session.get("username", "")
|
|
domain = session.get("domainname", "")
|
|
if not username or username == "(null)":
|
|
continue
|
|
entry = {"user": f"{domain}\\{username}", "sid": session.get("sid", ""),
|
|
"logon_server": session.get("logon_server", ""),
|
|
"logon_time": session.get("logon_time", ""), "cred_types": []}
|
|
for msv in session.get("msv_creds", []):
|
|
if msv.get("NThash"):
|
|
entry["cred_types"].append({"type": "NTLM", "hash": msv["NThash"]})
|
|
for kerb in session.get("kerberos_creds", []):
|
|
if kerb.get("password"):
|
|
entry["cred_types"].append({"type": "Kerberos_password", "value": kerb["password"]})
|
|
for ticket in kerb.get("tickets", []):
|
|
entry["cred_types"].append({"type": "Kerberos_ticket",
|
|
"server": ticket.get("server", ""), "enc_type": ticket.get("enc_type", "")})
|
|
for wd in session.get("wdigest_creds", []):
|
|
if wd.get("password"):
|
|
entry["cred_types"].append({"type": "WDigest", "value": wd["password"]})
|
|
for dpapi in session.get("dpapi_creds", []):
|
|
if dpapi.get("masterkey"):
|
|
entry["cred_types"].append({"type": "DPAPI_masterkey", "key": dpapi["masterkey"][:40]})
|
|
if entry["cred_types"]:
|
|
creds.append(entry)
|
|
return creds
|
|
|
|
|
|
def search_cloud_keys(dump_path: str) -> List[dict]:
|
|
"""Search memory strings for cloud credentials and auth tokens."""
|
|
output = run_vol3(dump_path, "windows.strings", ["--pid", "0"])
|
|
findings = []
|
|
for pattern, label in CLOUD_KEY_PATTERNS:
|
|
for match in re.findall(pattern, output):
|
|
findings.append({"type": label, "value": match[:30] + "..."})
|
|
for pattern in AUTH_STRING_PATTERNS:
|
|
for match in re.findall(pattern, output):
|
|
findings.append({"type": "auth_string", "value": match[:60]})
|
|
logger.info("Found %d cloud/auth credential fragments", len(findings))
|
|
return findings[:50]
|
|
|
|
|
|
def generate_report(dump_path: str, output_dir: str) -> dict:
|
|
"""Generate full credential extraction report."""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
report = {"analysis_date": datetime.utcnow().isoformat(), "source": dump_path}
|
|
|
|
report["dump_info"] = verify_dump(dump_path)
|
|
if not report["dump_info"].get("valid"):
|
|
return report
|
|
|
|
report["os_info"] = get_os_info(dump_path)
|
|
report["lsass_pid"] = find_lsass_pid(dump_path)
|
|
report["sam_hashes"] = extract_hashdump(dump_path)
|
|
report["lsa_secrets"] = extract_lsadump(dump_path)
|
|
report["cached_creds"] = extract_cachedump(dump_path)
|
|
|
|
pypykatz_data = run_pypykatz(dump_path, output_dir)
|
|
report["lsass_creds"] = parse_pypykatz_creds(pypykatz_data)
|
|
report["cloud_keys"] = search_cloud_keys(dump_path)
|
|
|
|
summary = {
|
|
"sam_hashes": len(report["sam_hashes"]),
|
|
"lsa_secrets": len(report["lsa_secrets"]),
|
|
"cached_creds": len(report["cached_creds"]),
|
|
"lsass_creds": len(report["lsass_creds"]),
|
|
"cloud_keys": len(report["cloud_keys"]),
|
|
}
|
|
report["summary"] = summary
|
|
report["actions"] = []
|
|
if summary["sam_hashes"] > 0:
|
|
report["actions"].append("Reset passwords for all local accounts with extracted NTLM hashes")
|
|
if summary["lsass_creds"] > 0:
|
|
report["actions"].append("Reset domain account passwords and perform double krbtgt rotation")
|
|
if summary["cloud_keys"] > 0:
|
|
report["actions"].append("Rotate all discovered cloud access keys and revoke active sessions")
|
|
|
|
logger.info("Report complete: %s", json.dumps(summary))
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Memory Dump Credential Extraction Agent")
|
|
parser.add_argument("--dump", required=True, help="Path to memory dump file")
|
|
parser.add_argument("--output-dir", default=".", help="Output directory")
|
|
parser.add_argument("--output", default="credential_report.json")
|
|
args = parser.parse_args()
|
|
|
|
report = generate_report(args.dump, args.output_dir)
|
|
out_path = os.path.join(args.output_dir, args.output)
|
|
with open(out_path, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
logger.info("Report saved to %s", out_path)
|
|
print(json.dumps(report, indent=2, default=str))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|