Detection rules › Kusto

Exchange SSRF Autodiscover ProxyShell - Detection

Severity
high
Time window
12h
Author
Thomas McElroy
Source
github.com/Azure/Azure-Sentinel

'This query looks for suspicious request patterns to Exchange servers that fit patterns recently blogged about by PeterJson. This exploitation chain utilises an SSRF vulnerability in Exchange which eventually allows the attacker to execute arbitrary Powershell on the server. In the example powershell can be used to write an email to disk with an encoded attachment containing a shell. Reference: https://peterjson.medium.com/reproducing-the-proxyshell-pwn2own-exploit-49743a4ea9a1'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1190 Exploit Public-Facing Application

Rule body kusto

id: 968358d6-6af8-49bb-aaa4-187b3067fb95
name: Exchange SSRF Autodiscover ProxyShell - Detection
description: |
  'This query looks for suspicious request patterns to Exchange servers that fit patterns recently blogged about by PeterJson. This exploitation chain utilises an SSRF vulnerability in Exchange which eventually allows the attacker to execute arbitrary Powershell on the server.
  In the example powershell can be used to write an email to disk with an encoded attachment containing a shell.
  Reference: https://peterjson.medium.com/reproducing-the-proxyshell-pwn2own-exploit-49743a4ea9a1'
severity: High
requiredDataConnectors:
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  let successCodes = dynamic([200, 302, 401]);
  W3CIISLog
  | where scStatus has_any (successCodes)
  | where ipv4_is_private(cIP) == False
  | where csUriStem hasprefix "/autodiscover/autodiscover.json"
  | project TimeGenerated, cIP, sIP, sSiteName, csUriStem, csUriQuery, Computer, csUserName, _ResourceId, FileUri
  | where (csUriQuery !has "Protocol" and isnotempty(csUriQuery))
  or (csUriQuery has_any("/mapi/", "powershell"))
  or (csUriQuery contains "@" and csUriQuery matches regex @"\.[a-zA-Z]{2,4}?(?:[a-zA-Z]{2,4}\/)")
  or (csUriQuery contains ":" and csUriQuery matches regex @"\:[0-9]{2,4}\/")
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(csUserName, "@")[0]), AccountUPNSuffix = tostring(split(csUserName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: csUserName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix  
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: cIP
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: _ResourceId
version: 1.0.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Thomas McElroy
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let successCodes = dynamic([200, 302, 401]);

Stage 1: source

W3CIISLog

Stage 2: where

| where scStatus has_any (successCodes)

Stage 3: where

| where ipv4_is_private(cIP) == False

Stage 4: where

| where csUriStem hasprefix "/autodiscover/autodiscover.json"

Stage 5: project

| project TimeGenerated, cIP, sIP, sSiteName, csUriStem, csUriQuery, Computer, csUserName, _ResourceId, FileUri

Stage 6: where

| where (csUriQuery !has "Protocol" and isnotempty(csUriQuery))
or (csUriQuery has_any("/mapi/", "powershell"))
or (csUriQuery contains "@" and csUriQuery matches regex @"\.[a-zA-Z]{2,4}?(?:[a-zA-Z]{2,4}\/)")
or (csUriQuery contains ":" and csUriQuery matches regex @"\:[0-9]{2,4}\/")

Stage 7: extend (3 consecutive steps)

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(csUserName, "@")[0]), AccountUPNSuffix = tostring(split(csUserName, "@")[1])

Exclusions

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

FieldKindExcluded values
cIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8

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
csUriQuerycontains
  • :
  • @
csUriQueryis_not_null
  • (no value, null check)
csUriQuerymatch
  • /mapi/
  • powershell
csUriQueryregex_match
  • .[a-zA-Z]{2,4}?(?:[a-zA-Z]{2,4}\/)
  • \:[0-9]{2,4}\/
csUriStemstarts_with
  • /autodiscover/autodiscover.json
scStatusmatch
  • 200
  • 302
  • 401

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
Computerproject
FileUriproject
TimeGeneratedproject
_ResourceIdproject
cIPproject
csUriQueryproject
csUriStemproject
csUserNameproject
sIPproject
sSiteNameproject
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNameextend
AccountUPNSuffixextend