Detection rules › Kusto

Malformed user agent

Severity
medium
Time window
1d
Group by
Account, AppDisplayName, ClientAppUsed, EventName, EventSource, Operation, OperationName, RecordType, SourceIP, Type, UserAgent, csMethod, csUriStem, host_s, httpMethod_s, requestQuery_s, requestUri_s, sSiteName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Malware authors will sometimes hardcode user agent string values when writing the network communication component of their malware. Malformed user agents can be an indication of such malware.

MITRE ATT&CK coverage

Telemetry coverage

Rule body

id: a357535e-f722-4afe-b375-cff362b2b376
name: Malformed user agent
description: |
  'Malware authors will sometimes hardcode user agent string values when writing the network communication component of their malware. Malformed user agents can be an indication of such malware.'
severity: Medium
requiredDataConnectors:
  - connectorId: WAF
    dataTypes:
      - AzureDiagnostics
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CommandAndControl
  - Execution
relevantTechniques:
  - T1189
  - T1071
  - T1203
query: |
  (union isfuzzy=true
  (OfficeActivity | where UserAgent != ""),
  (OfficeActivity
  | where RecordType in ("AzureActiveDirectory", "AzureActiveDirectoryStsLogon")
  | extend OperationName = Operation
  | parse ExtendedProperties with * 'User-Agent\\":\\"' UserAgent2 '\\' *
  | parse ExtendedProperties with * 'UserAgent",      "Value": "' UserAgent1 '"' *
  | where isnotempty(UserAgent1) or isnotempty(UserAgent2)
  | extend UserAgent = iff( RecordType == 'AzureActiveDirectoryStsLogon', UserAgent1, UserAgent2)
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP, Account = UserId, Type, RecordType, Operation
  ),
  (AzureDiagnostics
  | where ResourceType =~ "APPLICATIONGATEWAYS"
  | where OperationName =~ "ApplicationGatewayAccess"
  | extend ClientIP = columnifexists("clientIP_s", "None"), UserAgent = columnifexists("userAgent_s", "None")
  | where UserAgent != '-'
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP,  requestUri_s, httpMethod_s, host_s, requestQuery_s, Type
  ),
  (
  W3CIISLog
  | where isnotempty(csUserAgent)
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent = csUserAgent, SourceIP = cIP, Account = csUserName, Type, sSiteName, csMethod, csUriStem
  ),
  (
  AWSCloudTrail
  | where isnotempty(UserAgent)
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = SourceIpAddress, Account = UserIdentityUserName, Type, EventSource, EventName
  ),
  (SigninLogs
  | where isnotempty(UserAgent)
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed
  ),
  (AADNonInteractiveUserSignInLogs
  | where isnotempty(UserAgent)
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed
  )
  )
  // Likely artefact of hardcoding
  | where UserAgent startswith "User" or UserAgent startswith '\"'
  // Incorrect casing
  or (UserAgent startswith "Mozilla" and not(UserAgent contains_cs "Mozilla"))
  // Incorrect casing
  or UserAgent contains_cs  "(Compatible;"
  // Missing MSIE version
  or UserAgent matches regex @"MSIE\s?;"
  // Incorrect spacing around MSIE version
  or UserAgent matches regex  @"MSIE(?:\d|.{1,5}?\d\s;)"
  | extend AccountName = split(Account, "@")[0], UPNSuffix = split(Account, "@")[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
version: 1.0.6
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

union isfuzzy=true (7 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: OfficeActivity, OfficeActivity, AzureDiagnostics, W3CIISLog, AWSCloudTrail, SigninLogs, AADNonInteractiveUserSignInLogs

Leg 1: OfficeActivity

OfficeActivity | where UserAgent != ""

Leg 2: OfficeActivity

OfficeActivity
| where RecordType in ("AzureActiveDirectory", "AzureActiveDirectoryStsLogon")
| extend OperationName = Operation
| parse ExtendedProperties with * 'User-Agent\\":\\"' UserAgent2 '\\' *
| parse ExtendedProperties with * 'UserAgent",      "Value": "' UserAgent1 '"' *
| where isnotempty(UserAgent1) or isnotempty(UserAgent2)
| extend UserAgent = iff( RecordType == 'AzureActiveDirectoryStsLogon', UserAgent1, UserAgent2)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP, Account = UserId, Type, RecordType, Operation

Leg 3: AzureDiagnostics

AzureDiagnostics
| where ResourceType =~ "APPLICATIONGATEWAYS"
| where OperationName =~ "ApplicationGatewayAccess"
| extend ClientIP = columnifexists("clientIP_s", "None"), UserAgent = columnifexists("userAgent_s", "None")
| where UserAgent != '-'
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = ClientIP,  requestUri_s, httpMethod_s, host_s, requestQuery_s, Type

Leg 4: W3CIISLog

W3CIISLog
| where isnotempty(csUserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent = csUserAgent, SourceIP = cIP, Account = csUserName, Type, sSiteName, csMethod, csUriStem

Leg 5: AWSCloudTrail

AWSCloudTrail
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = SourceIpAddress, Account = UserIdentityUserName, Type, EventSource, EventName

Leg 6: SigninLogs

SigninLogs
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed

Leg 7: AADNonInteractiveUserSignInLogs

AADNonInteractiveUserSignInLogs
| where isnotempty(UserAgent)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserAgent, SourceIP = IPAddress, Account = UserPrincipalName, Type, OperationName, tostring(LocationDetails), tostring(DeviceDetail), AppDisplayName, ClientAppUsed

Applied to the combined result

| where UserAgent startswith "User" or UserAgent startswith '\"'
or (UserAgent startswith "Mozilla" and not(UserAgent contains_cs "Mozilla"))
or UserAgent contains_cs  "(Compatible;"
or UserAgent matches regex @"MSIE\s?;"
or UserAgent matches regex  @"MSIE(?:\d|.{1,5}?\d\s;)"
| extend AccountName = split(Account, "@")[0], UPNSuffix = split(Account, "@")[1]

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
AppDisplayNamesummarize
ClientAppUsedsummarize
EndTimesummarize
OperationNamesummarize
SourceIPsummarize
StartTimesummarize
Typesummarize
UserAgentsummarize
AccountNameextend
UPNSuffixextend