In mid-October, an attacker used stolen ConnectWise credentials to request access tokens for Microsoft 365 tenants, including many that never had the app installed. Huntress, Todyl, and ConnectWise have published their findings. On October 21 Microsoft attributed the activity to Storm-0940, a China-state-aligned actor.
Obsidian Security analyzed the campaign using our own Entra ID and Okta data across impacted customers. Most of what we observed lines up with the public reporting, but our wide network intelligence and detection around SaaS supply chain adds additional findings and context.

The campaign is notable for three recurring elements:
All of the activity we attribute to the threat actor came from four IP addresses at Frantech Solutions (AS14956). None of them appear anywhere in our telemetry before the dates below, and during the campaign they carried nothing but the actor’s scripted tooling.
Tenant spray is what we’re calling the core technique in this campaign. The actor takes a credential for a multi-tenant app, a client secret or certificate it shouldn’t have, and uses it to request tokens from one tenant after another. It’s a password spray turned around: instead of one password tried against many accounts, it’s one app credential tried against many organizations, including ones that never installed the app.
This is different from Salesloft Drift and Klue, where attackers used OAuth tokens that had already been issued. With the app credential itself, the actor can mint new tokens in any tenant where the app is installed and enabled, and can test the credential against any tenant. These are app-only sign-ins, so there’s no user and no MFA prompt. Conditional Access doesn’t help either, as user policies don’t apply to service principals, and Microsoft’s Conditional Access for workload identities only covers single-tenant service principals registered in your own tenant. For a vendor’s app, the only native control a tenant has is disabling it. Tenant spray is also cheap to run at scale: Huntress identified almost 1,000 Entra tenants with sign-in activity from 144[.]172[.]64[.]128
A tenant spray doesn’t have to succeed to be useful. Entra’s token endpoint returns the error code to whoever made the request, so every attempt tells the actor something about the credential and the tenant. The same result lands in the tenant’s service principal sign-in logs. Two fields carry most of it: the servicePrincipalId, which is all zeros when the app has no service principal in the tenant, and the error code.
The error code that matters most is 7000229. It looks like a failure, but Entra checks the credential before it checks for a service principal. In our testing, a valid secret returned 7000229 and an invalid one returned 7000215. So when 7000229 comes from infrastructure that isn’t the vendor’s, someone else holds a working credential. That’s how we know the Atera secret is valid, even though none of the Atera attempts produced a token.
An important note is that SP-less tokens on their own aren’t a sign of compromise. Third-party apps receive about 3,000 a day across our customers on average. The ConnectWise ones stood out because of where they came from, the client that requested them, and the fact that the app was never installed.
Each one is covered in the Incident Breakdown below. From inside any single tenant, a spray looks like one token request or a few failed sign-ins. We come back to why that makes it hard to catch in the Detection and Hunting section.
We analyzed the campaign across affected customers using Entra ID and Okta telemetry. The activity is broken out below in the order it happened.
The first activity we tie to the actor started on October 10 at 17:00 UTC, when 144[.]172[.]64[.]99 began password spraying one organization’s Okta tenant. It tried 51 usernames, once each, first with curl/7.88.1 and then with a spoofed Chrome user agent. Okta ThreatInsight blocked the spray, and the actor came back about 12 hours later with a different spoofed user agent. Nothing succeeded.
On October 13, the same IP probed a second organization’s Okta tenant with failed logins, failed OAuth authorizations, and a recovery token attempt. Between 19:09 and 19:29 UTC, 144[.]172[.]64[.]128 hit the same tenant at the same time. Two days later, that IP is the one that requested the ConnectWise tokens.
Microsoft reported this same IP spraying Microsoft 365 accounts across eight organizations. We only observed it in Okta. Both of these targets federate authentication to Okta, so a failed Okta login never produces an Entra sign-in. If your Microsoft 365 tenant is federated to Okta, check the Okta System Log for the IPs in this post.
On October 15, 144[.]172[.]64[.]128 used the ConnectWise credential in three passes:
Python-urllib/3.12): several tenants. One had ConnectWise Cloud Backup installed and received a token with the app’s permissions. The others received SP-less Graph tokens.python-requests/2.31): additional tenants, all SP-less Graph tokens. One single outlier tenant was hit at 12:33.Passes 2 and 3 line up with what Huntress describes as the second targeted wave. Passes 1 and 3 worked through tenants in ascending tenant ID order, consistent with the sorted crawl other researchers described.
On October 14, 144[.]172[.]64[.]128 tried app credentials belonging to a legal-sector IT provider against a law firm. On October 16 and 18, 144[.]172[.]64[.]60 came back to the same firm and a second one, adding two more of the provider’s apps. Across 24 attempts and five apps, every one failed with 7000215. The actor knew the app IDs but didn’t have working secrets.
The client changed on nearly every run, from a bare Mozilla/5.0 to curl, Python, and a spoofed Chrome user agent, but the apps and the alternating order between the two firms stayed the same. The provider’s own automation calls one of the same apps daily from AWS and gets 7000229, which is what a valid secret looks like. Neither law firm saw any ConnectWise activity, which fits Huntress’s report of the actor spraying law firms on October 13 and 14. We don’t know how the actor got these app IDs, and we have no evidence the provider itself was compromised. However, this activity is a suggestion of how widespread the campaign was.
From 23:58 UTC on October 18 to 00:20 on October 19, 172[.]86[.]125[.]73 tried an Atera app credential against Microsoft Graph several organizations, in two bursts about 15 minutes apart, using curl/8.0. All ten attempts returned 7000229. The secret is valid, and the attempts only failed because Atera wasn’t installed in those tenants.
The target list ties this to the ConnectWise activity. The Atera sweep hit the same organizations, in the same order, as ConnectWise Pass 2, including a repeat of the first organization at the end. That order isn’t sorted, so it isn’t something a generic crawl would reproduce. The only Pass 2 tenant missing is the one hit separately at 12:33.
We didn’t see the Atera credential succeed in any of our monitored tenants.
Obsidian reached out to Atera on 20 October regarding the activity we observed. Atera was responsive to this communication - in response claiming that they were aware of the issue, had investigated it thoroughly, and that it was contained and necessary measures have been taken.
On 23 October, Atera provided the following statement: “Atera was aware of and was already investigating this activity and taking the necessary actions before being contacted by Obsidian, engaged external incident response specialists, and mitigated the attack. In addition, Atera services were not affected.”
On October 21, from 09:16 UTC until at least 22:22, 144[.]172[.]64[.]128 password sprayed Okta and Entra at an organization outside both the ConnectWise and Atera target lists. It used the same toolkit as the October 10 spray: curl/7.88.1, a bare Mozilla/5.0, and spoofed Chrome user agents. We saw no successful sign-ins. This IP was still in use a week after the ConnectWise apps were disabled, so it’s worth blocking, not just hunting for.
Beyond timing and targets, a few other signals separated the actor from legitimate traffic. They were:
Legitimate ConnectWise Cloud Backup sign-ins carry no user agent at all. One tenant logged roughly 378,000 sign ins within 119 days, every one blank. The actor’s sign-ins used Python-urllib/3.12 and python-requests/2.31, so any user agent on this app was a deviation.
In the Okta spraying and app credential attempts againt the law firms, The spoofed browser user agents had hand-written Chrome versions with only one or two parts, such as Chrome/126, Chrome/124.0 and Chrome/120.0. Real Chrome sends four parts, like Chrome/124.0.0.0 . It’s a useful pivot, but not unique to this actor, since other spray tools do the same thing.
Microsoft lists python-requests/2.34.2 and curl/7.88.1 as indicators. Both are common. python-requests/2.34.2 alone appeared in more than 129 million events across ~100 customers this month, so it won’t find much on its own. Microsoft’s other user agent indicator, azure-toolkit/1.0, is the opposite. We haven’t seen it anywhere across our customers in four months, so any hit is worth a look.
Legitimate ConnectWise sign-ins came from Microsoft’s internal IPv6 space. The actor’s came from a VPS provider. A few other fields that look odd on these sign-ins, such as the 009ae85a- session ID prefix, an empty key ID, and the CAE flag, also appear on the legitimate ConnectWise traffic, so they don’t separate the actor from the vendor.
The three 187.15.x.x IPs in Microsoft’s report are NordVPN exit nodes. Real users at a number of our customers have signed in from them since August. They’re useful for hunting October 15 and 16, but blocking them will hit legitimate users.
Frantech sells VPS and VPN hosting, so before tying everything on these four IPs to one actor, we wanted to rule out shared infrastructure.
We searched activity logs across all of our customers for any traffic from the four IPs, going back about four months for most customers. None of them appeared before the campaign, and the first event on each IP was already the actor’s tooling. We also checked a year of IP prevalence data and found no successful user activity from any of them.
To make sure that approach would catch a legitimate tenant, we ran the same checks against Frantech IPs that our customers’ users do connect from, including a corporate VPN. Those showed months of steady, multi-user activity and endpoint telemetry, and even single-day sessions showed up. During the campaign, every event from the actor’s four IPs came from scripted tooling, and two of them were used against the same tenant at the same time.
The IP ranges have been registered to Frantech since 2014, so we can’t say when these specific addresses were leased, and we can’t see how they were used outside our customer base. Within it, they carried the actor’s activity and nothing else.
Microsoft attributes the ConnectWise compromise to Storm-0940, and ties the October 10 to 14 password spraying from 144[.]172[.]64[.]99 to the same actor. Our data agrees, and adds independent support from a different vantage point. Two of the actor’s IPs were used against the same Okta tenant at the same time. The IP that requested the ConnectWise tokens ran the same spray toolkit against a new target on October 21. The IP used against the law firms tried the same app IDs the token IP had tried days earlier. And the Atera sweep reused the target list and order from one of the ConnectWise passes. Huntress’s early reporting treated the spraying as likely unrelated, but Microsoft’s attribution and our data both point the other way.
We assess with moderate to high confidence that the password spraying, the ConnectWise token activity, the legal-IT provider attempts and the Atera sweep are the work of the same operator. The tooling did vary by task, curl for spraying and Python for tokens, so this could be one operator running several scripts, or access passed between people working together. Either way, it very likely indicates the same campaign.
Across our customers, over a dozen tenants received tokens from the ConnectWise credential, and one of those tokens carried the app’s permissions. We saw no post-authentication actions in the audit logs we collect. That differs from Microsoft’s reporting, which describes Graph reconnaissance, credential changes and role changes in other environments.
Taken together, the activity points to an actor that is collecting vendor app credentials and testing them against a prepared list of targets, rather than relying on a single compromise.
Supply chain attacks are cross-tenant by nature, but each organization sees only its own slice. Inside a single tenant, this campaign showed up as a few isolated anomalies: SP-less tokens, 7000229 failures, and a vendor app signing in with an unusual user agent or from unusual infrastructure. On their own, these signals are noisy. In a 20-day backtest across our customers, a simple rule for each one fired hundreds of times a day, and the SP-less and 7000229 rule missed the one token that carried permissions. They also reveal little: most targeted tenants saw only a token request or a few failed sign-ins.
Obsidian builds a cross-org view of each supply chain integration, keyed on its app ID, which lets us combine these weak signals per app. For ConnectWise Cloud Backup, that view surfaced several deviations at once:
The one tenant with the app installed saw one sign-in from a new IP with a Python client, and nothing showed that the same credential from novel infrastructure was being used elsewhere. The tenants without the app saw an SP-less token or a few 7000229 failures, with no legitimate traffic to compare them against. Only a view across all of these tenants shows the sweep itself: over a dozen tenants in about seven hours. Combined on one app ID across organizations, these partial views add up to a strong, high-confidence signal. The cross-org baseline also gives each piece more weight on its own.
This is one of Obsidian's core advantages in supply chain detection. Signals that are noise in a single tenant become a more complete, higher-confidence detection when they're combined per integration across organizations. That detection also covers tenants where the app isn't installed.
For teams hunting in their own tenant, three signals matter most. The first is 7000229 failures from hosting or VPS providers, which mean someone outside the vendor holds a working credential. The second is SP-less tokens for vendor apps you never installed. The third is bursts of 7000215 across vendor apps from one IP. A user agent on an app whose legitimate traffic never has one is also worth a look. Give the most weight to signals that appear together for the same app. If your tenant is federated to Okta, also check the Okta System Log for the indicators below.
The ConnectWise incident is another example of how quickly a trusted vendor credential becomes an attack path. Across the customers we analyzed, the signals were there: an app that only ever authenticated from Microsoft’s network suddenly signed in from a VPS provider, with a scripted client, against tenants that never installed it. Several of the most important events were logged as failures, and some of the related password spraying only showed up in Okta.
It also wasn’t limited to one vendor. The same actor tested credentials from a legal-sector IT provider and from Atera against an overlapping set of targets, and kept going after the ConnectWise apps were disabled. That pattern is the bigger risk. Every multi-tenant app with a client secret is a potential entry point if that secret leaks.
Vendor credentials will keep leaking. The question for security teams is whether they’ll know which of their integrations could be used against them, and whether they’ll see it when one is.
Obsidian helps teams do exactly that. Customers use Obsidian’s posture management and supply chain security to inventory the third-party apps and integrations connected to Microsoft 365 and other SaaS platforms, flag the ones holding high-risk permissions, and see which tenants and users are tied to each. Obsidian’s threat detection baselines how each integration normally behaves and flags deviations in infrastructure, clients and activity, across identity providers like Entra ID and Okta. When a vendor discloses a compromise, that makes it possible to answer the first questions quickly: are we exposed, did anything happen, and what do we disable?
For vendors, the work starts with the credentials themselves: rotate client secrets, remove ones that are no longer used, prefer certificates or managed identities where possible, and keep a clear record of where your app legitimately authenticates from so customers can tell when it doesn’t.
For customers, the priority is knowing which vendor apps can reach your Microsoft 365 tenant and what they can do once they’re in: