----[ SMTP Client
By default, OpenSMTPD is configured to accept email from local users,
and connects to remote SMTP servers in order to relay and deliver it.
The code for these client-side MTA sessions (smtpd/mta_session.c) is
reachable remotely (and is also used for bounces) and represents an
important attack vector.
----[ SMTP Server
OpenSMTPD can be configured to accept email from remote SMTP clients,
and relay or deliver it to local users. The code for these server-side
SMTP sessions (smtpd/smtp_session.c) is reachable remotely and
represents another important attack vector.
----[ DNS Resolver
The libasr, an asynchronous DNS resolver, is used by OpenSMTPD and
represents yet another remote vector. However, its codebase is pretty
much independent and therefore beyond the scope of our OpenSMTPD audit.
The same can be said of OpenSSL and LibreSSL.
--[ Inter-Process Vectors ]-----------------------------
If we ever manage to compromise one of OpenSMTPD's processes, a
vulnerability in the inter-process communication code may allow us to
escalate from an unprivileged, chrooted process to a privileged,
non-chrooted process. For example, pivoting from PROC_PONY to
PROC_PARENT, or even PROC_LKA, would be a good move.
==============================
Local Vulnerabilities
==============================
------------------------------
CVE-2015-ABCD - Portable fgetln() can return a zero length
------------------------------
Constructs similar to the following appear several times throughout
OpenSMTPD's codebase:
while ((buf = fgetln(fp, &len))) {
if (buf[len - 1] == '\n')
buf[len - 1] = '\0';
and:
line = fgetln(s->msgfp, &len);
if (line == NULL) break;
line[len - 1] = '\0';
In theory, if fgetln() succeeds (i.e., does not return NULL) but stores
a 0 length in len, an out-of-bounds memory read and (possibly) write is
triggered. In practice, this is impossible because OpenBSD's libc
implementation of fgetln() guarantees what the manpage says:
The length of the line, including the final newline, is stored
in the memory location to which len points and is guaranteed to
be greater than 0 upon successful completion.
Unfortunately, the portable implementation of fgetln() in
openbsd-compat/fgetln.c (which is used on Linux, at least) offers no
such guarantee:
38 char *
39 fgetln(stream, len)
40 FILE *stream;
41 size_t *len;
42 {
..
50 if (fgets(buffer, buflen+1, stream) == NULL)
51 return NULL;
52 *len = strlen(buffer);
..
60 return buffer;
61 }
For example, if fgets() reads the line "\0\n", fgetln() succeeds and
stores a 0 string-length in len (which should be impossible), and the
out-of-bounds memory is accessed upon return.
------------------------------
CVE-2015-ABCD - Local denial-of-service (invalid imsg)
------------------------------
The fatalx(NULL) in mproc_dispatch() can be triggered locally by
connecting directly to the control socket and sending an invalid imsg
(one that is smaller than IMSG_HEADER_SIZE or larger than MAX_IMSGSIZE).
imsg_get() will fail, fatalx() will be called, and PROC_CONTROL will
exit() (and, as mentioned earlier, if one OpenSMTPD process dies, all
OpenSMTPD processes die):
187 if ((n = imsg_get(&p->imsgbuf, &imsg)) == -1) {
188 log_warn("fatal: %s: error in imsg_get for %s",
189 proc_name(smtpd_process), p->name);
190 fatalx(NULL);
191 }
This local denial-of-service has been discovered independently by
OpenSMTPD's developers and fixed in version 5.4.6p1 (released on June
11, 2015):
188 if ((n = imsg_get(&p->imsgbuf, &imsg)) == -1) {
189
190 if (smtpd_process == PROC_CONTROL &&
191 p->proc == PROC_CLIENT) {
192 log_warnx("warn: client sent invalid imsg "
193 "over control socket");
194 p->handler(p, NULL);
195 return;
196 }
197 log_warn("fatal: %s: error in imsg_get for %s",
198 proc_name(smtpd_process), p->name);
199 fatalx(NULL);
200 }
------------------------------
CVE-2015-ABCD - Local denial-of-service (file-descriptor exhaustion)
------------------------------
By connecting locally to the control socket and passing many file
descriptors (~1024) to PROC_CONTROL (which does not really expect this),
it is possible to exhaust almost all of its available fds.
- In OpenSMTPD 5.4.4p1, PROC_CONTROL ends up calling fatal("exiting") in
mproc_dispatch():
153 if ((n = imsg_read(&p->imsgbuf)) == -1) {
154 log_warn("warn: %s -> %s: imsg_read",
155 proc_name(smtpd_process), p->name);
156 fatal("exiting");
157 }
- In OpenSMTPD 5.7.1p1, PROC_CONTROL does not call fatal("exiting")
(thanks to the EAGAIN check at the beginning of mproc_dispatch()), but
it will never again accept new client connections (because of how
control_accept() handles file-descriptor exhaustion):
155 if ((n = imsg_read(&p->imsgbuf)) == -1) {
156 log_warn("warn: %s -> %s: imsg_read",
157 proc_name(smtpd_process), p->name);
158 if (errno == EAGAIN)
159 return;
160 fatal("exiting");
161 }
There are actually three different ways to trigger this local
denial-of-service:
1/ Send one fd per imsg, with the IMSGF_HASFD flag turned on: imsg_get()
will move the fd from ibuf->fds to imsg->fd, but because PROC_CONTROL
does not expect a fd to be passed, this fd is leaked forever when
imsg_free() is called by mproc_dispatch().
2/ Send one fd per imsg, but with the IMSGF_HASFD flag turned off:
imsg_get() will leave the fd in ibuf->fds, which are supposed to be
closed when control_close() is called, but this never happens if all fds
are exhausted first.
3/ Send only one large (>1024) imsg, one byte at a time, with one fd
attached to every single byte sent: this will accumulate all passed fds
into ibuf->fds.
------------------------------
CVE-2015-ABCD - Local denial-of-service (connection-id wrap)
------------------------------
In control_accept(), it is possible to trigger the errx() of the
following tree_xset() call:
348 c = xcalloc(1, sizeof(*c), "control_accept");
349 if (getpeereid(connfd, &c->euid, &c->egid) == -1)
350 fatal("getpeereid");
351 c->id = ++connid;
352 c->mproc.proc = PROC_CLIENT;
353 c->mproc.handler = control_dispatch_ext;
354 c->mproc.data = c;
355 mproc_init(&c->mproc, connfd);
356 mproc_enable(&c->mproc);
357 tree_xset(&ctl_conns, c->id, c);
If we establish a first connection to the control socket (and keep it
alive), and then establish (and immediately close) new connections in a
loop, the "static uint32_t connid" will eventually wrap and collide with
our (kept-alive) first connection id, and the exclusive tree_xset() will
fail and terminate PROC_CONTROL with errx().
------------------------------
CVE-2015-ABCD - Local denial-of-service (WIFSTOPPED() child)
------------------------------
In parent_sig_handler(), it is possible to trigger the following
fatalx() call:
366 pid = waitpid(-1, &status, WNOHANG);
367 if (pid <= 0)
368 continue;
...
371 if (WIFSIGNALED(status)) {
...
375 } else if (WIFEXITED(status)) {
...
381 } else
382 fatalx("smtpd: unexpected cause of SIGCHLD");
If the child is ptraced, WIFSIGNALED() and WIFEXITED() can return false,
but WIFSTOPPED() can return true, even if WUNTRACED was not specified in
waitpid(). In order to trigger this in the context of OpenSMTPD, a local
user can add a "|exec /tmp/ptraceme" line to his ~/.forward file, where
ptraceme is a small program that simply calls ptrace(PT_TRACE_ME) and
execve() (it does not matter which binary is executed).
------------------------------
CVE-2015-ABCD - Local denial-of-service (blocking open() call)
------------------------------
The open() call in parent_forward_open() can block forever (if the
~/.forward was created by mkfifo, for example) and this will effectively
block OpenSMTPD as a whole (PROC_PARENT will not respond to
IMSG_LKA_OPEN_FORWARD and IMSG_MDA_FORK requests anymore):
1232 if (! bsnprintf(pathname, sizeof (pathname), "%s/.forward",
1233 directory))
1234 fatal("smtpd: parent_forward_open: snprintf");
....
1247 do {
1248 fd = open(pathname, O_RDONLY);
1249 } while (fd == -1 && errno == EINTR);
------------------------------
Multiple hardlink attacks in the offline directory
------------------------------
In the world-writable "/var/spool/smtpd/offline" directory, local users
can create hardlinks to files they do not own, and wait until the server
reboots (or, crash OpenSMTPD with a denial-of-service and wait until the
administrator restarts it) to carry out assorted attacks.
1/ The following code in offline_enqueue() allows an attacker to
chflags(0) arbitrary files, by hardlinking them to the offline directory
(CVE-2015-ABCD):
1117 if (lstat(path, &sb) == -1) {
1118 log_warn("warn: smtpd: lstat: %s", path);
1119 _exit(1);
1120 }
1121
1122 #ifdef HAVE_CHFLAGS
1123 if (chflags(path, 0) == -1) {
1124 log_warn("warn: smtpd: chflags: %s", path);
1125 _exit(1);
1126 }
1127 #endif
2/ The following code in offline_enqueue() allows an attacker to
execvp() "/usr/sbin/smtpctl" as "sendmail", with a command-line argument
that is the hardlinked file's first line (CVE-2015-ABCD):
1149 if ((fp = fopen(path, "r")) == NULL)
1150 _exit(1);
....
1160 if ((p = fgetln(fp, &len)) == NULL)
1161 _exit(1);
....
1167 addargs(&args, "%s", "sendmail");
1168
1169 while ((tmp = strsep(&p, "|")) != NULL)
1170 addargs(&args, "%s", tmp);
....
1179 execvp(PATH_SMTPCTL, args.list);
1180 _exit(1);
For example, an attacker can hardlink /etc/master.passwd to the offline
directory, and retrieve its first line (root's encrypted password) by
running ps (or a small program that simply calls sysctl() with
KERN_FILE_BYUID and KERN_PROC_ARGV) in a loop:
In the attacker's terminal:
$ ln /etc/master.passwd /var/spool/smtpd/offline
$ ./getargs &
[1] 23460
In the administrator's terminal:
# /etc/rc.d/smtpd restart
smtpd(ok)
smtpd(ok)
On the attacker's terminal:
root:$2b$09$
3/ If an attacker controls at least part of another user's file, he can
hardlink this file to the offline directory, and try to exploit one of
the vulnerable fgetln() calls in the enqueue code, which runs with the
privileges of this other user. For example, in offline_enqueue():
1160 if ((p = fgetln(fp, &len)) == NULL)
1161 _exit(1);
1162
1163 if (p[len - 1] != '\n')
1164 _exit(1);
1165 p[len - 1] = '\0';
And in savedeadletter():
898 while ((buf = fgetln(in, &len))) {
899 if (buf[len - 1] == '\n')
900 buf[len - 1] = '\0';
However, we did not investigate this vector any further, because on
OpenBSD (where an attacker is allowed to hardlink another user's file)
fgetln() is not vulnerable, and on Linux (where fgetln() is vulnerable)
an attacker is usually not allowed to hardlink another user's file.
4/ If an attacker is able to reach another user's file (i.e., +x on all
directories that lead to the file) but not read it, he can hardlink the
file to the offline directory, and wait for savedeadletter() to create a
world-readable copy of the file in this other user's home directory:
854 (void)snprintf(buffer, sizeof buffer, "%s/dead.letter", pw->pw_dir);
...
859 if ((fp = fopen(buffer, "w")) == NULL)
860 return 0;
...
898 while ((buf = fgetln(in, &len))) {
...
909 fprintf(fp, "%s\n", buf);
910 }
However, there are three reasons why this particular vector is useless
in practice:
a) In OpenSMTPD 5.4.4p1, the getlogin() call in enqueue() will always
return "root", which means that the world-readable "dead.letter" will
always be created in /root, unreachable by the attacker (drwx------).
b) In OpenSMTPD 5.4.5p2, smtpctl's -S command-line option was added to
work around the getlogin() problem, but the getopt() string was
incorrectly modified to "RS:" instead of "R:S".
c) In OpenSMTPD 5.7.1p1, the getopt() string was fixed to "R:S", but the
savedeadletter() code was removed altogether.
Komentarų nėra:
Rašyti komentarą