Azure tenant compromise investigation using KQL (Kusto Query Language) in Microsoft Sentinel. The attacker executed a password spray, compromised an admin account, registered malicious applications for persistence, escalated a second account to Global Administrator, then exfiltrated sensitive data from Azure Blob Storage.
Key lesson: Azure log tables in Sentinel use _CL suffix for custom logs — SigninLogs becomes InteractiveSignIns_CL, AuditLogs becomes AuditLogs_CL. Also allow 5-10 minutes for log ingestion after lab start, and set time range to Last 7 days.
Microsoft Sentinel → Logs → KQL mode. Three tables available:
search *
| distinct $table
| order by $table asc
| Table | Contents |
|---|---|
InteractiveSignIns_CL |
Azure AD sign-in events |
AuditLogs_CL |
Admin actions, app registrations, role changes |
StorageBlobLogs_CL |
Blob storage access and downloads |
A password spray differs from brute force — one IP attempts multiple usernames with common passwords rather than hammering a single account. Look for high failure counts across multiple target users from a single IP.
InteractiveSignIns_CL
| where Status == "Failure"
| summarize FailedAttempts = count(), TargetUsers = dcount(Username) by IPAddress
| order by FailedAttempts desc

52.59.240.166 — 31 failed attempts across 2 users. The multi-user targeting confirms spray behaviour vs targeted brute force.
MITRE: T1110.003 — Brute Force: Password Spraying
Query successful logins from the spray IP to find the first account that was successfully compromised:
InteractiveSignIns_CL
| where IPAddress == "52.59.240.166"
| where Status == "Success"
| project EventTime, Username, IPAddress, Status
| order by EventTime asc

Compromised account: mharmon@compliantsecure.store
MITRE: T1078 — Valid Accounts
After compromising mharmon, the attacker switched to a different IP for post-exploitation — a common OpSec practice to separate noisy spray activity from quiet post-exploitation. Track all successful logins for the compromised account:
InteractiveSignIns_CL
| where Username == "mharmon@compliantsecure.store"
| where Status == "Success"
| project EventTime, Username, IPAddress, Status, Location
| order by EventTime asc

Timeline:
18.192.125.237 — earlier access (pre-compromise, likely recon)52.59.240.166 — spray IP, successful 2025-11-14 09:5652.221.180.165 — new IP at 11:34 on 2025-11-14, post-exploitation pivotMITRE: T1550.001 — Use Alternate Authentication Material
Query everything mharmon did after the compromise date to see the full attack chain:
AuditLogs_CL
| where EventTime >= datetime(2025-11-14)
| where ActorUserPrincipalName == "mharmon@compliantsecure.store"
| project EventTime, Activity, ActorUserPrincipalName, Target1DisplayName, IPAddress
| order by EventTime asc

Full chain visible in chronological order:
| Time | Action | Target |
|---|---|---|
| 11:35:14 | Create application | OfficeRead |
| 11:35:28 | Update application | OfficeRead |
| 11:35:38 | Add service principal | OfficeRead |
| 11:35:38 | Add delegated permission grant | Microsoft Graph |
| 11:35:38 | Consent to application | OfficeRead |
| 11:35:52 | Create application | VaultApp |
| 11:36:02 | Update application | VaultApp |
| 11:36:43 | Add service principal | VaultApp |
| 11:38:03 | Add member to role | lwilliams |
Two malicious applications registered within minutes of each other using the Singapore IP — a persistence technique that survives password resets since the apps authenticate independently via service principals.
First app: OfficeRead
Second app: VaultApp
MITRE: T1136 — Create Account (Service Principal) MITRE: T1098.001 — Account Manipulation: Additional Cloud Credentials
Query role assignment events to identify the backdoor account:
AuditLogs_CL
| where Activity == "Add member to role"
| where EventTime >= datetime(2025-11-14)
| project EventTime, ActorUserPrincipalName, Target1DisplayName, TargetUserPrincipalName, NewRule, IPAddress
| order by EventTime asc

At 11:38, mharmon assigned lwilliams@compliantsecure.store the Global Administrator role from 52.221.180.165. This creates a redundant backdoor — even if mharmon is remediated, lwilliams retains full tenant control.
MITRE: T1484.001 — Domain Policy Modification: Group Policy Modification
Query blob storage for GetBlob operations — downloads from storage accounts — filtering to the attacker’s IP:
StorageBlobLogs_CL
| where TimeGenerated >= datetime(2025-11-14)
| where OperationName == "GetBlob"
| project TimeGenerated, AccountName, Uri, CallerIpAddress, OperationName
| order by TimeGenerated asc

52.221.180.165 (Singapore IP) issued a GetBlob request to:
https://mainstoragestore01[.]blob[.]core[.]windows[.]net/conf-images/Confidintal[.]png
Note the deliberate typo — “Confidintal” not “Confidential” — likely an attacker-created decoy or staging file.
MITRE: T1530 — Data from Cloud Storage MITRE: T1567 — Exfiltration Over Web Service
| Type | Value |
|---|---|
| Password Spray IP | 52[.]59[.]240[.]166 |
| Post-Exploitation IP | 52[.]221[.]180[.]165 |
| Compromised Account | mharmon[@]compliantsecure[.]store |
| Backdoor Account | lwilliams[@]compliantsecure[.]store |
| Malicious App 1 | OfficeRead |
| Malicious App 2 | VaultApp |
| Storage Account | mainstoragestore01 |
| Exfiltrated File | Confidintal[.]png |
| Storage URI | hxxps[://]mainstoragestore01[.]blob[.]core[.]windows[.]net/conf-images/Confidintal[.]png |
| Technique | ID | Notes |
|---|---|---|
| Password Spraying | T1110.003 | 31 attempts across 2 accounts from 52.59.240.166 |
| Valid Accounts | T1078 | mharmon credentials compromised |
| Infrastructure Pivot | T1550.001 | Switched to Singapore IP post-compromise |
| Create Service Principal | T1136 | OfficeRead + VaultApp registered |
| Additional Cloud Credentials | T1098.001 | Service principals for persistent API access |
| Global Admin Assignment | T1484.001 | lwilliams escalated to Global Administrator |
| Data from Cloud Storage | T1530 | mainstoragestore01 blob accessed |
| Exfiltration Over Web | T1567 | Confidintal.png downloaded via HTTPS |
where = where, summarize = stats, project = fields, order by = sort. The mental model transfers well once you know the table names_CL suffix — Custom log tables in Sentinel always append _CL. Always run search * | distinct $table first to discover actual table names before writing queriesdcount(Username) — spray hits multiple users, brute force hits one. High FailedAttempts with TargetUsers > 1 from a single IP is the signatureI successfully completed Rogue Azure Blue Team Lab at @CyberDefenders! https://cyberdefenders.org/blueteam-ctf-challenges/achievements/inksec/rogue-azure/
#CyberDefenders #CyberSecurity #BlueYard #BlueTeam #InfoSec #SOC #SOCAnalyst #DFIR #CCD #CyberDefender