Commit | Line | Data |
---|---|---|
0c0c20aa AM |
1 | From 5fec3406547fd1e46838a76f000102beb6bfe468 Mon Sep 17 00:00:00 2001 |
2 | From: "Heiko Schlittermann (HS12-RIPE)" <hs@schlittermann.de> | |
3 | Date: Sun, 14 Mar 2021 12:16:57 +0100 | |
4 | Subject: [PATCH 24/29] CVE-2020-28008: Assorted attacks in Exim's spool | |
5 | directory | |
6 | ||
7 | We patch dbfn_open() by introducing two functions priv_drop_temp() and | |
8 | priv_restore() (inspired by OpenSSH's functions temporarily_use_uid() | |
9 | and restore_uid()), which temporarily drop and restore root privileges | |
10 | thanks to seteuid(). This goes against Exim's developers' wishes ("Exim | |
11 | (the project) doesn't trust seteuid to work reliably") but, to the best | |
12 | of our knowledge, seteuid() works everywhere and is the only way to | |
13 | securely fix dbfn_open(). | |
14 | ||
15 | (cherry picked from commit 18da59151dbafa89be61c63580bdb295db36e374) | |
16 | (cherry picked from commit b05dc3573f4cd476482374b0ac0393153d344338) | |
17 | --- | |
18 | doc/ChangeLog | 6 +++ | |
19 | src/dbfn.c | 110 +++++++++++++++++++++++++----------------- | |
20 | test/stderr/0275 | 2 +- | |
21 | test/stderr/0278 | 2 +- | |
22 | test/stderr/0386 | 2 +- | |
23 | test/stderr/0388 | 2 +- | |
24 | test/stderr/0402 | 2 +- | |
25 | test/stderr/0403 | 2 +- | |
26 | test/stderr/0404 | 2 +- | |
27 | test/stderr/0408 | 2 +- | |
28 | test/stderr/0487 | 2 +- | |
29 | 11 files changed, 80 insertions(+), 54 deletions(-) | |
30 | ||
31 | diff --git a/doc/ChangeLog b/doc/ChangeLog | |
32 | index 5741fb212..b32347c5b 100644 | |
33 | --- a/doc/ChangeLog | |
34 | +++ b/doc/ChangeLog | |
35 | @@ -14,6 +14,12 @@ JH/42 Bug 2545: Fix CHUNKING for all RCPT commands rejected. Previously we | |
36 | ||
37 | Exim version 4.92.2 | |
38 | ------------------- | |
39 | +QS/02 PID file creation/deletion: only possible if uid=0 or uid is the Exim | |
40 | + runtime user. | |
41 | + | |
42 | +QS/01 Creation of (database) files in $spool_dir: only uid=0 or the euid of | |
43 | + the Exim runtime user are allowed to create files. | |
44 | + | |
45 | ||
46 | HS/01 Handle trailing backslash gracefully. (CVE-2019-15846) | |
47 | ||
48 | diff --git a/src/dbfn.c b/src/dbfn.c | |
49 | index 336cfe73e..902756508 100644 | |
50 | --- a/src/dbfn.c | |
51 | +++ b/src/dbfn.c | |
52 | @@ -59,6 +59,66 @@ log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg); | |
53 | ||
54 | ||
55 | ||
56 | +static enum { | |
57 | + PRIV_DROPPING, PRIV_DROPPED, | |
58 | + PRIV_RESTORING, PRIV_RESTORED | |
59 | +} priv_state = PRIV_RESTORED; | |
60 | + | |
61 | +static uid_t priv_euid; | |
62 | +static gid_t priv_egid; | |
63 | +static gid_t priv_groups[EXIM_GROUPLIST_SIZE + 1]; | |
64 | +static int priv_ngroups; | |
65 | + | |
66 | +/* Inspired by OpenSSH's temporarily_use_uid(). Thanks! */ | |
67 | + | |
68 | +static void | |
69 | +priv_drop_temp(const uid_t temp_uid, const gid_t temp_gid) | |
70 | +{ | |
71 | +if (priv_state != PRIV_RESTORED) _exit(EXIT_FAILURE); | |
72 | +priv_state = PRIV_DROPPING; | |
73 | + | |
74 | +priv_euid = geteuid(); | |
75 | +if (priv_euid == root_uid) | |
76 | + { | |
77 | + priv_egid = getegid(); | |
78 | + priv_ngroups = getgroups(nelem(priv_groups), priv_groups); | |
79 | + if (priv_ngroups < 0) _exit(EXIT_FAILURE); | |
80 | + | |
81 | + if (priv_ngroups > 0 && setgroups(1, &temp_gid) != 0) _exit(EXIT_FAILURE); | |
82 | + if (setegid(temp_gid) != 0) _exit(EXIT_FAILURE); | |
83 | + if (seteuid(temp_uid) != 0) _exit(EXIT_FAILURE); | |
84 | + | |
85 | + if (geteuid() != temp_uid) _exit(EXIT_FAILURE); | |
86 | + if (getegid() != temp_gid) _exit(EXIT_FAILURE); | |
87 | + } | |
88 | + | |
89 | +priv_state = PRIV_DROPPED; | |
90 | +} | |
91 | + | |
92 | +/* Inspired by OpenSSH's restore_uid(). Thanks! */ | |
93 | + | |
94 | +static void | |
95 | +priv_restore(void) | |
96 | +{ | |
97 | +if (priv_state != PRIV_DROPPED) _exit(EXIT_FAILURE); | |
98 | +priv_state = PRIV_RESTORING; | |
99 | + | |
100 | +if (priv_euid == root_uid) | |
101 | + { | |
102 | + if (seteuid(priv_euid) != 0) _exit(EXIT_FAILURE); | |
103 | + if (setegid(priv_egid) != 0) _exit(EXIT_FAILURE); | |
104 | + if (priv_ngroups > 0 && setgroups(priv_ngroups, priv_groups) != 0) _exit(EXIT_FAILURE); | |
105 | + | |
106 | + if (geteuid() != priv_euid) _exit(EXIT_FAILURE); | |
107 | + if (getegid() != priv_egid) _exit(EXIT_FAILURE); | |
108 | + } | |
109 | + | |
110 | +priv_state = PRIV_RESTORED; | |
111 | +} | |
112 | + | |
113 | + | |
114 | + | |
115 | + | |
116 | /************************************************* | |
117 | * Open and lock a database file * | |
118 | *************************************************/ | |
119 | @@ -89,7 +149,6 @@ dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof) | |
120 | { | |
121 | int rc, save_errno; | |
122 | BOOL read_only = flags == O_RDONLY; | |
123 | -BOOL created = FALSE; | |
124 | flock_t lock_data; | |
125 | uschar dirname[256], filename[256]; | |
126 | ||
127 | @@ -111,12 +170,13 @@ exists, there is no error. */ | |
128 | snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory); | |
129 | snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name); | |
130 | ||
131 | +priv_drop_temp(exim_uid, exim_gid); | |
132 | if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0) | |
133 | { | |
134 | - created = TRUE; | |
135 | (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, TRUE); | |
136 | dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE); | |
137 | } | |
138 | +priv_restore(); | |
139 | ||
140 | if (dbblock->lockfd < 0) | |
141 | { | |
142 | @@ -165,57 +225,17 @@ it easy to pin this down, there are now debug statements on either side of the | |
143 | open call. */ | |
144 | ||
145 | snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name); | |
146 | -EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr)); | |
147 | ||
148 | +priv_drop_temp(exim_uid, exim_gid); | |
149 | +EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr)); | |
150 | if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR) | |
151 | { | |
152 | DEBUG(D_hints_lookup) | |
153 | debug_printf_indent("%s appears not to exist: trying to create\n", filename); | |
154 | - created = TRUE; | |
155 | EXIM_DBOPEN(filename, dirname, flags|O_CREAT, EXIMDB_MODE, &(dbblock->dbptr)); | |
156 | } | |
157 | - | |
158 | save_errno = errno; | |
159 | - | |
160 | -/* If we are running as root and this is the first access to the database, its | |
161 | -files will be owned by root. We want them to be owned by exim. We detect this | |
162 | -situation by noting above when we had to create the lock file or the database | |
163 | -itself. Because the different dbm libraries use different extensions for their | |
164 | -files, I don't know of any easier way of arranging this than scanning the | |
165 | -directory for files with the appropriate base name. At least this deals with | |
166 | -the lock file at the same time. Also, the directory will typically have only | |
167 | -half a dozen files, so the scan will be quick. | |
168 | - | |
169 | -This code is placed here, before the test for successful opening, because there | |
170 | -was a case when a file was created, but the DBM library still returned NULL | |
171 | -because of some problem. It also sorts out the lock file if that was created | |
172 | -but creation of the database file failed. */ | |
173 | - | |
174 | -if (created && geteuid() == root_uid) | |
175 | - { | |
176 | - DIR *dd; | |
177 | - struct dirent *ent; | |
178 | - uschar *lastname = Ustrrchr(filename, '/') + 1; | |
179 | - int namelen = Ustrlen(name); | |
180 | - | |
181 | - *lastname = 0; | |
182 | - dd = opendir(CS filename); | |
183 | - | |
184 | - while ((ent = readdir(dd))) | |
185 | - if (Ustrncmp(ent->d_name, name, namelen) == 0) | |
186 | - { | |
187 | - struct stat statbuf; | |
188 | - Ustrcpy(lastname, ent->d_name); | |
189 | - if (Ustat(filename, &statbuf) >= 0 && statbuf.st_uid != exim_uid) | |
190 | - { | |
191 | - DEBUG(D_hints_lookup) debug_printf_indent("ensuring %s is owned by exim\n", filename); | |
192 | - if (Uchown(filename, exim_uid, exim_gid)) | |
193 | - DEBUG(D_hints_lookup) debug_printf_indent("failed setting %s to owned by exim\n", filename); | |
194 | - } | |
195 | - } | |
196 | - | |
197 | - closedir(dd); | |
198 | - } | |
199 | +priv_restore(); | |
200 | ||
201 | /* If the open has failed, return NULL, leaving errno set. If lof is TRUE, | |
202 | log the event - also for debugging - but debug only if the file just doesn't | |
203 | -- | |
204 | 2.30.2 | |
205 |