Service Accounts Performing Remote PS
Id | d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a |
Rulename | 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 |
Tactics | LateralMovement |
Techniques | T1210 |
Required data connectors | MicrosoftThreatProtection |
Kind | Scheduled |
Query frequency | 1h |
Query period | 1h |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Defender XDR/Analytic Rules/Lateral Movement/ServiceAccountsPerformingRemotePS.yaml |
Version | 1.0.0 |
Arm template | d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a.json |
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), "")
relevantTechniques:
- T1210
name: Service Accounts Performing Remote PS
requiredDataConnectors:
- dataTypes:
- DeviceLogonEvents
- DeviceEvents
connectorId: MicrosoftThreatProtection
entityMappings:
- fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
entityType: Host
- fieldMappings:
- identifier: FullName
columnName: AccountName
- identifier: DnsDomain
columnName: AccountDomain
- identifier: Name
columnName: AccountName
entityType: Account
triggerThreshold: 0
id: d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a
tactics:
- LateralMovement
version: 1.0.0
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Defender XDR/Analytic Rules/Lateral Movement/ServiceAccountsPerformingRemotePS.yaml
queryPeriod: 1h
kind: Scheduled
queryFrequency: 1h
severity: High
status: Available
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.
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), "")
triggerOperator: gt
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"apiVersion": "2024-01-01-preview",
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a')]",
"kind": "Scheduled",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a')]",
"properties": {
"alertRuleTemplateName": "d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a",
"customDetails": null,
"description": "Service Accounts Performing Remote PowerShell.\nThe purpose behind this detection is for finding service accounts that are performing remote powershell sessions.\nThere are two phases to the detection: Identify service accounts, Find remote PS cmdlets being ran by these accounts.\nTo accomplish this, we utilize DeviceLogonEvents and DeviceEvents to find cmdlets ran that meet the criteria.\nOne of the main advantages of this method is that only requires server telemetry, and not the attacking client.\nThe first phase relies on the DeviceLogonEvents to determine whether an account is a service account or not, consider the following accounts with logons:.\nRandom_user has DeviceLogonEvents with type 2, 3, 7, 10, 11 & 13.\nRandom_service_account 'should' only have DeviceLogonEvents with type 3,4 or 5.\n",
"displayName": "Service Accounts Performing Remote PS",
"enabled": true,
"entityMappings": [
{
"entityType": "Host",
"fieldMappings": [
{
"columnName": "DeviceName",
"identifier": "FullName"
},
{
"columnName": "HostName",
"identifier": "HostName"
},
{
"columnName": "DnsDomain",
"identifier": "DnsDomain"
}
]
},
{
"entityType": "Account",
"fieldMappings": [
{
"columnName": "AccountName",
"identifier": "FullName"
},
{
"columnName": "AccountDomain",
"identifier": "DnsDomain"
},
{
"columnName": "AccountName",
"identifier": "Name"
}
]
}
],
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Microsoft Defender XDR/Analytic Rules/Lateral Movement/ServiceAccountsPerformingRemotePS.yaml",
"query": "let InteractiveTypes = pack_array( // Declare Interactive logon type names\n 'Interactive',\n 'CachedInteractive',\n 'Unlock',\n 'RemoteInteractive',\n 'CachedRemoteInteractive',\n 'CachedUnlock'\n);\nlet WhitelistedCmdlets = pack_array( // List of whitelisted commands that don't provide a lot of value\n 'prompt',\n 'Out-Default',\n 'out-lineoutput',\n 'format-default',\n 'Set-StrictMode',\n 'TabExpansion2'\n);\nlet WhitelistedAccounts = pack_array('FakeWhitelistedAccount'); // List of accounts that are known to perform this activity in the environment and can be ignored\nDeviceLogonEvents // Get all logon events...\n| where AccountName !in~ (WhitelistedAccounts) // ...where it is not a whitelisted account...\n| where ActionType == \"LogonSuccess\" // ...and the logon was successful...\n| where AccountName !contains \"$\" // ...and not a machine logon.\n| 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.\n| extend IsInteractive=(LogonType in (InteractiveTypes)) // Determine if the logon is interactive (True=1,False=0)...\n| summarize HasInteractiveLogon=max(IsInteractive) // ...then bucket and get the maximum interactive value (0 or 1)...\n by AccountName // ... by the AccountNames\n| where HasInteractiveLogon == 0 // ...and filter out all accounts that had an interactive logon.\n// At this point, we have a list of accounts that we believe to be service accounts\n// Now we need to find RemotePS sessions that were spawned by those accounts\n// Note that we look at all powershell cmdlets executed to form a 29-day baseline to evaluate the data on today\n| join kind=rightsemi ( // Start by dropping the account name and only tracking the...\n\tDeviceEvents // ...\n\t| where ActionType == 'PowerShellCommand' // ...PowerShell commands seen...\n\t| where InitiatingProcessFileName =~ 'wsmprovhost.exe' // ...whose parent was wsmprovhost.exe (RemotePS Server)...\n | extend AccountName = InitiatingProcessAccountName // ...and add an AccountName field so the join is easier\n) on AccountName\n// At this point, we have all of the commands that were ran by service accounts\n| extend Command = tostring(extractjson('$.Command', tostring(AdditionalFields))) // Extract the actual PowerShell command that was executed\n| where Command !in (WhitelistedCmdlets) // Remove any values that match the whitelisted cmdlets\n| summarize (Timestamp, ReportId)=arg_max(TimeGenerated, ReportId), // Then group all of the cmdlets and calculate the min/max times of execution...\n make_set(Command, 100000), count(), min(TimeGenerated) by // ...as well as creating a list of cmdlets ran and the count..\n AccountName, AccountDomain, DeviceName, DeviceId // ...and have the commonality be the account, DeviceName and DeviceId\n// 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\n| order by AccountName asc // Order the final list by AccountName just to make it easier to go through\n| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)\n| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), \"\") \n",
"queryFrequency": "PT1H",
"queryPeriod": "PT1H",
"severity": "High",
"status": "Available",
"subTechniques": [],
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"LateralMovement"
],
"techniques": [
"T1210"
],
"templateVersion": "1.0.0",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0
},
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
}
]
}