2015 m. spalio 6 d., antradienis

Qualys Security Advisory - OpenSMTPD Audit Report 2prt

--[ Remote Vectors ]----------------------------------------------------

----[ 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$pN5WRvGaiPHEXPsrIwSNWe1S0U5iTIvtWqPQgHmd0BAJK02GOYG.W:0:0:daemon:0:0:Charlie &:/root:/bin/ksh

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ą