1 From 99ae249e9857e80ff4d65b2388bc68c624dcb739 Mon Sep 17 00:00:00 2001
2 From: Qualys Security Advisory <qsa@qualys.com>
3 Date: Tue, 23 Feb 2021 08:33:03 -0800
4 Subject: [PATCH 23/29] CVE-2020-28007: Link attack in Exim's log directory
6 We patch this vulnerability by opening (instead of just creating) the
7 log file in an unprivileged (exim) child process, and by passing this
8 file descriptor back to the privileged (root) parent process. The two
9 functions log_send_fd() and log_recv_fd() are inspired by OpenSSH's
10 functions mm_send_fd() and mm_receive_fd(); thanks!
12 This patch also fixes:
14 - a NULL-pointer dereference in usr1_handler() (this signal handler is
15 installed before process_log_path is initialized);
17 - a file-descriptor leak in dmarc_write_history_file() (two return paths
18 did not close history_file_fd).
20 Note: the use of log_open_as_exim() in dmarc_write_history_file() should
21 be fine because the documentation explicitly states "Make sure the
22 directory of this file is writable by the user exim runs as."
24 (cherry picked from commit 2502cc41d1d92c1413eca6a4ba035c21162662bd)
25 (cherry picked from commit 93e9a18fbf09deb59bd133986f4c89aeb2d2d86a)
27 src/dmarc.c | 179 ++++++++++++++++++------------------
29 src/functions.h | 3 +-
30 src/log.c | 214 ++++++++++++++++++++++++++++----------------
31 test/stderr/0397 | 6 +-
32 5 files changed, 234 insertions(+), 182 deletions(-)
34 diff --git a/src/dmarc.c b/src/dmarc.c
35 index f29f7eba6..c5e01c7ee 100644
38 @@ -204,6 +204,97 @@ if ( dmarc_policy == DMARC_POLICY_REJECT && action == DMARC_RESULT_REJECT
44 +dmarc_write_history_file()
47 +u_char **rua; /* aggregate report addressees */
48 +uschar *history_buffer = NULL;
50 +if (!dmarc_history_file)
52 + DEBUG(D_receive) debug_printf("DMARC history file not set\n");
53 + return DMARC_HIST_DISABLED;
56 +/* Generate the contents of the history file */
57 +history_buffer = string_sprintf(
58 + "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
59 + message_id, primary_hostname, time(NULL), sender_host_address,
60 + header_from_sender, expand_string(US"$sender_address_domain"));
63 + history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_ares_result);
64 + /* history_buffer = string_sprintf("%sspf -1\n", history_buffer); */
66 +history_buffer = string_sprintf(
67 + "%s%spdomain %s\npolicy %d\n",
68 + history_buffer, dkim_history_buffer, dmarc_used_domain, dmarc_policy);
70 +if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1)))
71 + for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++)
72 + history_buffer = string_sprintf("%srua %s\n", history_buffer, rua[tmp_ans]);
74 + history_buffer = string_sprintf("%srua -\n", history_buffer);
76 +opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans);
77 +history_buffer = string_sprintf("%spct %d\n", history_buffer, tmp_ans);
79 +opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans);
80 +history_buffer = string_sprintf("%sadkim %d\n", history_buffer, tmp_ans);
82 +opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans);
83 +history_buffer = string_sprintf("%saspf %d\n", history_buffer, tmp_ans);
85 +opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
86 +history_buffer = string_sprintf("%sp %d\n", history_buffer, tmp_ans);
88 +opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans);
89 +history_buffer = string_sprintf("%ssp %d\n", history_buffer, tmp_ans);
91 +history_buffer = string_sprintf(
92 + "%salign_dkim %d\nalign_spf %d\naction %d\n",
93 + history_buffer, da, sa, action);
95 +/* Write the contents to the history file */
97 + debug_printf("DMARC logging history data for opendmarc reporting%s\n",
98 + (host_checking || f.running_in_test_harness) ? " (not really)" : "");
99 +if (host_checking || f.running_in_test_harness)
102 + debug_printf("DMARC history data for debugging:\n%s", history_buffer);
106 + ssize_t written_len;
107 + const int history_file_fd = log_open_as_exim(dmarc_history_file);
109 + if (history_file_fd < 0)
111 + log_write(0, LOG_MAIN|LOG_PANIC, "failure to create DMARC history file: %s",
112 + dmarc_history_file);
113 + return DMARC_HIST_FILE_ERR;
116 + written_len = write_to_fd_buf(history_file_fd,
118 + Ustrlen(history_buffer));
120 + (void)close(history_file_fd);
122 + if (written_len <= 0)
124 + log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
125 + dmarc_history_file);
126 + return DMARC_HIST_WRITE_ERR;
129 +return DMARC_HIST_OK;
133 /* dmarc_process adds the envelope sender address to the existing
134 context (if any), retrieves the result, sets up expansion
135 strings and evaluates the condition outcome. */
136 @@ -486,94 +577,6 @@ if (!f.dmarc_disable_verify)
141 -dmarc_write_history_file()
143 -int history_file_fd;
144 -ssize_t written_len;
146 -u_char **rua; /* aggregate report addressees */
147 -uschar *history_buffer = NULL;
149 -if (!dmarc_history_file)
151 - DEBUG(D_receive) debug_printf("DMARC history file not set\n");
152 - return DMARC_HIST_DISABLED;
154 -history_file_fd = log_create(dmarc_history_file);
156 -if (history_file_fd < 0)
158 - log_write(0, LOG_MAIN|LOG_PANIC, "failure to create DMARC history file: %s",
159 - dmarc_history_file);
160 - return DMARC_HIST_FILE_ERR;
163 -/* Generate the contents of the history file */
164 -history_buffer = string_sprintf(
165 - "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
166 - message_id, primary_hostname, time(NULL), sender_host_address,
167 - header_from_sender, expand_string(US"$sender_address_domain"));
170 - history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_ares_result);
171 - /* history_buffer = string_sprintf("%sspf -1\n", history_buffer); */
173 -history_buffer = string_sprintf(
174 - "%s%spdomain %s\npolicy %d\n",
175 - history_buffer, dkim_history_buffer, dmarc_used_domain, dmarc_policy);
177 -if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1)))
178 - for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++)
179 - history_buffer = string_sprintf("%srua %s\n", history_buffer, rua[tmp_ans]);
181 - history_buffer = string_sprintf("%srua -\n", history_buffer);
183 -opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans);
184 -history_buffer = string_sprintf("%spct %d\n", history_buffer, tmp_ans);
186 -opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans);
187 -history_buffer = string_sprintf("%sadkim %d\n", history_buffer, tmp_ans);
189 -opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans);
190 -history_buffer = string_sprintf("%saspf %d\n", history_buffer, tmp_ans);
192 -opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
193 -history_buffer = string_sprintf("%sp %d\n", history_buffer, tmp_ans);
195 -opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans);
196 -history_buffer = string_sprintf("%ssp %d\n", history_buffer, tmp_ans);
198 -history_buffer = string_sprintf(
199 - "%salign_dkim %d\nalign_spf %d\naction %d\n",
200 - history_buffer, da, sa, action);
202 -/* Write the contents to the history file */
204 - debug_printf("DMARC logging history data for opendmarc reporting%s\n",
205 - (host_checking || f.running_in_test_harness) ? " (not really)" : "");
206 -if (host_checking || f.running_in_test_harness)
209 - debug_printf("DMARC history data for debugging:\n%s", history_buffer);
213 - written_len = write_to_fd_buf(history_file_fd,
215 - Ustrlen(history_buffer));
216 - if (written_len == 0)
218 - log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
219 - dmarc_history_file);
220 - return DMARC_HIST_WRITE_ERR;
222 - (void)close(history_file_fd);
224 -return DMARC_HIST_OK;
229 dmarc_exim_expand_query(int what)
231 diff --git a/src/exim.c b/src/exim.c
232 index a7dc48c4e..f0a168983 100644
235 @@ -227,18 +227,8 @@ int fd;
237 os_restarting_signal(sig, usr1_handler);
239 -if ((fd = Uopen(process_log_path, O_APPEND|O_WRONLY, LOG_MODE)) < 0)
241 - /* If we are already running as the Exim user, try to create it in the
242 - current process (assuming spool_directory exists). Otherwise, if we are
243 - root, do the creation in an exim:exim subprocess. */
245 - int euid = geteuid();
246 - if (euid == exim_uid)
247 - fd = Uopen(process_log_path, O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
248 - else if (euid == root_uid)
249 - fd = log_create_as_exim(process_log_path);
251 +if (!process_log_path) return;
252 +fd = log_open_as_exim(process_log_path);
254 /* If we are neither exim nor root, or if we failed to create the log file,
255 give up. There is not much useful we can do with errors, since we don't want
256 diff --git a/src/functions.h b/src/functions.h
257 index cab7a7363..366cb2f26 100644
258 --- a/src/functions.h
259 +++ b/src/functions.h
260 @@ -281,8 +281,7 @@ extern int ip_streamsocket(const uschar *, uschar **, int);
261 extern int ipv6_nmtoa(int *, uschar *);
263 extern uschar *local_part_quote(uschar *);
264 -extern int log_create(uschar *);
265 -extern int log_create_as_exim(uschar *);
266 +extern int log_open_as_exim(uschar *);
267 extern void log_close_all(void);
269 extern macro_item * macro_create(const uschar *, const uschar *, BOOL);
270 diff --git a/src/log.c b/src/log.c
271 index c8313890e..15c88c13e 100644
274 @@ -264,14 +264,19 @@ overwrite it temporarily if it is necessary to create the directory.
275 Returns: a file descriptor, or < 0 on failure (errno set)
279 -log_create(uschar *name)
281 +log_open_already_exim(uschar * const name)
283 -int fd = Uopen(name,
287 - O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
289 +const int flags = O_WRONLY | O_APPEND | O_CREAT | O_NONBLOCK;
291 +if (geteuid() != exim_uid)
297 +fd = Uopen(name, flags, LOG_MODE);
299 /* If creation failed, attempt to build a log directory in case that is the
301 @@ -285,11 +290,7 @@ if (fd < 0 && errno == ENOENT)
302 DEBUG(D_any) debug_printf("%s log directory %s\n",
303 created ? "created" : "failed to create", name);
305 - if (created) fd = Uopen(name,
309 - O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
310 + if (created) fd = Uopen(name, flags, LOG_MODE);
314 @@ -297,6 +298,81 @@ return fd;
318 +/* Inspired by OpenSSH's mm_send_fd(). Thanks! */
321 +log_send_fd(const int sock, const int fd)
325 + struct cmsghdr hdr;
326 + char buf[CMSG_SPACE(sizeof(int))];
328 +struct cmsghdr *cmsg;
333 +memset(&msg, 0, sizeof(msg));
334 +memset(&cmsgbuf, 0, sizeof(cmsgbuf));
335 +msg.msg_control = &cmsgbuf.buf;
336 +msg.msg_controllen = sizeof(cmsgbuf.buf);
338 +cmsg = CMSG_FIRSTHDR(&msg);
339 +cmsg->cmsg_len = CMSG_LEN(sizeof(int));
340 +cmsg->cmsg_level = SOL_SOCKET;
341 +cmsg->cmsg_type = SCM_RIGHTS;
342 +*(int *)CMSG_DATA(cmsg) = fd;
349 +while ((n = sendmsg(sock, &msg, 0)) == -1 && errno == EINTR);
350 +if (n != 1) return -1;
354 +/* Inspired by OpenSSH's mm_receive_fd(). Thanks! */
357 +log_recv_fd(const int sock)
361 + struct cmsghdr hdr;
362 + char buf[CMSG_SPACE(sizeof(int))];
364 +struct cmsghdr *cmsg;
370 +memset(&msg, 0, sizeof(msg));
376 +memset(&cmsgbuf, 0, sizeof(cmsgbuf));
377 +msg.msg_control = &cmsgbuf.buf;
378 +msg.msg_controllen = sizeof(cmsgbuf.buf);
380 +while ((n = recvmsg(sock, &msg, 0)) == -1 && errno == EINTR);
381 +if (n != 1 || ch != 'A') return -1;
383 +cmsg = CMSG_FIRSTHDR(&msg);
384 +if (cmsg == NULL) return -1;
385 +if (cmsg->cmsg_type != SCM_RIGHTS) return -1;
386 +fd = *(const int *)CMSG_DATA(cmsg);
387 +if (fd < 0) return -1;
393 /*************************************************
394 * Create a log file as the exim user *
395 *************************************************/
396 @@ -312,41 +388,60 @@ Returns: a file descriptor, or < 0 on failure (errno set)
400 -log_create_as_exim(uschar *name)
401 +log_open_as_exim(uschar * const name)
406 +const uid_t euid = geteuid();
408 -/* In the subprocess, change uid/gid and do the creation. Return 0 from the
409 -subprocess on success. If we don't check for setuid failures, then the file
410 -can be created as root, so vulnerabilities which cause setuid to fail mean
411 -that the Exim user can use symlinks to cause a file to be opened/created as
412 -root. We always open for append, so can't nuke existing content but it would
413 -still be Rather Bad. */
416 +if (euid == exim_uid)
418 - if (setgid(exim_gid) < 0)
419 - die(US"exim: setgid for log-file creation failed, aborting",
420 - US"Unexpected log failure, please try later");
421 - if (setuid(exim_uid) < 0)
422 - die(US"exim: setuid for log-file creation failed, aborting",
423 - US"Unexpected log failure, please try later");
424 - _exit((log_create(name) < 0)? 1 : 0);
425 + fd = log_open_already_exim(name);
427 +else if (euid == root_uid)
430 + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock) == 0)
432 + const pid_t pid = fork();
435 + (void)close(sock[0]);
436 + if (setgroups(1, &exim_gid) != 0) _exit(EXIT_FAILURE);
437 + if (setgid(exim_gid) != 0) _exit(EXIT_FAILURE);
438 + if (setuid(exim_uid) != 0) _exit(EXIT_FAILURE);
440 + if (getuid() != exim_uid || geteuid() != exim_uid) _exit(EXIT_FAILURE);
441 + if (getgid() != exim_gid || getegid() != exim_gid) _exit(EXIT_FAILURE);
443 + fd = log_open_already_exim(name);
444 + if (fd < 0) _exit(EXIT_FAILURE);
445 + if (log_send_fd(sock[1], fd) != 0) _exit(EXIT_FAILURE);
446 + (void)close(sock[1]);
447 + _exit(EXIT_SUCCESS);
450 -/* If we created a subprocess, wait for it. If it succeeded, try the open. */
452 -while (pid > 0 && waitpid(pid, &status, 0) != pid);
453 -if (status == 0) fd = Uopen(name,
457 - O_APPEND|O_WRONLY, LOG_MODE);
458 + (void)close(sock[1]);
461 + fd = log_recv_fd(sock[0]);
462 + while (waitpid(pid, NULL, 0) == -1 && errno == EINTR);
464 + (void)close(sock[0]);
468 -/* If we failed to create a subprocess, we are in a bad way. We return
469 -with fd still < 0, and errno set, letting the caller handle the error. */
473 + flags = fcntl(fd, F_GETFD);
474 + if (flags != -1) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
475 + flags = fcntl(fd, F_GETFL);
476 + if (flags != -1) (void)fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
485 @@ -459,52 +554,17 @@ if (!ok)
486 die(US"exim: log file path too long: aborting",
487 US"Logging failure; please try later");
489 -/* We now have the file name. Try to open an existing file. After a successful
490 -open, arrange for automatic closure on exec(), and then return. */
491 +/* We now have the file name. After a successful open, return. */
497 - O_APPEND|O_WRONLY, LOG_MODE);
498 +*fd = log_open_as_exim(buffer);
503 - (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC);
508 -/* Open was not successful: try creating the file. If this is a root process,
509 -we must do the creating in a subprocess set to exim:exim in order to ensure
510 -that the file is created with the right ownership. Otherwise, there can be a
511 -race if another Exim process is trying to write to the log at the same time.
512 -The use of SIGUSR1 by the exiwhat utility can provoke a lot of simultaneous
517 -/* If we are already running as the Exim user (even if that user is root),
518 -we can go ahead and create in the current process. */
520 -if (euid == exim_uid) *fd = log_create(buffer);
522 -/* Otherwise, if we are root, do the creation in an exim:exim subprocess. If we
523 -are neither exim nor root, creation is not attempted. */
525 -else if (euid == root_uid) *fd = log_create_as_exim(buffer);
527 -/* If we now have an open file, set the close-on-exec flag and return. */
532 - (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC);
537 /* Creation failed. There are some circumstances in which we get here when
538 the effective uid is not root or exim, which is the problem. (For example, a
539 non-setuid binary with log_arguments set, called in certain ways.) Rather than