10.9 MITRE ATT&CK-Driven Hunting

14-18 hours · Module 10

MITRE ATT&CK-Driven Hunting

Introduction

MITRE ATT&CK provides a structured catalogue of adversary techniques organised by tactic (the attacker’s goal) and technique (the method). Using ATT&CK to drive hunting means: systematically identifying techniques that are relevant to your environment, checking which have detection coverage (Module 9.11), and hunting for the ones that do not. This ensures hunting is not random — it methodically closes the visibility gaps that analytics rules leave open.


The ATT&CK-driven hunting workflow

Step 1: Identify relevant techniques. Not every ATT&CK technique applies to your environment. Filter by: your platform (Windows, Azure AD/Entra ID, M365, Linux), the tactics most commonly used against your industry (check industry threat reports), and the techniques used by threat groups known to target similar organisations.

Step 2: Check detection coverage. Navigate to Sentinel → MITRE ATT&CK. The blade shows which techniques have active analytics rules (green) and which do not (grey). Cross-reference with your hunting query coverage — some uncovered techniques may have hunting queries but no automated rules.

Step 3: Prioritise uncovered techniques. Not every uncovered technique needs a hunt. Prioritise: techniques in the “Initial Access” and “Persistence” tactics (these are the entry points and footholds — if you miss them, the attacker stays), techniques reported in recent threat intelligence for your industry, and techniques with high impact if undetected (privilege escalation, data exfiltration).

Step 4: Hunt for each prioritised technique. Formulate a hypothesis: “Technique T1078.004 (Valid Accounts: Cloud Accounts) may have been used to access our environment via compromised cloud credentials.” Write KQL to search for evidence of the technique. Execute and analyse.

Step 5: Update coverage. If the hunt finds evidence → create an incident AND an analytics rule. If the hunt finds nothing → document the hunt. Either way, the technique has been “covered” by hunting. Update your coverage tracking.


Hunting queries mapped to high-priority ATT&CK techniques

T1078.004 — Valid Accounts: Cloud Accounts. Hunt for compromised cloud credentials by looking for sign-ins from unusual infrastructure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Hunt: cloud account access from suspicious infrastructure
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == "0"
| where not(ipv4_is_private(IPAddress))
| extend ASN = tostring(AutonomousSystemNumber)
| summarize
    UniqueUsers = dcount(UserPrincipalName),
    Users = make_set(UserPrincipalName, 10),
    SigninCount = count()
    by ASN, IPAddress
| where UniqueUsers > 3  // Same IP accessing multiple accounts
| order by UniqueUsers desc

Multiple users signing in from the same external IP suggests: either a corporate VPN exit point (expected) or an attacker using compromised credentials for multiple accounts (investigate). Cross-reference with your known corporate IPs.

T1098.003 — Account Manipulation: Additional Cloud Roles. Hunt for unexpected role assignments that grant persistent privileged access.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Hunt: privileged role assignments to unexpected users
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName has "Add member to role"
| where Result == "success"
| extend RoleName = tostring(TargetResources[0].modifiedProperties[1].newValue)
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Assigner = tostring(InitiatedBy.user.userPrincipalName)
| where RoleName has_any ("Global Admin", "Exchange Admin",
    "Security Admin", "Privileged Role Admin")
| project TimeGenerated, Assigner, TargetUser, RoleName
| order by TimeGenerated desc

T1136.003 — Create Account: Cloud Account. Hunt for new accounts created outside normal HR provisioning.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Hunt: new account creation not from HR provisioning system
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName == "Add user"
| where Result == "success"
| extend Creator = tostring(InitiatedBy.user.userPrincipalName)
| extend NewUser = tostring(TargetResources[0].userPrincipalName)
| where Creator !in (
    _GetWatchlist('HRProvisioningAccounts') | project SearchKey)
| project TimeGenerated, Creator, NewUser,
    IPAddress = tostring(InitiatedBy.user.ipAddress)

T1114.003 — Email Collection: Email Forwarding Rule. Hunt for mailbox rules forwarding to external addresses.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Hunt: inbox rules forwarding to non-corporate domains
CloudAppEvents
| where TimeGenerated > ago(30d)
| where ActionType in ("New-InboxRule", "Set-InboxRule")
| extend RuleData = parse_json(RawEventData)
| extend ForwardTo = tostring(RuleData.Parameters
    | mv-expand RuleData.Parameters
    | where tostring(RuleData.Parameters.Name) in ("ForwardTo", "RedirectTo")
    | project tostring(RuleData.Parameters.Value))
| where isnotempty(ForwardTo)
| where ForwardTo !endswith "@northgateeng.com"
| extend Creator = tostring(RuleData.UserId)
| project TimeGenerated, Creator, ForwardTo,
    IPAddress = tostring(RuleData.ClientIP)

Additional high-value technique hunts

T1550.001 — Use Alternate Authentication Material: Application Access Token. Hunt for OAuth token abuse — applications accessing resources with unusual patterns.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Hunt: OAuth applications accessing unusual numbers of mailboxes
CloudAppEvents
| where TimeGenerated > ago(30d)
| where Application == "Microsoft Exchange Online"
| where ActionType in ("MailItemsAccessed", "Send")
| extend AppId = tostring(parse_json(RawEventData).AppId)
| where isnotempty(AppId) and AppId != "00000000-0000-0000-0000-000000000000"
| summarize
    MailboxesAccessed = dcount(AccountObjectId),
    Mailboxes = make_set(AccountDisplayName, 10),
    ActionCount = count()
    by AppId
| where MailboxesAccessed > 5
| order by MailboxesAccessed desc

An application accessing many mailboxes may be a legitimate service (backup, eDiscovery) or a malicious OAuth app harvesting email. Cross-reference the AppId with known legitimate applications.

T1059.001 — Command and Scripting Interpreter: PowerShell. Hunt for encoded PowerShell — a common obfuscation technique.

1
2
3
4
5
6
7
8
9
// Hunt: encoded PowerShell execution
DeviceProcessEvents
| where TimeGenerated > ago(14d)
| where FileName in ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("-enc", "-EncodedCommand",
    "FromBase64String", "Convert]::FromBase64", "-e ")
| project TimeGenerated, DeviceName, AccountName,
    ProcessCommandLine, InitiatingProcessFileName
| order by TimeGenerated desc

Encoded PowerShell is used by both attackers (to hide malicious commands) and some legitimate tools (deployment scripts, configuration management). The hunter’s job: distinguish between them by examining the decoded command content and the context (who ran it, on which device, at what time).

T1003.006 — OS Credential Dumping: DCSync. Hunt for DCSync attacks that replicate directory data from domain controllers.

1
2
3
4
5
6
7
// Hunt: DCSync replication requests from non-DC machines
IdentityDirectoryEvents
| where TimeGenerated > ago(30d)
| where ActionType == "Directory Services Replication"
| where DestinationDeviceName !has "DC"  // Not from a domain controller
| project TimeGenerated, AccountName, DestinationDeviceName,
    TargetDeviceName, Application

DCSync requests from non-domain-controller machines are almost always malicious — an attacker using mimikatz or a similar tool to extract credentials from Active Directory.


M365-specific ATT&CK technique hunts

These hunts target techniques specifically relevant to M365 environments — the primary environment this course covers.

T1621 — Multi-Factor Authentication Request Generation (MFA fatigue). Hunt for users receiving many MFA challenges without successfully authenticating.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Hunt: MFA fatigue patterns
SigninLogs
| where TimeGenerated > ago(14d)
| where ResultType in ("50074", "500121", "50076")  // MFA-related result codes
| summarize
    MFAChallenges = count(),
    UniqueIPs = dcount(IPAddress),
    IPs = make_set(IPAddress, 5),
    TimeSpan = max(TimeGenerated) - min(TimeGenerated)
    by UserPrincipalName, bin(TimeGenerated, 1h)
| where MFAChallenges > 5
| order by MFAChallenges desc

More than 5 MFA challenges in a single hour for the same user is highly suspicious — either an attacker pushing MFA prompts hoping the user will approve, or a misconfigured application repeatedly triggering MFA. The hunter checks: did the user eventually approve (indicating successful fatigue attack)? Were the IPs external or internal?

T1098.002 — Account Manipulation: Additional Email Delegate Permissions. Hunt for mailbox delegation changes that grant an attacker access to read another user’s email.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Hunt: mailbox delegation changes
CloudAppEvents
| where TimeGenerated > ago(30d)
| where ActionType in ("Add-MailboxPermission", "Set-Mailbox")
| extend Details = parse_json(RawEventData)
| extend TargetMailbox = tostring(Details.ObjectId)
| extend GrantedTo = tostring(Details.Parameters
    | mv-expand Details.Parameters
    | where tostring(Details.Parameters.Name) == "User"
    | project tostring(Details.Parameters.Value))
| extend AccessRights = tostring(Details.Parameters
    | mv-expand Details.Parameters
    | where tostring(Details.Parameters.Name) == "AccessRights"
    | project tostring(Details.Parameters.Value))
| where AccessRights has_any ("FullAccess", "ReadPermission")
| project TimeGenerated, TargetMailbox, GrantedTo, AccessRights,
    Performer = tostring(Details.UserId)

An attacker who compromises one mailbox may grant themselves delegate access to other mailboxes — avoiding the need to compromise additional accounts.

T1537 — Transfer Data to Cloud Account. Hunt for data transfer to personal cloud storage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Hunt: data uploads to consumer cloud services
DeviceNetworkEvents
| where TimeGenerated > ago(14d)
| where RemoteUrl has_any ("dropbox.com", "drive.google.com",
    "onedrive.live.com", "icloud.com", "mega.nz", "wetransfer.com")
| summarize
    UploadCount = count(),
    UniqueDevices = dcount(DeviceName),
    Devices = make_set(DeviceName, 5),
    TotalBytes = sum(SentBytes)
    by AccountName, RemoteUrl
| where UploadCount > 10 or TotalBytes > 100000000  // >100MB
| order by TotalBytes desc

Cross-reference with the departing employees watchlist and UEBA anomaly data for the strongest insider threat signal.


Tactic-based hunting rotation

Rotate through MITRE ATT&CK tactics systematically to ensure comprehensive hunting coverage over time.

Month 1: Initial Access + Credential Access. Hunt for: phishing (T1566), valid accounts (T1078), brute force (T1110), MFA fatigue (T1621). These are the entry points — if you catch the attacker here, the attack is stopped before any damage occurs.

Month 2: Persistence + Privilege Escalation. Hunt for: account manipulation (T1098), scheduled tasks (T1053), OAuth application consent (T1550), role assignment (T1098.003). These are the footholds — if you find persistence, the attacker is already in but can still be evicted.

Month 3: Lateral Movement + Collection. Hunt for: remote services (T1021), token replay (T1550), email forwarding rules (T1114), data staging (T1074). These are the actions — if you find collection activity, data may already be exfiltrated.

Month 4: Execution + Defense Evasion. Hunt for: command interpreters (T1059), encoded commands, log clearing (T1070), indicator removal. These are the tools and techniques — finding them reveals the attacker’s operational methods.

Repeat the rotation quarterly. Each cycle, the hunting queries improve (refined from previous hunts), new techniques are added (from threat intelligence), and the coverage tracker shows steady improvement.


ATT&CK coverage gap analysis query

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Identify techniques with zero detection AND zero hunting coverage
// Requires: analytics rule MITRE mappings + hunt log tracking
// This is a conceptual query  adapt for your tracking method
let DetectedTechniques = SecurityIncident
| where TimeGenerated > ago(90d)
| extend Techniques = parse_json(tostring(AdditionalData)).techniques
| mv-expand Techniques
| distinct tostring(Techniques);
let HuntedTechniques = HuntingBookmark
| where TimeGenerated > ago(90d)
| extend Tags = parse_json(Tags)
| mv-expand Tags
| where tostring(Tags) startswith "T"  // MITRE technique IDs in tags
| distinct tostring(Tags);
let AllPriorityTechniques = datatable(Technique:string) [
    "T1566", "T1078", "T1110", "T1621",  // Initial Access
    "T1098", "T1136", "T1053", "T1550",  // Persistence
    "T1021", "T1114", "T1059", "T1003"   // Lateral/Execution
];
AllPriorityTechniques
| join kind=leftanti DetectedTechniques on $left.Technique == $right.Column1
| join kind=leftanti HuntedTechniques on $left.Technique == $right.Column1
| project Technique, Status = "⚠️ NO COVERAGE — neither detected nor hunted"

| where ForwardTo !endswith “@northgateeng.com” | extend Creator = tostring(RuleData.UserId) | project TimeGenerated, Creator, ForwardTo, IPAddress = tostring(RuleData.ClientIP)


---

### Building the ATT&CK hunting coverage tracker

Maintain a tracker that records which techniques have been hunted, when, and with what result.

| Technique | Last Hunted | Result | Analytics Rule? | Next Hunt |
|---|---|---|---|---|
| T1078.004 Cloud Accounts | 2026-03-22 | No findings | ✓ Yes | 2026-06-22 |
| T1098.003 Additional Roles | 2026-03-15 | Benign | ✓ Yes | 2026-06-15 |
| T1136.003 Create Cloud Acct | Not hunted | — | ✗ No | Priority |
| T1114.003 Email Forwarding | 2026-03-10 | Threat confirmed | ✓ Yes (new) | 2026-04-10 |

Techniques marked "Not hunted" with no analytics rule are your highest-priority blind spots. Techniques with recent hunts and analytics rules have the strongest coverage. Rotate through the tracker quarterly — hunt each priority technique at least once per quarter.

<div class="try-it">
<h3>Try it yourself</h3>
<p>Navigate to Sentinel → MITRE ATT&CK. Identify 3 techniques that are relevant to your environment but have no analytics rule coverage (grey/uncovered). For one technique, write and execute a hunting query. Document the result in a hunt record. If you find suspicious activity, create a bookmark. If you find nothing, document the negative finding. This is one iteration of ATT&CK-driven hunting.</p>
<details>
<summary>What you should observe</summary>
<p>The MITRE ATT&CK blade highlights your coverage gaps visually. The hunting query either returns suspicious activity (investigate further) or nothing (document as "no findings for this period"). Either way, you have now actively hunted for that technique — the coverage tracker reflects this activity.</p>
</details>
</div>

---

### Knowledge check

<div class="knowledge-check">
<h3>Check your understanding</h3>

<div class="kc-question">
<p>1. The MITRE ATT&CK blade shows T1136.003 (Create Cloud Account) has no analytics rule and has never been hunted. What do you do?</p>
<div class="kc-options">
<div class="kc-option" onclick="checkAnswer(this, 'q109-1', 0)">Hunt for it. Write a KQL query that searches for cloud account creation events not originating from your HR provisioning system. If the hunt finds unauthorised account creation: promote to incident and create an analytics rule. If the hunt finds nothing: document the negative finding and create an analytics rule to detect this technique automatically going forward. Either way, the technique moves from "uncovered" to "covered."</div>
<div class="kc-option" onclick="checkAnswer(this, 'q109-1', 0)">Ignore it — not every technique needs coverage</div>
<div class="kc-option" onclick="checkAnswer(this, 'q109-1', 0)">Wait for Content Hub to release a rule</div>
<div class="kc-option" onclick="checkAnswer(this, 'q109-1', 0)">Add it to next year's roadmap</div>
</div>
<div class="kc-feedback correct" data-correct>Hunt first, then build a rule. The combination closes the gap immediately (hunting) and permanently (analytics rule).</div>
</div>

<div class="kc-score"></div>
</div>