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
page_inject — CVE-2026-31431-killed page-cache exploit — exécution de code dans des conteneurs partageant la même couche d'image | Kitploit
Outils/GitHubGitHub/sgkdev/page_inject
Analyse des VulnérabilitésExploitationPost-ExploitationTests d'IntrusionRed TeamingÉvasion de ConteneurExploitation de Binaires
GitHubsgkdev/page_inject

page_inject

CVE-2026-31431-killed page-cache exploit — exécution de code dans des conteneurs partageant la même couche d'image

Voir le dépôt
7414il y a 3 moisVérifié par Kitploit

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

page_inject - Évasion inter-conteneurs AF_ALG aead

Exploitation inter-conteneurs de la vulnérabilité AF_ALG aead -- pivot depuis un conteneur compromis vers chaque conteneur frère partageant la même couche d'image libc.so.6.

Il s'agit d'une primitive d'évasion : elle s'exécute depuis l'intérieur d'un conteneur non privilégié que l'attaquant a déjà compromis, et exploite le bug d'écriture arbitraire de 4 octets lié à la rotation ESN authencesn d'AF_ALG (CVE-2026-31431) pour implanter un hook read() persistant dans les pages du cache de pages de libc.so.6. Comme Docker / containerd adossent les fichiers de la couche inférieure d'overlayfs à des inodes partagés, ces pages sont visibles par chaque conteneur frère instancié à partir de la même image -- le hook se déclenche aussi dans leurs processus, et l'attaquant obtient l'exécution de commandes dans chacun d'eux.

Modèle de menace

  • L'attaquant dispose d'un accès shell à un seul conteneur (appelons-le victim) sur un hôte qui exécute d'autres conteneurs (siblings) à partir de la même image que victim.
  • victim s'exécute avec la posture Docker/k8s par défaut : uid non privilégié dans l'espace de noms utilisateur du conteneur, profil seccomp par défaut, profil AppArmor par défaut, aucune capacité spéciale, aucun montage bind de l'hôte.
  • victim dispose uniquement de :
    • un accès en lecture à sa propre libc (/usr/lib/x86_64-linux-gnu/libc.so.6 ou là où la distribution l'installe)
    • la famille d'appels système standard socket(AF_ALG, ...)
    • les appels système standards splice / vmsplice
    • un accès en écriture à un répertoire qu'il peut chmod +x (par exemple /tmp)
  • Le noyau doit être vulnérable à CVE-2026-31431 (toute compilation algif_aead + authencesn antérieure au correctif de réversion en amont).

C'est tout. Aucune CAP_* spéciale, aucun accès au système de fichiers de l'hôte. L'attaquant dépose un binaire autonome lié statiquement dans le conteneur, l'exécute, et la corruption du cache de pages -- et donc le hook -- devient visible pour chaque conteneur frère.

Comment l'exploitation s'enchaîne

  1. Identité des pages du cache de pages. Dans un conteneur overlayfs, /usr/lib/.../libc.so.6 est servi par l'inode ext4 de la couche inférieure de l'image. Chaque conteneur démarré à partir de la même image partage cet inode sous-jacent, et le cache de pages du noyau est indexé par l'inode sous-jacent -- pas par l'overlay ni par l'espace de noms. Une simple écriture de 4 octets dans une page du cache de pages est donc visible par tous les processus des conteneurs frères qui ont cette page mmapée.

  2. La vulnérabilité AF_ALG aead transforme une telle écriture en plusieurs. algif_aead chaîne l'iovec RX utilisateur avec les octets authsize de fin du SGL TX splicé, et la rotation ESN d'authencesn dépose 4 octets du champ seq_high de l'AAD à dst[assoclen + cryptlen] -- soit le premier octet de cette queue étrangère chaînée. La page splicée est une page du cache de pages d'un fichier auquel l'attaquant n'a qu'un accès en lecture, mais le chiffreur y copie quand même des octets, sans comptabilité de pages sales. (Voir crypto/algif_aead.c et crypto/authencesn.c pour les mécanismes sous-jacents.)

  3. Amorçage d'une primitive appelable. La première chose que fait page_inject est d'amorcer -- une ré-implémentation encodée en asm du même manège AF_ALG (), placée dans la cavité de la libc. Cela fait de l'écriture de 4 octets un simple depuis n'importe quelle future charge utile de hook, sans configuration de socket à chaque appel.

Compilation

L'injecteur est compilé à l'extérieur du conteneur victime -- généralement sur la machine de développement de l'attaquant -- car la plupart des images de conteneurs de production ne fournissent pas de compilateur. Un environnement de développement Linux x86_64 standard avec gcc (prenant en charge la liaison -static) et nasm suffit.

root@kitploit:~
make            # assembles .asm sources via gen_arrays.sh, links static page_inject
make shellcode  # also produces inspectable .bin flat binaries
make clean      # removes generated files and the binary

Le résultat est un unique ELF statique (./page_inject) qui s'exécute sur tout noyau Linux x86_64 moderne.

Livraison et utilisation (depuis le conteneur victime)

Une fois que l'attaquant a un shell sur victim, il téléverse le binaire dans un répertoire accessible en écriture (généralement /tmp) :

root@kitploit:~
# inside the compromised container, attacker session
victim$ ./page_inject

Sans argument, page_inject utilise par défaut /usr/lib/x86_64-linux-gnu/libc.so.6 (l'emplacement Debian/Ubuntu post-merge). Pour les autres distributions, la libc se trouve à un autre chemin ; on peut soit la passer explicitement, soit utiliser --root / pour analyser la table de correspondance intégrée depuis la racine du conteneur :

root@kitploit:~
# Fedora / Rocky / CentOS
victim$ ./page_inject /usr/lib64/libc.so.6

# Arch
victim$ ./page_inject /usr/lib/libc.so.6

# Auto-detect, regardless of distro:
victim$ ./page_inject --root /

Les deux invocations font la même chose : parser l'ELF de la libc du conteneur, installer le hook dans son cache de pages, surveiller la table de slots pendant ~30 s pendant que les conteneurs frères s'enregistrent, et exécuter un id ponctuel contre le premier frère enregistré à titre de vérification.

Après le bootstrap, on bascule dans le shell de commande pour piloter n'importe quel frère enregistré :

root@kitploit:~
victim$ ./page_inject --shell --no-bootstrap
=== page-cache shell ===
Containers (3):
  [0] 0x0018598d  <- target
  [1] 0x001859ab
  [2] 0x001859cd

inject:0018598d> exec id
uid=0(root) gid=0(root) groups=0(root)
inject:0018598d> target 0x001859ab
inject:001859ab> exec hostname
1ccd66abee9d
inject:001859ab> exec cat /etc/shadow
root:$6$.....
inject:001859ab> unhook
... read() prologue restored, slot table zeroed ...

unhook purge le hook de chaque conteneur frère d'un seul coup et laisse les enfants hooks s'arrêter d'eux-mêmes.

root@kitploit:~
Usage: page_inject [OPTIONS] [LIBC_PATH]

Options:
  --root <prefix>   Auto-resolve libc.so.6 under <prefix> using the
                    built-in fixed-path lookup table. Inside the
                    victim container that's normally --root / .
  --shell [0xKEY]   Drop into interactive command shell after
                    injection. Optional KEY pre-selects the target.
  --no-bootstrap    Skip injection (shell-only; hook must already
                    be live in the page cache).
  --timeout SEC     Slot monitoring timeout in --shell mode
                    (default 30 s).
  --help, -h        Show help.

Default libc (when no --root and no LIBC_PATH given):
  /usr/lib/x86_64-linux-gnu/libc.so.6

Double chemin d'injection

Les différentes compilations de glibc laissent des quantités variables d'espace de cavité .text entre le segment LOAD exécutable et le LOAD en lecture seule suivant. page_inject choisit entre deux dispositions au moment de l'injection :

  • Chemin A -- libc uniquement (par défaut). Zone C et Zone A vivent toutes deux dans la cavité .text de la libc. Les zones table de slots + CMD + OUTPUT vivent dans la section .hash de la libc -- des données de hachage SysV héritées que ld.so ne lit pas à l'exécution puisqu'il utilise .gnu.hash à la place. Quand .hash est absente (toolchain moderne d'Arch), page_inject découpe la région de slots dans la queue de .eh_frame_hdr à la place, après avoir d'abord réduit le champ fde_count afin que l'unwinder ne considère plus les octets libérés comme faisant partie de l'index de recherche binaire FDE (l'unwinder retombe de manière transparente sur un balayage linéaire de .eh_frame pour toute IP dont le FDE se trouvait dans la plage tronquée -- comportement imposé par la LSB).

  • Chemin B -- trampoline libc + charge utile ld.so. Certaines compilations de glibc réduisent la cavité de la libc sous la taille nécessaire à la charge utile complète Zone C + Zone A (Ubuntu 24.04 / glibc 2.39 fournit une cavité de 711 octets). Dans ce cas, page_inject écrit un trampoline de 36 octets dans la cavité de la libc -- il effectue le contrôle de la clé .bss du chemin rapide intra-libc -- et sur le chemin lent, il calcule la base d'exécution de ld.so à partir du slot GOT de la libc pour (un symbole côté ld.so que chaque glibc importe en privé) et saute vers une variante à registre de base de Zone C dans la cavité de . La table de slots + CMD + OUTPUT + la clé restent toutes dans la libc ; la Zone C côté ld.so y accède via après que le trampoline a initialisé .

Si aucune des deux dispositions ne convient, page_inject refuse proprement sans rien écrire dans libc ou ld.so sur disque ni dans le cache de pages.

Gestion du prologue de read()

Les différentes versions de glibc émettent des séquences d'ouverture différentes dans read(). L'injecteur reconnaît chacune d'elles, relit les octets que le hook déplace, et les émule dans le chemin rapide de Zone C afin que read() mono-thread reprenne correctement à read+N :

Le slot d'émulation du chemin rapide dans Zone C est dimensionné pour le prologue connu le plus long (8 octets) plus le jmp rel32 de 5 octets ; les prologues plus courts remplissent l'octet de fin de slot avec un NOP de remplissage afin que la longueur totale du slot soit constante.

Structure des fichiers

root@kitploit:~
page_inject/
  page_inject.c           Main injector: ELF parsing, vuln primitive,
                          dual-path layout selection, inject + unhook.
  zone_c.asm              Path-A hook dispatcher shellcode.
  zone_c_ld.asm           Path-B hook dispatcher (rbp-base variant).
  trampoline.asm          Path-B 36-byte libc-side stub.
  write_cache.asm         Zone A (vuln write primitive shellcode).
  gen_arrays.sh           Assemble .asm -> asm_bytecode.c.
  asm_bytecode.c          [generated] shellcode byte arrays.
  Makefile                Build system.

Matrice testée et prise en charge

L'exploit a été vérifié de bout en bout sur les distributions de snapshots de conteneurs suivantes. Pour chaque entrée, page_inject a été injecté depuis l'intérieur d'un conteneur et son hook a été observé se déclencher dans un conteneur frère démarré à partir de la même image ; les commandes se sont exécutées correctement via le canal du cache de pages ; et unhook a restauré proprement l'état des pages de la libc.

Notes opérationnelles

  • page_inject est volontairement lié statiquement afin que le propre processus de l'attaquant ne soit pas affecté par le hook qu'il installe.
  • page_inject reconnaît une libc « déjà hookée » (E9 + nops au niveau du prologue de read()) et refuse de ré-injecter. Si vous êtes dans un environnement de test et que votre cache de pages est bloqué dans cet état, arrêtez tous les conteneurs utilisant l'image et faites un drop_caches pour réinitialiser.
Télécharger l’outil
Zone A
write_cache.asm
.text
call
  • Installation du hook. L'injecteur écrit ensuite Zone C (zone_c.asm) dans la cavité .text de la libc et patch les 7 à 12 premiers octets de read() avec un saut E9 disp32 vers elle. Les octets déplacés du prologue sont émulés fidèlement dans le chemin rapide de Zone C (trois prologues glibc différents sont reconnus -- voir « Gestion du prologue de read() » ci-dessous). Le hook est désormais actif dans le cache de pages de la libc.

  • Propagation du hook. Chaque conteneur frère exécute des processus qui appellent read() constamment (démons de journalisation, healthchecks, cat /etc/hostname, n'importe quoi). Au premier appel de ce type dans un conteneur frère, le prologue détourné saute dans Zone C, qui :

    • appelle stat("/") sur l'inode racine du conteneur (un ID stable par espace de noms) et l'utilise comme clé de slot du conteneur,
    • parcourt la table de slots pour trouver une entrée existante avec cette clé,
    • si elle est absente, enregistre la clé et fork() un enfant de boucle de commandes de longue durée qui interroge la zone CMD à la recherche d'ordres,
    • revient à read()+N afin que l'appelant n'y voie que du feu. Le processus frère d'origine continue de s'exécuter. Dès lors, l'attaquant dispose d'un démon dans ce conteneur.
  • Canal de commande. L'attaquant utilise le même binaire page_inject en mode --shell pour écrire des commandes dans la zone CMD de la région de slots. L'enfant hook de chaque conteneur frère enregistré interroge, fork /bin/sh -c <cmd>, capture stdout/stderr dans la zone OUTPUT, signale la fin, puis reprend son interrogation. Le shell affiche la sortie. Comme chaque écriture CMD/OUTPUT passe également par la primitive de la vulnérabilité, aucun privilège spécial n'est nécessaire.

  • Désinstallation du hook. Une fois l'opération terminée, unhook restaure les octets d'origine du prologue de read() et met la table de slots à zéro ; les enfants hooks voient un slot vide à leur prochaine itération et s'arrêtent d'eux-mêmes. Les modifications du cache de pages elles-mêmes sont propres (le noyau n'a jamais marqué les pages modifiées comme sales), donc une fois que chaque conteneur ayant la libc mmapée est arrêté, un drop_caches rétablit intégralement le cache -- aucune artefact sur disque ne subsiste.

  • _rtld_global
    .text
    ld.so
    .bss
    rbp + offset
    rbp = libc_base
    Plage glibcPrologue (après endbr64 optionnel)Remarques
    2.36 / 2.39cmpb $0x0, __libc_single_threaded(%rip)7 octets ; le cmpb émulé positionne ZF pour le jne .Lthreaded d'origine.
    2.43push rbp; movsxd rdi,edi; xor r9d,r9d7 octets ; émulé octet par octet.
    2.31 / 2.35mov eax, fs:[0x18]8 octets ; émulé octet par octet (le [disp32] préfixé FS est absolu, pas relatif à RIP, donc la copie d'octets est fidèle).
    ImageglibcChemin d'injectionPrologue de read()Région de slots
    debian:bookworm2.36Acmpb.hash
    ubuntu:24.042.39Bcmpb.hash (côté libc, adressé via rbp depuis ld.so)
    ubuntu:22.042.35ATLS-fs.hash
    fedora:402.39Acmpb.hash
    archlinux:latest2.43Apush-rbp.eh_frame_hdr (queue tronquée)