← Back to SOC feed Coverage →

ServiceAccountsPerformingRemotePS

kql MEDIUM Azure-Sentinel
DeviceEventsDeviceLogonEvents
huntingmicrosoftofficialpowershell
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-23T23:00:00Z · Confidence: medium

Hunt Hypothesis

Service accounts executing remote PowerShell commands may indicate unauthorized lateral movement or persistence by adversaries. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential compromise of privileged accounts and mitigate advanced threat activity.

KQL Query

let InteractiveTypes = pack_array(                                  // Declare Interactive logon type names
    'Interactive',
    'CachedInteractive',
    'Unlock',
    'RemoteInteractive',
    'CachedRemoteInteractive',
    'CachedUnlock'
);
let WhitelistedCmdlets = pack_array(                                // List of whitelisted commands that don't provide a lot of value
    'prompt',
    'Out-Default',
    'out-lineoutput',
    'format-default',
    'Set-StrictMode',
    'TabExpansion2'
);
let WhitelistedAccounts = pack_array('FakeWhitelistedAccount');     // List of accounts that are known to perform this activity in the environment and can be ignored
DeviceLogonEvents                                                         // Get all logon events...
| where AccountName !in~ (WhitelistedAccounts)                      // ...where it is not a whitelisted account...
| where ActionType == "LogonSuccess"                                // ...and the logon was successful...
| where AccountName !contains "$"                                   // ...and not a machine logon.
| where AccountName !has "winrm va_"                                // WinRM will have pseudo account names that match this if there is an explicit permission for an admin to run the cmdlet, so assume it is good.
| extend IsInteractive=(LogonType in (InteractiveTypes))            // Determine if the logon is interactive (True=1,False=0)...
| summarize HasInteractiveLogon=max(IsInteractive)                  // ...then bucket and get the maximum interactive value (0 or 1)...
            by AccountName                                          // ... by the AccountNames
| where HasInteractiveLogon == 0                                    // ...and filter out all accounts that had an interactive logon.
// At this point, we have a list of accounts that we believe to be service accounts
// Now we need to find RemotePS sessions that were spawned by those accounts
// Note that we look at all powershell cmdlets executed to form a 29-day baseline to evaluate the data on today
| join kind=rightsemi (                                             // Start by dropping the account name and only tracking the...
	DeviceEvents                                                      // ...
	| where ActionType == 'PowerShellCommand'                       // ...PowerShell commands seen...
	| where InitiatingProcessFileName =~ 'wsmprovhost.exe'          // ...whose parent was wsmprovhost.exe (RemotePS Server)...
    | extend AccountName = InitiatingProcessAccountName             // ...and add an AccountName field so the join is easier
) on AccountName
// At this point, we have all of the commands that were ran by service accounts
| extend Command = tostring(extractjson('$.Command', AdditionalFields))   // Extract the actual PowerShell command that was executed
| where Command !in (WhitelistedCmdlets)                            // Remove any values that match the whitelisted cmdlets
| summarize (Timestamp, ReportId)=argmax(Timestamp, ReportId),      // Then group all of the cmdlets and calculate the min/max times of execution...
    makeset(Command), count(), min(Timestamp) by                    // ...as well as creating a list of cmdlets ran and the count..
    AccountName, DeviceName, DeviceId                            // ...and have the commonality be the account, DeviceName and DeviceId
// At this point, we have machine-account pairs along with the list of commands run as well as the first/last time the commands were ran
| order by AccountName asc                                          // Order the final list by AccountName just to make it easier to go through
| where min_Timestamp > ago(1d)                                     // Included to restrict the scope for the custom detection page

Analytic Rule Definition

id: 9fb83ddb-ff6c-49e4-920a-ac72e63c191d
name: ServiceAccountsPerformingRemotePS
description: |
  Service Accounts Performing Remote PowerShell.
  Author: miflower.
  The purpose behind this detection is for finding service accounts that are performing remote powershell sessions.
  There are two phases to the detection: Identify service accounts, Find remote PS cmdlets being ran by these accounts.
  To accomplish this, we utilize DeviceLogonEvents and DeviceEvents to find cmdlets ran that meet the criteria.
  One of the main advantages of this method is that only requires server telemetry, and not the attacking client.
  The first phase relies on the DeviceLogonEvents to determine whether an account is a service account or not, consider the following accounts with logons:.
  Random_user has DeviceLogonEvents with type 2, 3, 7, 10, 11 & 13.
  Random_service_account 'should' only have DeviceLogonEvents with type 3,4 or 5.
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
  dataTypes:
  - DeviceLogonEvents
  - DeviceEvents
query: |
  let InteractiveTypes = pack_array(                                  // Declare Interactive logon type names
      'Interactive',
      'CachedInteractive',
      'Unlock',
      'RemoteInteractive',
      'CachedRemoteInteractive',
      'CachedUnlock'
  );
  let WhitelistedCmdlets = pack_array(                                // List of whitelisted commands that don't provide a lot of value
      'prompt',
      'Out-Default',
      'out-lineoutput',
      'format-default',
      'Set-StrictMode',
      'TabExpansion2'
  );
  let WhitelistedAccounts = pack_array('FakeWhitelistedAccount');     // List of accounts that are known to perform this activity in the environment and can be ignored
  DeviceLogonEvents                                                         // Get all logon events...
  | where AccountName !in~ (WhitelistedAccounts)                      // ...where it is not a whitelisted account...
  | where ActionType == "LogonSuccess"                                // ...and the logon was successful...
  | where AccountName !contains "$"                                   // ...and not a machine logon.
  | where AccountName !has "winrm va_"                                // WinRM will have pseudo account names that match this if there is an explicit permission for an admin to run the cmdlet, so assume it is good.
  | extend IsInteractive=(LogonType in (InteractiveTypes))            // Determine if the logon is interactive (True=1,False=0)...
  | summarize HasInteractiveLogon=max(IsInteractive)                  // ...then bucket and get the maximum interactive value (0 or 1)...
              by AccountName                                          // ... by the AccountNames
  | where HasInteractiveLogon == 0                                    // ...and filter out all accounts that had an interactive logon.
  // At this point, we have a list of accounts that we believe to be service accounts
  // Now 

Required Data Sources

Sentinel TableNotes
DeviceEventsEnsure this data connector is enabled
DeviceLogonEventsEnsure this data connector is enabled

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/Microsoft 365 Defender/Lateral Movement/ServiceAccountsPerformingRemotePS.yaml