Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2026-25243 — Стабильный POC для CVE-2026-25243 (Redis RESTORE double-free -> удалённое выполнение кода) | Kitploit
Инструменты/GitHubGitHub/captain-woof/cve-2026-25243
Анализ уязвимостейЭксплуатацияПост-эксплуатацияТестирование на ПроникновениеRed TeamingБезопасность Баз ДанныхЭксплуатация Бинарных Файлов
GitHubcaptain-woof/cve-2026-25243

CVE-2026-25243

Стабильный POC для CVE-2026-25243 (Redis RESTORE double-free -> удалённое выполнение кода)

Репозиторий
111 месяц назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-25243 — Redis RESTORE double-free → удалённое выполнение кода

Проверено на Rocky Linux 8.10, aarch64, Redis 8.6.2, jemalloc 5.3.0.

Ссылка: https://www.zeroday.cloud/blog/redis-cve-2026-25243-deep-dive

TLDR; Стабильный эксплойт, работает на различных дистрибутивах ОС и архитектурах.


Краткое резюме

Что это? Уязвимость повреждения памяти в Redis, которая позволяет аутентифицированному атакующему выполнять произвольные команды от имени пользователя Redis. Атака реальна и требует лишь одной команды RESTORE — обычной операции Redis, а не только для администратора. Этот эксплойт демонстрирует полный RCE менее чем за секунду.

Влияние? Любой аутентифицированный клиент Redis может её вызвать, и ущерб тотален: произвольное выполнение кода в процессе Redis (часто работающем от root в контейнерах). Без патча самого Redis смягчить это невозможно.

Как это работает в двух словах? У Redis есть функция сериализации (RESTORE), которая принимает блоб двоичных данных и восстанавливает его как объект Redis. Код, проверяющий формат блоба, и код, десериализующий его, расходятся в том, как разбирать определённые последовательности, — ошибка, которую атакующий использует для повреждения кучи. После повреждения кучи атакующий получает возможность читать и записывать любой адрес памяти в процессе Redis, а оттуда перехватывает внутреннее состояние сервера, чтобы выполнить команду оболочки.

Реальная техника эксплойта: Это не простой краш. Это цепочка heap-эксплойта: corrupt → overlap → произвольное чтение/запись → утечка информации → поиск структуры server → перехват указателей на функции → RCE. Эксплойт проходит 9 стадий и требует утечки множества адресов в рантайме, разбора двоичных структур и обнаружения алиасинга памяти. То, что заставляет его работать на разных архитектурах (x86-64, aarch64 и т. д.), — все адреса утекают из самой цели, а не предполагаются.


1. Уязвимость — подробно

CVE-2026-25243 — это пара double-free ошибок, достижимых одной аутентифицированной командой RESTORE. RESTORE key ttl <serialized-value> десериализует управляемый атакующим RDB-блоб; обе ошибки находятся в разрыве между валидатором, проверяющим блоб, и конвертером, который материализует его.

Ошибка 1 — преобразование устаревшего zipmap (CWE-415, путь, который использует этот эксплойт). Валидатор zipmap (zipmapValidateIntegrity()) и конвертер (zipmapNext()) расходятся во мнении об избыточной кодировке длины. Маленькая длина 4 может быть легально записана в длинной пятибайтовой форме FE 04 00 00 00. Валидатор потребляет одно количество байт, конвертер — другое: рассинхронизация парсинга на 4 байта. Поэтому конвертер обходит иную структуру, не ту, что была проверена; lpSafeToAdd() завершается ошибкой уже после того, как поле вставлено в словарь, и путь очистки освобождает поле дважды: один раз через dictRelease() и ещё раз через sdsfree().

Ошибка 2 — загрузка PEL consumer'а потока (CWE-415). В rdbLoadStreamConsumersGroup() PEL consumer'а, содержащий повторяющийся ID записи, приводит к сбою второго raxTryInsert(), который вызывает streamFreeNACK() для streamNACK, всё ещё принадлежащего глобальному PEL группы. Освобождается дважды. (Выбирается с помощью --vuln-type stream.)

Любая из ошибок даёт атакующему чанк памяти, который одновременно свободен и на который есть ссылка, — классическая отправная точка для heap-overlap эксплойта.

Воздействие: аутентифицированный клиент Redis (без прав администратора, RESTORE — обычная команда работы с данными) получает произвольное выполнение кода от имени пользователя redis — root в образе контейнера по умолчанию.

2. Как работает эксплойт

Девять стадий, каждая из которых превращает более слабый примитив в более сильный:

СтадияПолученный примитивМеханизм
0профиль целиINFO server / INFO memory → версия, архитектура, дистрибутив, pid, путь к исполняемому файлу, время запуска, аллокатор
1double freeнекорректный zipmap (или stream) RESTORE
2два ключа, разделяющие памятьраспылить маркерные ключи на освобождённый чанк, обнаружить алиасинг, затем перезаписать SDS-заголовок одного ключа через его двойника, чтобы раздуть его до 1 МБ «memview»
3произвольное чтение/записьнайти объект INCRBYFLOAT внутри memview, перехватить его поле ptr: GETRANGE/SETRANGE по этому ключу теперь читают/записывают любой адрес
4указатель на образсканировать кучу назад в поисках значения внутри образа redis-server
5&serverспуститься к ELF-заголовку, разобрать program headers, слить сегмент, доступный на запись, сопоставить server.pid
6пейлоад в памятизаписать "/bin/sh", "-c", "<cmd>" и массив argv в memview
7перехваченная структураперезаписать server.executable, server.exec_argv и server.enable_debug_cmd
8RCEDEBUG CRASH-AND-RECOVER → restartServer() → execve(server.executable, server.exec_argv, environ)

Как запустить

root@kitploit:~
python3 exploit.py --host 127.0.0.1 --port 6379 \
    --password mypassword --cmd 'id > /tmp/pwned123.txt'

Проверка:

root@kitploit:~
cat /tmp/pwned123.txt
# uid=0(root) gid=0(root) groups=0(root)

3. Журнал изменений

2026-08-06 — переработка переносимости, надёжности и скорости

Отправная точка: эксплойт работал только на x86-64 и погибал на стадии 3 на цели aarch64. Конечное состояние: полный RCE на aarch64 Rocky Linux 8.10 менее чем за секунду, 116 команд Redis.

a) Снятие цифрового отпечатка цели в рантайме (новое, стадия 0). О цели больше ничего не предполагается. INFO server + INFO memory дают версию Redis, архитектуру CPU (из строки os:), семейство дистрибутива (выводится из gcc_version), аллокатор и — самое важное — три якоря валидации: process_id, executable и точный stat_starttime (server_time_usec/1e6 - uptime_in_seconds). Последующие стадии сверяются с ними вместо догадок.

b) Независимая от архитектуры компоновка памяти. Четыре захардкоженных константы x86-64 (BINARY_ADDR_MIN/MAX, HEAP_ADDR_MIN/MAX) заменены таблицей по архитектурам (ARCH_PROFILES), покрывающей x86_64, aarch64 (VA и 39-, и 48-бит), riscv64, ppc64le и s390x, с размещениями как ET_EXEC, так и ET_DYN для каждой, плюс широкий универсальный запасной вариант для всего остального. Это и была настоящая причина, по которой эксплойт падал на этой цели: утёкший указатель 0x0000ffff8a5fdf32 — вполне себе валидный aarch64 mmap-адрес, который проверка диапазонов x86-64 отвергала.

c) Валидация утечки на основе консенсуса (стадия 3). Вместо доверия захардкоженному окну кучи сканирование теперь собирает каждый структурно валидный объект 1337.NNNNNN в memview и требует, чтобы по крайней мере два из них давали один и тот же базовый адрес memview (ptr - offset_of_value). На практике совпадают 502 кандидата — это доказательство, которого не может дать ни одна таблица диапазонов. Подтверждённый указатель затем калибрует окно кучи в рантайме. Проверка формата также перенесена перед дорогим по round-trip тестом контроля записи.

d) Граница сканирования на стадии 3 (исправление ошибки). Сканирование уходило до захардкоженных 10 МБ, тогда как memview — 1 МБ, поэтому оно читало за конец, получало пустой ответ и падало с AssertionError: Empty data from memview. Теперь оно ограничено реальным STRLEN memview, читает 256 КБ за round-trip вместо 64 КБ, а бессмысленный цикл повторов со сном 6×1 с убран.

e) Стадия 5 переписана: по ELF-ориентирам, без падений (главное). Старая реализация сканировала вперёд от указателя на образ, опрашивая адреса и читая ту длину, которую заявлял мусорный SDS-заголовок. На этой цели она ушла прямо за конец сегмента только для чтения в незамапленную дыру на 0x715000 и убила сервер (SIGSEGV в getrangeCommand → memcpy). Слепое сканирование нельзя сделать безопасным. Замена детерминирована:

  1. Найти базовый адрес образа. Спускаться страница за страницей от самого нижнего утёкшего указателя на образ. Проба бесплатна: первые пять байт каждого ELF64-образа — это 7f 45 4c 46 02, а sdslen() берёт байт флагов из ptr[-1] — так что наведение перехваченного объекта на base+5 делает e_ident[EI_CLASS]=0x02 байтом флагов, то есть SDS_TYPE_16, чья длина — это uint16 по адресу base+0 = 0x457f (0x7f45 в big-endian). STRLEN, равный ровно 17791, и есть сигнатура ELF. Локальная копия бинарника не нужна — заголовок читается из памяти самой цели.
  2. Разобрать program headers, чтобы получить точные границы каждого сегмента PT_LOAD в рантайме (с учётом load bias ET_DYN для PIE-целей). Каждое последующее чтение ограничено реальным маппингом, так что падение в незамапленной дыре теперь структурно невозможно.
  3. Создать один SDS-заголовок в занулённом слоте сегмента, доступного на запись, что делает весь .data/.bss читаемым за несколько round-trip вместо сотен тысяч байтовых проб. Перезаписанные байты сохраняются и восстанавливаются.
  4. Сопоставить server.pid с pid из INFO — точная проверка равенства 8 байт — затем подтвердить разыменованием server.executable и сравнением строки с executable из INFO. Старый код довольствовался слабой эвристикой по форме из семи полей; теперь структура идентифицируется наверняка.

f) Стадия 4 ужесточена. Lua-валидатор принимает полный список диапазонов по архитектурам (так что и не-PIE образ по адресу 0x400000, и PIE-образ по адресу 0xaaaa… распознаются) и исключает калиброванное окно кучи. Он возвращает несколько кандидатов вместо одного, так что неудачный выбор стоит повтора, а не всего запуска.

g) Стадия 7 с самопроверкой. enable_debug_cmd определялся по захардкоженному смещению stat_starttime - 0x3c. Теперь ожидаемое значение stat_starttime точно известно из INFO (окно в 3 секунды вместо 30 дней), окно чтения структуры выросло с 4 КБ до 32 КБ (stat_starttime находится на смещении 0x9e0, далеко за старым пределом), и — что решающе — каждый кандидат-смещение проверяется живым оракулом: установить байт, отправить DEBUG SET-ACTIVE-EXPIRE 1 и посмотреть, примет ли его сервер. Неверные догадки восстанавливаются перед следующей попыткой, так что флаг находится на любой сборке, а не предполагается. -0x3c по-прежнему пробуется первым и подтверждён корректным для 8.6.2 (смещение 0x9a4).

h) Записи действительно достигают цели (стадия 5). setrangeCommand() вызывает dbUnshareStringValue(), который дублирует значение, если только не encoding == RAW && refcount == 1. Байт кодировки теперь зануляется перед первой записью через перехваченный указатель, так что записи достигают целевого адреса вместо приватной копии.

i) Пейлоад упрощён. Убраны все механизмы backconnect/reverse-shell, ASCII-баннер и добавленный ;sleep 5. Пейлоад — это ровно /bin/sh -c '<--cmd>' и ничего больше. По умолчанию --cmd равен id > /tmp/pwned123.txt.

j) Скорость. Стадия 4 собирает 3 кандидатов вместо 8; стадия 5 заменяет ~10^5 байтовых проб на ~40 массовых чтений; стадия 3 использует чтения по 256 КБ и пропускает round-trip для кандидатов, не прошедших локальную валидацию. Вся цепочка: 116 команд, <1 с.

Результат: uid=0(root) gid=0(root) groups=0(root) в /tmp/pwned123.txt в целевом контейнере.

Заметки о переносимости

  • Архитектура определяется, а не предполагается. Основанная на ELF стадия 5 нейтральна к архитектуре по построению (она читает собственные program headers цели) и обрабатывает как PIE-, так и не-PIE образы, little- и big-endian.
  • Дистрибутив сообщается для удобства оператора; функциональной зависимости от него у эксплойта нет. Единственное допущение о файловой системе — /bin/sh, который требуют POSIX и FHS.
  • Версия: версия RDB и размер структуры stream выбираются из redis_version (поддерживаются 7.x и 8.x). Смещения полей структуры (executable=24, exec_argv=32) следуют из ABI LP64, а enable_debug_cmd обнаруживается и проверяется в рантайме, а не захардкожен.
  • 32-битные цели явно отклоняются на стадии 0 (пейлоад строит 64-битные указатели) вместо того, чтобы падать непонятно позже.

2026-08-06 (позже) — усиление стабильности после тестирования повторными запусками

Замерено 13/13 успешных запусков на пути zipmap по умолчанию (5 + 8 подряд), каждый завершается ≤1 секунды. Три проблемы проявились только при повторном выполнении и теперь исправлены:

k) Гонка SAVE на стадии 0. Запуск мог прерываться с ERR Background save already in progress, когда фоновое сохранение от предыдущего запуска (или самого redis) всё ещё выполнялось. Теперь SAVE повторяется до 15 секунд, а если не получается, запуск продолжается без контрольной точки вместо прерывания.

l) Переподключение, пока цель перезапускается (--connect-retries, по умолчанию 10). Неудачная попытка оставляет кучу повреждённой, так что FLUSHALL следующего запуска освобождает отравленные чанки и роняет сервер. Он перезапускается через несколько секунд и полностью эксплуатируем, поэтому стадия 0 теперь переподключается и повторяет попытки, а не падает. Наши собственные ошибки валидации (неподдерживаемая версия/архитектура) никогда не повторяются. Это устранило перемежающуюся ошибку «stage 0 failed with an empty error», наблюдавшуюся примерно в 1 запуске из 3 во время стресс-тестирования.

m) sizeof(streamNACK) исправлен для 8.6.x. Путь --vuln-type stream распылял не тот jemalloc size class, поскольку предполагалось, что структура занимает 24 или 32 байта. В 8.6.2 это 64 байта (delivery_time, delivery_count, consumer, cgroup_ref_node, streamID id, pel_prev, pel_next). С правильным размером путь stream теперь доходит до стадии 5 вместо провала на стадии 2 с «key overlap not found».

Известные ограничения

  • --vuln-type stream ненадёжен на 8.6.2. С исправлением размера он проходит double-free, overlap, примитив чтения/записи и разбор ELF, но затем дестабилизирует keyspace: сервер падает в setrangeCommand при чтении o->ptr по адресу NULL+8, то есть поиск ключа возвращает повреждённый объект. Освобождаемый им 64-байтовый чанк разделяется с другими живыми выделениями, из-за чего этот путь наносит гораздо больше сопутствующего ущерба, чем путь zipmap. Используйте путь по умолчанию --vuln-type zipmap, который 13/13.
  • --random-heap-massage (сначала 100k случайных ключей) срабатывает, но не всегда — распылённая куча иногда размещает double-free чанк там, куда не попадает ни один маркерный ключ. Повторный запуск срабатывает.
  • Проверено только на aarch64 / Rocky Linux 8.10 / Redis 8.6.2 (non-PIE ET_EXEC). Пути x86-64 и PIE реализованы и нейтральны к архитектуре по построению, но в этой сессии против живой цели не выполнялись.
Скачать инструмент