Detection rules › Kusto

Service Accounts Performing Remote PS

Status
available
Severity
high
Time window
1h
Group by
AccountDomain, AccountName, DeviceId, DeviceName
Source
github.com/Azure/Azure-Sentinel

Service Accounts Performing Remote PowerShell. 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.

MITRE ATT&CK coverage

TacticTechniques
Lateral MovementT1210 Exploitation of Remote Services

Event coverage

Rule body kusto

id: d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a
name: Service Accounts Performing Remote PS
description: |
  Service Accounts Performing Remote PowerShell.
  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.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceLogonEvents
      - DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1210
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', tostring(AdditionalFields))) // Extract the actual PowerShell command that was executed
  | where Command !in (WhitelistedCmdlets)                                          // Remove any values that match the whitelisted cmdlets
  | summarize (Timestamp, ReportId)=arg_max(TimeGenerated, ReportId),               // Then group all of the cmdlets and calculate the min/max times of execution...
      make_set(Command, 100000), count(), min(TimeGenerated) by                     // ...as well as creating a list of cmdlets ran and the count..
      AccountName, AccountDomain, 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
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")                                           
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountName
      - identifier: DnsDomain
        columnName: AccountDomain
      - identifier: Name
        columnName: AccountName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let WhitelistedAccounts = pack_array('FakeWhitelistedAccount');

Let binding: InteractiveTypes

let InteractiveTypes = pack_array(
    'Interactive',
    'CachedInteractive',
    'Unlock',
    'RemoteInteractive',
    'CachedRemoteInteractive',
    'CachedUnlock'
);

Let binding: WhitelistedCmdlets

let WhitelistedCmdlets = pack_array(
    'prompt',
    'Out-Default',
    'out-lineoutput',
    'format-default',
    'Set-StrictMode',
    'TabExpansion2'
);

Stage 1: source

DeviceLogonEvents

Stage 2: where

| where AccountName !in~ (WhitelistedAccounts)

Stage 3: where

| where ActionType == "LogonSuccess"

Stage 4: where

| where AccountName !contains "$"

Stage 5: where

| where AccountName !has "winrm va_"

Stage 6: extend

| extend IsInteractive=(LogonType in (InteractiveTypes))

References InteractiveTypes (defined above).

Stage 7: summarize

| summarize HasInteractiveLogon=max(IsInteractive)
            by AccountName

Stage 8: where

| where HasInteractiveLogon == 0

Stage 9: join

| join kind=rightsemi (
	DeviceEvents
	| where ActionType == 'PowerShellCommand'
	| where InitiatingProcessFileName =~ 'wsmprovhost.exe'
    | extend AccountName = InitiatingProcessAccountName
) on AccountName

Stage 10: extend

| extend Command = tostring(extractjson('$.Command', tostring(AdditionalFields)))

Stage 11: where

| where Command !in (WhitelistedCmdlets)

References WhitelistedCmdlets (defined above).

Stage 12: summarize

| summarize (Timestamp, ReportId)=arg_max(TimeGenerated, ReportId),
    make_set(Command, 100000), count(), min(TimeGenerated) by
    AccountName, AccountDomain, DeviceName, DeviceId

Stage 13: sort

| order by AccountName asc

Stage 14: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 15: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
AccountNameeqWhitelistedAccounts
AccountNamecontains$
AccountNamematchwinrm va_
CommandeqWhitelistedCmdlets

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
ActionTypeeq
  • LogonSuccess transforms: cased corpus 5 (kusto 5)
  • PowerShellCommand transforms: cased corpus 2 (kusto 2)
HasInteractiveLogoneq
  • 0 transforms: cased
InitiatingProcessFileNameeq
  • wsmprovhost.exe corpus 6 (elastic 2, splunk 2, kusto 2)

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AccountDomainsummarize
AccountNamesummarize
DeviceIdsummarize
DeviceNamesummarize
Timestampsummarize
HostNameextend
DnsDomainextend