-
Notifications
You must be signed in to change notification settings - Fork 674
[New Rule] Splunk Enterprise PostgreSQL Sidecar Pre-Auth RCE (CVE-2026-20253) #6279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0762910
8686202
8837033
5a8af3b
6a61854
92f205b
792836b
df52659
78b1c93
c388e00
f5367bb
c2a9d42
24c1081
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| [metadata] | ||
| creation_date = "2026/06/15" | ||
| integration = ["network_traffic", "zeek", "suricata"] | ||
| maturity = "production" | ||
| updated_date = "2026/06/24" | ||
|
|
||
| [rule] | ||
| author = ["Elastic"] | ||
| description = """ | ||
| Detects a POST to the Splunk Enterprise PostgreSQL backup endpoint followed by a POST to the restore endpoint from the | ||
| same client to the same host within a 15-minute window. This sequence is unusual and can align with the public | ||
| CVE-2026-20253 pre-authentication RCE chain, where an attacker stages a database dump via the backup path and executes | ||
| attacker-controlled SQL via the restore path. | ||
| """ | ||
| false_positives = [ | ||
| """ | ||
| Legitimate PostgreSQL recovery operations performed by Splunk administrators through the backup and restore API. | ||
| These should be rare and originate from known management networks. If such operations occur in your environment, | ||
| scope exceptions by source IP or approved management network rather than suppressing the rule entirely. | ||
| """, | ||
| ] | ||
| from = "now-19m" | ||
| language = "esql" | ||
| license = "Elastic License v2" | ||
| max_signals = 5 | ||
| name = "Splunk Enterprise PostgreSQL Backup-to-Restore Potential RCE Sequence" | ||
| note = """## Triage and analysis | ||
|
|
||
| > **Disclaimer**: | ||
| > This investigation guide was created using generative AI technology and has been reviewed to improve its accuracy and relevance. While every effort has been made to ensure its quality, we recommend validating the content and adapting it to suit your specific environment and operational needs. | ||
|
|
||
| ### Investigating Splunk Enterprise PostgreSQL Backup-to-Restore Potential RCE Sequence | ||
|
|
||
| This rule fires when the same source IP sends a POST to both the `/backup` and `/restore` recovery | ||
| endpoints on the same Splunk host within 15 minutes. This two-step sequence is unusual and aligns with | ||
| the watchTowr CVE-2026-20253 RCE chain: the backup request can place an attacker-controlled | ||
| PostgreSQL dump on the Splunk filesystem (using `backupFile` path traversal or absolute path | ||
| injection), and the restore request can load that dump and execute attacker-controlled SQL through the | ||
| local PostgreSQL instance. A backup-plus-restore pair from an unrecognized source IP on a production | ||
| Splunk host should be investigated as potential exploitation. | ||
|
|
||
| ### Possible investigation steps | ||
|
|
||
| - Review `http.response.status_code` for both requests. A `400` on the backup step indicates the | ||
| sidecar handler was reached (the request parsed but failed), consistent with a vulnerable host. | ||
| - Check whether the backup request body (if captured by a WAF or proxy) contains `hostaddr=`, | ||
| `host=`, or `passfile=` in the `database` field, or path traversal (`../`) or absolute paths | ||
| in the `backupFile` field. These are the connection-string injection and file-placement artifacts | ||
| specific to this exploit chain. | ||
| - Correlate with host telemetry on the Splunk server: look for new or modified files under | ||
| `/opt/splunk/etc/apps/`, `/opt/splunk/var/packages/`, or `/tmp/` around the time of the requests. | ||
| - Check for Splunk process execution of `pg_dump` or `pg_restore` with unusual arguments, | ||
| particularly connection strings containing external hostnames or IP addresses. | ||
| - Check for outbound network connections from the Splunk host to external PostgreSQL services | ||
| (port 5432 or similar) following the backup request — this indicates successful connection-string | ||
| injection causing the server to pivot to an attacker-controlled database. | ||
| - Verify whether the target Splunk host is running an affected version (10.0.0–10.0.6 or | ||
| 10.2.0–10.2.3). Splunk Enterprise 10.4 and Splunk Cloud are not affected. | ||
|
|
||
| ### False positive analysis | ||
|
|
||
| - Authorized administrative recovery operations using the sidecar API. These should originate from | ||
| known management IPs; create exceptions for approved management network ranges. | ||
| - Red-team or vulnerability management tooling that runs a full backup-to-restore probe as part of | ||
| a CVE-2026-20253 exposure check. | ||
|
|
||
| ### Response and remediation | ||
|
|
||
| - Treat a confirmed backup-and-restore sequence from an unrecognized source as active exploitation. | ||
| Isolate the Splunk host from the network immediately and preserve forensic state. | ||
| - Examine the Splunk host for new or modified files, scheduled tasks, and PostgreSQL extension | ||
| objects that may have been placed by the restore step. | ||
| - Patch Splunk Enterprise to an unaffected version (SVD-2026-0603). There is no vendor-provided | ||
| workaround; patching is the only mitigation. | ||
| - Block Splunk Web port (default 8000) at the perimeter and restrict access to management networks | ||
| while patching is in progress. | ||
| """ | ||
| references = [ | ||
| "https://nvd.nist.gov/vuln/detail/CVE-2026-20253", | ||
| "https://advisory.splunk.com/advisories/SVD-2026-0603", | ||
| "https://labs.watchtowr.com/why-use-app-level-auth-when-every-database-has-auth-splunk-enterprise-cve-2026-20253-pre-auth-rce/", | ||
| "https://attack.mitre.org/techniques/T1190/", | ||
| ] | ||
| risk_score = 47 | ||
| rule_id = "7c7d2a89-b7e9-4e8d-bbf2-5a782fdcc803" | ||
| setup = """## Setup | ||
|
|
||
| This rule requires HTTP request metadata with `url.path` and `http.request.method` populated from | ||
| one of the following sources visible to the sensor without TLS decryption: | ||
| - Zeek (`logs-zeek.http*`) where Splunk Web traffic is cleartext or the sensor is downstream of TLS | ||
| termination. | ||
| - Suricata (`logs-suricata.eve*`) in the same deployment conditions | ||
| - Elastic Agent `network_traffic` integration (`logs-network_traffic.http*`) with HTTP parsing enabled | ||
|
|
||
| Splunk Web listens on TCP port 8000 by default (`web.conf` `httpport`), which is included in the | ||
| default Network Packet Capture/Packetbeat HTTP port list. Add any custom Splunk Web `httpport` value | ||
| to the HTTP protocol configuration. Splunk's management service defaults to TCP port 8089 | ||
| (`mgmtHostPort`) and commonly uses TLS; add 8089 only if management/API traffic is directly exposed or | ||
| visible to the sensor after decryption. If the PostgreSQL sidecar is directly exposed or monitored | ||
| locally on TCP port 5435, add port 5435 as an HTTP port as well. Zeek and Suricata can identify | ||
| plaintext HTTP on non-standard ports through protocol detection when their HTTP analyzers are enabled. | ||
| For TLS deployments, the sensor must observe decrypted HTTP, sit downstream of TLS termination, or use | ||
| proxy or load balancer logs that expose the HTTP path, method, and status code. | ||
|
|
||
| The rule uses a 19-minute lookback and verifies that the first and last matching recovery events are | ||
| no more than 15 minutes apart. Ensure `event.ingested` is populated and `timestamp_override` is set. | ||
| """ | ||
| severity = "medium" | ||
| tags = [ | ||
| "Domain: Network", | ||
| "Use Case: Threat Detection", | ||
| "Use Case: Vulnerability", | ||
| "Use Case: Network Security Monitoring", | ||
| "Tactic: Initial Access", | ||
| "Data Source: Network Packet Capture", | ||
| "Data Source: Network Traffic", | ||
| "Data Source: Zeek", | ||
| "Data Source: Suricata", | ||
| "Resources: Investigation Guide", | ||
| ] | ||
| timestamp_override = "event.ingested" | ||
| type = "esql" | ||
|
|
||
| query = ''' | ||
| from logs-network_traffic.http*, logs-zeek.http*, logs-suricata.eve* | ||
| | where http.request.method == "POST" | ||
| and ( | ||
| url.path like "*splunkd/__raw/v1/postgres/recovery/*" or | ||
| url.path like "/v1/postgres/recovery/*" | ||
| ) | ||
| | eval Esql.is_backup = case(url.path like "*/backup", 1, 0) | ||
| | eval Esql.is_restore = case(url.path like "*/restore", 1, 0) | ||
| | stats | ||
| Esql.backup_count = SUM(Esql.is_backup), | ||
| Esql.restore_count = SUM(Esql.is_restore), | ||
| Esql.first_seen = MIN(@timestamp), | ||
| Esql.last_seen = MAX(@timestamp), | ||
| Esql.statuses = VALUES(http.response.status_code) | ||
| by source.ip, destination.ip | ||
| | eval Esql.duration_minutes = DATE_DIFF("minute", Esql.first_seen, Esql.last_seen) | ||
| | where Esql.backup_count >= 1 and Esql.restore_count >= 1 | ||
| and Esql.duration_minutes <= 15 | ||
| | keep source.ip, destination.ip, Esql.* | ||
| ''' | ||
|
|
||
|
|
||
| [[rule.threat]] | ||
| framework = "MITRE ATT&CK" | ||
| [[rule.threat.technique]] | ||
| id = "T1190" | ||
| name = "Exploit Public-Facing Application" | ||
| reference = "https://attack.mitre.org/techniques/T1190/" | ||
|
|
||
|
|
||
| [rule.threat.tactic] | ||
| id = "TA0001" | ||
| name = "Initial Access" | ||
| reference = "https://attack.mitre.org/tactics/TA0001/" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| [metadata] | ||
| creation_date = "2026/06/15" | ||
| integration = ["endpoint", "network_traffic", "zeek", "suricata", "azure", "gcp"] | ||
| maturity = "production" | ||
| updated_date = "2026/06/15" | ||
|
|
||
| [rule] | ||
| author = ["Elastic"] | ||
| description = """ | ||
| Detects CVE-2026-20253 exploit artifacts against the Splunk Enterprise PostgreSQL sidecar recovery endpoints via | ||
| complementary signals. Where endpoint or Network Packet Capture request-body logging is available, the rule matches | ||
| PostgreSQL connection-string injection keywords, suspicious `backupFile` destinations, and known filesystem artifacts | ||
| used to pivot from backup/restore primitives to file write or RCE. It also detects vulnerable recovery endpoint probing | ||
| and empty-password Basic auth credentials observed in public exploit tooling. | ||
| """ | ||
| false_positives = [ | ||
| """ | ||
| Authorized red-team or penetration testing tooling exercising the CVE-2026-20253 exploit chain. Legitimate Splunk | ||
| user activity should not produce PostgreSQL connection-string keywords, suspicious filesystem targets, empty Basic | ||
| auth credentials, or unauthenticated 400 responses on the recovery endpoints. | ||
| """, | ||
| ] | ||
| from = "now-9m" | ||
| index = [ | ||
| "logs-endpoint.events.network*", | ||
| "logs-network_traffic.http*", | ||
| "logs-zeek.http*", | ||
| "logs-suricata.eve*", | ||
| "logs-azure.application_gateway*", | ||
| "logs-gcp.loadbalancing_logs*", | ||
| ] | ||
|
Copilot marked this conversation as resolved.
|
||
| language = "kuery" | ||
| license = "Elastic License v2" | ||
| name = "Splunk Enterprise PostgreSQL Recovery Endpoint Injection Artifacts" | ||
| note = """## Triage and analysis | ||
|
|
||
| > **Disclaimer**: | ||
| > This investigation guide was created using generative AI technology and has been reviewed to improve its accuracy and relevance. While every effort has been made to ensure its quality, we recommend validating the content and adapting it to suit your specific environment and operational needs. | ||
|
|
||
| ### Investigating Splunk Enterprise PostgreSQL Recovery Endpoint Injection Artifacts | ||
|
|
||
| This rule fires on distinct signals from the CVE-2026-20253 exploit chain and closely related recovery endpoint abuse: | ||
|
|
||
| **Body injection** (endpoint/WAF/proxy sources): A POST to the recovery endpoints with a body | ||
| containing PostgreSQL connection-string keywords (`hostaddr=`, `host=`, `port=`, `passfile=`, | ||
| `dbname=`, `user=`, `password=`, `sslmode=`, or `service=`), `backupFile` path traversal, suspicious | ||
| absolute destinations (`/tmp/`, `/var/tmp/`, `/dev/shm/`, `/opt/splunk/etc/apps/`, cron paths, or SSH | ||
| authorized keys), or known filesystem artifacts (`.pgpass`, `/opt/splunk/etc/apps/`). The `database` | ||
| JSON field is passed directly to `pg_dump` or `pg_restore` as a connection string, so | ||
| attacker-supplied keywords override the hardcoded local configuration. The `backupFile` parameter | ||
| controls the dump file path, enabling arbitrary file placement. | ||
|
|
||
| **Auth credential artifact** (Zeek sources): A POST to the recovery endpoints with an empty password | ||
| in the HTTP Basic auth header (`url.password : ""`). The exploit passes the Basic auth username | ||
| directly to `pg_dump` or `pg_restore` as a PostgreSQL username — any value works, so this branch does | ||
| not filter on specific usernames. | ||
|
|
||
| **Vulnerable endpoint probing**: A POST to `/v1/postgres/recovery/backup` or | ||
| `/v1/postgres/recovery/restore` returning HTTP 400. Public probes use this response distinction because | ||
| vulnerable handlers parse the request and return 400, while patched or protected paths return 401. | ||
|
|
||
| ### Possible investigation steps | ||
|
|
||
| - Check `http.response.status_code`. A `400` indicates the request reached the vulnerable sidecar | ||
| handler; a `401` indicates access was blocked or the host is patched. A `400` on the backup or | ||
| restore path is consistent with probing, and a `400` with injection content in the body is a | ||
| high-confidence indicator of active exploitation. | ||
| - Identify which injection artifact triggered the rule: | ||
| - `hostaddr=` or `host=` in `database` → server-side database pivot; check for outbound connections | ||
| from the Splunk host to port 5432 or the attacker-supplied `port=` value. | ||
| - `port=`, `user=`, `password=`, `sslmode=`, or `service=` in `database` → generic libpq connection | ||
| string smuggling; review the full body for remote database or credential manipulation. | ||
| - `passfile=/opt/splunk/var/packages/data/postgres/.pgpass` → local PostgreSQL credential reuse; | ||
| this exact string is the public restore-chain artifact from watchTowr. | ||
| - `backupFile` with `../` traversal or suspicious absolute paths (`/tmp/`, `/var/tmp/`, `/dev/shm/`, | ||
| `/opt/splunk/etc/apps/`, cron paths, or SSH authorized keys) → arbitrary file placement; check for | ||
| new or modified files at those paths. | ||
| - `dbname=template1` with `passfile=` → the published two-stage restore payload. | ||
| - Correlate with host telemetry: new files under `/opt/splunk/etc/apps/`, `/opt/splunk/var/packages/`, | ||
| or `/tmp/` created around the request time. | ||
| - Check for outbound PostgreSQL connections from the Splunk host following any `hostaddr=` or `host=` | ||
| injection (see companion rule "Splunk Enterprise PostgreSQL Backup-to-Restore Potential RCE Sequence"). | ||
| - Verify whether the target Splunk host is running an affected version (10.0.0-10.0.6 or | ||
| 10.2.0-10.2.3). Splunk Enterprise 10.4 and Splunk Cloud are not affected. | ||
|
|
||
| ### False positive analysis | ||
|
|
||
| - No legitimate Splunk user workflow sends PostgreSQL connection-string keywords or filesystem paths | ||
| to these recovery endpoints. This rule has very low false positive potential when the path filter | ||
| is in scope. | ||
|
|
||
| ### Response and remediation | ||
|
|
||
| - Any confirmed body injection on the recovery endpoints should be treated as active exploitation. | ||
| Isolate the Splunk host immediately and preserve forensic state. | ||
| - Check for files placed by `backupFile` traversal and SQL objects loaded by the restore step. | ||
| - Patch Splunk Enterprise to an unaffected version. There is no vendor-provided workaround for | ||
| CVE-2026-20253. | ||
| """ | ||
| references = [ | ||
| "https://nvd.nist.gov/vuln/detail/CVE-2026-20253", | ||
| "https://advisory.splunk.com/advisories/SVD-2026-0603", | ||
| "https://labs.watchtowr.com/why-use-app-level-auth-when-every-database-has-auth-splunk-enterprise-cve-2026-20253-pre-auth-rce/", | ||
| "https://attack.mitre.org/techniques/T1190/", | ||
| ] | ||
| risk_score = 73 | ||
| rule_id = "2a7823db-0bc2-48f6-aa2f-e6aef233c6dc" | ||
| setup = """## Setup | ||
|
|
||
| This rule covers two detection paths with different telemetry requirements: | ||
|
|
||
| **Body injection branch** (`http.request.body.content`): Populated by: | ||
| - Elastic Defend (`logs-endpoint.events.network*`) on the Splunk host, capturing HTTP network | ||
| events at the endpoint level | ||
| - Network Packet Capture (`logs-network_traffic.http*`) with HTTP body capture enabled for the | ||
| Splunk recovery paths | ||
|
|
||
| Avoid enabling broad request-body logging without masking or filtering — bodies can contain | ||
| credentials and PII. Scope capture to specific URL paths (e.g., `*/splunkd/*`) where possible. | ||
|
|
||
| **Auth artifact branch** (`url.password`, `url.username`): Populated by: | ||
| - Zeek (`logs-zeek.http*`) - parses HTTP Basic auth headers natively, no additional configuration | ||
|
|
||
| **Vulnerable endpoint probing branch** (`http.response.status_code`): Populated by Network Packet | ||
| Capture, Zeek, Suricata, Azure Application Gateway, and GCP Load Balancing where HTTP response | ||
| metadata is visible to the sensor. | ||
|
|
||
| Splunk Web listens on TCP port 8000 by default (`web.conf` `httpport`), which is included in the | ||
| default Network Packet Capture/Packetbeat HTTP port list. Add any custom Splunk Web `httpport` value | ||
| to the HTTP protocol configuration. Splunk's management service defaults to TCP port 8089 | ||
| (`mgmtHostPort`) and commonly uses TLS; add 8089 only if management/API traffic is directly exposed or | ||
| visible to the sensor after decryption. If the PostgreSQL sidecar is directly exposed or monitored | ||
| locally on TCP port 5435, add port 5435 as an HTTP port as well. Zeek and Suricata can identify | ||
| plaintext HTTP on non-standard ports through protocol detection when their HTTP analyzers are enabled. | ||
| For TLS deployments, the sensor must observe decrypted HTTP, sit downstream of TLS termination, or use | ||
| proxy or load balancer logs that expose the HTTP path, method, and status code. Body-content detection | ||
| still requires request-body capture for the Splunk recovery paths. | ||
|
|
||
| Use the companion rule "Splunk Enterprise PostgreSQL Backup-to-Restore Potential RCE Sequence" for detections that work without | ||
| request-body capture or auth header logging. | ||
| """ | ||
| severity = "high" | ||
| tags = [ | ||
| "Domain: Network", | ||
| "Use Case: Threat Detection", | ||
| "Use Case: Vulnerability", | ||
| "Use Case: Network Security Monitoring", | ||
| "Tactic: Initial Access", | ||
| "Data Source: Azure", | ||
| "Data Source: Elastic Defend", | ||
| "Data Source: GCP", | ||
| "Data Source: Google Cloud Platform", | ||
| "Data Source: Network Packet Capture", | ||
| "Data Source: Network Traffic", | ||
| "Data Source: Zeek", | ||
| "Data Source: Suricata", | ||
| "Resources: Investigation Guide", | ||
| ] | ||
| timestamp_override = "event.ingested" | ||
| type = "query" | ||
|
|
||
| query = ''' | ||
| http.request.method:POST and | ||
| url.path:("*splunkd/__raw/v1/postgres/recovery/*" or "/v1/postgres/recovery/*") and | ||
| ( | ||
| http.request.body.content:( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To get body content, you need advanced NPC settings, I believe? If I'm correct, maybe add these instructions to the setup guide.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, won't some of this be blocked from usage through TLS?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the TLS part we do make note of that, it depends on where the TLS occurs and where the sensor is located. This path is largely optional for detection, but is high fidelity if you do have your network setup this way, the other paths do not require TLS decryption nor body content.
As an example, some deployments would not have Splunk run with https, but have nginx or a proxy wrap the connection in TLS. In these cases, if your sensor is on the host or inside of network prior to nginx you would then not need TLS decryption. For the body content for Endpoint's Network Traffic Integration, you generally would just need to update the config to include it (optionally given this is an optional path for higher fidelity). (ref) which we also reference:
Happy to add more detail, but the steps are to add the recovery paths to the monitored body parsing setting in the integration.
Does this address what you are referencing? Or would you prefer if we add more detail? Thanks! |
||
| "*\"backupFile\"*../*" or | ||
| "*\"backupFile\"*/dev/shm/*" or | ||
| "*\"backupFile\"*/etc/cron*" or | ||
| "*\"backupFile\"*/home/*/.ssh/*" or | ||
| "*\"backupFile\"*/opt/splunk/bin/scripts/*" or | ||
| "*\"backupFile\"*/opt/splunk/etc/apps/*" or | ||
| "*\"backupFile\"*/root/*" or | ||
| "*\"backupFile\"*/tmp/*" or | ||
| "*\"backupFile\"*/var/tmp/*" or | ||
| "*\"backupFile\"*authorized_keys*" or | ||
| "*\"database\"*dbname=*" or | ||
| "*\"database\"*host=*" or | ||
| "*\"database\"*hostaddr=*" or | ||
| "*\"database\"*passfile=*" or | ||
| "*\"database\"*password=*" or | ||
| "*\"database\"*port=*" or | ||
| "*\"database\"*service=*" or | ||
| "*\"database\"*sslmode=*" or | ||
| "*\"database\"*user=*" or | ||
| "*/opt/splunk/etc/apps/*" or | ||
| "*/opt/splunk/var/packages/data/postgres/.pgpass*" | ||
| ) or | ||
| data_stream.dataset:zeek.http and url.password:"" or | ||
| data_stream.dataset:(azure.application_gateway or gcp.loadbalancing_logs or network_traffic.http or suricata.eve or zeek.http) and | ||
| url.path:( | ||
| "*splunkd/__raw/v1/postgres/recovery/backup" or | ||
| "*splunkd/__raw/v1/postgres/recovery/restore" or | ||
| /v1/postgres/recovery/backup or | ||
| /v1/postgres/recovery/restore | ||
| ) and | ||
| http.response.status_code:400 | ||
| ) | ||
| ''' | ||
|
|
||
|
|
||
| [[rule.threat]] | ||
| framework = "MITRE ATT&CK" | ||
| [[rule.threat.technique]] | ||
| id = "T1190" | ||
| name = "Exploit Public-Facing Application" | ||
| reference = "https://attack.mitre.org/techniques/T1190/" | ||
|
|
||
|
|
||
| [rule.threat.tactic] | ||
| id = "TA0001" | ||
| name = "Initial Access" | ||
| reference = "https://attack.mitre.org/tactics/TA0001/" | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.