========================================================================
CVE-2020-CLOSE -- Missing close-on-exec flag for privileged pipe
========================================================================

Exim supports a special kind of .forward file called "exim filter" (if
allow_filter is true, the default on Debian). To handle such a filter,
the privileged Exim process creates an unprivileged process and a pipe
for communication. The filter process can fork() and execute arbitrary
commands with expand_string(); this is not a security issue in itself,
because the filter process is unprivileged. Unfortunately, the writable
end of the communication pipe is not closed-on-exec and an unprivileged
local attacker can therefore send arbitrary data to the privileged Exim
process (which is running as root).

We exploit this vulnerability through the following code in
rda_interpret(), which reads our arbitrary data in the privileged Exim
process:

791 fd = pfd[pipe_read];
792 if (read(fd, filtertype, sizeof(int)) != sizeof(int) ||
793     read(fd, &yield, sizeof(int)) != sizeof(int) ||
794     !rda_read_string(fd, error)) goto DISASTER;
...
804     if (!rda_read_string(fd, &s)) goto DISASTER;
...
956   *error = string_sprintf("internal problem in %s: failure to transfer "
957     "data from subprocess: status=%04x%s%s%s", rname,
958     status, readerror,
959     (*error == NULL)? US"" : US": error=",
960     (*error == NULL)? US"" : *error);
961   log_write(0, LOG_MAIN|LOG_PANIC, "%s", *error);

where:

467 static BOOL
468 rda_read_string(int fd, uschar **sp)
469 {
470 int len;
471
472 if (read(fd, &len, sizeof(int)) != sizeof(int)) return FALSE;
...
479   if (read(fd, *sp = store_get(len, FALSE), len) != len) return FALSE;
480 return TRUE;
481 }

- at line 794, we allocate an arbitrary string (of arbitrary length),
  error;

- at line 804 (and line 479), we back-jump to the beginning of the heap
  (Digression 1b) and avoid the forward-overflow (Digression 1a) because
  our len is negative;

- at line 956, we overwrite the beginning of the heap with a string that
  we control (error); we tried to overwrite Exim's configuration strings
  (Digression 2) but failed to execute arbitrary commands; instead, we
  overwrite file_path, a copy of log_file_path (mentioned in
  CVE-2020-LFDIR);

- at line 961, we append an arbitrary string that we control (error) to
  a file whose name we control (file_path): we add an arbitrary user to
  /etc/passwd and obtain full root privileges.

This first version of our exploit succeeds on Debian oldstable's
exim4_4.89-2+deb9u7 (it fails on Debian stable's exim4_4.92-8+deb10u4
because of a gstring_reset_unused() in string_sprintf(); we have not
tried to work around this problem), but it fails on Debian testing's
exim4_4.94-8: the pool of memory that we back-jump at line 804 is
untainted, but the string at line 956 is tainted and written to a
different pool of memory (because our primary recipient, and hence
rname, are tainted).

To work around this problem, our "exim filter" generates a secondary
recipient that is naturally untainted (line 479). When this secondary
recipient is processed, the string at line 956 is untainted and thus
overwrites the beginning of the heap (because it is allocated in the
untainted pool of memory that we back-jumped at line 804): our exploit
also succeeds on Debian testing.

Finally, we use one noteworthy trick in our exploit: in theory, the
string that overwrites file_path at line 956 cannot be longer than 256
bytes (LOG_NAME_SIZE); this significantly slows the brute-force of the
correct back-jump distance. In practice, we can overwrite file_path with
a much longer string (up to 8KB, LOG_BUFFER_SIZE) because file_path is a
format string and "%0Lu" (or "%.0D") is a NOP in Exim's string_format()
function: it consumes no argument and produces no output, thus avoiding
the overflow of buffer[LOG_NAME_SIZE] in open_log().

