Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2026-25243 — POC stable pour CVE-2026-25243 (double-free RESTORE Redis -> exécution de code à distance) | Kitploit
Outils/GitHubGitHub/captain-woof/cve-2026-25243
Analyse des VulnérabilitésExploitationPost-ExploitationTests d'IntrusionRed TeamingSécurité des Bases de DonnéesExploitation de Binaires
GitHubcaptain-woof/cve-2026-25243

CVE-2026-25243

POC stable pour CVE-2026-25243 (double-free RESTORE Redis -> exécution de code à distance)

Voir le dépôt
11il y a 1 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-25243 — Redis RESTORE double-free → exécution de code à distance

Vérifié sur Rocky Linux 8.10, aarch64, Redis 8.6.2, jemalloc 5.3.0.

Référence : https://www.zeroday.cloud/blog/redis-cve-2026-25243-deep-dive

TLDR ; Exploit stable, fonctionne avec une grande variété de distributions et d'architectures OS.


Résumé exécutif

Qu'est-ce que c'est ? Une vulnérabilité de corruption mémoire dans Redis qui permet à un attaquant authentifié d'exécuter des commandes arbitraires en tant qu'utilisateur Redis. L'attaque est réelle et ne nécessite qu'une seule commande RESTORE — une opération Redis normale, pas réservée à l'admin. Cet exploit démontre une RCE complète en moins d'une seconde.

Impact ? N'importe quel client Redis authentifié peut la déclencher, et les dégâts sont totaux : exécution de code arbitraire dans le processus Redis (souvent exécuté en root dans les conteneurs). Il n'existe aucun moyen d'atténuation sans corriger Redis lui-même.

Comment ça fonctionne en un coup d'œil ? Redis dispose d'une fonctionnalité de sérialisation (RESTORE) qui prend un blob de données binaires et le reconstruit en tant qu'objet Redis. Le code qui valide le format du blob et le code qui le désérialise ne sont pas d'accord sur la façon d'analyser certaines séquences — un bug que l'attaquant exploite pour corrompre le tas (heap). Une fois le tas corrompu, l'attaquant obtient la capacité de lire et d'écrire n'importe quelle adresse mémoire dans le processus Redis, puis détourne l'état interne du serveur pour exécuter une commande shell.

La véritable technique de l'exploit : Il ne s'agit pas d'un simple crash. C'est une chaîne d'exploitation du tas : corruption → chevauchement → R/W arbitraire → fuite d'informations → localisation de la structure du serveur → détournement des pointeurs de fonction → RCE. L'exploit s'exécute en 9 étapes et nécessite de fuiter plusieurs adresses à l'exécution, d'analyser des structures binaires et de détecter l'aliasing mémoire. Ce qui le fait fonctionner sur plusieurs architectures (x86-64, aarch64, etc.), c'est que toutes les adresses sont fuitées depuis la cible elle-même, et non supposées.


1. La vulnérabilité — en détail

CVE-2026-25243 est une paire de bugs de double-free exploitables depuis une seule commande RESTORE authentifiée. RESTORE key ttl <serialized-value> désérialise un blob RDB contrôlé par l'attaquant ; les deux bugs se situent dans l'écart entre le validateur qui vérifie le blob et le convertisseur qui le matérialise.

Bug 1 — conversion zipmap legacy (CWE-415, le chemin utilisé par cet exploit). Le validateur zipmap (zipmapValidateIntegrity()) et le convertisseur (zipmapNext()) ne sont pas d'accord sur un encodage de longueur redondant. La petite longueur 4 peut légalement être écrite sous la forme longue de cinq octets FE 04 00 00 00. Le validateur consomme un certain nombre d'octets, le convertisseur un autre — une désynchronisation d'analyse de 4 octets. Le convertisseur parcourt donc une structure différente de celle qui a été validée, lpSafeToAdd() échoue après que le champ a déjà été inséré dans le dictionnaire, et le chemin de nettoyage libère le champ deux fois : une fois via dictRelease() puis à nouveau via sdsfree().

Bug 2 — chargement du PEL des consommateurs de stream (CWE-415). Dans rdbLoadStreamConsumersGroup(), un PEL de consommateur contenant un ID d'entrée en double fait échouer le second raxTryInsert(), qui appelle streamFreeNACK() sur un streamNACK toujours possédé par le PEL global du groupe. Libéré deux fois. (Sélectionnable avec --vuln-type stream.)

Chacun de ces bugs offre à l'attaquant un bloc de mémoire à la fois libre et référencé — le point de départ classique d'un exploit par chevauchement de tas.

Impact : un client Redis authentifié (sans droits admin, RESTORE est une commande de données normale) obtient l'exécution de code arbitraire en tant qu'utilisateur redis — root dans l'image de conteneur par défaut.

2. Comment fonctionne l'exploit

Neuf étapes, chacune transformant une primitive plus faible en une primitive plus forte :

ÉtapePrimitive obtenueMécanisme
0profil de la cibleINFO server / INFO memory → version, arch, distribution, pid, chemin de l'exécutable, heure de démarrage, allocateur
1double-freeRESTORE zipmap (ou stream) malformé
2deux clés partageant la mémoirepulvériser des clés marqueurs sur le bloc libéré, détecter l'aliasing, puis écraser l'en-tête SDS d'une clé via son jumeau pour la gonfler en « memview » de 1 Mo
3R/W arbitrairetrouver un objet INCRBYFLOAT dans la memview, détourner son champ ptr : GETRANGE/SETRANGE sur cette clé lit/écrit désormais n'importe quelle adresse
4pointeur d'imagescanner le tas à rebours pour trouver une valeur dans l'image redis-server
5&serverdescendre jusqu'à l'en-tête ELF, analyser les en-têtes de programme, extraire le segment inscriptible, faire correspondre server.pid
6payload en mémoireécrire "/bin/sh", "-c", "<cmd>" plus un tableau argv dans la memview
7structure détournéeécraser server.executable, server.exec_argv et server.enable_debug_cmd
8RCEDEBUG CRASH-AND-RECOVER → restartServer() → execve(server.executable, server.exec_argv, environ)

Comment déclencher

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

Vérification :

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

3. Journal des modifications

2026-08-06 — refonte de la portabilité, de la fiabilité et de la vitesse

Point de départ : l'exploit était exclusivement x86-64 et échouait à l'étape 3 sur la cible aarch64. État final : RCE complète sur aarch64 Rocky Linux 8.10 en moins d'une seconde, 116 commandes Redis.

a) Empreinte de la cible à l'exécution (nouveau, étape 0). Plus rien n'est supposé sur la cible. INFO server + INFO memory fournissent la version de Redis, l'architecture CPU (à partir de la ligne os:), la famille de distribution (déduite de gcc_version), l'allocateur et — surtout — trois ancres de validation : process_id, executable et le stat_starttime exact (server_time_usec/1e6 - uptime_in_seconds). Les étapes suivantes comparent leurs résultats à ces valeurs au lieu de deviner.

b) Disposition mémoire indépendante de l'architecture. Les quatre constantes x86-64 codées en dur (BINARY_ADDR_MIN/MAX, HEAP_ADDR_MIN/MAX) sont remplacées par un tableau par architecture (ARCH_PROFILES) couvrant x86_64, aarch64 (VA de 39 et 48 bits), riscv64, ppc64le et s390x, avec à la fois les emplacements ET_EXEC et ET_DYN pour chacune, plus un large repli générique pour tout ce qui n'est pas listé. C'était la raison réelle de l'échec de l'exploit sur cette cible : le pointeur fuité 0x0000ffff8a5fdf32 est une adresse mmap aarch64 parfaitement valide que la vérification de plage x86-64 rejetait.

c) Validation des fuites par consensus (étape 3). Plutôt que de se fier à une fenêtre de tas codée en dur, l'analyse collecte désormais tous les objets structurellement valides 1337.NNNNNN dans la memview et exige qu'au moins deux d'entre eux aboutissent à la même adresse de base de la memview (ptr - offset_of_value). En pratique, 502 candidats concordent, une preuve qu'aucun tableau de plages ne peut offrir. Le pointeur confirmé calibre ensuite la fenêtre de tas à l'exécution. La validation du format a également été déplacée avant le test de contrôle d'écriture (coûteux en allers-retours).

d) Limite d'analyse de l'étape 3 (correction de bug). L'analyse s'exécutait jusqu'à 10 Mo codés en dur alors que la memview fait 1 Mo : elle lisait donc au-delà de la fin, recevait une réponse vide et s'interrompait avec AssertionError: Empty data from memview. Elle est désormais bornée par le vrai STRLEN de la memview, lit 256 Ko par aller-retour au lieu de 64 Ko, et la boucle inutile de 6×1 s de sommeil avec nouvelle tentative a disparu.

e) Étape 5 réécrite : guidée par ELF, sans crash (la grosse modification). L'ancienne implémentation analysait vers l'avant depuis un pointeur d'image, sondant des adresses et lisant la longueur annoncée par un en-tête SDS poubelle. Sur cette cible, elle sortait directement de l'extrémité du segment en lecture seule dans le trou non mappé à 0x715000 et tuait le serveur (SIGSEGV dans getrangeCommand → memcpy). Une analyse aveugle ne peut pas être rendue sûre. Le remplacement est déterministe :

  1. Trouver la base de l'image. Descendre page par page depuis le pointeur d'image fuité le plus bas. Le test ne coûte rien : les cinq premiers octets de toute image ELF64 sont 7f 45 4c 46 02, et sdslen() prend son octet de flags dans ptr[-1] — donc en pointant l'objet détourné vers base+5, e_ident[EI_CLASS]=0x02 devient l'octet de flags, c'est-à-dire SDS_TYPE_16, dont la longueur est le uint16 à base+0 = 0x457f (0x7f45 en big-endian). Un STRLEN d'exactement 17791 est la signature ELF. Aucune copie locale du binaire n'est nécessaire — l'en-tête est lu dans la mémoire de la cible elle-même.
  2. Analyser les en-têtes de programme pour obtenir les limites exactes à l'exécution de chaque segment PT_LOAD (en gérant le biais de chargement ET_DYN pour les cibles PIE). Chaque lecture ultérieure est bornée à un mappage réel, donc le crash par trou non mappé est désormais structurellement impossible.
  3. Forger un en-tête SDS dans un emplacement remis à zéro du segment inscriptible, ce qui rend l'ensemble .data/.bss lisible en une poignée d'allers-retours au lieu de centaines de milliers de sondages d'octets. Les octets écrasés sont sauvegardés puis restaurés.
  4. Faire correspondre server.pid avec le pid d'INFO — un test d'égalité exact sur 8 octets — puis confirmer en déréférençant server.executable et en comparant la chaîne au champ executable d'INFO. L'ancien code acceptait une heuristique de forme souple à sept champs ; la structure est désormais identifiée de manière positive.

f) Étape 4 renforcée. Le validateur Lua prend la liste complète des plages par architecture (ainsi une image non-PIE à 0x400000 et une image PIE à 0xaaaa… sont toutes deux reconnues) et exclut la fenêtre de tas calibrée. Il renvoie plusieurs candidats au lieu d'un seul, donc un mauvais choix coûte une nouvelle tentative plutôt que l'ensemble de l'exécution.

g) Étape 7 auto-vérifiante. enable_debug_cmd était localisé via un stat_starttime - 0x3c codé en dur. Désormais, la valeur attendue de stat_starttime est connue exactement depuis INFO (une fenêtre de 3 secondes au lieu de 30 jours), la fenêtre de lecture de la structure est passée de 4 Ko à 32 Ko (stat_starttime se trouve à l'offset 0x9e0, bien au-delà de l'ancienne limite) et — de manière décisive — chaque offset candidat est vérifié avec un oracle vivant : définir l'octet, envoyer DEBUG SET-ACTIVE-EXPIRE 1, et voir si le serveur l'accepte. Les mauvaises suppositions sont restaurées avant la tentative suivante, donc le flag est trouvé sur n'importe quelle compilation plutôt que supposé. -0x3c est toujours essayé en premier et confirmé correct pour 8.6.2 (offset 0x9a4).

h) Les écritures aboutissent réellement (étape 5). setrangeCommand() appelle dbUnshareStringValue(), qui duplique la valeur sauf si encoding == RAW && refcount == 1. L'octet d'encodage est désormais mis à zéro avant la première écriture via le pointeur détourné, donc les écritures atteignent l'adresse cible au lieu d'une copie privée.

i) Payload simplifié. Toute la mécanique de backconnect/shell inversée, la bannière ASCII et le ;sleep 5 ajouté ont été supprimés. Le payload est exactement /bin/sh -c '<--cmd>' et rien d'autre. --cmd a pour valeur par défaut id > /tmp/pwned123.txt.

j) Vitesse. L'étape 4 collecte 3 candidats au lieu de 8 ; l'étape 5 remplace ~10^5 sondages d'octets par ~40 lectures en bloc ; l'étape 3 utilise des lectures de 256 Ko et ignore les allers-retours pour les candidats qui échouent à la validation locale. Chaîne complète : 116 commandes, <1 s.

Résultat : uid=0(root) gid=0(root) groups=0(root) dans /tmp/pwned123.txt sur le conteneur cible.

Notes de portabilité

  • L'architecture est détectée, non supposée. L'étape 5 basée sur ELF est neutre vis-à-vis de l'architecture par construction (elle lit les en-têtes de programme de la cible elle-même) et gère à la fois les images PIE et non-PIE, en little-endian et big-endian.
  • La distribution est signalée pour le bénéfice de l'opérateur ; l'exploit n'en dépend pas fonctionnellement. La seule hypothèse sur le système de fichiers est /bin/sh, exigée par POSIX et le FHS.
  • Version : la version RDB et la taille de la structure stream sont sélectionnées à partir de redis_version (7.x et 8.x pris en charge). Les offsets des champs de la structure (executable=24, exec_argv=32) découlent de l'ABI LP64, et enable_debug_cmd est découvert et vérifié à l'exécution plutôt que codé en dur.
  • Les cibles 32 bits sont rejetées explicitement à l'étape 0 (le payload construit des pointeurs 64 bits) au lieu d'échouer de manière obscure plus tard.

2026-08-06 (plus tard) — durcissement de la stabilité après des tests d'exécutions répétées

Mesuré : 13/13 exécutions réussies sur le chemin zipmap par défaut (5 + 8 consécutives), chacune se terminant en ≤1 seconde. Trois problèmes ne sont apparus qu'en exécution répétée et sont désormais corrigés :

k) Course SAVE à l'étape 0. Une exécution pouvait s'interrompre avec ERR Background save already in progress lorsqu'une sauvegarde en arrière-plan d'une exécution précédente (ou de redis lui-même) était encore en cours. SAVE est désormais réessayée jusqu'à 15 secondes et, en cas d'échec, l'exécution se poursuit sans le point de contrôle au lieu de s'interrompre.

l) Reconnexion pendant le redémarrage de la cible (--connect-retries, défaut 10). Une tentative échouée laisse le tas corrompu, donc le FLUSHALL de l'exécution suivante libère les blocs empoisonnés et fait tomber le serveur. Il redémarre quelques secondes plus tard et reste parfaitement exploitable, donc l'étape 0 se reconnecte et réessaie désormais au lieu d'échouer. Nos propres erreurs de validation (version/architecture non prise en charge) ne sont jamais réessayées. Cela a éliminé l'échec intermittent « stage 0 failed with an empty error » observé environ 1 fois sur 3 lors des tests de stress.

m) sizeof(streamNACK) corrigé pour 8.6.x. Le chemin --vuln-type stream pulvérisait la mauvaise classe de taille jemalloc car la structure était supposée faire 24 ou 32 octets. En 8.6.2, elle fait 64 octets (delivery_time, delivery_count, consumer, cgroup_ref_node, streamID id, pel_prev, pel_next). Avec la bonne taille, le chemin stream atteint désormais l'étape 5 au lieu d'échouer à l'étape 2 avec « key overlap not found ».

Limites connues

  • --vuln-type stream n'est pas fiable sur 8.6.2. Avec la correction de taille, il traverse le double-free, le chevauchement, la primitive R/W et l'analyse ELF, puis déstabilise l'espace de clés : le serveur meurt dans setrangeCommand en lisant o->ptr à NULL+8, c'est-à-dire qu'une recherche de clé renvoie un objet corrompu. Le bloc de 64 octets qu'il libère est partagé avec d'autres allocations vivantes, ce qui le rend beaucoup plus lourd en dégâts collatéraux que le chemin zipmap. Utilisez le --vuln-type zipmap par défaut, qui est 13/13.
  • --random-heap-massage (100 000 clés aléatoires d'abord) réussit mais pas invariablement — le tas pulvérisé place parfois le bloc doublement libéré là où aucune clé marqueur n'atterrit. Relancer l'exploit réussit.
  • Vérifié uniquement sur aarch64 / Rocky Linux 8.10 / Redis 8.6.2 (non-PIE ET_EXEC). Les chemins x86-64 et PIE sont implémentés et neutres vis-à-vis de l'architecture par construction, mais n'ont pas été exécutés contre une cible réelle lors de cette session.
Télécharger l’outil