Commit | Line | Data |
---|---|---|
0c0c20aa AM |
1 | From c166890023f56388cb3482cff3def04171a488c4 Mon Sep 17 00:00:00 2001 |
2 | From: "Heiko Schlittermann (HS12-RIPE)" <hs@schlittermann.de> | |
3 | Date: Thu, 25 Mar 2021 22:48:09 +0100 | |
4 | Subject: [PATCH 26/29] CVE-2020-28014, CVE-2021-27216: Arbitrary PID file | |
5 | creation, clobbering, and deletion | |
6 | ||
7 | Arbitrary PID file creation, clobbering, and deletion. | |
8 | Patch provided by Qualys. | |
9 | ||
10 | (cherry picked from commit 974f32939a922512b27d9f0a8a1cb5dec60e7d37) | |
11 | (cherry picked from commit 43c6f0b83200b7082353c50187ef75de3704580a) | |
12 | --- | |
13 | doc/ChangeLog | 5 + | |
14 | src/daemon.c | 212 ++++++++++++++++++++++++++++++++++++++---- | |
15 | src/exim.c | 12 ++- | |
16 | test/stderr/0433 | 24 +++++ | |
17 | 4 files changed, 232 insertions(+), 21 deletions(-) | |
18 | ||
19 | --- a/doc/ChangeLog | |
20 | +++ b/doc/ChangeLog | |
21 | @@ -10,13 +10,18 @@ QS/02 PID file creation/deletion: only p | |
22 | runtime user. | |
23 | ||
24 | QS/01 Creation of (database) files in $spool_dir: only uid=0 or the euid of | |
25 | the Exim runtime user are allowed to create files. | |
26 | ||
27 | +QS/01 Creation of (database) files in $spool_dir: only uid=0 or the uid of | |
28 | + the Exim runtime user are allowed to create files. | |
29 | ||
30 | HS/01 Handle trailing backslash gracefully. (CVE-2019-15846) | |
31 | ||
32 | +QS/02 PID file creation/deletion: only possible if uid=0 or uid is the Exim | |
33 | + runtime user. | |
34 | + | |
35 | ||
36 | Since version 4.92 | |
37 | ------------------ | |
38 | ||
39 | JH/06 Fix buggy handling of autoreply bounce_return_size_limit, and a possible | |
40 | --- a/src/daemon.c | |
41 | +++ b/src/daemon.c | |
42 | @@ -886,10 +886,198 @@ while ((pid = waitpid(-1, &status, WNOHA | |
43 | } | |
44 | } | |
45 | } | |
46 | ||
47 | ||
48 | +static void | |
49 | +set_pid_file_path(void) | |
50 | +{ | |
51 | +if (override_pid_file_path) | |
52 | + pid_file_path = override_pid_file_path; | |
53 | + | |
54 | +if (!*pid_file_path) | |
55 | + pid_file_path = string_sprintf("%s/exim-daemon.pid", spool_directory); | |
56 | + | |
57 | +if (pid_file_path[0] != '/') | |
58 | + log_write(0, LOG_PANIC_DIE, "pid file path %s must be absolute\n", pid_file_path); | |
59 | +} | |
60 | + | |
61 | + | |
62 | +enum pid_op { PID_WRITE, PID_CHECK, PID_DELETE }; | |
63 | + | |
64 | +/* Do various pid file operations as safe as possible. Ideally we'd just | |
65 | +drop the privileges for creation of the pid file and not care at all about removal of | |
66 | +the file. FIXME. | |
67 | +Returns: true on success, false + errno==EACCES otherwise | |
68 | +*/ | |
69 | +static BOOL | |
70 | +operate_on_pid_file(const enum pid_op operation, const pid_t pid) | |
71 | +{ | |
72 | +char pid_line[sizeof(int) * 3 + 2]; | |
73 | +const int pid_len = snprintf(pid_line, sizeof(pid_line), "%d\n", (int)pid); | |
74 | +BOOL lines_match = FALSE; | |
75 | + | |
76 | +char * path = NULL; | |
77 | +char * base = NULL; | |
78 | +char * dir = NULL; | |
79 | + | |
80 | +const int dir_flags = O_RDONLY | O_NONBLOCK; | |
81 | +const int base_flags = O_NOFOLLOW | O_NONBLOCK; | |
82 | +const mode_t base_mode = 0644; | |
83 | +struct stat sb; | |
84 | + | |
85 | +int cwd_fd = -1; | |
86 | +int dir_fd = -1; | |
87 | +int base_fd = -1; | |
88 | + | |
89 | +BOOL success = FALSE; | |
90 | +errno = EACCES; | |
91 | + | |
92 | +set_pid_file_path(); | |
93 | +if (!f.running_in_test_harness && real_uid != root_uid && real_uid != exim_uid) goto cleanup; | |
94 | +if (pid_len < 2 || pid_len >= (int)sizeof(pid_line)) goto cleanup; | |
95 | + | |
96 | +path = CS string_copy(pid_file_path); | |
97 | +if ((base = Ustrrchr(path, '/')) == NULL) /* should not happen, but who knows */ | |
98 | + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "pid file path \"%s\" does not contain a '/'", pid_file_path); | |
99 | + | |
100 | +dir = (base != path) ? path : "/"; | |
101 | +*base++ = '\0'; | |
102 | + | |
103 | +if (!dir || !*dir || *dir != '/') goto cleanup; | |
104 | +if (!base || !*base || strchr(base, '/') != NULL) goto cleanup; | |
105 | + | |
106 | +cwd_fd = open(".", dir_flags); | |
107 | +if (cwd_fd < 0 || fstat(cwd_fd, &sb) != 0 || !S_ISDIR(sb.st_mode)) goto cleanup; | |
108 | +dir_fd = open(dir, dir_flags); | |
109 | +if (dir_fd < 0 || fstat(dir_fd, &sb) != 0 || !S_ISDIR(sb.st_mode)) goto cleanup; | |
110 | + | |
111 | +/* emulate openat */ | |
112 | +if (fchdir(dir_fd) != 0) goto cleanup; | |
113 | +base_fd = open(base, O_RDONLY | base_flags); | |
114 | +if (fchdir(cwd_fd) != 0) | |
115 | + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); | |
116 | + | |
117 | +if (base_fd >= 0) | |
118 | + { | |
119 | + char line[sizeof(pid_line)]; | |
120 | + ssize_t len = -1; | |
121 | + | |
122 | + if (fstat(base_fd, &sb) != 0 || !S_ISREG(sb.st_mode)) goto cleanup; | |
123 | + if ((sb.st_mode & 07777) != base_mode || sb.st_nlink != 1) goto cleanup; | |
124 | + if (sb.st_size < 2 || sb.st_size >= (off_t)sizeof(line)) goto cleanup; | |
125 | + | |
126 | + len = read(base_fd, line, sizeof(line)); | |
127 | + if (len != (ssize_t)sb.st_size) goto cleanup; | |
128 | + line[len] = '\0'; | |
129 | + | |
130 | + if (strspn(line, "0123456789") != (size_t)len-1) goto cleanup; | |
131 | + if (line[len-1] != '\n') goto cleanup; | |
132 | + lines_match = (len == pid_len && strcmp(line, pid_line) == 0); | |
133 | + } | |
134 | + | |
135 | +if (operation == PID_WRITE) | |
136 | + { | |
137 | + if (!lines_match) | |
138 | + { | |
139 | + if (base_fd >= 0) | |
140 | + { | |
141 | + int error = -1; | |
142 | + /* emulate unlinkat */ | |
143 | + if (fchdir(dir_fd) != 0) goto cleanup; | |
144 | + error = unlink(base); | |
145 | + if (fchdir(cwd_fd) != 0) | |
146 | + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); | |
147 | + if (error) goto cleanup; | |
148 | + (void)close(base_fd); | |
149 | + base_fd = -1; | |
150 | + } | |
151 | + /* emulate openat */ | |
152 | + if (fchdir(dir_fd) != 0) goto cleanup; | |
153 | + base_fd = open(base, O_WRONLY | O_CREAT | O_EXCL | base_flags, base_mode); | |
154 | + if (fchdir(cwd_fd) != 0) | |
155 | + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); | |
156 | + if (base_fd < 0) goto cleanup; | |
157 | + if (fchmod(base_fd, base_mode) != 0) goto cleanup; | |
158 | + if (write(base_fd, pid_line, pid_len) != pid_len) goto cleanup; | |
159 | + DEBUG(D_any) debug_printf("pid written to %s\n", pid_file_path); | |
160 | + } | |
161 | + } | |
162 | +else | |
163 | + { | |
164 | + if (!lines_match) goto cleanup; | |
165 | + if (operation == PID_DELETE) | |
166 | + { | |
167 | + int error = -1; | |
168 | + /* emulate unlinkat */ | |
169 | + if (fchdir(dir_fd) != 0) goto cleanup; | |
170 | + error = unlink(base); | |
171 | + if (fchdir(cwd_fd) != 0) | |
172 | + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); | |
173 | + if (error) goto cleanup; | |
174 | + } | |
175 | + } | |
176 | + | |
177 | +success = TRUE; | |
178 | +errno = 0; | |
179 | + | |
180 | +cleanup: | |
181 | +if (cwd_fd >= 0) (void)close(cwd_fd); | |
182 | +if (dir_fd >= 0) (void)close(dir_fd); | |
183 | +if (base_fd >= 0) (void)close(base_fd); | |
184 | +return success; | |
185 | +} | |
186 | + | |
187 | + | |
188 | +/* Remove the daemon's pidfile. Note: runs with root privilege, | |
189 | +as a direct child of the daemon. Does not return. */ | |
190 | + | |
191 | +void | |
192 | +delete_pid_file(void) | |
193 | +{ | |
194 | +const BOOL success = operate_on_pid_file(PID_DELETE, getppid()); | |
195 | + | |
196 | +DEBUG(D_any) | |
197 | + debug_printf("delete pid file %s %s: %s\n", pid_file_path, | |
198 | + success ? "success" : "failure", strerror(errno)); | |
199 | + | |
200 | +exim_exit(EXIT_SUCCESS, US""); | |
201 | +} | |
202 | + | |
203 | + | |
204 | +/* Called by the daemon; exec a child to get the pid file deleted | |
205 | +since we may require privs for the containing directory */ | |
206 | + | |
207 | +static void | |
208 | +daemon_die(void) | |
209 | +{ | |
210 | +int pid; | |
211 | + | |
212 | +DEBUG(D_any) debug_printf("SIGTERM/SIGINT seen\n"); | |
213 | +#if defined(SUPPORT_TLS) && (defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)) | |
214 | +tls_watch_invalidate(); | |
215 | +#endif | |
216 | + | |
217 | +if (f.running_in_test_harness || write_pid) | |
218 | + { | |
219 | + if ((pid = fork()) == 0) | |
220 | + { | |
221 | + if (override_pid_file_path) | |
222 | + (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, 3, | |
223 | + "-oP", override_pid_file_path, "-oPX"); | |
224 | + else | |
225 | + (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, 1, "-oPX"); | |
226 | + | |
227 | + /* Control never returns here. */ | |
228 | + } | |
229 | + if (pid > 0) | |
230 | + child_close(pid, 1); | |
231 | + } | |
232 | +exim_exit(EXIT_SUCCESS, US""); | |
233 | +} | |
234 | + | |
235 | + | |
236 | ||
237 | /************************************************* | |
238 | * Exim Daemon Mainline * | |
239 | *************************************************/ | |
240 | ||
241 | @@ -1538,32 +1726,18 @@ automatically. Consequently, Exim 4 writ | |
242 | ||
243 | The variable daemon_write_pid is used to control this. */ | |
244 | ||
245 | if (f.running_in_test_harness || write_pid) | |
246 | { | |
247 | - FILE *f; | |
248 | - | |
249 | - if (override_pid_file_path) | |
250 | - pid_file_path = override_pid_file_path; | |
251 | - | |
252 | - if (pid_file_path[0] == 0) | |
253 | - pid_file_path = string_sprintf("%s/exim-daemon.pid", spool_directory); | |
254 | - | |
255 | - if ((f = modefopen(pid_file_path, "wb", 0644))) | |
256 | - { | |
257 | - (void)fprintf(f, "%d\n", (int)getpid()); | |
258 | - (void)fclose(f); | |
259 | - DEBUG(D_any) debug_printf("pid written to %s\n", pid_file_path); | |
260 | - } | |
261 | - else | |
262 | - DEBUG(D_any) | |
263 | - debug_printf("%s\n", string_open_failed(errno, "pid file %s", | |
264 | - pid_file_path)); | |
265 | + const enum pid_op operation = (f.running_in_test_harness | |
266 | + || real_uid == root_uid | |
267 | + || (real_uid == exim_uid && !override_pid_file_path)) ? PID_WRITE : PID_CHECK; | |
268 | + if (!operate_on_pid_file(operation, getpid())) | |
269 | + DEBUG(D_any) debug_printf("%s pid file %s: %s\n", (operation == PID_WRITE) ? "write" : "check", pid_file_path, strerror(errno)); | |
270 | } | |
271 | ||
272 | /* Set up the handler for SIGHUP, which causes a restart of the daemon. */ | |
273 | - | |
274 | sighup_seen = FALSE; | |
275 | signal(SIGHUP, sighup_handler); | |
276 | ||
277 | /* Give up root privilege at this point (assuming that exim_uid and exim_gid | |
278 | are not root). The third argument controls the running of initgroups(). | |
279 | --- a/src/exim.c | |
280 | +++ b/src/exim.c | |
281 | @@ -3042,12 +3042,20 @@ for (i = 1; i < argc; i++) | |
282 | ||
283 | else if (Ustrcmp(argrest, "o") == 0) {} | |
284 | ||
285 | /* -oP <name>: set pid file path for daemon */ | |
286 | ||
287 | - else if (Ustrcmp(argrest, "P") == 0) | |
288 | - override_pid_file_path = argv[++i]; | |
289 | + else if (*argrest == 'P') | |
290 | + { | |
291 | + if (!f.running_in_test_harness && real_uid != root_uid && real_uid != exim_uid) | |
292 | + exim_fail("exim: only uid=%d or uid=%d can use -oP and -oPX " | |
293 | + "(uid=%d euid=%d | %d)\n", | |
294 | + root_uid, exim_uid, getuid(), geteuid(), real_uid); | |
295 | + if (Ustrcmp(argrest, "P") == 0) override_pid_file_path = argv[++i]; | |
296 | + else if (Ustrcmp(argrest, "PX") == 0) delete_pid_file(); | |
297 | + else badarg = TRUE; | |
298 | + } | |
299 | ||
300 | /* -or <n>: set timeout for non-SMTP acceptance | |
301 | -os <n>: set timeout for SMTP acceptance */ | |
302 | ||
303 | else if (*argrest == 'r' || *argrest == 's') |