Detection rules › Kusto

IP address of Windows host encoded in web request

Severity
medium
Time window
1d
Group by
End, RemoteIP, Start, decoded_ip_candidate, extracted_encoded_ip_candidate, ipmatch
Author
Thomas McElroy
Source
github.com/Azure/Azure-Sentinel

'This detection will identify network requests in HTTP proxy data that contains Base64 encoded IP addresses. After identifying candidates the query joins with DeviceNetworkEvents to idnetify any machine within the network using that IP address. Alerts indicate that the IP address of a machine within your network was seen with it's IP address base64 encoded in an outbound web request. This method of egressing the IP was seen used in POLONIUM's RunningRAT tool, however the detection is generic.'

MITRE ATT&CK coverage

Rule body kusto

id: a4ce20ae-a2e4-4d50-b40d-d49f1353b6cc
name: IP address of Windows host encoded in web request
description: |
  'This detection will identify network requests in HTTP proxy data that contains Base64 encoded IP addresses. After identifying candidates the query joins with DeviceNetworkEvents to idnetify any machine within the network using that IP address. Alerts indicate that the IP address of a machine within your network was seen with it's IP address base64 encoded in an outbound web request. This method of egressing the IP was seen used in POLONIUM's RunningRAT tool, however the detection is generic.'
severity: Medium
requiredDataConnectors:
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - CommandAndControl
relevantTechniques:
  - T1041
  - T1071.001
tags:
  - POLONIUM
query: |
  // Extracts plaintext IPv4 addresses
  let ipv4_plaintext_extraction_regex = @"((?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.)){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]){1,3})";
  // Identified base64 encoded IPv4 addresses
  let ipv4_encoded_identification_regex = @"\=([a-zA-Z0-9\/\+]*(?:(?:MC|Au|wL|MS|Eu|xL|Mi|Iu|yL|My|Mu|zL|NC|Qu|0L|NS|Uu|1L|Ni|Yu|2L|Ny|cu|3L|OC|gu|4L|OS|ku|5L){1}[a-zA-Z0-9\/\+]{2,4}){3}[a-zA-Z0-9\/\+\=]*)";
  // Extractes IPv4 addresses as hex values
  let ipv4_decoded_hex_extract = @"((?:(?:61|62|63|64|65|66|67|68|69|6a|6b|6c|6d|6e|6f|70|71|72|73|74|75|76|77|78|79|7a|41|42|43|44|45|46|47|48|49|4a|4b|4c|4d|4e|4f|50|51|52|53|54|55|56|57|58|59|5a|2f|2b|3d),){7,15})";
  CommonSecurityLog
  | where isnotempty(RequestURL)
  // Identify requests with encoded IPv4 addresses
  | where RequestURL matches regex ipv4_encoded_identification_regex
  | project TimeGenerated, RequestURL
  // Extract IP candidates in their base64 encoded format, significantly reducing the dataset
  | extend extracted_encoded_ip_candidate = extract_all(ipv4_encoded_identification_regex, RequestURL)
  // We could have more than one candidate, expand them out
  | mv-expand extracted_encoded_ip_candidate to typeof(string)
  | summarize Start=min(TimeGenerated), End=max(TimeGenerated), make_set(RequestURL) by extracted_encoded_ip_candidate
  // Pad if we need to
  | extend extracted_encoded_ip_candidate = iff(strlen(extracted_encoded_ip_candidate) % 2 == 0, extracted_encoded_ip_candidate, strcat(extracted_encoded_ip_candidate, "="))
  // Now decode the candidate to a long array, we cannot go straight to string as it cannot handle non-UTF8, we need to strip that first
  | extend extracted_encoded_ip_candidate = tostring(base64_decode_toarray(extracted_encoded_ip_candidate))
  // Extract the IP candidates from the array
  | extend hex_extracted = extract_all(ipv4_decoded_hex_extract, extracted_encoded_ip_candidate)
  // Expand, it's still possible that we might have more than 1 IP
  | mv-expand hex_extracted
  // Now we should have a clean string. We need to put it back into a dynamic array to convert back to a string.
  | extend hex_extracted = trim_end(",", tostring(hex_extracted))
  | extend hex_extracted = strcat("[",hex_extracted,"]")
  | extend hex_extracted = todynamic(hex_extracted)
  | extend extracted_encoded_ip_candidate = todynamic(extracted_encoded_ip_candidate)
  // Convert the array back into a string
  | extend decoded_ip_candidate = make_string(hex_extracted)
  | summarize by decoded_ip_candidate, tostring(set_RequestURL), Start, End
  // Now the IP candidates will be in plaintext, extract the IPs using a regex
  | extend ipmatch = extract_all(ipv4_plaintext_extraction_regex, decoded_ip_candidate)
  // If it's not an IP, throw it out
  | where isnotnull(ipmatch)
  | mv-expand ipmatch to typeof(string)
  // Join with DeviceNetworkEvents to find instances where an IP of a machine in our MDE estate sent it's IP in a base64 encoded string
  | join (
      DeviceNetworkEvents
      | summarize make_set(DeviceId), make_set(DeviceName) by RemoteIP
  ) on $left.ipmatch == $right.RemoteIP
  | project Start, End, IPmatch=ipmatch, RequestURL=set_RequestURL, DeviceNames=set_DeviceName, DeviceIds=set_DeviceId, RemoteIP
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPmatch
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceNames
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
version: 1.0.2
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Thomas McElroy
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Networking" ]

Stages and Predicates

Let binding: ipv4_plaintext_extraction_regex

let ipv4_plaintext_extraction_regex = @"((?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\.)){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]){1,3})";

Let binding: ipv4_encoded_identification_regex

let ipv4_encoded_identification_regex = @"\=([a-zA-Z0-9\/\+]*(?:(?:MC|Au|wL|MS|Eu|xL|Mi|Iu|yL|My|Mu|zL|NC|Qu|0L|NS|Uu|1L|Ni|Yu|2L|Ny|cu|3L|OC|gu|4L|OS|ku|5L){1}[a-zA-Z0-9\/\+]{2,4}){3}[a-zA-Z0-9\/\+\=]*)";

Let binding: ipv4_decoded_hex_extract

let ipv4_decoded_hex_extract = @"((?:(?:61|62|63|64|65|66|67|68|69|6a|6b|6c|6d|6e|6f|70|71|72|73|74|75|76|77|78|79|7a|41|42|43|44|45|46|47|48|49|4a|4b|4c|4d|4e|4f|50|51|52|53|54|55|56|57|58|59|5a|2f|2b|3d),){7,15})";

Stage 1: source

CommonSecurityLog

Stage 2: where

| where isnotempty(RequestURL)

Stage 3: where

| where RequestURL matches regex ipv4_encoded_identification_regex

References ipv4_encoded_identification_regex (defined above).

Stage 4: project

| project TimeGenerated, RequestURL

Stage 5: extend

| extend extracted_encoded_ip_candidate = extract_all(ipv4_encoded_identification_regex, RequestURL)

References ipv4_encoded_identification_regex (defined above).

Stage 6: mv-expand

| mv-expand extracted_encoded_ip_candidate to typeof(string)

Stage 7: summarize

| summarize Start=min(TimeGenerated), End=max(TimeGenerated), make_set(RequestURL) by extracted_encoded_ip_candidate

Stage 8: extend (3 consecutive steps)

| extend extracted_encoded_ip_candidate = iff(strlen(extracted_encoded_ip_candidate) % 2 == 0, extracted_encoded_ip_candidate, strcat(extracted_encoded_ip_candidate, "="))
| extend extracted_encoded_ip_candidate = tostring(base64_decode_toarray(extracted_encoded_ip_candidate))
| extend hex_extracted = extract_all(ipv4_decoded_hex_extract, extracted_encoded_ip_candidate)

References ipv4_decoded_hex_extract (defined above).

extracted_encoded_ip_candidate =
if/* macro: ((strlen(extracted_encoded_ip_candidate) % 2) == 0) */extracted_encoded_ip_candidate
elsestrcat(extracted_encoded_ip_candidate, "=")

Stage 9: mv-expand

| mv-expand hex_extracted

Stage 10: extend (5 consecutive steps)

| extend hex_extracted = trim_end(",", tostring(hex_extracted))
| extend hex_extracted = strcat("[",hex_extracted,"]")
| extend hex_extracted = todynamic(hex_extracted)
| extend extracted_encoded_ip_candidate = todynamic(extracted_encoded_ip_candidate)
| extend decoded_ip_candidate = make_string(hex_extracted)

Stage 11: summarize

| summarize by decoded_ip_candidate, tostring(set_RequestURL), Start, End

Stage 12: extend

| extend ipmatch = extract_all(ipv4_plaintext_extraction_regex, decoded_ip_candidate)

References ipv4_plaintext_extraction_regex (defined above).

Stage 13: where

| where isnotnull(ipmatch)

Stage 14: mv-expand

| mv-expand ipmatch to typeof(string)

Stage 15: join

| join (
    DeviceNetworkEvents
    | summarize make_set(DeviceId), make_set(DeviceName) by RemoteIP
) on $left.ipmatch == $right.RemoteIP

Stage 16: project

| project Start, End, IPmatch=ipmatch, RequestURL=set_RequestURL, DeviceNames=set_DeviceName, DeviceIds=set_DeviceId, RemoteIP

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
RequestURLis_not_null
  • (no value, null check)
RequestURLregex_match
  • \=([a-zA-Z0-9\/+]*(?:(?:MC|Au|wL|MS|Eu|xL|Mi|Iu|yL|My|Mu|zL|NC|Qu|0L|NS|Uu|1L|Ni|Yu|2L|Ny|cu|3L|OC|gu|4L|OS|ku|5L){1}[a-zA-Z0-9\/+]{2,4}){3}[a-zA-Z0-9\/+\=]*)
ipmatchis_not_null
  • (no value, null check)

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
DeviceIdsproject
DeviceNamesproject
Endproject
IPmatchproject
RemoteIPproject
RequestURLproject
Startproject