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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
|
From 5fec3406547fd1e46838a76f000102beb6bfe468 Mon Sep 17 00:00:00 2001
From: "Heiko Schlittermann (HS12-RIPE)" <hs@schlittermann.de>
Date: Sun, 14 Mar 2021 12:16:57 +0100
Subject: [PATCH 24/29] CVE-2020-28008: Assorted attacks in Exim's spool
directory
We patch dbfn_open() by introducing two functions priv_drop_temp() and
priv_restore() (inspired by OpenSSH's functions temporarily_use_uid()
and restore_uid()), which temporarily drop and restore root privileges
thanks to seteuid(). This goes against Exim's developers' wishes ("Exim
(the project) doesn't trust seteuid to work reliably") but, to the best
of our knowledge, seteuid() works everywhere and is the only way to
securely fix dbfn_open().
(cherry picked from commit 18da59151dbafa89be61c63580bdb295db36e374)
(cherry picked from commit b05dc3573f4cd476482374b0ac0393153d344338)
---
doc/ChangeLog | 6 +++
src/dbfn.c | 110 +++++++++++++++++++++++++-----------------
test/stderr/0275 | 2 +-
test/stderr/0278 | 2 +-
test/stderr/0386 | 2 +-
test/stderr/0388 | 2 +-
test/stderr/0402 | 2 +-
test/stderr/0403 | 2 +-
test/stderr/0404 | 2 +-
test/stderr/0408 | 2 +-
test/stderr/0487 | 2 +-
11 files changed, 80 insertions(+), 54 deletions(-)
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 5741fb212..b32347c5b 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -14,6 +14,12 @@ JH/42 Bug 2545: Fix CHUNKING for all RCPT commands rejected. Previously we
Exim version 4.92.2
-------------------
+QS/02 PID file creation/deletion: only possible if uid=0 or uid is the Exim
+ runtime user.
+
+QS/01 Creation of (database) files in $spool_dir: only uid=0 or the euid of
+ the Exim runtime user are allowed to create files.
+
HS/01 Handle trailing backslash gracefully. (CVE-2019-15846)
diff --git a/src/dbfn.c b/src/dbfn.c
index 336cfe73e..902756508 100644
--- a/src/dbfn.c
+++ b/src/dbfn.c
@@ -59,6 +59,66 @@ log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
+static enum {
+ PRIV_DROPPING, PRIV_DROPPED,
+ PRIV_RESTORING, PRIV_RESTORED
+} priv_state = PRIV_RESTORED;
+
+static uid_t priv_euid;
+static gid_t priv_egid;
+static gid_t priv_groups[EXIM_GROUPLIST_SIZE + 1];
+static int priv_ngroups;
+
+/* Inspired by OpenSSH's temporarily_use_uid(). Thanks! */
+
+static void
+priv_drop_temp(const uid_t temp_uid, const gid_t temp_gid)
+{
+if (priv_state != PRIV_RESTORED) _exit(EXIT_FAILURE);
+priv_state = PRIV_DROPPING;
+
+priv_euid = geteuid();
+if (priv_euid == root_uid)
+ {
+ priv_egid = getegid();
+ priv_ngroups = getgroups(nelem(priv_groups), priv_groups);
+ if (priv_ngroups < 0) _exit(EXIT_FAILURE);
+
+ if (priv_ngroups > 0 && setgroups(1, &temp_gid) != 0) _exit(EXIT_FAILURE);
+ if (setegid(temp_gid) != 0) _exit(EXIT_FAILURE);
+ if (seteuid(temp_uid) != 0) _exit(EXIT_FAILURE);
+
+ if (geteuid() != temp_uid) _exit(EXIT_FAILURE);
+ if (getegid() != temp_gid) _exit(EXIT_FAILURE);
+ }
+
+priv_state = PRIV_DROPPED;
+}
+
+/* Inspired by OpenSSH's restore_uid(). Thanks! */
+
+static void
+priv_restore(void)
+{
+if (priv_state != PRIV_DROPPED) _exit(EXIT_FAILURE);
+priv_state = PRIV_RESTORING;
+
+if (priv_euid == root_uid)
+ {
+ if (seteuid(priv_euid) != 0) _exit(EXIT_FAILURE);
+ if (setegid(priv_egid) != 0) _exit(EXIT_FAILURE);
+ if (priv_ngroups > 0 && setgroups(priv_ngroups, priv_groups) != 0) _exit(EXIT_FAILURE);
+
+ if (geteuid() != priv_euid) _exit(EXIT_FAILURE);
+ if (getegid() != priv_egid) _exit(EXIT_FAILURE);
+ }
+
+priv_state = PRIV_RESTORED;
+}
+
+
+
+
/*************************************************
* Open and lock a database file *
*************************************************/
@@ -89,7 +149,6 @@ dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof)
{
int rc, save_errno;
BOOL read_only = flags == O_RDONLY;
-BOOL created = FALSE;
flock_t lock_data;
uschar dirname[256], filename[256];
@@ -111,12 +170,13 @@ exists, there is no error. */
snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory);
snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name);
+priv_drop_temp(exim_uid, exim_gid);
if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0)
{
- created = TRUE;
(void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, TRUE);
dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE);
}
+priv_restore();
if (dbblock->lockfd < 0)
{
@@ -165,57 +225,17 @@ it easy to pin this down, there are now debug statements on either side of the
open call. */
snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name);
-EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr));
+priv_drop_temp(exim_uid, exim_gid);
+EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr));
if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR)
{
DEBUG(D_hints_lookup)
debug_printf_indent("%s appears not to exist: trying to create\n", filename);
- created = TRUE;
EXIM_DBOPEN(filename, dirname, flags|O_CREAT, EXIMDB_MODE, &(dbblock->dbptr));
}
-
save_errno = errno;
-
-/* If we are running as root and this is the first access to the database, its
-files will be owned by root. We want them to be owned by exim. We detect this
-situation by noting above when we had to create the lock file or the database
-itself. Because the different dbm libraries use different extensions for their
-files, I don't know of any easier way of arranging this than scanning the
-directory for files with the appropriate base name. At least this deals with
-the lock file at the same time. Also, the directory will typically have only
-half a dozen files, so the scan will be quick.
-
-This code is placed here, before the test for successful opening, because there
-was a case when a file was created, but the DBM library still returned NULL
-because of some problem. It also sorts out the lock file if that was created
-but creation of the database file failed. */
-
-if (created && geteuid() == root_uid)
- {
- DIR *dd;
- struct dirent *ent;
- uschar *lastname = Ustrrchr(filename, '/') + 1;
- int namelen = Ustrlen(name);
-
- *lastname = 0;
- dd = opendir(CS filename);
-
- while ((ent = readdir(dd)))
- if (Ustrncmp(ent->d_name, name, namelen) == 0)
- {
- struct stat statbuf;
- Ustrcpy(lastname, ent->d_name);
- if (Ustat(filename, &statbuf) >= 0 && statbuf.st_uid != exim_uid)
- {
- DEBUG(D_hints_lookup) debug_printf_indent("ensuring %s is owned by exim\n", filename);
- if (Uchown(filename, exim_uid, exim_gid))
- DEBUG(D_hints_lookup) debug_printf_indent("failed setting %s to owned by exim\n", filename);
- }
- }
-
- closedir(dd);
- }
+priv_restore();
/* If the open has failed, return NULL, leaving errno set. If lof is TRUE,
log the event - also for debugging - but debug only if the file just doesn't
--
2.30.2
|