threat hunting

Detecting Pass The Hash Attacks

Detect Pass-the-Hash attacks by analyzing NTLM authentication patterns, identifying Type 3 logons with NTLM where Kerberos is expected, and correlating with credential dumping.

credential-accessmitre-attackpass-the-hashproactive-detectiont1550threat-hunting
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When proactively hunting for indicators of detecting pass the hash attacks in the environment
  • After threat intelligence indicates active campaigns using these techniques
  • During incident response to scope compromise related to these techniques
  • When EDR or SIEM alerts trigger on related indicators
  • During periodic security assessments and purple team exercises

Prerequisites

  • EDR platform with process and network telemetry (CrowdStrike, MDE, SentinelOne)
  • SIEM with relevant log data ingested (Splunk, Elastic, Sentinel)
  • Sysmon deployed with comprehensive configuration
  • Windows Security Event Log forwarding enabled
  • Threat intelligence feeds for IOC correlation

Workflow

  1. Formulate Hypothesis: Define a testable hypothesis based on threat intelligence or ATT&CK gap analysis.
  2. Identify Data Sources: Determine which logs and telemetry are needed to validate or refute the hypothesis.
  3. Execute Queries: Run detection queries against SIEM and EDR platforms to collect relevant events.
  4. Analyze Results: Examine query results for anomalies, correlating across multiple data sources.
  5. Validate Findings: Distinguish true positives from false positives through contextual analysis.
  6. Correlate Activity: Link findings to broader attack chains and threat actor TTPs.
  7. Document and Report: Record findings, update detection rules, and recommend response actions.

Key Concepts

Concept Description
T1550.002 Pass the Hash
T1550.003 Pass the Ticket
T1078 Valid Accounts

Tools & Systems

Tool Purpose
CrowdStrike Falcon EDR telemetry and threat detection
Microsoft Defender for Endpoint Advanced hunting with KQL
Splunk Enterprise SIEM log analysis with SPL queries
Elastic Security Detection rules and investigation timeline
Sysmon Detailed Windows event monitoring
Velociraptor Endpoint artifact collection and hunting
Sigma Rules Cross-platform detection rule format

Common Scenarios

  1. Scenario 1: Mimikatz sekurlsa::pth with stolen NTLM hash
  2. Scenario 2: Impacket psexec.py remote execution with hash
  3. Scenario 3: CrackMapExec hash spraying across hosts
  4. Scenario 4: WMI lateral movement via pass-the-hash

Output Format

Hunt ID: TH-DETECT-[DATE]-[SEQ]
Technique: T1550.002
Host: [Hostname]
User: [Account context]
Evidence: [Log entries, process trees, network data]
Risk Level: [Critical/High/Medium/Low]
Confidence: [High/Medium/Low]
Recommended Action: [Containment, investigation, monitoring]
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md1.5 KB

API Reference: Detecting Pass-the-Hash Attacks

Windows Event ID 4624 Fields

Field PtH Signal
LogonType 3 (Network)
AuthenticationPackageName NTLM (not Kerberos)
LogonProcessName NtLmSsp
IpAddress Source of authentication
TargetUserName Account being used

python-evtx Usage

import Evtx.Evtx as evtx
with evtx.Evtx("Security.evtx") as log:
    for record in log.records():
        xml = record.xml()
        # Filter EventID 4624, LogonType=3, AuthPackage=NTLM

PtH Detection Logic

src_targets = defaultdict(set)
for event in ntlm_logons:
    src_targets[event["source_ip"]].add(event["computer"])
# Alert when single source authenticates to 3+ targets via NTLM

Splunk SPL Detection

index=wineventlog EventCode=4624 Logon_Type=3
| where Authentication_Package="NTLM"
| stats dc(Computer) as targets by Source_Network_Address, Account_Name
| where targets >= 3
| sort -targets

KQL (Microsoft Sentinel)

SecurityEvent
| where EventID == 4624 and LogonType == 3
| where AuthenticationPackageName == "NTLM"
| summarize TargetCount=dcount(Computer) by IpAddress, TargetUserName
| where TargetCount >= 3

Mitigation

# Restrict NTLM authentication
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RestrictSendingNTLMTraffic" -Value 2

CLI Usage

python agent.py --security-log Security.evtx
python agent.py --security-log Security.evtx --target-threshold 5
standards.md1.5 KB

Standards and References - Detecting Pass The Hash Attacks

MITRE ATT&CK Mappings

Technique Name Description
T1550.002 Pass the Hash See attack.mitre.org/techniques/T1550/002
T1550.003 Pass the Ticket See attack.mitre.org/techniques/T1550/003
T1078 Valid Accounts See attack.mitre.org/techniques/T1078

Detection Data Sources

Source Event ID Purpose
Sysmon 1 Process creation with command line
Sysmon 3 Network connection initiated
Sysmon 7 Image loaded (DLL)
Sysmon 10 Process access (LSASS)
Sysmon 11 File creation
Sysmon 12/13 Registry create/set
Sysmon 22 DNS query
Sysmon 25 Process tampering
Windows Security 4624 Successful logon
Windows Security 4625 Failed logon
Windows Security 4648 Explicit credential logon
Windows Security 4672 Special privileges assigned
Windows Security 4688 Process creation
Windows Security 4697 Service installed
Windows Security 4698 Scheduled task created
Windows Security 4769 Kerberos TGS requested
Windows Security 5140 Network share accessed

References

workflows.md2.9 KB

Detailed Hunting Workflow - Detecting Pass The Hash Attacks

Phase 1: Data Collection and Querying

Splunk SPL Query

index=wineventlog EventCode=4624 Logon_Type=3 Authentication_Package=NTLM
| where NOT match(Account_Name, "(?i)(ANONYMOUS|\\$|SYSTEM)")
| stats count dc(Computer) as targets by Account_Name Source_Network_Address
| where targets > 3
| sort -targets

KQL Query (Microsoft Defender for Endpoint)

SecurityEvent
| where EventID == 4624 and LogonType == 3
| where AuthenticationPackageName == "NTLM"
| summarize TargetCount=dcount(Computer) by SubjectUserName, IpAddress
| where TargetCount > 3

Phase 2: Baseline and Anomaly Detection

Step 2.1 - Establish Normal Behavior Baseline

  • Collect 30 days of historical data for the targeted technique
  • Document expected patterns, frequencies, and legitimate use cases
  • Identify known false positive sources and document exceptions
  • Build statistical baseline (mean, standard deviation) for key metrics

Step 2.2 - Identify Anomalies

  • Compare current activity against the 30-day baseline
  • Flag events exceeding 3 standard deviations from normal
  • Prioritize anomalies by risk score and potential business impact
  • Cross-reference with threat intelligence for known IOCs

Phase 3: Investigation and Correlation

Step 3.1 - Deep Dive Analysis

  • For each anomaly, collect full process tree context
  • Correlate with network activity, file operations, and authentication events
  • Check binary signatures, file hashes, and certificate validity
  • Review user account context and access patterns

Step 3.2 - Attack Chain Reconstruction

  • Map findings to MITRE ATT&CK kill chain stages
  • Identify initial access vector if applicable
  • Trace lateral movement and privilege escalation paths
  • Determine data access and potential exfiltration

Phase 4: Validation and Response

Step 4.1 - True/False Positive Determination

  • Verify findings with system owners and IT operations
  • Check change management records for authorized activities
  • Validate user context (authorized actions vs. compromised account)
  • Document determination rationale for each finding

Step 4.2 - Response Actions

  • For confirmed threats: initiate incident response procedures
  • For detection gaps: create or update detection rules
  • For false positives: tune existing rules and update exclusions
  • Update threat hunting playbook with lessons learned

Phase 5: Documentation and Reporting

Step 5.1 - Hunt Report

  • Summarize hypothesis, methodology, and findings
  • Include all queries executed and their results
  • Document IOCs discovered and detection rules created
  • Provide recommendations for security improvements

Step 5.2 - Knowledge Base Update

  • Add findings to threat intelligence platform
  • Update MITRE ATT&CK coverage heatmap
  • Share detection rules via Sigma format
  • Schedule follow-up hunts for related techniques

Scripts 2

agent.py4.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Pass-the-Hash attack detection agent.

Detects NTLM hash reuse attacks by analyzing Windows Security Event ID 4624
for Type 3 NTLM logons with anomalous patterns across multiple targets.
"""

import argparse
import json
import re
from collections import defaultdict
from datetime import datetime

try:
    import Evtx.Evtx as evtx
except ImportError:
    evtx = None

LEGITIMATE_SOURCES = {"127.0.0.1", "::1", "-", ""}


def parse_logon_events(filepath):
    if evtx is None:
        return {"error": "python-evtx not installed: pip install python-evtx"}
    events = []
    with evtx.Evtx(filepath) as log:
        for record in log.records():
            xml = record.xml()
            if "<EventID>4624</EventID>" not in xml:
                continue
            logon_type = re.search(r'<Data Name="LogonType">(\d+)', xml)
            auth_pkg = re.search(r'<Data Name="AuthenticationPackageName">([^<]+)', xml)
            account = re.search(r'<Data Name="TargetUserName">([^<]+)', xml)
            domain = re.search(r'<Data Name="TargetDomainName">([^<]+)', xml)
            src_ip = re.search(r'<Data Name="IpAddress">([^<]+)', xml)
            computer = re.search(r'<Data Name="Computer">([^<]+)', xml)
            time_match = re.search(r'SystemTime="([^"]+)"', xml)
            lt = logon_type.group(1) if logon_type else ""
            ap = auth_pkg.group(1) if auth_pkg else ""
            if lt == "3" and "NTLM" in ap.upper():
                events.append({
                    "timestamp": time_match.group(1) if time_match else "",
                    "logon_type": int(lt), "auth_package": ap.strip(),
                    "account": account.group(1) if account else "",
                    "domain": domain.group(1) if domain else "",
                    "source_ip": src_ip.group(1) if src_ip else "",
                    "computer": computer.group(1) if computer else "",
                })
    return events


def detect_pth_patterns(events, target_threshold=3):
    if isinstance(events, dict) and "error" in events:
        return [events]
    findings = []
    src_targets = defaultdict(lambda: {"computers": set(), "count": 0,
                                        "source_ip": "", "account": ""})
    for evt in events:
        src = evt.get("source_ip", "")
        if src in LEGITIMATE_SOURCES:
            continue
        key = f"{src}|{evt.get('account', '')}"
        src_targets[key]["computers"].add(evt.get("computer", ""))
        src_targets[key]["count"] += 1
        src_targets[key]["source_ip"] = src
        src_targets[key]["account"] = evt.get("account", "")

    for key, data in src_targets.items():
        target_count = len(data["computers"])
        if target_count >= target_threshold:
            findings.append({
                "type": "ntlm_type3_multi_target",
                "source_ip": data["source_ip"],
                "account": data["account"],
                "target_count": target_count,
                "targets": list(data["computers"])[:20],
                "total_logons": data["count"],
                "severity": "CRITICAL" if target_count >= 10 else "HIGH",
                "mitre": "T1550.002",
            })

    admin_sources = defaultdict(int)
    for evt in events:
        if evt.get("account", "").lower() in ("administrator", "admin"):
            admin_sources[evt.get("source_ip", "")] += 1
    for src, count in admin_sources.items():
        if count >= 2 and src not in LEGITIMATE_SOURCES:
            findings.append({
                "type": "admin_ntlm", "source_ip": src,
                "logon_count": count, "severity": "HIGH", "mitre": "T1550.002",
            })
    return findings


def main():
    parser = argparse.ArgumentParser(description="Pass-the-Hash Detector")
    parser.add_argument("--security-log", required=True, help="Windows Security EVTX")
    parser.add_argument("--target-threshold", type=int, default=3)
    args = parser.parse_args()
    events = parse_logon_events(args.security_log)
    findings = detect_pth_patterns(events, args.target_threshold)
    ntlm_count = len(events) if isinstance(events, list) else 0
    results = {
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "total_ntlm_type3_logons": ntlm_count,
        "findings": findings, "total_findings": len(findings),
    }
    print(json.dumps(results, indent=2))


if __name__ == "__main__":
    main()
process.py3.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Pass-the-Hash Detection - Analyzes authentication logs for NTLM-based lateral movement patterns."""

import json, csv, argparse, datetime, re
from collections import defaultdict
from pathlib import Path

SYSTEM_ACCOUNTS = {"system", "anonymous logon", "anonymous", "local service", "network service"}

def parse_logs(path):
    p = Path(path)
    if p.suffix == ".json":
        with open(p, encoding="utf-8") as f:
            data = json.load(f)
            return data if isinstance(data, list) else data.get("events", [])
    elif p.suffix == ".csv":
        with open(p, encoding="utf-8-sig") as f:
            return [dict(r) for r in csv.DictReader(f)]
    return []

def detect_pth(event):
    eid = str(event.get("EventCode", event.get("EventID", "")))
    if eid != "4624": return None
    logon_type = str(event.get("Logon_Type", event.get("LogonType", "")))
    if logon_type != "3": return None
    auth_pkg = event.get("Authentication_Package", event.get("AuthenticationPackageName", "")).lower()
    if "ntlm" not in auth_pkg: return None
    account = event.get("Account_Name", event.get("TargetUserName", "")).lower()
    if account in SYSTEM_ACCOUNTS or account.endswith("$"): return None
    src_ip = event.get("Source_Network_Address", event.get("IpAddress", ""))
    if not src_ip or src_ip in ("-", "::1", "127.0.0.1"): return None
    return {
        "technique": "T1550.002",
        "account": event.get("Account_Name", event.get("TargetUserName", "")),
        "source_ip": src_ip,
        "dest_host": event.get("Computer", event.get("hostname", "")),
        "auth_package": auth_pkg,
        "logon_process": event.get("Logon_Process", event.get("LogonProcessName", "")),
        "timestamp": event.get("_time", event.get("timestamp", "")),
        "risk_score": 45,
        "risk_level": "MEDIUM",
        "indicators": ["Type 3 NTLM logon - potential Pass-the-Hash"],
    }

def analyze_velocity(findings, threshold=3):
    account_dests = defaultdict(set)
    for f in findings:
        account_dests[f["account"]].add(f["dest_host"])
    alerts = []
    for acct, dests in account_dests.items():
        if len(dests) >= threshold:
            alerts.append({
                "technique": "T1550.002", "account": acct,
                "unique_targets": len(dests), "targets": list(dests),
                "risk_score": 80, "risk_level": "CRITICAL",
                "indicators": [f"NTLM auth to {len(dests)} hosts - PtH spray pattern"],
            })
    return alerts

def run_hunt(input_path, output_dir):
    print(f"[*] Pass-the-Hash Hunt - {datetime.datetime.now().isoformat()}")
    events = parse_logs(input_path)
    findings = [f for f in (detect_pth(e) for e in events) if f]
    velocity = analyze_velocity(findings)
    all_findings = findings + velocity
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    with open(Path(output_dir) / "pth_findings.json", "w", encoding="utf-8") as f:
        json.dump({"hunt_id": f"TH-PTH-{datetime.date.today()}", "findings": all_findings}, f, indent=2)
    print(f"[+] {len(all_findings)} findings ({len(velocity)} velocity alerts)")

def main():
    p = argparse.ArgumentParser(description="Pass-the-Hash Detection")
    sp = p.add_subparsers(dest="cmd")
    h = sp.add_parser("hunt"); h.add_argument("--input", "-i", required=True); h.add_argument("--output", "-o", default="./pth_output")
    sp.add_parser("queries")
    args = p.parse_args()
    if args.cmd == "hunt": run_hunt(args.input, args.output)
    elif args.cmd == "queries":
        print("=== Splunk PtH Query ===")
        print('''index=wineventlog EventCode=4624 Logon_Type=3 Authentication_Package=NTLM
| stats count dc(Computer) as targets by Account_Name Source_Network_Address
| where targets > 3 | sort -targets''')
    else: p.print_help()

if __name__ == "__main__": main()

Assets 1

template.mdtext/markdown · 2.6 KB
Keep exploring