Detection rules › Kusto

Detect presence of private IP addresses in URLs (ASIM Web Session)

Status
available
Severity
medium
Time window
1h
Group by
SrcHostname, SrcIpAddr, SrcUsername, extracted_encoded_ip_inURL, ip_inURL
Source
github.com/Azure/Azure-Sentinel

'This rule identifies requests made to atypical URLs, as malware can exploit IP addresses for communication with command-and-control (C2) servers. The detection identifies network requests that contain either plain text or Base64 encoded IP addresses. Alerts are triggered when a private IP address is observed as plain text or base64 encoded in an outbound web request. This method of concealing the IP address was observed in the utilization of the RunningRAT tool by POLONIUM.'

MITRE ATT&CK coverage

Rule body kusto

id: e3a7722a-e099-45a9-9afb-6618e8f05405
name: Detect presence of private IP addresses in URLs (ASIM Web Session)
description: |
  'This rule identifies requests made to atypical URLs, as malware can exploit IP addresses for communication with command-and-control (C2) servers. The detection identifies network requests that contain either plain text or Base64 encoded IP addresses. Alerts are triggered when a private IP address is observed as plain text or base64 encoded in an outbound web request. This method of concealing the IP address was observed in the utilization of the RunningRAT tool by POLONIUM.'
severity: Medium
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - CommandAndControl
relevantTechniques:
  - T1041
  - T1071.001
  - T1001
query: |
  let lookback = 1h;
  // 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),){6,14}(?: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))";
  let ipV4_Private_FromPlainString = _Im_WebSession(starttime=ago(lookback), eventresult='Success')
      | where isnotempty(Url)
      | extend ip_inURL = extract(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", 0, Url)
      | where ipv4_is_private(ip_inURL)
      | where not(ipv4_is_private(DstIpAddr)) // only take traffic going to internet
      | summarize
          EventCount=count(),
          EventEndTime = max(TimeGenerated),
          EventStartTime = min(TimeGenerated),
          Urls=tostring(make_set(Url, 100))
          by SrcIpAddr, SrcUsername, SrcHostname, ip_inURL;
  let ipV4_Private_FromEncodedString = 
      _Im_WebSession(starttime=ago(lookback), eventresult='Success')
      | where isnotempty(Url)
      // Identify requests with encoded IPv4 addresses
      | where Url matches regex ipv4_encoded_identification_regex
      | where not(ipv4_is_private(DstIpAddr)) // only take traffic going to internet
      | project TimeGenerated, Url, SrcIpAddr, SrcUsername, SrcHostname
      // Extract IP candidates in their base64 encoded format, significantly reducing the dataset
      | extend extracted_encoded_ip_inURL = extract_all(ipv4_encoded_identification_regex, Url)
      // We could have more than one ip, expand them out
      | mv-expand extracted_encoded_ip_inURL to typeof(string)
      | summarize
          EventStartTime=min(TimeGenerated),
          EventEndTime=max(TimeGenerated),
          make_set(Url, 100),
          EventCount=count()
          by extracted_encoded_ip_inURL, SrcIpAddr, SrcUsername, SrcHostname
      // Pad if we need to
      | extend extracted_encoded_ip_inURL = iff(strlen(extracted_encoded_ip_inURL) % 2 == 0, extracted_encoded_ip_inURL, strcat(extracted_encoded_ip_inURL, "="))
      // 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_inURL = tostring(base64_decode_toarray(extracted_encoded_ip_inURL))
      // Extract the IP candidates from the array
      | extend hex_extracted = extract_all(ipv4_decoded_hex_extract, extracted_encoded_ip_inURL)
      // 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)
      // Convert the array back into a string
      | extend decoded_ip_inURL = unicode_codepoints_to_string(hex_extracted)
      | where ipv4_is_private(decoded_ip_inURL)
      | project
          ip_inURL=decoded_ip_inURL,
          Urls=tostring(set_Url),
          EventStartTime,
          EventEndTime,
          EventCount,
          SrcIpAddr,
          SrcUsername,
          SrcHostname;
  union ipV4_Private_FromPlainString, ipV4_Private_FromEncodedString
  | extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ip_inURL
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventCount: EventCount
  Urls: Urls
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
alertDetailsOverride:
  alertDisplayNameFormat: "Detected a private ip address '{{ip_inURL}}' carved in URL"
  alertDescriptionFormat: "User '{{SrcUsername}}' has been detected requesting URL '{{Urls}}' that contains private IP address '{{ip_inURL}}'. Encoding private IP addresses in a URL can be a method used by attackers to exfiltrate data from a compromised system"
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 1h;

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),){6,14}(?: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))";

Let binding: ipV4_Private_FromPlainString

let ipV4_Private_FromPlainString = _Im_WebSession(starttime=ago(lookback), eventresult='Success')
    | where isnotempty(Url)
    | extend ip_inURL = extract(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", 0, Url)
    | where ipv4_is_private(ip_inURL)
    | where not(ipv4_is_private(DstIpAddr))
    | summarize
        EventCount=count(),
        EventEndTime = max(TimeGenerated),
        EventStartTime = min(TimeGenerated),
        Urls=tostring(make_set(Url, 100))
        by SrcIpAddr, SrcUsername, SrcHostname, ip_inURL;

Derived from lookback.

Let binding: ipV4_Private_FromEncodedString

let ipV4_Private_FromEncodedString = _Im_WebSession(starttime=ago(lookback), eventresult='Success')
    | where isnotempty(Url)
    | where Url matches regex ipv4_encoded_identification_regex
    | where not(ipv4_is_private(DstIpAddr))
    | project TimeGenerated, Url, SrcIpAddr, SrcUsername, SrcHostname
    | extend extracted_encoded_ip_inURL = extract_all(ipv4_encoded_identification_regex, Url)
    | mv-expand extracted_encoded_ip_inURL to typeof(string)
    | summarize
        EventStartTime=min(TimeGenerated),
        EventEndTime=max(TimeGenerated),
        make_set(Url, 100),
        EventCount=count()
        by extracted_encoded_ip_inURL, SrcIpAddr, SrcUsername, SrcHostname
    | extend extracted_encoded_ip_inURL = iff(strlen(extracted_encoded_ip_inURL) % 2 == 0, extracted_encoded_ip_inURL, strcat(extracted_encoded_ip_inURL, "="))
    | extend extracted_encoded_ip_inURL = tostring(base64_decode_toarray(extracted_encoded_ip_inURL))
    | extend hex_extracted = extract_all(ipv4_decoded_hex_extract, extracted_encoded_ip_inURL)
    | mv-expand hex_extracted
    | extend hex_extracted = trim_end(",", tostring(hex_extracted))
    | extend hex_extracted = strcat("[", hex_extracted, "]")
    | extend hex_extracted = todynamic(hex_extracted)
    | extend decoded_ip_inURL = unicode_codepoints_to_string(hex_extracted)
    | where ipv4_is_private(decoded_ip_inURL)
    | project
        ip_inURL=decoded_ip_inURL,
        Urls=tostring(set_Url),
        EventStartTime,
        EventEndTime,
        EventCount,
        SrcIpAddr,
        SrcUsername,
        SrcHostname;

Derived from lookback, ipv4_encoded_identification_regex, ipv4_decoded_hex_extract.

union (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: ipV4_Private_FromPlainString, ipV4_Private_FromEncodedString

Leg 1: ipV4_Private_FromPlainString

Leg 2: ipV4_Private_FromEncodedString

Applied to the combined result

| extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")

Exclusions

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

FieldKindExcluded values
DstIpAddrcidr_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
DstIpAddrcidr_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
Urlis_not_null
  • (no value, null check)
Urlregex_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\/+\=]*)
decoded_ip_inURLcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
ip_inURLcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16

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
EventCountproject
EventEndTimeproject
EventStartTimeproject
SrcHostnameproject
SrcIpAddrproject
SrcUsernameproject
Urlsproject
ip_inURLproject
Nameextend
UPNSuffixextend