Detection rules › Elastic

Kubernetes Pod Exec with Curl or Wget to HTTPS

Status
production
Severity
high
Time window
6m
Author
Elastic
Source
github.com/elastic/detection-rules

Detects pod or attach exec API calls where the decoded request query implies curl or wget fetching an https URL. Attackers with permission to exec into workloads often run one-liners to stage tooling, pull scripts or binaries, or exfiltrate data over HTTPS—activity that should be rare compared to shells, debuggers, or expected health checks. The rule decodes the audit requestURI, reconstructs a readable command string from repeated command parameters, and applies noise filters for common cluster health and OIDC/JWKS endpoints so benign automation is less likely to alert.

MITRE ATT&CK coverage

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body elastic

[metadata]
creation_date = "2026/04/23"
integration = ["kubernetes"]
maturity = "production"
updated_date = "2026/04/23"

[rule]
author = ["Elastic"]
description = """
Detects pod or attach exec API calls where the decoded request query implies **curl** or wget fetching an
**https** URL. Attackers with permission to exec into workloads often run one-liners to stage tooling, pull
scripts or binaries, or exfiltrate data over HTTPS—activity that should be rare compared to shells, debuggers, or
expected health checks. The rule decodes the audit requestURI, reconstructs a readable command string from
repeated command parameters, and applies **noise filters** for common cluster health and OIDC/JWKS endpoints so
benign automation is less likely to alert.
"""
from = "now-6m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "Kubernetes Pod Exec with Curl or Wget to HTTPS"
note = """## Triage and analysis

### Investigating Kubernetes Pod Exec with Curl or Wget to HTTPS

Kubernetes audit logs record exec (and similar attach) calls on requestURI, including URL-encoded
command segments. This rule URL-decodes the URI, extracts the query portion into a single string, and 
flags curl or wget combined with https, excluding several ommon health, localhost, and OIDC/JWKS patterns.

### Possible investigation steps

- Confirm who may exec into the target namespace: review kubernetes.audit.user.username, groups, impersonation, and
  source.ip / user_agent.original (kubectl, CI, webhooks).
- Map the pod (kubernetes.audit.objectRef.name) and workload owner; retrieve the exact decoded URI from
  Esql.decoded_uri and the reconstructed Esql.command in the alert.
- Search for adjacent audit events from the same identity: secret reads, additional execs, RBAC changes, or anonymous
  access.
- If malicious, revoke credentials used for exec, review RoleBindings for **`pods/exec`**, and inspect the pod
  filesystem or snapshot for dropped artifacts.

### False positive analysis

- Approved runbooks or support sessions may use kubectl exec with curl/wget to test egress or download vendor tools;
  document break-glass identities and tune exclusions.
- Some cluster components use HTTPS to **kubernetes.default.svc** or **.well-known** endpoints; the rule attempts to
  filter those—expand the exclusion list if your platform uses additional first-party URLs.

### Response and remediation

- Rotate any secrets accessible from the pod, cordon or delete the workload if compromised, and tighten RBAC so only
  required principals retain **`pods/exec`** on sensitive namespaces.
"""
references = [
    "https://attack.mitre.org/techniques/T1609/",
    "https://attack.mitre.org/techniques/T1105/",
]
risk_score = 73
rule_id = "c9d4e8f1-2a3b-4c5d-8e9f-0a1b2c3d4e5f"
severity = "high"
tags = [
    "Data Source: Kubernetes",
    "Domain: Kubernetes",
    "Use Case: Threat Detection",
    "Tactic: Execution",
    "Tactic: Command and Control",
    "Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
FROM logs-kubernetes.audit_logs-* metadata _id, _index, _version
| WHERE kubernetes.audit.objectRef.subresource == "exec"
  AND kubernetes.audit.requestURI LIKE "*command=*"
| EVAL decoded_uri = URL_DECODE(kubernetes.audit.requestURI)
| GROK decoded_uri "%{DATA}/exec\\?%{DATA:raw_commands}&(?:container|stdin|stdout|stderr)=%{GREEDYDATA}"
| EVAL command = REPLACE(raw_commands, "command=", "")
| EVAL command = REPLACE(command, "&", " ")
| EVAL Esql.executed_command = REPLACE(command, "\\+", " ")
| WHERE Esql.executed_command IS NOT NULL 
  AND Esql.executed_command RLIKE """.*(curl.*https|wget.*https).*"""
  AND NOT Esql.executed_command RLIKE """.*(/api/v1/health|/healthz|/readyz|/livez|127\.0\.0\.1|localhost|/openid/v1/jwks|/openid-connect/certs|/.well-known/openid-configuration|/.well-known/jwks\.json|kubernetes\.default\.svc).*"""
| KEEP *
'''

[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1609"
name = "Container Administration Command"
reference = "https://attack.mitre.org/techniques/T1609/"

[rule.threat.tactic]
id = "TA0002"
name = "Execution"
reference = "https://attack.mitre.org/tactics/TA0002/"

[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1105"
name = "Ingress Tool Transfer"
reference = "https://attack.mitre.org/techniques/T1105/"

[rule.threat.tactic]
id = "TA0011"
name = "Command and Control"
reference = "https://attack.mitre.org/tactics/TA0011/"

Stages and Predicates

Stage 1: from

FROM logs-kubernetes.audit_logs-* metadata _id, _index, _version

Stage 2: where

| WHERE kubernetes.audit.objectRef.subresource == "exec"
  AND kubernetes.audit.requestURI LIKE "*command=*"

Stage 3: eval

| EVAL decoded_uri = URL_DECODE(kubernetes.audit.requestURI)

Stage 4: grok

| GROK decoded_uri "%{DATA}/exec\\?%{DATA:raw_commands}&(?:container|stdin|stdout|stderr)=%{GREEDYDATA}"

Stage 5: eval

| EVAL command = REPLACE(raw_commands, "command=", "")

Stage 6: eval

| EVAL command = REPLACE(command, "&", " ")

Stage 7: eval

| EVAL Esql.executed_command = REPLACE(command, "\\+", " ")

Stage 8: where

| WHERE Esql.executed_command IS NOT NULL 
  AND Esql.executed_command RLIKE """.*(curl.*https|wget.*https).*"""
  AND NOT Esql.executed_command RLIKE """.*(/api/v1/health|/healthz|/readyz|/livez|127\.0\.0\.1|localhost|/openid/v1/jwks|/openid-connect/certs|/.well-known/openid-configuration|/.well-known/jwks\.json|kubernetes\.default\.svc).*"""

Stage 9: keep

| KEEP *

Exclusions

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

FieldKindExcluded values
Esql.executed_commandregex_match.*(/api/v1/health|/healthz|/readyz|/livez|127.0.0.1|localhost|/openid/v1/jwks|/openid-connect/certs|/.well-known/openid-configuration|/.well-known/jwks.json|kubernetes.default.svc).*

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
Esql.executed_commandis_not_null
  • (no value, null check)
Esql.executed_commandregex_match
  • .*(curl.*https|wget.*https).*
kubernetes.audit.objectRef.subresourceeq
  • exec
kubernetes.audit.requestURIwildcard
  • *command=*

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
*KEEP *