Rare firewall rule changes using netsh


Description

This query will show rare firewall rule changes using netsh utility by comparing rule names and program names from the previous day with those from the historical chosen time frame. - This technique was seen in relation to Solarigate attack but the results can indicate potential malicious activity used in different attacks. - The process name in each data source is commented out as an adversary could rename it. It is advisable to keep process name commented but if the results show unrelated false positives, users may want to uncomment it. - Note also that the queries use the KQL "has_all" operator, which hasn't yet been documented officially, but will be soon. In short, "has_all" will only match when the referenced field has all strings in the list. Refer to netsh syntax: https://docs.microsoft.com/windows-server/administration/windows-commands/netsh Refer to our Microsoft Defender XDR blog for details on use during the Solorigate attack: https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/

Query · kql

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
// historical time frame
let lookback = totimespan((endtime-starttime)*7);
let AccountAllowList = dynamic(['SYSTEM']);
let tokens = dynamic(["add", "delete", "set"]);
(union isfuzzy=true
(
SecurityEvent
| where TimeGenerated >= ago(lookback)
// remove comment below to adjust for noise
// | where Process =~ "netsh.exe"
| where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
| where AccountType !~ "Machine" and Account !in~ (AccountAllowList)
| extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
| mv-apply KeyValuePairs on (
  summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
 )
| extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
| join kind=leftanti (
SecurityEvent
| where TimeGenerated between (starttime..endtime)
// remove comment below to adjust for noise
// | where Process =~ "netsh.exe"
| where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
| where AccountType !~ "Machine" and Account !in~ (AccountAllowList)
| extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
| mv-apply KeyValuePairs on (
  summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
 )
| extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
) on RuleName, Program
| summarize count() , StartTime= min(TimeGenerated), EndTime=max(TimeGenerated) by Type, Computer, Account, SubjectDomainName, SubjectUserName, RuleName, Program, CommandLineParsed = tostring(CommandLineParsed), Process, ParentProcessName
| extend timestamp = StartTime, AccountCustomEntity = Account, HostCustomEntity = Computer
),
(
DeviceProcessEvents
| where TimeGenerated >= ago(lookback)
// remove comment below to adjust for noise
// | where InitiatingProcessFileName =~ "netsh.exe"
| where InitiatingProcessCommandLine has_all ("advfirewall", "firewall") and InitiatingProcessCommandLine has_any (tokens)
| where AccountName !in~ (AccountAllowList)
| extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), InitiatingProcessCommandLine)
| mv-apply KeyValuePairs on (
  summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
)
| extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
| join kind=leftanti (
DeviceProcessEvents
| where TimeGenerated between (starttime..endtime)
// remove comment below to adjust for noise
// | where InitiatingProcessFileName =~ "netsh.exe"
| where InitiatingProcessCommandLine has_all ("advfirewall", "firewall") and InitiatingProcessCommandLine has_any (tokens)
| where AccountName !in~ (AccountAllowList)
| extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), InitiatingProcessCommandLine)
| mv-apply KeyValuePairs on (
  summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
)
| extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
) on RuleName, Program
| summarize count() , StartTime= min(TimeGenerated), EndTime=max(TimeGenerated) by Type, DeviceName, AccountName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, RuleName, Program,  CommandLineParsed = tostring(CommandLineParsed), InitiatingProcessFileName, InitiatingProcessParentFileName
| extend timestamp = StartTime, AccountCustomEntity = InitiatingProcessAccountName, HostCustomEntity = DeviceName
),
(
Event
| where TimeGenerated > ago(lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
// remove comment below to adjust for noise
// | where OriginalFileName =~ "netsh.exe"
| where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
| where User !in~ (AccountAllowList)
| extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
| mv-apply KeyValuePairs on (
  summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
)
| extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
| join kind=leftanti (
Event
| where TimeGenerated > ago(lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
// remove comment below to adjust for noise
// | where OriginalFileName =~ "netsh.exe"
| where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
| where User !in~ (AccountAllowList)
| extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
| mv-apply KeyValuePairs on (
  summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
)
| extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
) on RuleName, Program
| extend Type = strcat(Type, ": ", Source)
| summarize count() , StartTime= min(TimeGenerated), EndTime=max(TimeGenerated) by Type, Computer, User, Process, RuleName, Program, CommandLineParsed = tostring(CommandLineParsed), ParentImage
| extend timestamp = StartTime, AccountCustomEntity = User, HostCustomEntity = Computer
)
)
Raw source Rare firewall rule changes using netsh · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: 3dc5dc8b-160b-407e-9925-24a91e3599df
name: Rare firewall rule changes using netsh
description: |
  This query will show rare firewall rule changes using netsh utility by comparing rule names and program names from the previous day
  with those from the historical chosen time frame.
  - This technique was seen in relation to Solarigate attack but the results can indicate potential  malicious activity used in different attacks.
  - The process name in each data source is commented out as an adversary could rename it. It is advisable to keep process name commented but
    if the results show unrelated false positives, users may want to uncomment it.
  - Note also that the queries use the KQL "has_all" operator, which hasn't yet been documented officially, but will be soon.
    In short, "has_all" will only match when the referenced field has all strings in the list.
  Refer to netsh syntax: https://docs.microsoft.com/windows-server/administration/windows-commands/netsh
  Refer to our Microsoft Defender XDR blog for details on use during the Solorigate attack:
  https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
tactics:
  - Execution
relevantTechniques:
  - T1204
tags:
  - Solorigate
  - NOBELIUM
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  // historical time frame
  let lookback = totimespan((endtime-starttime)*7);
  let AccountAllowList = dynamic(['SYSTEM']);
  let tokens = dynamic(["add", "delete", "set"]);
  (union isfuzzy=true
  (
  SecurityEvent
  | where TimeGenerated >= ago(lookback)
  // remove comment below to adjust for noise
  // | where Process =~ "netsh.exe"
  | where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
  | where AccountType !~ "Machine" and Account !in~ (AccountAllowList)
  | extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
  | mv-apply KeyValuePairs on (
    summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
   )
  | extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
  | join kind=leftanti (
  SecurityEvent
  | where TimeGenerated between (starttime..endtime)
  // remove comment below to adjust for noise
  // | where Process =~ "netsh.exe"
  | where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
  | where AccountType !~ "Machine" and Account !in~ (AccountAllowList)
  | extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
  | mv-apply KeyValuePairs on (
    summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
   )
  | extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
  ) on RuleName, Program
  | summarize count() , StartTime= min(TimeGenerated), EndTime=max(TimeGenerated) by Type, Computer, Account, SubjectDomainName, SubjectUserName, RuleName, Program, CommandLineParsed = tostring(CommandLineParsed), Process, ParentProcessName
  | extend timestamp = StartTime, AccountCustomEntity = Account, HostCustomEntity = Computer
  ),
  (
  DeviceProcessEvents
  | where TimeGenerated >= ago(lookback)
  // remove comment below to adjust for noise
  // | where InitiatingProcessFileName =~ "netsh.exe"
  | where InitiatingProcessCommandLine has_all ("advfirewall", "firewall") and InitiatingProcessCommandLine has_any (tokens)
  | where AccountName !in~ (AccountAllowList)
  | extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), InitiatingProcessCommandLine)
  | mv-apply KeyValuePairs on (
    summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
  )
  | extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
  | join kind=leftanti (
  DeviceProcessEvents
  | where TimeGenerated between (starttime..endtime)
  // remove comment below to adjust for noise
  // | where InitiatingProcessFileName =~ "netsh.exe"
  | where InitiatingProcessCommandLine has_all ("advfirewall", "firewall") and InitiatingProcessCommandLine has_any (tokens)
  | where AccountName !in~ (AccountAllowList)
  | extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), InitiatingProcessCommandLine)
  | mv-apply KeyValuePairs on (
    summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
  )
  | extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
  ) on RuleName, Program
  | summarize count() , StartTime= min(TimeGenerated), EndTime=max(TimeGenerated) by Type, DeviceName, AccountName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, RuleName, Program,  CommandLineParsed = tostring(CommandLineParsed), InitiatingProcessFileName, InitiatingProcessParentFileName
  | extend timestamp = StartTime, AccountCustomEntity = InitiatingProcessAccountName, HostCustomEntity = DeviceName
  ),
  (
  Event
  | where TimeGenerated > ago(lookback)
  | where Source == "Microsoft-Windows-Sysmon"
  | where EventID == 1
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key=tostring(['@Name']), Value=['#text']
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
  // remove comment below to adjust for noise
  // | where OriginalFileName =~ "netsh.exe"
  | where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
  | where User !in~ (AccountAllowList)
  | extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
  | mv-apply KeyValuePairs on (
    summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
  )
  | extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
  | join kind=leftanti (
  Event
  | where TimeGenerated > ago(lookback)
  | where Source == "Microsoft-Windows-Sysmon"
  | where EventID == 1
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key=tostring(['@Name']), Value=['#text']
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
  // remove comment below to adjust for noise
  // | where OriginalFileName =~ "netsh.exe"
  | where CommandLine has_all ("advfirewall", "firewall") and CommandLine has_any (tokens)
  | where User !in~ (AccountAllowList)
  | extend KeyValuePairs = extract_all(@'(?P<key>\w+)=(?P<value>[a-zA-Z0-9-\":\\\s$_@()."]+\"|[a-zA-Z0-9-\":$_\\@()."]+)', dynamic(["key","value"]), CommandLine)
  | mv-apply KeyValuePairs on (
    summarize CommandLineParsed = make_bag(pack(tostring(KeyValuePairs[0]), KeyValuePairs[1]))
  )
  | extend RuleName = tostring(parse_json(CommandLineParsed).name), Program = tostring(parse_json(CommandLineParsed).program)
  ) on RuleName, Program
  | extend Type = strcat(Type, ": ", Source)
  | summarize count() , StartTime= min(TimeGenerated), EndTime=max(TimeGenerated) by Type, Computer, User, Process, RuleName, Program, CommandLineParsed = tostring(CommandLineParsed), ParentImage
  | extend timestamp = StartTime, AccountCustomEntity = User, HostCustomEntity = Computer
  )
  )
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostCustomEntity

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.