# ConnectWise Cloud Backup (formerly SkyKick) application-identity compromise, Storm-0940: detection pack

Attackers gained unauthorised access to application identities in the ConnectWise Cloud Backup environment. These are multitenant Entra ID apps that MSPs and customers had already consented into their Microsoft 365 tenants. On **21 Oct 2026** Microsoft attributed the activity to **Storm-0940**, a China-state-aligned actor, which used the agentic AI framework ARTEX. Using stolen app credentials, the actor authenticated into customer tenants **without any user sign-in**. In some tenants it went on to do Microsoft Graph reconnaissance, change application credentials and try to escalate privileges. **Credentials the attacker added inside your tenant survive the vendor's containment.**

Sources (the only ones used):
- Microsoft Threat Intelligence, 21 Oct 2026: https://www.microsoft.com/en-us/security/blog/2026/10/21/storm-0940-uses-artex-agentic-ai-connectwise-supply-chain-compromise/
- ConnectWise Trust Center advisory, first published 16 Oct 2026 17:47 UTC and last updated 22 Oct 2026 00:45 UTC: https://www.connectwise.com/company/trust/advisories
- ConnectWise *Cloud Backup technical investigation and remediation guide* (dated 18 Oct 2026): https://www.connectwise.com/contentassets/10777cd3364d440c86ed5270a94a5f24/connectwise-cloud-backup-technical-investigation-and-remediation-guide-1.pdf

All times are UTC (BST = UTC+1).

## Timeline (verified)
| When (UTC) | What | Source |
|---|---|---|
| Sept 2026 ("as early as") | Storm-0940 starts using ARTEX | Microsoft |
| 10–14 Oct | Password spray from **144.172.64.99**: 327 accounts across 8 organisations, 151,000+ failures, peak 2,900/min on 11 Oct | Microsoft |
| 14 Oct | ConnectWise discovers the problem | ConnectWise FAQ |
| 15 Oct | First public reports of the compromise | Microsoft |
| 16 Oct 17:47 | First ConnectWise advisory | ConnectWise |
| 17 Oct 05:00 | Mitigations begin. ConnectWise has seen no successful threat activity in its monitored environments since then | ConnectWise |
| 17 Oct 07:00 | ConnectWise **disables the 7 app identities and revokes their credentials** | ConnectWise guide and advisory |
| 19 Oct | Accounts and credentials in the affected Cloud Backup environment fully reset | ConnectWise |
| 21 Oct | Microsoft attributes the activity to Storm-0940. ARTEX attempted sign-ins to **more than 2,250 tenants** | Microsoft |

## IOCs
**The 7 application (client) IDs that ConnectWise disabled.** Match on the App ID, not the display name; older installs may show SkyKick names.

| Application | App ID |
|---|---|
| ConnectWise Cloud Backup | `506e08f9-6e9a-4da4-942b-a099a03644ae` |
| ConnectWise Cloud Management | `2399d300-6fca-4e6a-a686-399fefe75b83` |
| Cloud Management - MS Teams | `24205314-2b89-4e07-a2a4-579e898a5640` |
| Cloud Management - Exchange Online | `b78f6a77-d4e5-46fb-87e5-d1648bd02519` |
| Cloud Management - MS Graph | `0de14c77-b9b0-4137-9360-34878a929827` |
| Cloud Management - Partner Center | `412dcf96-176d-4b3a-a1b0-4981ad66971e` |
| Cloud Management - SharePoint Online | `b2b55f5b-7758-4719-8d8f-88ae172e6bdb` |

Microsoft's hunting queries cover only the first three of these (Cloud Backup, MS Teams and MS Graph). This pack covers all 7. ConnectWise FAQ 15 also mentions `c921256f-04a7-44e8-9b2a-69c33929ba19`, another ConnectWise app: "no direct threat was observed", but ConnectWise recommends disabling it until services are restored. The checker reports this app but does not score it.

**IPs:**
- `144.172.64.128`, `144.172.64.99` and `144.172.64.60` were used for sign-ins and Graph activity (Microsoft and ConnectWise). `.99` also ran the password spray.
- `187.15.88.186`, `187.15.90.217` and `187.15.89.186` are **VPN** IPs that accessed Graph through the compromised apps. Microsoft says they are transient and only relevant **on 15–16 Oct** together with the ConnectWise apps.

**User agents (Microsoft):** `Python-urllib/3.12`, `python-requests/2.31.0`, `python-requests/2.34.2`, `curl/7.88.1` and `azure-toolkit/1.0`. ConnectWise lists `Python-urllib/3.12`, `python-requests` and `curl`, and notes that legitimate admins use these too. ConnectWise associates `kiota-dotnet` and Azure private IPv6 with **expected vendor** activity.

**Defender alert titles (Microsoft):** *Unusual Microsoft Graph activity associated with Compromised ConnectWise Application*, *Application self-assigned Global Administrator role*, *Graph API activity from a Microsoft threat intelligence IP address*, *Suspicious sign-in from verified nation-state threat actor infrastructure* and *Malicious sign-in from known threat actor IP address*. Defender for Cloud Apps adds 7 more; all are listed in Q10 of the KQL.

## Files
| File | Purpose |
|---|---|
| `Check-ConnectWiseCloudBackup.ps1` | Read-only tenant check that works across multiple tenants. Writes a JSON report. |
| `Export-EvidenceLogs.ps1` | Read-only export of sign-in and audit logs (and Log Analytics tables) since 1 Oct to CSV and JSON |
| `connectwise-cloud-backup-hunting.kql` | Sentinel and Defender XDR hunting queries Q1–Q11, with an IOC datatable at the top |
| `sigma/*.yml` | 6 Sigma rules (azure `signinlogs` and `auditlogs`) |
| `tests/` | Pester tests using mocked Graph cmdlets, plus a parse and PSScriptAnalyzer runner (`pwsh tests/run-tests.ps1`) |

## 1. Run the tenant check (proactive: do this in every tenant, including dormant ones)
```powershell
Install-Module Microsoft.Graph -Scope CurrentUser          # plus Microsoft.Graph.Beta.Reports for SP sign-ins
pwsh ./Check-ConnectWiseCloudBackup.ps1 -TenantId contoso.onmicrosoft.com,fabrikam.onmicrosoft.com
```
The check uses only these scopes: `Application.Read.All`, `Directory.Read.All`, `AuditLog.Read.All` and `RoleManagement.Read.Directory`. It calls only `Connect-`, `Disconnect-` and `Get-Mg*` cmdlets and changes nothing. MSPs get one sign-in per tenant, with GDAP or a customer admin as appropriate. Reviewing your own tenant does **not** review your customers' tenants (ConnectWise).

For each app ID, the check reports the service principal and its Object ID, enabled state, credentials, owners, app role assignments, OAuth2 grants and directory roles (Global Administrator is flagged). It also reports any app or SP credential added since 15 Oct, from the audit log, and SP sign-ins from the IOC IPs, which needs the beta API and Entra P1/P2.

| Verdict | Exit | Meaning and action |
|---|---|---|
| `NOT_PRESENT` | 0 | None of the 7 apps is in the tenant and nothing suspicious was found. Still review the logs if retention allows. |
| `PRESENT_DISABLED` | 1 | Present and disabled locally. Keep it disabled and review the logs. |
| `PRESENT_ENABLED` | 2 | **Disable now** (see below), then preserve evidence. |
| `SUSPICIOUS_CHANGES` | 3 | Found one or more of: a local credential on a ConnectWise SP, a directory role, an owner, any new app/SP credential since 15 Oct, or an SP sign-in from an IOC IP. **Start incident response.** |
| `ERROR` | 4 | The check could not connect or query, so the result is unknown. |

ConnectWise says the vendor-managed credentials for these apps live in **ConnectWise's tenant and are not visible to customers**. A credential that *does* appear on one of these service principals in your tenant was therefore added locally, and the check flags it. Validity (start) dates do not prove when a credential was added, so correlate with the audit log.

## 2. Disable, don't delete (containment)
ConnectWise disabled the apps at the vendor side. That **does not change the enterprise-application setting in your tenant**, and previously issued tokens may stay valid until they expire. Disable the app locally as well:
- Portal: go to Entra admin centre → Enterprise applications → All applications, search by **Application ID**, open Properties, set *Enabled for users to sign-in?* to **No** and save. Then record the App ID, Object ID, the admin who made the change and the UTC time. This needs the Application Administrator or Cloud Application Administrator role, or ownership of the app. Global Administrator is **not** needed.
- PowerShell (run this yourself; the pack never does it): `Connect-MgGraph -Scopes Application.ReadWrite.All; Update-MgServicePrincipal -ServicePrincipalId <ObjectId> -AccountEnabled:$false`
- **Do not delete** the enterprise app as a first response. Deleting it destroys configuration, grants and credentials that are evidence (Microsoft and ConnectWise).
- Keep it disabled until ConnectWise publishes formal restoration instructions.

## 3. Preserve evidence (reactive), then eradicate
1. `pwsh ./Export-EvidenceLogs.ps1 -TenantId <t> [-WorkspaceId <LA workspace>]` exports directory audits and the 4 sign-in categories since 1 Oct to CSV and JSON. If Graph activity logs are already flowing to Log Analytics, add `-WorkspaceId` to include them. Hash the output with `Get-FileHash` and store it read-only. Don't let exports delay containment.
2. Export **Purview** unified audit data. Graph does not cover it, so use ExchangeOnlineManagement: `Connect-ExchangeOnline; Search-UnifiedAuditLog -StartDate 2026-10-01 -EndDate (Get-Date) -SessionCommand ReturnLargeSet -ResultSize 5000`. Page through the results, and add `-Operations New-InboxRule,Set-InboxRule,Set-Mailbox,Add-MailboxPermission,Add-RecipientPermission,New-TransportRule,Set-TransportRule` for the Exchange checks.
3. Run the KQL queries. Q5 and Q5b cover credential, role, consent, owner, CA and auth-method changes. Q6 is Graph activity, and Q8 and Q9 cover mailbox rules and bulk access.
4. **Added credentials survive disabling the app.** Record each credential's keyId, thumbprint and validity first. Then have an authorised responder remove the unauthorised credentials (`removePassword` / `removeKey`; federated credentials need separate removal), roles, owners, app role assignments and grants, and **rotate secrets** for any identity the attacker could have touched. A replacement legitimate credential does not remove an attacker-added one (ConnectWise). Look beyond the 7 apps, because Storm-0940 persists by adding credentials to *other* existing OAuth apps (Microsoft).
5. If a user account succeeded in the password spray (Q4), reset its password, revoke its sessions and review its MFA methods.

## 4. When to contact ConnectWise
Send relevant findings to **SecurityPartnerSupport@ConnectWise.com** if you find unauthorised credentials, privileged assignments, suspicious successful activity or evidence of data access. Escalate to your IR provider straight away as well. Include the tenant and app IDs, UTC timestamps, event and correlation IDs and a description. **Never send passwords, keys, tokens or secrets.** Use Partner Care for service (backup) questions. If you resell through a distributor or marketplace, send the ConnectWise guide to every customer tenant admin.

## 5. Limitations
- **Retention:** Entra keeps sign-in and audit logs for 7 days on Free and 30 days on P1/P2 (ConnectWise). Activity from 10–17 Oct may already have aged out of Free tenants. A licence upgrade does not recover history. Sentinel and SIEM copies may go back further.
- **Licensing:** sign-in logs through Graph, including the SP sign-ins this pack uses, need **Entra ID P1/P2**. `MailItemsAccessed` needs Purview Audit (Premium) / E5-class licensing. The Defender XDR tables need the relevant Defender products.
- **Graph activity logs** (`MicrosoftGraphActivityLogs`) exist only if a diagnostic setting was already sending them to Log Analytics, storage or an event hub, so you cannot get logs from before it was enabled. Turning logging on now does not recover past events (ConnectWise).
- Workload-identity Conditional Access does **not** cover third-party multitenant apps like these, so disabling them is the control (ConnectWise).
- A clean verdict, or a match on a scripting user agent or an IP alone, proves neither compromise nor its absence. Normal backup reads before 17 Oct look like bulk access.
- The checker's IOC-IP sign-in check uses the beta Graph API. If `Microsoft.Graph.Beta.Reports` is missing, the output says so (`[UNKNOWN]`) rather than failing silently.
- The KQL was reviewed by hand but has not been run against a live workspace. Column names can vary between workspaces (OfficeActivity uses `column_ifexists`).

## Tests (offline, no tenant)
`pwsh tests/run-tests.ps1` runs the parse check on both scripts, PSScriptAnalyzer and 20 Pester tests. The tests cover pure verdict logic, Global Admin and credential detection, and end-to-end runs against stubbed `Connect-MgGraph` and `Get-Mg*` cmdlets that check exit codes 0, 1, 2 and 3. They also assert that the script contains no Graph write cmdlets. The Sigma rules pass `sigma check` with 0 errors, 0 condition errors and 0 issues.
