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 sendmail
s 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.