E-mail, TLS, and Me: Backflipping into a Faceplant (Part 1)
So. I decided to set up a contact form a few days ago. I wrote all the HTML,
figured out how to make it look nice on mobile and desktop, rigged up the route in
web-pylon, and... hm.
How... how do you send an email??
Scouting other implementations
I've worked on a few projects that send emails before. One of my first jobs had a heavily modified
WordPress backend, so we had access to PHP's mail()
function. Let's take a look at what that...
does...
oh.
/* {{{ php_mail */
PHPAPI bool php_mail(const char *to, const char *subject, const char *message, const char *headers, const char *extra_cmd)
{
/* ... snip ... */
#ifdef PHP_WIN32
sendmail = popen_ex(sendmail_cmd, "wb", NULL, NULL);
#else
errno = 0;
sendmail = popen(sendmail_cmd, "w");
#endif
/* ... snip ... */
}
Okay, that's a little disappointing, but all that means is we have to go find the source for the
sendmail command. From what I've read, that command can actually be one of a few different
sendmail implementations, but on my machine, it's the
Postfix version. I read this version for my research, but
just know there are other sendmails running around out there.
So, let's check out sendmail.c in the Postfix source
tree.
int main(int argc, char **argv)
{
/* ... snip ... */
case SM_MODE_ENQUEUE:
if (site_to_flush) {
if (argv[OPTIND])
msg_fatal_status(EX_USAGE, "flush site requires no recipient");
ext_argv = argv_alloc(2);
argv_add(ext_argv, "postqueue", "-s", site_to_flush, (char *) 0);
for (n = 0; n < msg_verbose; n++)
argv_add(ext_argv, "-v", (char *) 0);
argv_terminate(ext_argv);
mail_run_replace(var_command_dir, ext_argv->argv);
/* NOTREACHED */
} else if (id_to_flush) {
if (argv[OPTIND])
msg_fatal_status(EX_USAGE, "flush queue_id requires no recipient");
ext_argv = argv_alloc(2);
argv_add(ext_argv, "postqueue", "-i", id_to_flush, (char *) 0);
for (n = 0; n < msg_verbose; n++)
argv_add(ext_argv, "-v", (char *) 0);
argv_terminate(ext_argv);
mail_run_replace(var_command_dir, ext_argv->argv);
/* NOTREACHED */
} else {
enqueue(flags, encoding, dsn_envid, dsn_ret, dsn_notify,
rewrite_context, sender, full_name, argv + OPTIND);
exit(0);
/* NOTREACHED */
}
break;
/* ... snip ... */
}
We could follow the enqueue() function, which looks like the only one that does anything, but I
have a more interesting idea. Let's just run it with strace and see what kind of system calls it
makes.
Digging through system calls
# echo test | strace -s 1024 -o sendmail.log --follow-forks --output-separately sendmail nobody@example.com
I'll spare you the full traces (you can obtain them easily if you run the same command as above),
but I discovered the following interesting things:
- The initial
sendmail process does a decent amount of work, but never actually sends the email
anywhere; instead, it opens a socket, then clone()s itself. It then performs a bit of cleanup
and finally wait4()s on the other process it spawned.
- The child process very quickly
execve()s itself into /usr/sbin/postdrop. This definitely
sounds like a mail program! It then chdir()s into /var/spool/postfix.
- If we search for
test or nobody@example.com, we can see that postdrop reads the message,
recipient, and some other information from standard input, then immediately writes it to a file
called maildrop/nnnnnn.nnnnnnn (n is just some digits). It then connects to a Unix socket
called public/pickup, writes the sequence "W\0" to it, and then closes it.
So. All that, and it turns out all that happens is we write some stuff to a file, ping a control
socket, and let somebody else take care of it.
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
# E-mail, TLS, and Me: Backflipping into a Faceplant (Part 1)
So. I decided to set up a [contact form](/page/contact) a few days ago. I wrote all the HTML,
figured out how to make it look nice on mobile and desktop, rigged up the route in
[web-pylon](https://git.echowritescode.dev/repository/web-pylon/head), and... hm.
How... how do you send an email??
## Scouting other implementations
I've worked on a few projects that send emails before. One of my first jobs had a heavily modified
WordPress backend, so we had access to PHP's [`mail()`
function](https://www.php.net/manual/en/function.mail.php). Let's take a look at what that...
does...
[oh](https://github.com/php/php-src/blob/201c691fab036b40f8b2ddcfd253fd21089ed799/ext/standard/mail.c#L558).
```
/* {{{ php_mail */
PHPAPI bool php_mail(const char *to, const char *subject, const char *message, const char *headers, const char *extra_cmd)
{
/* ... snip ... */
#ifdef PHP_WIN32
sendmail = popen_ex(sendmail_cmd, "wb", NULL, NULL);
#else
errno = 0;
sendmail = popen(sendmail_cmd, "w");
#endif
/* ... snip ... */
}
```
Okay, that's a little disappointing, but all that means is we have to go find the source for the
`sendmail` command. From what I've read, that command can actually be one of a few different
`sendmail` implementations, but on my machine, it's the
[Postfix](https://www.postfix.org/mailq.1.html) version. I read this version for my research, but
just know there are other `sendmail`s running around out there.
So, let's check out [`sendmail.c` in the Postfix source
tree](https://github.com/vdukhovni/postfix/blob/e6eb5ba2b6f0f6b159d95ad8670da650c7aa2c5a/postfix/src/sendmail/sendmail.c#L1409).
```
int main(int argc, char **argv)
{
/* ... snip ... */
case SM_MODE_ENQUEUE:
if (site_to_flush) {
if (argv[OPTIND])
msg_fatal_status(EX_USAGE, "flush site requires no recipient");
ext_argv = argv_alloc(2);
argv_add(ext_argv, "postqueue", "-s", site_to_flush, (char *) 0);
for (n = 0; n < msg_verbose; n++)
argv_add(ext_argv, "-v", (char *) 0);
argv_terminate(ext_argv);
mail_run_replace(var_command_dir, ext_argv->argv);
/* NOTREACHED */
} else if (id_to_flush) {
if (argv[OPTIND])
msg_fatal_status(EX_USAGE, "flush queue_id requires no recipient");
ext_argv = argv_alloc(2);
argv_add(ext_argv, "postqueue", "-i", id_to_flush, (char *) 0);
for (n = 0; n < msg_verbose; n++)
argv_add(ext_argv, "-v", (char *) 0);
argv_terminate(ext_argv);
mail_run_replace(var_command_dir, ext_argv->argv);
/* NOTREACHED */
} else {
enqueue(flags, encoding, dsn_envid, dsn_ret, dsn_notify,
rewrite_context, sender, full_name, argv + OPTIND);
exit(0);
/* NOTREACHED */
}
break;
/* ... snip ... */
}
```
We _could_ follow the `enqueue()` function, which looks like the only one that does anything, but I
have a more interesting idea. Let's just run it with `strace` and see what kind of system calls it
makes.
## Digging through system calls
```
# echo test | strace -s 1024 -o sendmail.log --follow-forks --output-separately sendmail nobody@example.com
```
I'll spare you the full traces (you can obtain them easily if you run the same command as above),
but I discovered the following interesting things:
- The initial `sendmail` process does a decent amount of work, but never actually sends the email
anywhere; instead, it opens a socket, then `clone()`s itself. It then performs a bit of cleanup
and finally `wait4()`s on the other process it spawned.
- The child process very quickly `execve()`s itself into `/usr/sbin/postdrop`. _This_ definitely
sounds like a mail program! It then `chdir()`s into `/var/spool/postfix`.
- If we search for `test` or `nobody@example.com`, we can see that `postdrop` reads the message,
recipient, and some other information from standard input, then immediately writes it to a file
called `maildrop/nnnnnn.nnnnnnn` (`n` is just some digits). It then connects to a Unix socket
called `public/pickup`, writes the sequence `"W\0"` to it, and then closes it.
So. All that, and it turns out all that happens is we write some stuff to a file, ping a control
socket, and let somebody else take care of it.