← All hunts critical TLP:CLEAR

ConnectWise M365 Application Abuse

An adversary has exploited compromised ConnectWise (SkyKick) vendor credentials to access M365 tenants, escalate directory privileges, and persist by adding secrets to secondary applications.

Based on research by Huntress 2026-10-18 11 steps · 4 queries T1098.001 T1110.003 T1190

Brief

Why this hunt matters

A recent investigation by Huntress (https://www.huntress.com/blog/vendor-credential-enables-unauthorized-access) details a supply-chain compromise where attackers abused vendor-held credentials for ConnectWise (SkyKick) applications. This is not a simple case of unauthorized access; the adversary uses these trusted identities to pivot into a Global Administrator role and plant long-term persistence across other applications in the tenant. This hunt provides a systematic way to find where these apps are active and whether they performed unauthorized administrative actions.

How the hunt flows

The hunt starts by scoping the environment. The first query identifies every tenant where the specific ConnectWise Exchange, Teams, or SharePoint applications are active. This limits the search space to at-risk tenants and provides a baseline of normal application activity.

Next, the hunt runs two parallel queries to find initial access. One query searches for password spraying from the reported actor IP address. The second query looks for successful sign-ins by the ConnectWise applications that originate from unusual IP addresses or use rare User-Agent strings. An analyst then triages these signals to confirm if a specific service principal has been hijacked.

The final phase moves to the cloud control plane. The hunt queries the Unified Audit Log for any administrative changes made by the identified applications. Specifically, it looks for the 'Add member to role' operation or the addition of client secrets to other service principals. This step is critical because it identifies the actual damage and persistence mechanisms left behind by the attacker.

Why this is a hunt, not a detection

A static detection for the known actor IP address is insufficient because attackers rotate their hosting infrastructure frequently. Furthermore, ConnectWise applications perform legitimate administrative tasks daily. A hunt is necessary to differentiate routine vendor maintenance from the specific, sequential pattern of privilege escalation and secondary credential addition described in the breach report.

Blind spots

This hunt has three primary limitations. First, it requires the Unified Audit Log (UAL) to be active; without these logs, the privilege escalation and persistence phases remain invisible. Second, Microsoft does not log most data-read operations through the Graph API. If an attacker only read mailbox or directory data without changing settings, this hunt will not see that activity. Finally, cloud-to-cloud sign-ins often lack a physical host association, meaning the hunt may identify a compromised tenant without being able to point to a specific infected workstation.

Steps

  1. Scope tenants with ConnectWise apps

    Query · scoping

    Identify which tenants have active ConnectWise applications to focus the search for initial access and persistence.

    reads hb_auth_signinsql
    SELECT DISTINCT home_tenant_id, device_hostname FROM hb_auth_signin WHERE instr(',' || '{{connectwise_app_ids}}' || ',', ',' || app_id || ',') > 0 AND time >= datetime('now', '-{{lookback_days}} days')

    What a hit looks like. A list of tenant IDs and any associated hostnames. Silence means no recent sign-in activity was recorded for these applications.

  2. Password spray from actor IP

    Query · baseline

    Identify password spraying attempts using the curl user agent from the reported actor IP, even if no hostnames are provided in scope.

    reads hb_auth_signinsql
    SELECT home_tenant_id, actor_user_name, src_endpoint_ip, user_agent, status_code, COUNT(*) AS attempts, MIN(time) AS first_seen FROM hb_auth_signin WHERE src_endpoint_ip = '{{actor_ip}}' AND LOWER(user_agent) LIKE '%curl%' AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY home_tenant_id, actor_user_name, src_endpoint_ip, user_agent, status_code HAVING attempts > 3

    What a hit looks like. Bursts of failed sign-ins from the actor IP against multiple accounts within a tenant. Silence means the IP was not used for spraying.

  3. Anomalous ConnectWise app authentication

    Query · detection candidate

    Detect successful authentication of ConnectWise apps from the known actor IP or from any IP/User-Agent combination that is rare or new for each tenant.

    reads hb_auth_signinsql
    SELECT home_tenant_id, app_id, src_endpoint_ip, user_agent, status_id, COUNT(*) AS auth_count, MIN(time) AS first_seen FROM hb_auth_signin WHERE instr(',' || '{{connectwise_app_ids}}' || ',', ',' || app_id || ',') > 0 AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY home_tenant_id, app_id, src_endpoint_ip, user_agent, status_id

    What a hit looks like. Successful sign-ins by the vendor apps from datacenter IPs or unusual user agents. Silence proves the apps behaved as expected for the lookback window.

  4. Triage initial access signals

    Agent triage

    Synthesize the spray and anomaly results to identify specific tenants and applications that were successfully breached.

  5. Follow-on role changes and persistence

    Query · triage

    The query identifies administrative changes the hijacked applications made, such as role self-assignments or credential additions.

    reads hb_cloud_api_activitysql
    SELECT cloud_account_uid, actor_app_uid, api_operation, resource_name, target_modified_property_names, target_modified_property_new_values, user_agent, time FROM hb_cloud_api_activity WHERE (instr(',' || '{{connectwise_app_ids}}' || ',', ',' || actor_app_uid || ',') > 0 OR LOWER(user_agent) LIKE '%python-urllib%') AND (api_operation IN ('Add member to role', 'Update application', 'Add service principal credential')) AND time >= datetime('now', '-{{lookback_days}} days')

    What a hit looks like. The query returns rows where ConnectWise applications assign themselves the Global Administrator role or add client secrets to other applications like the Huntress Security Platform. Silence means the apps made no such changes in the audit window.

  6. Triage complete attack chain

    Agent triage

    Connect the initial unauthorized sign-ins to the subsequent administrative activity to confirm a successful supply-chain breach.

  7. Decide on remediation path

    Decision

    Route the hunt based on whether the adversary established persistence or escalated privileges.

  8. Isolate identity and revoke credentials

    Response action

    Immediately remove unauthorized persistence and revoke elevated roles from the vendor application.

  9. Analyst manual impact review

    Analyst task

    Verify the extent of the access and investigate potential data exfiltration through Microsoft Graph.

  10. Hunt close out

    Analyst task

    Log the negative result and recommend proactive controls.

Coverage

Scenario coverage

StageCoveredHow, or why not
Password spraying against law firms
T1110.003
Yes password-spray-detection
Service principal sign-in via vendor credentials
T1190
Yes app-auth-anomaly
Self-assignment of directory roles
T1098
Yes audit-log-follow-on
New client secrets added to applications
T1098.001
Yes audit-log-follow-on

Blind spots

  • Needs Unified Audit Log (hb_cloud_api_activity) integration. Without audit log integration, a successful initial sign-in looks like a dead end; the persistence phase remains completely invisible. It would answer whether the hijacked application modified directory roles or added credentials.
  • Needs Microsoft Graph Activity Logs. Microsoft does not log most data-read operations; tokens issued for Graph can be used to scrape data without a visible audit trail. It would answer what directory or mailbox data was read by the attacker.
  • Needs Endpoint telemetry with cloud-provider correlation. Pure cloud-to-cloud sign-ins usually have a NULL device_hostname; the scoping step may produce an empty scope_hosts list, making physical-host-based filtering ineffective. It would answer which local host initiated the cloud authentication.

Parameters & data

Parameters

ParameterTypeDefaultWhat it is
actor_ipip144.172.64.128The hosting provider IP address associated with the unauthorized sign-ins and password sprays.
connectwise_app_idslist[string]b78f6a77-d4e5-46fb-87e5-d1648bd02519, 24205314-2b89-4e07-a2a4-579e898a5640, b2b55f5b-7758-4719-8d8f-88ae172e6bdbThe Entra application IDs for ConnectWise Exchange Online, MS Teams, and SharePoint Online apps.
lookback_daysnumber14Days of history to examine.
scope_hostslist[host]—Optional list of hostnames to narrow the search; leave empty to check the whole estate.

Telemetry

SourceCategoryTelemetry
Endpoint telemetry (hb_ surfaces)endpointendpoint
Identity / sign-in telemetryidentityidentity

Source

Download hunt.md Definition (JSON) An open hunt.md file; it runs anywhere that reads the format.
---
analysis: A static rule for the actor IP is insufficient as the actor rotates infrastructure.
  This hunt pivots from sign-in anomalies to the Unified Audit Log to reconstruct
  a chain of post-exploitation persistence on third-party applications, which requires
  behavioral triage across two distinct surfaces.
blind_spots:
- id: no-cloud-audit-logs
  question: whether the hijacked application modified directory roles or added credentials
  requires: Unified Audit Log (hb_cloud_api_activity) integration
  risk: Without audit log integration, a successful initial sign-in looks like a dead
    end; the persistence phase remains completely invisible.
  stage: privilege-escalation-role-assignment
- id: directory-read-blind-spot
  question: what directory or mailbox data was read by the attacker
  requires: Microsoft Graph Activity Logs
  risk: Microsoft does not log most data-read operations; tokens issued for Graph
    can be used to scrape data without a visible audit trail.
- id: cloud-host-anonymity
  question: which local host initiated the cloud authentication
  requires: Endpoint telemetry with cloud-provider correlation
  risk: Pure cloud-to-cloud sign-ins usually have a NULL device_hostname; the scoping
    step may produce an empty scope_hosts list, making physical-host-based filtering
    ineffective.
coverage:
- stage: credential-access-password-spray
  status: covered
  steps:
  - password-spray-detection
- stage: unauthorized-application-authentication
  status: covered
  steps:
  - app-auth-anomaly
- stage: privilege-escalation-role-assignment
  status: covered
  steps:
  - audit-log-follow-on
- stage: persistence-additional-cloud-credentials
  status: covered
  steps:
  - audit-log-follow-on
guardrails:
  claims: no_unsupported
  evidence: citation_required
  missing_data: not_benign
  telemetry: untrusted
hunt:
  applicability: campaign-specific
  handoff: keep-as-periodic-hunt
  justification: This hunt addresses a confirmed supply-chain credential compromise
    affecting thousands of tenants. The adversary's ability to establish long-term
    persistence in secondary, trusted applications makes a manual review of all tenants
    with these apps a critical security obligation.
  methodology: model-assisted
  trigger: intel-report
hypothesis: An adversary has exploited compromised ConnectWise (SkyKick) vendor credentials
  to access M365 tenants, escalate directory privileges, and persist by adding secrets
  to secondary applications.
labels:
- hunt
- attack.t1098.001
- attack.t1110.003
- attack.t1190
- credential access
- initial access
- persistence
- privilege escalation
name: ConnectWise M365 Application Abuse
parameters:
  actor_ip:
    default: 144.172.64.128
    description: The hosting provider IP address associated with the unauthorized
      sign-ins and password sprays.
    from:
      kind: article
      observed: '2026-10-15'
      ref: huntress-vendor-credential-abuse
    type: ip
  connectwise_app_ids:
    default:
    - b78f6a77-d4e5-46fb-87e5-d1648bd02519
    - 24205314-2b89-4e07-a2a4-579e898a5640
    - b2b55f5b-7758-4719-8d8f-88ae172e6bdb
    description: The Entra application IDs for ConnectWise Exchange Online, MS Teams,
      and SharePoint Online apps.
    from:
      kind: article
      observed: '2026-10-17'
      ref: huntress-vendor-credential-abuse
    type: list[string]
  lookback_days:
    default: '14'
    description: Days of history to examine.
    type: number
  scope_hosts:
    default: []
    description: Optional list of hostnames to narrow the search; leave empty to check
      the whole estate.
    type: list[host]
provenance:
  authors:
  - name: Huntbase hunt generation
    org: huntbase.io
  generated:
    by: huntbase-hunt-generation
    from: https://www.huntress.com/blog/vendor-credential-enables-unauthorized-access
    gates:
    - dry-run
    - lint
    model: hb_google/gemini-3-flash-preview
rationale: Start with tenants where ConnectWise Cloud Backup (SkyKick) or Cloud Management
  apps are active. Use a 14-day lookback to cover the initial password sprays (Oct
  13) and the credential abuse window (Oct 15-17).
references:
- name: "Huntress \u2014 Vendor Credential Enables Unauthorized Access"
  url: https://www.huntress.com/blog/vendor-credential-enables-unauthorized-access
related:
- hunt: m365-unusual-application-credential-addition
  reason: A general hunt for T1098.001 across all apps, not just the ConnectWise incident.
  relation: sibling
scenario:
  stages:
  - name: Password spraying against law firms
    observables:
    - 'src_endpoint_ip: 144.172.64.128'
    - 'user_agent: curl/7.88.1'
    slug: credential-access-password-spray
    tactic: credential-access
    techniques:
    - T1110.003
  - name: Service principal sign-in via vendor credentials
    observables:
    - 'src_endpoint_ip: 144.172.64.128'
    - 'app_id: b78f6a77-d4e5-46fb-87e5-d1648bd02519 (Exchange Online)'
    - 'app_id: 24205314-2b89-4e07-a2a4-579e898a5640 (MS Teams)'
    - 'app_id: b2b55f5b-7758-4719-8d8f-88ae172e6bdb (SharePoint Online)'
    - 'client_app: Microsoft Graph'
    slug: unauthorized-application-authentication
    tactic: initial-access
    techniques:
    - T1190
  - name: Self-assignment of directory roles
    observables:
    - 'api_operation: Add member to role'
    - 'target_modified_property_new_values: Exchange Administrator'
    - 'target_modified_property_new_values: Global Administrator'
    - 'user_agent: kiota-dotnet'
    - 'actor_app_uid: 24205314-2b89-4e07-a2a4-579e898a5640'
    slug: privilege-escalation-role-assignment
    tactic: privilege-escalation
    techniques:
    - T1098
  - name: New client secrets added to applications
    observables:
    - 'api_operation: Update application'
    - 'user_agent: Python-urllib/3.12'
    - 'resource_name: Huntress Security Platform'
    - 'target_modified_property_names: KeyDescription'
    - 'target_modified_property_new_values: [Client Secret expiry 2028]'
    slug: persistence-additional-cloud-credentials
    tactic: persistence
    techniques:
    - T1098.001
  summary: Threat actors abused ConnectWise (SkyKick) vendor credentials to gain unauthorized
    access to Microsoft 365 tenants, primarily targeting organizations in the policy,
    national security, and trade sectors. Once authenticated, the actors escalated
    privileges by assigning directory roles to service principals and established
    persistence by adding new client secrets to legitimate security and management
    applications.
severity: critical
targets:
  analyst:
    name: Tier-2 analyst
    role: analyst
  endpoint:
    category: endpoint
    name: Endpoint telemetry (hb_ surfaces)
    telemetry:
    - endpoint
  hunter:
    agent: true
    name: Hunt agent
  identity:
    category: identity
    name: Identity / sign-in telemetry
    telemetry:
    - identity
tlp: clear
type: investigation
---


# ConnectWise M365 Application Abuse

This hunt reconstructs the multi-stage attack involving ConnectWise Cloud Management applications reported in October 2026. The adversary used valid vendor-held credentials to obtain Graph tokens from a datacenter IP address. Once inside, they self-assigned Global Administrator roles and modified trusted applications, such as the Huntress Security Platform and MSP management tools, to add long-lived client secrets. This hunt follows a phased flow: first scoping the estate for the affected applications, then triaging initial access signals from both the reported IP address and anomalous IP/User-Agent combinations for these apps, and finally hunting for the follow-on administrative changes and persistence triggers across the cloud control plane.

## scope-tenants
<!-- Scope tenants with ConnectWise apps -->
Identify which tenants have active ConnectWise applications to focus the search for initial access and persistence.

```sqlite target=identity role=scoping params=(lookback_days=lookback_days, connectwise_app_ids=connectwise_app_ids)
~~~yaml
expected: A list of tenant IDs and any associated hostnames. Silence means no recent
  sign-in activity was recorded for these applications.
reads:
- home_tenant_id
- app_id
- device_hostname
- time
silence: not_evidence_of_absence
source: hb_auth_signin
verified: dry-run
verified_at: '2026-10-18'
~~~
SELECT DISTINCT home_tenant_id, device_hostname FROM hb_auth_signin WHERE instr(',' || '{{connectwise_app_ids}}' || ',', ',' || app_id || ',') > 0 AND time >= datetime('now', '-{{lookback_days}} days')
```

## initial-access-parallel
<!-- Initial access parallel hunt -->
parallel:
- → password-spray-detection
- → app-auth-anomaly
join: → agent-triage-initial

## password-spray-detection
<!-- Password spray from actor IP -->
Identify password spraying attempts using the curl user agent from the reported actor IP, even if no hostnames are provided in scope.

```sqlite target=identity role=baseline params=(lookback_days=lookback_days, actor_ip=actor_ip, scope_hosts=scope_hosts)
~~~yaml
baseline:
  compare: new_this_window
  window: '{{lookback_days}}d'
expected: Bursts of failed sign-ins from the actor IP against multiple accounts within
  a tenant. Silence means the IP was not used for spraying.
prevalence:
  by: home_tenant_id
  key:
  - actor_user_name
  rare_below: 2
reads:
- home_tenant_id
- actor_user_name
- src_endpoint_ip
- user_agent
- status_code
- device_hostname
- time
silence: evidence_of_absence
source: hb_auth_signin
verified: dry-run
verified_at: '2026-10-18'
~~~
SELECT home_tenant_id, actor_user_name, src_endpoint_ip, user_agent, status_code, COUNT(*) AS attempts, MIN(time) AS first_seen FROM hb_auth_signin WHERE src_endpoint_ip = '{{actor_ip}}' AND LOWER(user_agent) LIKE '%curl%' AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY home_tenant_id, actor_user_name, src_endpoint_ip, user_agent, status_code HAVING attempts > 3
```

## app-auth-anomaly
<!-- Anomalous ConnectWise app authentication -->
Detect successful authentication of ConnectWise apps from the known actor IP or from any IP/User-Agent combination that is rare or new for each tenant.

```sqlite target=identity role=detection-candidate params=(lookback_days=lookback_days, connectwise_app_ids=connectwise_app_ids)
~~~yaml
baseline:
  compare: new_this_window
  window: '{{lookback_days}}d'
expected: Successful sign-ins by the vendor apps from datacenter IPs or unusual user
  agents. Silence proves the apps behaved as expected for the lookback window.
prevalence:
  by: home_tenant_id
  key:
  - src_endpoint_ip
  - user_agent
  rare_below: 2
reads:
- home_tenant_id
- app_id
- src_endpoint_ip
- user_agent
- status_id
- time
silence: evidence_of_absence
source: hb_auth_signin
verified: dry-run
verified_at: '2026-10-18'
~~~
SELECT home_tenant_id, app_id, src_endpoint_ip, user_agent, status_id, COUNT(*) AS auth_count, MIN(time) AS first_seen FROM hb_auth_signin WHERE instr(',' || '{{connectwise_app_ids}}' || ',', ',' || app_id || ',') > 0 AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY home_tenant_id, app_id, src_endpoint_ip, user_agent, status_id
```

## agent-triage-initial
<!-- Triage initial access signals -->
```agent target=hunter
cite: required
context:
- password-spray-detection
- app-auth-anomaly
max_iterations: 4
objective: Determine which tenants and service principals the adversary successfully
  compromised, citing sign-ins from the actor IP or rare/new infrastructure.
success_criteria: A list of compromised tenant IDs to focus the follow-on audit search.
tools:
- endpoint
- identity
```

## audit-log-follow-on
<!-- Follow-on role changes and persistence -->
The query identifies administrative changes the hijacked applications made, such as role self-assignments or credential additions.

```sqlite target=endpoint role=triage params=(lookback_days=lookback_days, connectwise_app_ids=connectwise_app_ids)
~~~yaml
expected: The query returns rows where ConnectWise applications assign themselves
  the Global Administrator role or add client secrets to other applications like the
  Huntress Security Platform. Silence means the apps made no such changes in the audit
  window.
reads:
- cloud_account_uid
- actor_app_uid
- api_operation
- resource_name
- target_modified_property_names
- target_modified_property_new_values
- user_agent
- time
silence: not_evidence_of_absence
source: hb_cloud_api_activity
verified: dry-run
verified_at: '2026-10-18'
~~~
SELECT cloud_account_uid, actor_app_uid, api_operation, resource_name, target_modified_property_names, target_modified_property_new_values, user_agent, time FROM hb_cloud_api_activity WHERE (instr(',' || '{{connectwise_app_ids}}' || ',', ',' || actor_app_uid || ',') > 0 OR LOWER(user_agent) LIKE '%python-urllib%') AND (api_operation IN ('Add member to role', 'Update application', 'Add service principal credential')) AND time >= datetime('now', '-{{lookback_days}} days')
```

## agent-triage-complete
<!-- Triage complete attack chain -->
```agent target=hunter
cite: required
context:
- agent-triage-initial
- audit-log-follow-on
max_iterations: 6
objective: Confirm whether the compromised ConnectWise apps successfully escalated
  privileges or established persistence via other service principals, citing the full
  chain of events.
success_criteria: A verdict of malicious for any tenant where compromised access led
  to administrative modifications.
tools:
- endpoint
- identity
```

## decide-remediation
<!-- Decide on remediation path -->
if~: "the agent-triage-complete verdict is malicious for at least one tenant, specifically citing rogue credentials or role changes" (confidence: high, judge=hunter)
then: → contain-identity
indeterminate: → analyst-review
unavailable: → analyst-review (blind_spot: no-cloud-audit-logs)
else: → close-out

## contain-identity
<!-- Isolate identity and revoke credentials -->
```action target=identity
~~~yaml
approval: required
~~~
Revoke the client secrets added to secondary applications identified in the triage. Remove the ConnectWise service principals from the Global Administrator or Exchange Administrator directory roles. Revoke all active sessions for the affected service principals.
```
→ analyst-review

## analyst-review
<!-- Analyst manual impact review -->
```manual target=analyst
Review the full audit trail for the compromised apps to determine what directory or mailbox data the adversary may have accessed. Check for any 'Graph' token issuance events and verify if secondary apps performed any other actions after the rogue credential was added.
```
→ end

## close-out
<!-- Hunt close out -->
```manual target=analyst
Record that no unauthorized access or administrative changes were found. Recommend enabling 'App Instance Property Lock' in Entra ID for all high-privilege applications to prevent unauthorized credential additions in the future.
```
→ end

Run it

Take this hunt into your environment.

Open it in Huntbase to run every step against your own connections, with Scout weighing the evidence and your analysts in command. Or take the open hunt.md file anywhere that reads the format.

Machine-drafted by huntbase-hunt-generation using hb_google/gemini-3-flash-preview, gated by dry-run, lint, then reviewed by a person.