
POC estable para CVE-2026-25243 (doble liberación en Redis RESTORE -> ejecución remota de código)
Verificado contra Rocky Linux 8.10, aarch64, Redis 8.6.2, jemalloc 5.3.0.
Referencia: https://www.zeroday.cloud/blog/redis-cve-2026-25243-deep-dive
TLDR; Exploit estable, funciona contra varias distribuciones de SO y arquitecturas.
¿Qué es? Una vulnerabilidad de corrupción de memoria en Redis que permite a un
atacante autenticado ejecutar comandos arbitrarios como el usuario de Redis. El
ataque se da en el mundo real y requiere solo un único comando RESTORE — una operación
normal de Redis, no exclusiva de administradores. Este exploit demuestra RCE completa en menos de
un segundo.
¿Impacto? Cualquier cliente Redis autenticado puede desencadenarla, y el daño es total: ejecución de código arbitrario en el proceso de Redis (a menudo ejecutándose como root en contenedores). No hay forma de mitigarla sin parchear el propio Redis.
¿Cómo funciona de un vistazo? Redis tiene una función de serialización (RESTORE)
que toma un blob de datos binarios y lo reconstruye como un objeto Redis. El
código que valida el formato del blob y el código que lo deserializa
no se ponen de acuerdo sobre cómo analizar ciertas secuencias — un bug que el atacante
explota para corromper el heap. Una vez corrompido el heap, el atacante obtiene
la capacidad de leer y escribir en cualquier dirección de memoria del proceso de Redis, y desde ahí
secuestra el estado interno del servidor para ejecutar un comando de shell.
La técnica real del exploit: Esto no es un simple fallo. Es una cadena de explotación del heap: corromper → solapar → R/W arbitrario → fuga de información → encontrar la estructura del servidor → secuestrar punteros de función → RCE. El exploit ejecuta 9 etapas y requiere filtrar múltiples direcciones en tiempo de ejecución, analizar estructuras binarias y detectar el aliasing de memoria. Lo que hace que funcione entre arquitecturas (x86-64, aarch64, etc.) es que todas las direcciones se filtran del propio objetivo, no se asumen.
CVE-2026-25243 es un par de bugs de double-free (doble liberación)
alcanzables desde un único comando RESTORE autenticado. RESTORE key ttl <serialized-value>
deserializa un blob RDB controlado por el atacante; ambos bugs viven en la brecha
entre el validador que comprueba el blob y el conversor que
lo materializa.
Bug 1 — conversión de zipmap heredado (CWE-415, la ruta que usa este exploit).
El validador de zipmap (zipmapValidateIntegrity()) y el conversor
(zipmapNext()) discrepan sobre una codificación de longitud redundante. La longitud pequeña
4 puede escribirse legalmente en la forma larga de cinco bytes FE 04 00 00 00. El
validador consume un número de bytes, el conversor otro — una desincronización de análisis
de 4 bytes. Por tanto, el conversor recorre una estructura diferente
de la que se validó, lpSafeToAdd() falla después de que el campo ya se haya
insertado en el diccionario, y la ruta de limpieza libera el campo dos veces: una
mediante dictRelease() y otra mediante sdsfree().
Bug 2 — carga de PEL de consumidores de stream (CWE-415). En
rdbLoadStreamConsumersGroup(), un PEL de consumidor que contiene un ID de entrada
duplicado hace que el segundo raxTryInsert() falle, lo que llama a streamFreeNACK() sobre un
streamNACK que todavía pertenece al PEL global del grupo. Se libera dos veces.
(Se puede seleccionar con --vuln-type stream).
Cualquiera de los dos bugs entrega al atacante un fragmento de memoria que está simultáneamente libre y referenciado — el punto de partida clásico para un exploit de solapamiento de heap.
Impacto: un cliente Redis autenticado (sin derechos de administrador, RESTORE es un
comando de datos normal) obtiene ejecución de código arbitrario como el usuario redis — root
en la imagen de contenedor predeterminada.
Nueve etapas, cada una de las cuales convierte una primitiva más débil en una más fuerte:
| Etapa | Primitiva obtenida | Mecanismo |
|---|---|---|
| 0 | perfil del objetivo | INFO server / INFO memory → versión, arquitectura, distribución, pid, ruta del ejecutable, tiempo de inicio, asignador |
| 1 | doble liberación | RESTORE de zipmap (o stream) malformado |
| 2 | dos claves que comparten memoria | rociar claves marcadoras sobre el fragmento liberado, detectar aliasing, luego sobrescribir el encabezado SDS de una clave a través de su gemela para inflarlo a una "memview" de 1 MB |
| 3 | R/W arbitrario | encontrar un objeto INCRBYFLOAT dentro de la memview, secuestrar su campo ptr: GETRANGE/SETRANGE sobre esa clave ahora lee/escribe cualquier dirección |
| 4 | puntero de imagen | escanear el heap hacia atrás en busca de un valor dentro de la imagen de redis-server |
| 5 | &server | bajar hasta el encabezado ELF, analizar los encabezados de programa, volcar el segmento escribible, comparar server.pid |
| 6 | payload en memoria | escribir "/bin/sh", "-c", "<cmd>" más un array argv en la memview |
| 7 | estructura secuestrada | sobrescribir server.executable, server.exec_argv y server.enable_debug_cmd |
| 8 | RCE | DEBUG CRASH-AND-RECOVER → restartServer() → execve(server.executable, server.exec_argv, environ) |
python3 exploit.py --host 127.0.0.1 --port 6379 \
--password mypassword --cmd 'id > /tmp/pwned123.txt'
Verificación:
cat /tmp/pwned123.txt
# uid=0(root) gid=0(root) groups=0(root)
Punto de partida: el exploit era solo para x86-64 y moría en la etapa 3 en el objetivo aarch64. Estado final: RCE completa en aarch64 Rocky Linux 8.10 en menos de un segundo, con 116 comandos Redis.
a) Huella digital del objetivo en tiempo de ejecución (nueva, etapa 0). Ya no se asume
nada sobre el objetivo. INFO server + INFO memory proporcionan la versión de Redis,
la arquitectura de CPU (de la línea os:), la familia de distribución (inferida de
gcc_version), el asignador y — lo más importante — tres anclas de validación:
process_id, executable y el stat_starttime exacto
(server_time_usec/1e6 - uptime_in_seconds). Las etapas posteriores comparan
contra estos valores en lugar de adivinar.
b) Disposición de memoria independiente de la arquitectura. Las cuatro constantes x86-64
codificadas (BINARY_ADDR_MIN/MAX, HEAP_ADDR_MIN/MAX) se sustituyen por una
tabla por arquitectura (ARCH_PROFILES) que cubre x86_64, aarch64 (VA de 39
y 48 bits), riscv64, ppc64le y s390x, con las colocaciones ET_EXEC y ET_DYN
para cada una, además de un fallback genérico amplio para cualquier cosa no listada. Esta
fue la razón real del fallo del exploit en este objetivo: el puntero filtrado
0x0000ffff8a5fdf32 es una dirección mmap aarch64 perfectamente válida que la
comprobación de rango x86-64 rechazaba.
c) Validación de fugas basada en consenso (etapa 3). En lugar de confiar en una
ventana de heap codificada, el escaneo ahora recopila todos los objetos
1337.NNNNNN estructuralmente válidos en la memview y requiere que al menos dos de ellos
deriven la misma dirección base de memview (ptr - offset_of_value). En la práctica,
502 candidatos coinciden, lo que es una prueba que ninguna tabla de rangos puede ofrecer. El
puntero confirmado calibra entonces la ventana de heap en tiempo de ejecución. La
validación de formato también se movió antes de la prueba de control de escritura (costosa en viajes de ida y vuelta).