mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 22:24:56 +03:00
248 lines
8.6 KiB
Python
248 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
"""PowerShell Script Block Logging threat hunting agent."""
|
|
|
|
import json
|
|
import sys
|
|
import argparse
|
|
import base64
|
|
import re
|
|
from datetime import datetime
|
|
from collections import defaultdict
|
|
|
|
try:
|
|
import Evtx.Evtx as evtx
|
|
from lxml import etree
|
|
except ImportError:
|
|
print("Install: pip install python-evtx lxml")
|
|
sys.exit(1)
|
|
|
|
NS = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
|
|
|
AMSI_INDICATORS = [
|
|
"amsiutils", "amsiinitfailed", "amsicontext", "amsisession",
|
|
"amsiinitialize", "amsi.dll", "amsiScanBuffer",
|
|
"System.Management.Automation.AmsiUtils",
|
|
]
|
|
|
|
SUSPICIOUS_KEYWORDS = [
|
|
"Invoke-Mimikatz", "Invoke-Kerberoast", "Invoke-ShellCode",
|
|
"Invoke-ReflectivePEInjection", "Invoke-TokenManipulation",
|
|
"Get-GPPPassword", "Get-Keystrokes", "Get-TimedScreenshot",
|
|
"Out-Minidump", "Invoke-NinjaCopy", "Invoke-CredentialInjection",
|
|
"Invoke-DllInjection", "Invoke-WMICommand", "PowerSploit",
|
|
"Empire", "BloodHound", "Rubeus", "SharpHound",
|
|
"Invoke-PSInject", "Invoke-RunAs", "PowerView",
|
|
]
|
|
|
|
DOWNLOAD_PATTERNS = [
|
|
r"Net\.WebClient", r"Invoke-WebRequest", r"wget\s", r"curl\s",
|
|
r"DownloadString", r"DownloadFile", r"DownloadData",
|
|
r"Start-BitsTransfer", r"Invoke-RestMethod",
|
|
r"New-Object\s+IO\.MemoryStream",
|
|
]
|
|
|
|
OBFUSCATION_PATTERNS = [
|
|
r"-[Ee]nc(?:oded)?[Cc]ommand",
|
|
r"\-e\s+[A-Za-z0-9+/=]{20,}",
|
|
r"IEX\s*\(",
|
|
r"Invoke-Expression",
|
|
r"\[Convert\]::FromBase64String",
|
|
r"\[System\.Text\.Encoding\]::",
|
|
r"\.Replace\(['\"][^'\"]+['\"],\s*['\"][^'\"]+['\"]\)",
|
|
r"-join\s*\[char\[\]\]",
|
|
r"\$env:comspec",
|
|
]
|
|
|
|
|
|
def parse_evtx_4104(evtx_path, max_events=10000):
|
|
"""Parse Event 4104 script block logging entries from EVTX."""
|
|
events = []
|
|
count = 0
|
|
with evtx.Evtx(evtx_path) as log:
|
|
for record in log.records():
|
|
if count >= max_events:
|
|
break
|
|
xml = record.xml()
|
|
root = etree.fromstring(xml.encode("utf-8"))
|
|
event_id_el = root.find(".//e:System/e:EventID", NS)
|
|
if event_id_el is None or event_id_el.text != "4104":
|
|
continue
|
|
count += 1
|
|
time_el = root.find(".//e:System/e:TimeCreated", NS)
|
|
timestamp = time_el.get("SystemTime", "") if time_el is not None else ""
|
|
data = {}
|
|
for el in root.findall(".//e:EventData/e:Data", NS):
|
|
name = el.get("Name", "")
|
|
data[name] = el.text or ""
|
|
events.append({
|
|
"timestamp": timestamp,
|
|
"script_block_id": data.get("ScriptBlockId", ""),
|
|
"script_block_text": data.get("ScriptBlockText", ""),
|
|
"message_number": data.get("MessageNumber", "1"),
|
|
"message_total": data.get("MessageTotal", "1"),
|
|
"path": data.get("Path", ""),
|
|
})
|
|
return events
|
|
|
|
|
|
def reassemble_script_blocks(events):
|
|
"""Reassemble multi-part script blocks by ScriptBlockId."""
|
|
blocks = defaultdict(list)
|
|
for ev in events:
|
|
sb_id = ev.get("script_block_id", "")
|
|
if sb_id:
|
|
blocks[sb_id].append(ev)
|
|
assembled = []
|
|
for sb_id, parts in blocks.items():
|
|
parts.sort(key=lambda x: int(x.get("message_number", "1")))
|
|
full_text = "".join(p.get("script_block_text", "") for p in parts)
|
|
assembled.append({
|
|
"script_block_id": sb_id,
|
|
"timestamp": parts[0].get("timestamp", ""),
|
|
"path": parts[0].get("path", ""),
|
|
"parts": len(parts),
|
|
"full_text": full_text,
|
|
})
|
|
return assembled
|
|
|
|
|
|
def detect_amsi_bypass(script_text):
|
|
"""Check script text for AMSI bypass indicators."""
|
|
findings = []
|
|
lower = script_text.lower()
|
|
for indicator in AMSI_INDICATORS:
|
|
if indicator.lower() in lower:
|
|
findings.append({"type": "amsi_bypass", "indicator": indicator})
|
|
return findings
|
|
|
|
|
|
def detect_suspicious_keywords(script_text):
|
|
"""Check for known offensive tool keywords."""
|
|
findings = []
|
|
for kw in SUSPICIOUS_KEYWORDS:
|
|
if kw.lower() in script_text.lower():
|
|
findings.append({"type": "credential_or_offensive_tool", "keyword": kw})
|
|
return findings
|
|
|
|
|
|
def detect_download_cradles(script_text):
|
|
"""Detect download cradle patterns in script text."""
|
|
findings = []
|
|
for pattern in DOWNLOAD_PATTERNS:
|
|
if re.search(pattern, script_text, re.IGNORECASE):
|
|
findings.append({"type": "download_cradle", "pattern": pattern})
|
|
return findings
|
|
|
|
|
|
def detect_obfuscation(script_text):
|
|
"""Detect obfuscation and encoded command patterns."""
|
|
findings = []
|
|
for pattern in OBFUSCATION_PATTERNS:
|
|
if re.search(pattern, script_text, re.IGNORECASE):
|
|
findings.append({"type": "obfuscation", "pattern": pattern})
|
|
b64_match = re.search(r"[A-Za-z0-9+/=]{40,}", script_text)
|
|
if b64_match:
|
|
try:
|
|
decoded = base64.b64decode(b64_match.group()).decode("utf-16-le", errors="ignore")
|
|
if any(c.isalpha() for c in decoded[:20]):
|
|
findings.append({
|
|
"type": "encoded_payload",
|
|
"decoded_preview": decoded[:200],
|
|
})
|
|
except Exception:
|
|
pass
|
|
return findings
|
|
|
|
|
|
def hunt_scripts(assembled_blocks):
|
|
"""Run all detection checks on assembled script blocks."""
|
|
results = []
|
|
for block in assembled_blocks:
|
|
text = block.get("full_text", "")
|
|
if not text.strip():
|
|
continue
|
|
findings = []
|
|
findings.extend(detect_amsi_bypass(text))
|
|
findings.extend(detect_suspicious_keywords(text))
|
|
findings.extend(detect_download_cradles(text))
|
|
findings.extend(detect_obfuscation(text))
|
|
if findings:
|
|
results.append({
|
|
"script_block_id": block["script_block_id"],
|
|
"timestamp": block["timestamp"],
|
|
"path": block["path"],
|
|
"text_preview": text[:300],
|
|
"findings": findings,
|
|
"severity": "high" if any(
|
|
f["type"] in ("amsi_bypass", "credential_or_offensive_tool")
|
|
for f in findings
|
|
) else "medium",
|
|
})
|
|
return results
|
|
|
|
|
|
def run_audit(args):
|
|
"""Execute PowerShell script block hunting."""
|
|
print(f"\n{'='*60}")
|
|
print(f" POWERSHELL SCRIPT BLOCK HUNTING")
|
|
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
|
print(f"{'='*60}\n")
|
|
|
|
report = {}
|
|
events = parse_evtx_4104(args.evtx, args.max_events)
|
|
report["total_4104_events"] = len(events)
|
|
print(f"Parsed {len(events)} Event 4104 records\n")
|
|
|
|
blocks = reassemble_script_blocks(events)
|
|
report["unique_script_blocks"] = len(blocks)
|
|
print(f"Reassembled {len(blocks)} unique script blocks\n")
|
|
|
|
results = hunt_scripts(blocks)
|
|
report["suspicious_blocks"] = len(results)
|
|
report["findings"] = results
|
|
|
|
amsi = sum(1 for r in results if any(f["type"] == "amsi_bypass" for f in r["findings"]))
|
|
cred = sum(1 for r in results if any(f["type"] == "credential_or_offensive_tool" for f in r["findings"]))
|
|
dl = sum(1 for r in results if any(f["type"] == "download_cradle" for f in r["findings"]))
|
|
obf = sum(1 for r in results if any(f["type"] == "obfuscation" for f in r["findings"]))
|
|
report["summary"] = {
|
|
"amsi_bypass_attempts": amsi,
|
|
"credential_access": cred,
|
|
"download_cradles": dl,
|
|
"obfuscation_detected": obf,
|
|
}
|
|
|
|
print(f"--- HUNT RESULTS ---")
|
|
print(f" AMSI bypass attempts: {amsi}")
|
|
print(f" Credential/offensive tools: {cred}")
|
|
print(f" Download cradles: {dl}")
|
|
print(f" Obfuscation detected: {obf}")
|
|
print(f"\n--- HIGH SEVERITY ---")
|
|
for r in results[:15]:
|
|
if r["severity"] == "high":
|
|
print(f" [{r['timestamp']}] {r['script_block_id']}")
|
|
for f in r["findings"]:
|
|
print(f" {f['type']}: {f.get('keyword', f.get('indicator', ''))}")
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="PowerShell Script Block Hunting Agent")
|
|
parser.add_argument("--evtx", required=True,
|
|
help="Path to PowerShell Operational .evtx file")
|
|
parser.add_argument("--max-events", type=int, default=10000,
|
|
help="Max events to parse (default: 10000)")
|
|
parser.add_argument("--output", help="Save report to JSON file")
|
|
args = parser.parse_args()
|
|
|
|
report = run_audit(args)
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
print(f"\n[+] Report saved to {args.output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|