LoFP LoFP / misconfigured phones, expired credentials, or provisioning errors can generate repeated register failures from a single device. validate the client, affected extensions, and registration state before escalating.

Techniques

Sample rules

Potential SIP REGISTER Brute Force

Description

Identifies repeated SIP REGISTER authentication rejection responses for one or more extensions from a client to a VoIP server within five minutes. The rule distinguishes repeated failures from the single 401 or 407 challenge expected in a normal digest-authentication flow. Attackers brute-force extension credentials to register rogue endpoints for toll fraud, call interception, or registration hijacking.

Detection logic

from logs-network_traffic.sip-*, packetbeat-* metadata _source
| eval
    Esql.method = TO_UPPER(COALESCE(
        JSON_EXTRACT(_source, "network_traffic.sip.cseq.method"),
        JSON_EXTRACT(_source, "sip.cseq.method"),
        JSON_EXTRACT(_source, "network_traffic.sip.method"),
        JSON_EXTRACT(_source, "sip.method")
    )),
    Esql.sip_type = TO_LOWER(COALESCE(
        JSON_EXTRACT(_source, "network_traffic.sip.type"),
        JSON_EXTRACT(_source, "sip.type")
    )),
    Esql.code = COALESCE(
        JSON_EXTRACT(_source, "network_traffic.sip.code"),
        JSON_EXTRACT(_source, "sip.code")
    ),
    Esql.client_ip = COALESCE(client.ip, destination.ip),
    Esql.server_ip = COALESCE(server.ip, source.ip),
    Esql.extension = COALESCE(
        JSON_EXTRACT(_source, "network_traffic.sip.to.uri.username"),
        JSON_EXTRACT(_source, "sip.to.uri.username")
    )
| where
    Esql.method == "REGISTER" and
    Esql.sip_type == "response" and
    Esql.code in ("401", "403", "407") and
    Esql.client_ip is not null and
    Esql.server_ip is not null and
    Esql.extension is not null
| eval Esql.time_window = DATE_TRUNC(5 minutes, @timestamp)
| stats
    Esql.failures_per_extension = COUNT(*)
  by Esql.time_window, Esql.client_ip, Esql.server_ip, Esql.extension
| eval Esql.repeated_extension = CASE(Esql.failures_per_extension >= 2, 1, 0)
| stats
    Esql.total_failures = SUM(Esql.failures_per_extension),
    Esql.max_failures_per_extension = MAX(Esql.failures_per_extension),
    Esql.distinct_extensions = COUNT_DISTINCT(Esql.extension),
    Esql.repeated_extensions = SUM(Esql.repeated_extension),
    Esql.sample_extensions = MV_SLICE(VALUES(Esql.extension), 0, 20)
  by Esql.time_window, Esql.client_ip, Esql.server_ip
| where
    Esql.max_failures_per_extension >= 10 or
    (
      Esql.total_failures >= 25 and
      Esql.distinct_extensions >= 5 and
      Esql.repeated_extensions >= 5
    )
| keep Esql.*