Detection rules › Kusto
External guest invitation followed by Microsoft Entra ID PowerShell signin
By default guests have capability to invite more external guest users, guests also can do suspicious Microsoft Entra ID enumeration. This detection look at guest users, who have been invited or have invited recently, who also are logging via various PowerShell CLI. Ref : 'https://danielchronlund.com/2021/11/18/scary-azure-ad-tenant-enumeration-using-regular-b2b-guest-accounts/
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Discovery |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Authentication Method Changed for Privileged Account (Kusto)
- Authentication Methods Changed for Privileged Account (Kusto)
Rule body
id: acc4c247-aaf7-494b-b5da-17f18863878a
name: External guest invitation followed by Microsoft Entra ID PowerShell signin
description: |
'By default guests have capability to invite more external guest users, guests also can do suspicious Microsoft Entra ID enumeration. This detection look at guest users, who have been invited or have invited recently, who also are logging via various PowerShell CLI.
Ref : 'https://danielchronlund.com/2021/11/18/scary-azure-ad-tenant-enumeration-using-regular-b2b-guest-accounts/'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Persistence
- Discovery
relevantTechniques:
- T1078.004
- T1136.003
- T1087.004
query: |
let queryfrequency = 1h;
let queryperiod = 1d;
AuditLogs
| where TimeGenerated > ago(queryperiod)
| where OperationName in ("Invite external user", "Bulk invite users - started (bulk)", "Invite external user with reset invitation status")
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatedBy = iff(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
// Uncomment the following line to filter events where the inviting user was a guest user
//| where InitiatedBy has_any ("live.com#", "#EXT#")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend InvitedUser = tostring(TargetResource.userPrincipalName)
)
| mv-expand UserToCompare = pack_array(InitiatedBy, InvitedUser) to typeof(string)
| where UserToCompare has_any ("live.com#", "#EXT#")
| extend
parsedUser = replace_string(tolower(iff(UserToCompare startswith "live.com#", tostring(split(UserToCompare, "#")[1]), tostring(split(UserToCompare, "#EXT#")[0]))), "@", "_"),
InvitationTime = TimeGenerated
| join (
(union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs)
| where TimeGenerated > ago(queryfrequency)
| where UserType != "Member"
| where AppId has_any // This web may contain a list of these apps: https://msshells.net/
("1b730954-1685-4b74-9bfd-dac224a7b894",// Azure Active Directory PowerShell
"04b07795-8ddb-461a-bbee-02f9e1bf7b46",// Microsoft Azure CLI
"1950a258-227b-4e31-a9cf-717495945fc2",// Microsoft Azure PowerShell
"a0c73c16-a7e3-4564-9a95-2bdf47383716",// Microsoft Exchange Online Remote PowerShell
"fb78d390-0c51-40cd-8e17-fdbfab77341b",// Microsoft Exchange REST API Based Powershell
"d1ddf0e4-d672-4dae-b554-9d5bdfd93547",// Microsoft Intune PowerShell
"9bc3ab49-b65d-410a-85ad-de819febfddc",// Microsoft SharePoint Online Management Shell
"12128f48-ec9e-42f0-b203-ea49fb6af367",// MS Teams Powershell Cmdlets
"23d8f6bd-1eb0-4cc2-a08c-7bf525c67bcd",// Power BI PowerShell
"31359c7f-bd7e-475c-86db-fdb8c937548e",// PnP Management Shell
"90f610bf-206d-4950-b61d-37fa6fd1b224",// Aadrm Admin Powershell
"14d82eec-204b-4c2f-b7e8-296a70dab67e",// Microsoft Graph PowerShell
"9cee029c-6210-4654-90bb-17e6e9d36617" // Power Platform CLI - pac"
)
| summarize arg_min(TimeGenerated, *) by UserPrincipalName
| extend
parsedUser = replace_string(UserPrincipalName, "@", "_"),
SigninTime = TimeGenerated
)
on parsedUser
| project InvitationTime, InitiatedBy, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, OperationName, InvitedUser, SigninTime, SigninCategory = Category1, SigninUserPrincipalName = UserPrincipalName, AppDisplayName, ResourceDisplayName, UserAgent, InvitationAdditionalDetails = AdditionalDetails, InvitationTargetResources = TargetResources
| extend InvitedUserName = tostring(split(InvitedUser,'@',0)[0]), InvitedUserUPNSuffix = tostring(split(InvitedUser,'@',1)[0]),
InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InvitedUser
- identifier: Name
columnName: InvitedUserName
- identifier: UPNSuffix
columnName: InvitedUserUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatedByName
- identifier: UPNSuffix
columnName: InitiatedByUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let queryfrequency = 1h;
let queryperiod = 1d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(queryperiod)
Stage 3: where
| where OperationName in ("Invite external user", "Bulk invite users - started (bulk)", "Invite external user with reset invitation status")
Stage 4: extend (6 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatedBy = iff(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
Stage 5: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend InvitedUser = tostring(TargetResource.userPrincipalName)
)
Stage 6: mv-expand
| mv-expand UserToCompare = pack_array(InitiatedBy, InvitedUser) to typeof(string)
Stage 7: where
| where UserToCompare has_any ("live.com#", "#EXT#")
Stage 8: extend
| extend
parsedUser = replace_string(tolower(iff(UserToCompare startswith "live.com#", tostring(split(UserToCompare, "#")[1]), tostring(split(UserToCompare, "#EXT#")[0]))), "@", "_"),
InvitationTime = TimeGenerated
Stage 9: join
| join (
(union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs)
| where TimeGenerated > ago(queryfrequency)
| where UserType != "Member"
| where AppId has_any
("1b730954-1685-4b74-9bfd-dac224a7b894",
"04b07795-8ddb-461a-bbee-02f9e1bf7b46",
"1950a258-227b-4e31-a9cf-717495945fc2",
"a0c73c16-a7e3-4564-9a95-2bdf47383716",
"fb78d390-0c51-40cd-8e17-fdbfab77341b",
"d1ddf0e4-d672-4dae-b554-9d5bdfd93547",
"9bc3ab49-b65d-410a-85ad-de819febfddc",
"12128f48-ec9e-42f0-b203-ea49fb6af367",
"23d8f6bd-1eb0-4cc2-a08c-7bf525c67bcd",
"31359c7f-bd7e-475c-86db-fdb8c937548e",
"90f610bf-206d-4950-b61d-37fa6fd1b224",
"14d82eec-204b-4c2f-b7e8-296a70dab67e",
"9cee029c-6210-4654-90bb-17e6e9d36617"
)
| summarize arg_min(TimeGenerated, *) by UserPrincipalName
| extend
parsedUser = replace_string(UserPrincipalName, "@", "_"),
SigninTime = TimeGenerated
)
on parsedUser
Stage 10: project
| project InvitationTime, InitiatedBy, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, OperationName, InvitedUser, SigninTime, SigninCategory = Category1, SigninUserPrincipalName = UserPrincipalName, AppDisplayName, ResourceDisplayName, UserAgent, InvitationAdditionalDetails = AdditionalDetails, InvitationTargetResources = TargetResources
Stage 11: extend
| extend InvitedUserName = tostring(split(InvitedUser,'@',0)[0]), InvitedUserUPNSuffix = tostring(split(InvitedUser,'@',1)[0]),
InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
Stage 12: summarize aggregation inside the join branch
summarize by UserPrincipalName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AppId | match |
| field:"AppId" kind:match |
OperationName | in |
| field:"OperationName" kind:in |
UserToCompare | match |
| field:"UserToCompare" kind:match |
UserType | ne |
| field:"UserType" kind:ne value:"Member" |
type | eq |
| field:"type" kind:eq value:"User" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserPrincipalName | summarize |