========================================================================
CVE-2020-FGETS -- Line truncation and injection in spool_read_header()
========================================================================

spool_read_header() calls fgets() to read the lines from a spool header
file into the 16KB big_buffer. The first section of spool_read_header()
enlarges big_buffer dynamically if fgets() truncates a line (if a line
is longer than 16KB):

 460   if (Ufgets(big_buffer, big_buffer_size, fp) == NULL) goto SPOOL_READ_ERROR;
 ...
 462   while (  (len = Ustrlen(big_buffer)) == big_buffer_size-1
 463         && big_buffer[len-1] != '\n'
 ...
 468     buf = store_get_perm(big_buffer_size *= 2, FALSE);

Unfortunately, the second section of spool_read_header() does not
enlarge big_buffer:

 756 for (recipients_count = 0; recipients_count < rcount; recipients_count++)
 ...
 765   if (Ufgets(big_buffer, big_buffer_size, fp) == NULL) goto SPOOL_READ_ERROR;

If DSN (Delivery Status Notification) is enabled (it is disabled by
default), an attacker can send a RCPT TO command with a long ORCPT=
parameter that is written to the spool header file by
spool_write_header():

292 for (int i = 0; i < recipients_count; i++)
293   {
294   recipient_item *r = recipients_list + i;
...
302     uschar * errors_to = r->errors_to ? r->errors_to : US"";
...
305     uschar * orcpt = r->orcpt ? r->orcpt : US"";
306
307     fprintf(fp, "%s %s %d,%d %s %d,%d#3\n", r->address, orcpt, Ustrlen(orcpt),
308       r->dsn_flags, errors_to, Ustrlen(errors_to), r->pno);

This long ORCPT= parameter truncates the recipient line (when read by
fgets() in spool_read_header()) and injects the remainder of the line as
a separate line, thereby emulating the '\n' injection of CVE-2020-NLEND
and CVE-2020-MAUTH (albeit in a weaker form).

We have not tried to exploit this vulnerability; if exploitable, it
would allow an unauthenticated remote attacker to execute arbitrary
commands as root (if DSN is enabled).

- Intuitively, it seems impossible to generate a recipient line longer
  than 16KB (big_buffer_size), because the Exim server reads our RCPT TO
  command into a 16KB buffer (smtp_cmd_buffer) that must also contain
  (besides our long ORCPT= parameter) "RCPT TO:", "NOTIFY=DELAY", and
  the recipient address.

- We can, however, use the special recipient "postmaster", which is
  automatically qualified (by appending Exim's primary hostname) before
  it is written to the spool header file. This allows us to enlarge the
  recipient line, but is not sufficient to control the end of the
  truncated line (unless Exim's primary hostname is longer than 24
  bytes, which is very unlikely).

- But we can do better: we can use CVE-2020-EXOPT to read our ORCPT=
  parameter out of smtp_cmd_data's bounds (from the end of the preceding
  smtp_cmd_buffer). This allows us to further enlarge the recipient line
  (by 10 bytes, because "postmaster" is now included in our ORCPT=), but
  is not sufficient to reliably control the end of the truncated line
  (unless Exim's primary hostname is longer than 14 bytes, which is
  still very unlikely).

- But we can do much better: we do not need postmaster's automatic
  qualification anymore, because the recipient is now included in our
  ORCPT= parameter -- the longer the recipient, the better. On Debian,
  the user "systemd-timesync" exists by default, and "localhost" is one
  of Exim's local_domains: the recipient "systemd-timesync@localhost" is
  long enough to reliably control the end of the truncated recipient
  line, and allows us to read and write out of big_buffer's bounds
  (lines 859 and 863, and beyond):

 840   else if (*p == '#')
 ...
 848     (void)sscanf(CS p+1, "%d", &flags);
 849
 850     if ((flags & 0x01) != 0)      /* one_time data exists */
 851       {
 852       int len;
 853       while (isdigit(*(--p)) || *p == ',' || *p == '-');
 854       (void)sscanf(CS p+1, "%d,%d", &len, &pno);
 855       *p = 0;
 856       if (len > 0)
 857         {
 858         p -= len;
 859         errors_to = string_copy_taint(p, TRUE);
 860         }
 861       }
 862
 863     *(--p) = 0;   /* Terminate address */

For example, the following proof-of-concept accesses memory at 1MB below
big_buffer:

(sleep 10; echo 'EHLO test'; sleep 3; echo 'MAIL FROM:<>'; sleep 3; perl -e 'print "NOOP"; print " " x (16384-9); print "ORCPT\n"'; sleep 3; echo 'RCPT TO:x"'; sleep 3; perl -e 'print "RCPT TO:(\")systemd-timesync\@localhost("; print "A" x (16384-74); print "xxx1048576,-1#1x NOTIFY=DELAY\n"'; sleep 3; echo 'DATA'; sleep 3; echo '.'; sleep 10) | nc -n -v 192.168.56.102 25
Program received signal SIGSEGV, Segmentation fault.

