Chapter 51

TTP Clustering

TTP (Tactics, Techniques, and Procedures) clustering groups attacks that share the same operational behaviors — not just the same tools or infrastructure, but the same decision-making patterns. This chapter covers mapping incident TTPs to MITRE ATT&CK, identifying behavioral overlap between incidents, and determining when two incidents are likely the same actor.

Scenario

You've analyzed three separate incidents across three companies in the same sector. Each used different C2 IPs, different malware hashes, and different initial access domains. But the TTPs are nearly identical: AiTM phishing → Cobalt Strike → Kerberoasting → DCSync → PSExec lateral movement → ZIP staging → HTTPS exfiltration to cloud storage. The specific sequence and specific tools in combination point to the same actor operating a repeatable playbook — even though every indicator was different. This is TTP-based attribution.

MITRE ATT&CK Mapping

Observed behaviorATT&CK TechniqueSub-technique
AiTM phishing email → session token theftPhishing (T1566)Spearphishing Link (T1566.002)
Cobalt Strike beacon executed via IEX downloadCommand and Scripting Interpreter (T1059)PowerShell (T1059.001)
Cobalt Strike beacon as C2Command and Control (TA0011)Application Layer Protocol: HTTPS (T1071.001)
LSASS memory dumpOS Credential Dumping (T1003)LSASS Memory (T1003.001)
Kerberoasting (RC4 service tickets)OS Credential Dumping (T1003)Kerberoasting (T1558.003)
DCSync (replication API)OS Credential Dumping (T1003)DCSync (T1003.006)
PSExec-style lateral movementLateral Movement (TA0008)Remote Services: SMB/Windows Admin Shares (T1021.002)
Staging data in ZIP archiveCollection (TA0009)Archive Collected Data (T1560)
HTTPS exfil to external IPExfiltration (TA0010)Exfiltration Over C2 Channel (T1041)
VSS deletion before ransomwareImpact (TA0040)Inhibit System Recovery (T1490)
Pythonttp-overlap.py
#!/usr/bin/env python3
"""
Compare TTPs from multiple incidents to identify clustering.
Input: list of ATT&CK technique IDs per incident.
Output: pairwise similarity scores — high score = likely same actor.
"""

INCIDENTS = {
    "CASE-2026-009": [
        "T1566.002",  # Spearphishing Link
        "T1059.001",  # PowerShell
        "T1071.001",  # HTTPS C2
        "T1003.001",  # LSASS dump
        "T1558.003",  # Kerberoasting
        "T1003.006",  # DCSync
        "T1021.002",  # SMB lateral movement
        "T1560",      # Archive collected data
        "T1041",      # Exfil over C2
    ],
    "CASE-2026-003": [
        "T1566.002",  # Same spearphishing
        "T1059.001",  # Same PS execution
        "T1071.001",  # Same HTTPS C2
        "T1003.001",  # LSASS
        "T1558.003",  # Kerberoasting
        "T1003.006",  # DCSync
        "T1021.002",  # SMB
        "T1560",      # Archive
        "T1041",      # Exfil
        "T1490",      # VSS deletion (also deployed ransomware)
    ],
    "CASE-2025-187": [  # Different actor
        "T1190",      # Exploit public-facing application
        "T1505.003",  # Webshell
        "T1071.001",  # HTTPS C2 (same — not enough alone)
        "T1048",      # Exfil over alternative protocol (DNS)
        "T1070.004",  # File deletion
    ]
}

def jaccard_similarity(set_a, set_b):
    intersection = len(set_a & set_b)
    union = len(set_a | set_b)
    return intersection / union if union > 0 else 0

cases = list(INCIDENTS.keys())
print("TTP Clustering — Pairwise Jaccard Similarity")
print("=" * 55)

for i in range(len(cases)):
    for j in range(i+1, len(cases)):
        a, b = cases[i], cases[j]
        ttps_a = set(INCIDENTS[a])
        ttps_b = set(INCIDENTS[b])
        score = jaccard_similarity(ttps_a, ttps_b)
        shared = sorted(ttps_a & ttps_b)

        print(f"\n{a} vs {b}")
        print(f"  Similarity: {score:.2f} ({'HIGH — likely same actor' if score > 0.6 else 'LOW — different actor'})")
        print(f"  Shared TTPs ({len(shared)}): {', '.join(shared)}")
        unique_a = sorted(ttps_a - ttps_b)
        unique_b = sorted(ttps_b - ttps_a)
        if unique_a: print(f"  Only in {a}: {', '.join(unique_a)}")
        if unique_b: print(f"  Only in {b}: {', '.join(unique_b)}")

Clustering Decision Criteria

  When to Attribute Two Incidents to the Same Actor
  ═══════════════════════════════════════════════════════════════════

  HIGH CONFIDENCE (attribute):
    ├── Shared infrastructure (same C2 IP, domain) AND shared TTPs
    ├── Identical custom malware (same code, same config format)
    ├── Same operational sequence (5+ techniques in same order)
    └── Multiple independent indicators across infrastructure + TTPs + timing

  MODERATE CONFIDENCE (possible link):
    ├── Same tool family (e.g., Cobalt Strike) BUT different profile
    ├── Similar TTPs (4-6 shared) with no shared infrastructure
    └── Shared targeting (same sector, same time period, same lures)

  LOW CONFIDENCE (coincidence likely):
    ├── Same commercial tool (Cobalt Strike, Meterpreter) — used by thousands of actors
    ├── Same technique alone (e.g., Kerberoasting) — in every pentest playbook
    └── One shared IP — may be shared hosting, Tor exit, or coincidence

  The Pyramid of Pain (David Bianco):
    Hash values    ← Trivial for attacker to change
    IP addresses   ← Easy to change
    Domain names   ← Easy to change
    Network artifacts (URI patterns, JA3) ← Annoying to change
    Host artifacts (registry paths, file names) ← Annoying to change
    TTPs           ← Hard to change (reflects actor's skills and habits)

  Strong attribution requires indicators HIGH on the pyramid.

Q & A

Q: Two incidents share identical TTPs but the TTP set is common pentest methodology (PowerShell + Mimikatz + PSExec). How do you avoid over-attributing?

The key discriminator is specificity vs. prevalence. Generic techniques (PowerShell, Mimikatz, PSExec) are used by hundreds of distinct actors. By themselves, they indicate nothing about actor identity. Raise the confidence threshold when: (1) Operational timing: same techniques used at the same time of day (UTC offset indicates attacker's timezone), same day of week pattern (e.g., attacks only on Tuesday/Thursday), same dwell time before exfiltration. (2) Configuration fingerprints: Cobalt Strike watermark (the unique integer embedded in beacon configs), Metasploit meterpreter stage config hash, or custom tool compile-time artifacts. Two incidents with the same Cobalt Strike watermark are definitively the same operator (or their toolkit was stolen/leaked). (3) Target selection logic: same sector, same job titles targeted in phishing, same data types exfiltrated — attacker interest profile matches. (4) Procedural details: directory names used for staging (C:\Windows\Temp\update), naming convention for dropped files, order of specific sub-techniques (Kerberoast → DCSync → 30 minute wait → SMB lateral = a specific playbook). The more procedural the similarity, the more likely same actor. When only generic techniques match, lower your confidence to "possible" and document your reasoning explicitly.