| 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') |