Why this matters now
Attackers no longer need to phish your users to reach your Microsoft 365 tenant — they just need to be your backup vendor. In mid-October 2026, attackers got hold of the application secrets behind ConnectWise Cloud Backup (the former SkyKick Cloud Backup) and signed directly into at least 162 customer tenants as the backup application itself. No phishing email, no malware, no MFA prompt: multi-factor authentication is meaningless when no human logs in. Scripted sign-ins from a single IP, 144.172.64[.]128, carried the curl/7.88.1 user agent — and Huntress observed nearly 50 tenants where the hijacked service principals assigned themselves directory roles, mostly Exchange Administrator. In one tenant the attacker escalated from sign-in to full persistence in roughly five minutes.
This playbook is about hunting that pattern in your tenant: third-party service principals that start behaving like attackers — signing in from places they never have, granting themselves directory roles, and adding fresh credentials to themselves and other apps.
How the takeover worked
Cloud backup tools for Microsoft 365 operate through permissions the customer once granted in Microsoft Entra ID. Each integration is represented by a service principal — an application identity holding whatever mailbox, file, and Teams permissions were approved at install time. Steal the application's client secret, and you can authenticate to Microsoft 365 as that application, from anywhere.
The ConnectWise intrusion ran like this, according to Huntress and Microsoft telemetry:
- Credential theft, then direct authentication. The attackers held the application secrets behind ConnectWise Cloud Backup and signed in directly as the backup app — Todyl researchers confirmed this pattern across nine tenants. MITRE maps it to T1078.004 (Valid Accounts: Cloud Accounts) and T1550.001 (Use Alternate Authentication Material: Application Access Token).
- Scripted reconnaissance. Automated sign-ins with the curl/7.88.1 user agent from a single IP swept tenant after tenant starting October 13–14.
- Role self-assignment. Huntress saw hijacked service principals grant themselves Exchange Administrator on nearly 50 tenants; in one tenant the Cloud Management – MS Teams app granted itself Global Administrator. This is classic T1098 (Account Manipulation).
- Credential planting. In that same tenant, the attacker added credentials to an MSP management tool and to Huntress's own application — planting persistence through trusted app identities. Microsoft separately observed persistence via a backdoor in at least one tenant. ConnectWise shut down Cloud Backup access at 05:01 UTC on October 17, but disabling the service does not remove credentials an attacker already planted in your tenant.
The critical lesson: a service principal's client secret is a skeleton key. Defender's built-in anomalous-service-principal alerts and Entra's PIM alerts help, but a custom hunt gives you control over thresholds, vendor allowlists, and history depth — exactly what you need when the compromise window is days, not hours.
The KQL hunts
These run in Microsoft Defender XDR advanced hunting. AADSignInEventsBeta carries Entra sign-in activity for service principals, and AuditLogs (the Defender XDR copy of Entra audit events) carries role assignments and credential operations. Keep a vendor inventory: pull your tenant's real service-principal display names and app IDs from the Entra portal and bake them into the watchlist.
Hunt 1 — Vendor service principal signing in from a new scripted fingerprint
Sign-ins to a backup-vendor app from an automation user agent, a first-seen IP, or a country the app never signs in from:
// -- Vendor service principals with scripted or anomalous sign-in fingerprints
let VendorApps = dynamic(["SkyKick", "Cloud Backup", "ConnectWise"]);
let Lookback = 30d;
AADSignInEventsBeta
| where Timestamp > ago(Lookback)
| where AppDisplayName has_any (VendorApps)
or ServicePrincipalName has_any (VendorApps)
| where ErrorCode == 0
| summarize
Signins = count(),
Countries = make_set(Country),
IPs = make_set(IPAddress),
UserAgents = make_set(UserAgent),
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp)
by AppDisplayName, ServicePrincipalId, bin(Timestamp, 1d)
| where UserAgent has "curl" or UserAgent has "python" or UserAgent has "PowerShell"
or array_length(IPs) == 1
| order by Timestamp desc
Triage notes: a legitimate backup app usually signs in from a stable vendor IP range with a consistent user agent. A single new IP plus curl is the exact ConnectWise pattern — treat it as high-confidence.
Hunt 2 — Service principal granting itself a directory role
The role self-assignment step. Any service principal assigning directory roles to itself outside a change window deserves an immediate call:
// -- Service principals assigning directory roles to themselves
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName == "Add member to role"
| where parse_json(InitiatedBy).user.userPrincipalName has "@"
or parse_json(InitiatedBy).app.displayName != ""
| extend InitiatedByApp = tostring(parse_json(InitiatedBy).app.displayName)
| extend TargetApp = tostring(parse_json(TargetResources)[0].displayName)
| where isnotempty(InitiatedByApp)
| where RoleDisplayName has_any ("Exchange Administrator", "Global Administrator",
"Privileged Authentication Administrator", "User Administrator")
| project TimeGenerated, RoleDisplayName, InitiatedByApp, TargetApp,
IpAddress, tostring(parse_json(InitiatedBy).app.servicePrincipalId)
| order by TimeGenerated desc
Extend the hunt to any role when you want full coverage: remove the has_any line and baseline the result against the past 90 days — vendor apps almost never self-assign directory roles, so the baseline should be near empty.
Hunt 3 — Credential adds to service principals and applications
The persistence step. Attackers added credentials to hijacked apps; the operation is "Add service principal credentials" or "Update application – certificates and secrets management" in Entra audit logs:
// -- New credentials or secrets added to service principals / applications
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName has_any ("Add service principal credentials",
"Update application – certificates and secrets management",
"Add application owner")
| extend InitiatedByApp = tostring(parse_json(InitiatedBy).app.displayName)
| extend InitiatedByUser = tostring(parse_json(InitiatedBy).user.userPrincipalName)
| extend Target = tostring(parse_json(TargetResources)[0].displayName)
| project TimeGenerated, OperationName, InitiatedByApp, InitiatedByUser, Target, IpAddress
| order by TimeGenerated desc
Join with Hunt 1: when the same tenant shows a scripted vendor-app sign-in and a credential add to that app within the same window, escalate immediately — that is the five-minute persistence pattern from the ConnectWise incident.
Splunk equivalents
Assuming Entra audit and sign-in logs land in Splunk (via the Azure Event Hub / Splunk Add-on for Microsoft Cloud Services), the same logic maps to two searches.
SPL 1 — Scripted sign-ins on vendor service principals
index=azure sourcetype="azure:aad:signin" earliest=-30d
| where like(appDisplayName, "%SkyKick%") OR like(appDisplayName, "%Cloud Backup%") OR like(appDisplayName, "%ConnectWise%")
| where status.errorCode=0
| stats dc(ipAddress) as ip_count values(ipAddress) as IPs values(userAgent) as UAs values(country) as countries count by appDisplayName, servicePrincipalId, _time
| where match(UAs, "(?i)curl|python|powershell") OR ip_count=1
| sort - _time
SPL 2 — Service-principal role self-grants and credential adds
index=azure sourcetype="azure:aad:audit" earliest=-30d
(operationName="Add member to role" OR operationName="Add service principal credentials")
| eval init_app = 'initiatedBy.app.displayName', target = 'targetResources{}.displayName'
| where isnotnull(init_app)
| stats earliest(_time) as first_seen latest(_time) as last_seen count values(target) as targets by operationName, init_app
| sort - count
Cross-reference the two result sets on init_app / appDisplayName: a vendor app appearing in both is your incident.
Triage: what to do when a hunt hits
- Freeze the secret first. Rotate or revoke the client secret of the suspected service principal before you do anything else — a hijacked secret lets the attacker re-authenticate at will.
- Confirm or clear the sign-in. Check the vendor's published egress IPs and the app's historical sign-in record in the Entra portal. Vendor IP plus vendor user agent: benign. Curl from a first-seen IP: assume compromise.
- Audit everything the app touched. Pull all directory role assignments and credential adds made by that service principal in the last 60 days. The ConnectWise attacker planted credentials on a different app (an MSP tool and even Huntress's own app) — so the blast radius is not limited to the vendor's own identity.
- Look for data access, not just identity moves. In nine of the tenants Todyl examined, the activity was sign-ins only. Verify by reviewing unified audit log (UAL) searches for the app's access to mailboxes, SharePoint, and Teams chats.
- Check the other tenants. If you manage multiple tenants (MSP) or have sister environments, repeat all three hunts everywhere — the ConnectWise attack was a spray across the customer base.
Remediation and hardening
- Inventory every third-party service principal and its permissions; remove integrations nobody owns. A backup app that still holds Global Administrator from a 2021 install is a loaded gun.
- Require least privilege on vendor apps. A backup tool that needs mailbox read access does not need directory roles.
- Alert on the Entra audit operations directly. Standing alerts for "Add member to role" initiated by a service principal and "Add service principal credentials" close the gap that a hunt only covers on cadence.
- Monitor certificate/secret lifecycle. Alert when a new secret is added to a service principal outside its normal renewal cadence — attackers' backdoor credentials look exactly like that.
- Require phishing-resistant MFA for all human admins (it was irrelevant in this attack, but do not let the vendor compromise distract from the human attack surface).
- After any vendor breach, assume the keys are burned — even when the vendor says its archives are safe. Rotate, re-consent, and re-verify every integration before trusting it again.
