{
  "hunt": {
    "meta": {
      "tlp": "clear",
      "hunt": {
        "handoff": "keep-as-periodic-hunt",
        "trigger": "intel-report",
        "methodology": "model-assisted",
        "applicability": "campaign-specific",
        "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."
      },
      "name": "ConnectWise M365 Application Abuse",
      "type": "investigation",
      "labels": [
        "hunt",
        "attack.t1098.001",
        "attack.t1110.003",
        "attack.t1190",
        "credential access",
        "initial access",
        "persistence",
        "privilege escalation"
      ],
      "related": [
        {
          "hunt": "m365-unusual-application-credential-addition",
          "reason": "A general hunt for T1098.001 across all apps, not just the ConnectWise incident.",
          "relation": "sibling"
        }
      ],
      "targets": {
        "hunter": {
          "name": "Hunt agent",
          "agent": true
        },
        "analyst": {
          "name": "Tier-2 analyst",
          "role": "analyst"
        },
        "endpoint": {
          "name": "Endpoint telemetry (hb_ surfaces)",
          "category": "endpoint",
          "telemetry": [
            "endpoint"
          ]
        },
        "identity": {
          "name": "Identity / sign-in telemetry",
          "category": "identity",
          "telemetry": [
            "identity"
          ]
        }
      },
      "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.",
      "coverage": [
        {
          "stage": "credential-access-password-spray",
          "steps": [
            "password-spray-detection"
          ],
          "status": "covered"
        },
        {
          "stage": "unauthorized-application-authentication",
          "steps": [
            "app-auth-anomaly"
          ],
          "status": "covered"
        },
        {
          "stage": "privilege-escalation-role-assignment",
          "steps": [
            "audit-log-follow-on"
          ],
          "status": "covered"
        },
        {
          "stage": "persistence-additional-cloud-credentials",
          "steps": [
            "audit-log-follow-on"
          ],
          "status": "covered"
        }
      ],
      "scenario": {
        "stages": [
          {
            "name": "Password spraying against law firms",
            "slug": "credential-access-password-spray",
            "tactic": "credential-access",
            "techniques": [
              "T1110.003"
            ],
            "observables": [
              "src_endpoint_ip: 144.172.64.128",
              "user_agent: curl/7.88.1"
            ]
          },
          {
            "name": "Service principal sign-in via vendor credentials",
            "slug": "unauthorized-application-authentication",
            "tactic": "initial-access",
            "techniques": [
              "T1190"
            ],
            "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"
            ]
          },
          {
            "name": "Self-assignment of directory roles",
            "slug": "privilege-escalation-role-assignment",
            "tactic": "privilege-escalation",
            "techniques": [
              "T1098"
            ],
            "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"
            ]
          },
          {
            "name": "New client secrets added to applications",
            "slug": "persistence-additional-cloud-credentials",
            "tactic": "persistence",
            "techniques": [
              "T1098.001"
            ],
            "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]"
            ]
          }
        ],
        "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",
      "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).",
      "guardrails": {
        "claims": "no_unsupported",
        "evidence": "citation_required",
        "telemetry": "untrusted",
        "missing_data": "not_benign"
      },
      "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.",
      "parameters": {
        "actor_ip": {
          "from": {
            "ref": "huntress-vendor-credential-abuse",
            "kind": "article",
            "observed": "2026-10-15"
          },
          "type": "ip",
          "default": "144.172.64.128",
          "description": "The hosting provider IP address associated with the unauthorized sign-ins and password sprays."
        },
        "scope_hosts": {
          "type": "list[host]",
          "default": [],
          "description": "Optional list of hostnames to narrow the search; leave empty to check the whole estate."
        },
        "lookback_days": {
          "type": "number",
          "default": "14",
          "description": "Days of history to examine."
        },
        "connectwise_app_ids": {
          "from": {
            "ref": "huntress-vendor-credential-abuse",
            "kind": "article",
            "observed": "2026-10-17"
          },
          "type": "list[string]",
          "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."
        }
      },
      "provenance": {
        "authors": [
          {
            "org": "huntbase.io",
            "name": "Huntbase hunt generation"
          }
        ],
        "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"
        }
      },
      "references": [
        {
          "url": "https://www.huntress.com/blog/vendor-credential-enables-unauthorized-access",
          "name": "Huntress \u2014 Vendor Credential Enables Unauthorized Access"
        }
      ],
      "blind_spots": [
        {
          "id": "no-cloud-audit-logs",
          "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",
          "question": "whether the hijacked application modified directory roles or added credentials",
          "requires": "Unified Audit Log (hb_cloud_api_activity) integration"
        },
        {
          "id": "directory-read-blind-spot",
          "risk": "Microsoft does not log most data-read operations; tokens issued for Graph can be used to scrape data without a visible audit trail.",
          "question": "what directory or mailbox data was read by the attacker",
          "requires": "Microsoft Graph Activity Logs"
        },
        {
          "id": "cloud-host-anonymity",
          "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.",
          "question": "which local host initiated the cloud authentication",
          "requires": "Endpoint telemetry with cloud-provider correlation"
        }
      ]
    },
    "name": "ConnectWise M365 Application Abuse",
    "description": "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."
  },
  "nodes": [
    {
      "id": "hypothesis",
      "type": "hypothesis",
      "label": "Hypothesis",
      "config": {
        "tags": [],
        "coverage": [
          {
            "stage": "credential-access-password-spray",
            "steps": [
              "password-spray-detection"
            ],
            "status": "covered"
          },
          {
            "stage": "unauthorized-application-authentication",
            "steps": [
              "app-auth-anomaly"
            ],
            "status": "covered"
          },
          {
            "stage": "privilege-escalation-role-assignment",
            "steps": [
              "audit-log-follow-on"
            ],
            "status": "covered"
          },
          {
            "stage": "persistence-additional-cloud-credentials",
            "steps": [
              "audit-log-follow-on"
            ],
            "status": "covered"
          }
        ],
        "rationale": "An adversary has exploited compromised ConnectWise (SkyKick) vendor credentials to access M365 tenants, escalate directory privileges, and persist by adding secrets to secondary applications.",
        "blind_spots": [
          {
            "id": "no-cloud-audit-logs",
            "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",
            "question": "whether the hijacked application modified directory roles or added credentials",
            "requires": "Unified Audit Log (hb_cloud_api_activity) integration"
          },
          {
            "id": "directory-read-blind-spot",
            "risk": "Microsoft does not log most data-read operations; tokens issued for Graph can be used to scrape data without a visible audit trail.",
            "question": "what directory or mailbox data was read by the attacker",
            "requires": "Microsoft Graph Activity Logs"
          },
          {
            "id": "cloud-host-anonymity",
            "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.",
            "question": "which local host initiated the cloud authentication",
            "requires": "Endpoint telemetry with cloud-provider correlation"
          }
        ],
        "scoping_notes": "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).",
        "beyond_detection": "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."
      }
    },
    {
      "id": "scope-tenants",
      "type": "query",
      "label": "Scope tenants with ConnectWise apps",
      "config": {
        "dsl": "sqlite",
        "role": "scoping",
        "source": "identity",
        "content": "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')",
        "surface": "hb_auth_signin",
        "description": "Identify which tenants have active ConnectWise applications to focus the search for initial access and persistence.",
        "expected_signal": "A list of tenant IDs and any associated hostnames. Silence means no recent sign-in activity was recorded for these applications."
      },
      "parents": [
        {
          "id": "hypothesis"
        }
      ],
      "primitive_config": {
        "dsl": "sqlite",
        "role": "scoping",
        "label": "Scope tenants with ConnectWise apps",
        "reads": [
          "home_tenant_id",
          "app_id",
          "device_hostname",
          "time"
        ],
        "source": "hb_auth_signin",
        "target": "identity",
        "content": "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')",
        "silence": "not_evidence_of_absence",
        "expected": "A list of tenant IDs and any associated hostnames. Silence means no recent sign-in activity was recorded for these applications.",
        "verified": "dry-run",
        "verified_at": "2026-10-18"
      }
    },
    {
      "id": "password-spray-detection",
      "type": "query",
      "label": "Password spray from actor IP",
      "config": {
        "dsl": "sqlite",
        "role": "baseline",
        "source": "identity",
        "content": "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",
        "surface": "hb_auth_signin",
        "description": "Identify password spraying attempts using the curl user agent from the reported actor IP, even if no hostnames are provided in scope.",
        "expected_signal": "Bursts of failed sign-ins from the actor IP against multiple accounts within a tenant. Silence means the IP was not used for spraying."
      },
      "parents": [
        {
          "id": "scope-tenants"
        }
      ],
      "primitive_config": {
        "dsl": "sqlite",
        "role": "baseline",
        "label": "Password spray from actor IP",
        "reads": [
          "home_tenant_id",
          "actor_user_name",
          "src_endpoint_ip",
          "user_agent",
          "status_code",
          "device_hostname",
          "time"
        ],
        "source": "hb_auth_signin",
        "target": "identity",
        "content": "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",
        "silence": "evidence_of_absence",
        "baseline": {
          "window": "{{lookback_days}}d",
          "compare": "new_this_window"
        },
        "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.",
        "verified": "dry-run",
        "prevalence": {
          "by": "home_tenant_id",
          "key": [
            "actor_user_name"
          ],
          "rare_below": 2
        },
        "verified_at": "2026-10-18"
      }
    },
    {
      "id": "app-auth-anomaly",
      "type": "query",
      "label": "Anomalous ConnectWise app authentication",
      "config": {
        "dsl": "sqlite",
        "role": "detection-candidate",
        "source": "identity",
        "content": "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",
        "surface": "hb_auth_signin",
        "description": "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.",
        "expected_signal": "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."
      },
      "parents": [
        {
          "id": "scope-tenants"
        }
      ],
      "primitive_config": {
        "dsl": "sqlite",
        "role": "detection-candidate",
        "label": "Anomalous ConnectWise app authentication",
        "reads": [
          "home_tenant_id",
          "app_id",
          "src_endpoint_ip",
          "user_agent",
          "status_id",
          "time"
        ],
        "source": "hb_auth_signin",
        "target": "identity",
        "content": "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",
        "silence": "evidence_of_absence",
        "baseline": {
          "window": "{{lookback_days}}d",
          "compare": "new_this_window"
        },
        "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.",
        "verified": "dry-run",
        "prevalence": {
          "by": "home_tenant_id",
          "key": [
            "src_endpoint_ip",
            "user_agent"
          ],
          "rare_below": 2
        },
        "verified_at": "2026-10-18"
      }
    },
    {
      "id": "agent-triage-initial",
      "type": "analytic",
      "label": "Triage initial access signals",
      "config": {
        "cite": "required",
        "tools": [
          "endpoint",
          "identity"
        ],
        "context": [
          "password-spray-detection",
          "app-auth-anomaly"
        ],
        "objective": "Determine which tenants and service principals the adversary successfully compromised, citing sign-ins from the actor IP or rare/new infrastructure.",
        "description": "Synthesize the spray and anomaly results to identify specific tenants and applications that were successfully breached.",
        "max_iterations": 4,
        "expected_signal": "A per-tenant verdict on the success of the initial access phase.",
        "success_criteria": "A list of compromised tenant IDs to focus the follow-on audit search."
      },
      "parents": [
        {
          "id": "password-spray-detection",
          "kind": "merge"
        },
        {
          "id": "app-auth-anomaly",
          "kind": "merge"
        }
      ]
    },
    {
      "id": "audit-log-follow-on",
      "type": "query",
      "label": "Follow-on role changes and persistence",
      "config": {
        "dsl": "sqlite",
        "role": "triage",
        "source": "endpoint",
        "content": "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')",
        "surface": "hb_cloud_api_activity",
        "description": "The query identifies administrative changes the hijacked applications made, such as role self-assignments or credential additions.",
        "expected_signal": "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."
      },
      "parents": [
        {
          "id": "agent-triage-initial"
        }
      ],
      "primitive_config": {
        "dsl": "sqlite",
        "role": "triage",
        "label": "Follow-on role changes and persistence",
        "reads": [
          "cloud_account_uid",
          "actor_app_uid",
          "api_operation",
          "resource_name",
          "target_modified_property_names",
          "target_modified_property_new_values",
          "user_agent",
          "time"
        ],
        "source": "hb_cloud_api_activity",
        "target": "endpoint",
        "content": "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')",
        "silence": "not_evidence_of_absence",
        "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.",
        "verified": "dry-run",
        "verified_at": "2026-10-18"
      }
    },
    {
      "id": "agent-triage-complete",
      "type": "analytic",
      "label": "Triage complete attack chain",
      "config": {
        "cite": "required",
        "tools": [
          "endpoint",
          "identity"
        ],
        "context": [
          "agent-triage-initial",
          "audit-log-follow-on"
        ],
        "objective": "Confirm whether the compromised ConnectWise apps successfully escalated privileges or established persistence via other service principals, citing the full chain of events.",
        "description": "Connect the initial unauthorized sign-ins to the subsequent administrative activity to confirm a successful supply-chain breach.",
        "max_iterations": 6,
        "expected_signal": "A comprehensive per-tenant verdict covering access, escalation, and persistence.",
        "success_criteria": "A verdict of malicious for any tenant where compromised access led to administrative modifications."
      },
      "parents": [
        {
          "id": "audit-log-follow-on"
        }
      ]
    },
    {
      "id": "decide-remediation",
      "type": "checkpoint",
      "label": "Decide on remediation path",
      "config": {
        "fuzzy": true,
        "judge": "hunter",
        "question": "the agent-triage-complete verdict is malicious for at least one tenant, specifically citing rogue credentials or role changes",
        "condition": "the agent-triage-complete verdict is malicious for at least one tenant, specifically citing rogue credentials or role changes",
        "blind_spot": "no-cloud-audit-logs",
        "confidence": "high",
        "description": "Route the hunt based on whether the adversary established persistence or escalated privileges.",
        "checkpoint_type": "mandatory"
      },
      "parents": [
        {
          "id": "agent-triage-complete"
        }
      ]
    },
    {
      "id": "contain-identity",
      "type": "action",
      "label": "Isolate identity and revoke credentials",
      "config": {
        "target": "identity",
        "description": "Immediately remove unauthorized persistence and revoke elevated roles from the vendor application.",
        "instructions": "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.",
        "action_approval": "required"
      },
      "parents": [
        {
          "id": "decide-remediation",
          "branch": "on_supports"
        }
      ]
    },
    {
      "id": "analyst-review",
      "type": "task",
      "label": "Analyst manual impact review",
      "config": {
        "assignee": "analyst",
        "description": "Verify the extent of the access and investigate potential data exfiltration through Microsoft Graph.",
        "instructions": "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."
      },
      "parents": [
        {
          "id": "decide-remediation",
          "branch": "default"
        },
        {
          "id": "decide-remediation",
          "branch": "on_unavailable"
        },
        {
          "id": "contain-identity"
        }
      ]
    },
    {
      "id": "close-out",
      "type": "task",
      "label": "Hunt close out",
      "config": {
        "assignee": "analyst",
        "description": "Log the negative result and recommend proactive controls.",
        "instructions": "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."
      },
      "parents": [
        {
          "id": "decide-remediation",
          "branch": "on_refutes"
        }
      ]
    }
  ]
}