Forged Sender Checking with MIMEDefang

Testing | Implementation

The Simple Mail Transport Protocol (SMTP) [RFC 2821] allows any sender to create e-mail as if from anyone. Spammers and malware abuse this feature, obfuscating the source of their therefore illegal messages. This page discusses how to make MIMEDefang query the Mail Exchange (MX) servers for the purported MAIL FROM to determine whether the e-mail is from a real sender.

The md_check_against_smtp_server function outlined below can be used to check both remote and internal mail servers for forgery. Only certain remote systems should be checked, as a spammer could easily forge e-mail from a SMTP tarpit. Querying internal mail servers via SMTP is less efficient than other methods (Lightweight Directory Access Protocol (LDAP) lookups, for instance), though will be a quick way to setup both sender and recipient checking should malware forge e-mail from a non-existent address of an internal domain.

Alternatives

Standards-based approaches to solving the e-mail forgery problem, independent of the custom measures outlined on this page, and the various legal measures to combat spam:

Overview

The following checks occur in MIMEDefang after the SMTP RCPT TO command has been sent, but before the DATA phase, which means forged e-mail can be rejected before bandwidth is wasted on the body of the message.

  1. Sending host connects to the server running Sendmail and MIMEDefang.
  2. Sending host issues the EHLO, MAIL FROM, and RCPT TO SMTP protocol commands.
  3. Code in MIMEDefang filter_recipient parses the sending domain from the sender address (given in MAIL FROM).
  4. If the sender domain is listed in a forged domains table, the MX servers for the domain in question are looked up.
  5. Each MX server is connected to with the MIMEDefang md_check_against_smtp_server function until a positive or negative response is obtained.
  6. If a negative response occurs (that is, the sender does not exist), we return an error and stop the processing of the message. Otherwise, the e-mail is accepted and processed as usual.

Reverse checks will slow down incoming e-mail, which may be unacceptable for a site that does large amounts of traffic with forged domains. In this case, figuring out the legitimate mailers for the domain being forged and excluding them from the reverse check will make sense. Even better would be for the domain being forged to implement SPF, so that other domains do not have to guess where legitimate e-mail originates from.

This method will fail if the MX servers for the domain in question return positive responses regardless of whether the user exists or not. This will be the case for sites that do not check whether the recipient is valid on their Internet-facing MX servers.

There is a chance that legitimate e-mail will be blocked should the remote server reject the test as far as md_check_against_smtp_server is concerned. This may be the case where the remote server is configured to reject connections from your system, or where the remote server brokenly rejects the special <> sender address used in the check.

Testing

To test whether a domain supports reverse checks, use the following manual lookup method, in this case for sial.org.

$ host -t mx sial.org
sial.org mail is handled (pri=0) by mail.sial.org
$ telnet mail.sial.org 25
Trying 216.39.146.75...
Connected to mail.sial.org.
Escape character is '^]'.
220 mail.sial.org ESMTP Sendmail 8.12.10/8.12.10; Thu, 1 Jan 2004 14:59:25 -0800 (PST)
ehlo example.com
250-mail.sial.org Hello example.com [192.0.2.1], pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-EXPN
250-VERB
250-8BITMIME
250-SIZE 16180340
250-DSN
250-ETRN
250-STARTTLS
250-DELIVERBY
250 HELP
mail from: <>
250 2.1.0 <>... Sender ok
rcpt to: <nowayany1suchuserexistsat@sial.org>
550 5.1.1 <nowayany1suchuserexistsat@sial.org>... User unknown
quit
221 2.0.0 mail.sial.org closing connection
Connection closed by foreign host.

If the e-mail server for the domain accepts the invented recipient address, then it will likely not support reverse checks, unlike the server in the above example. Granted, the nowayany1suchuserexistsat address might just happen to exist on the server in question?

Implementation

MIMEDefang is assumed to be installed and functioning on the e-mail server in question.

  1. Ensure filter_recipient support is enabled.
  2. This is usually done by setting MX_RECIPIENT_CHECK=yes in the MIMEDefang startup configuration file.

  3. Adapt the following code to the mimedefang-filter file.
  4. A list of domains to check for forgeries is required, to limit how much extra work is done by MIMEDefang, and to prevent malicious or misconfigured sites causing problems for the reverse check connections made. This list could be maintained in a database table; the following uses a hash defined at the top of mimedefang-filter. The domains listed below show significant numbers of forgeries based on the greylist records for e-mail at sial.org.

    our %forged_domains = (
    'hotmail.com' => undef,
    'yahoo.com' => undef,
    'yahoo.co.kr' => undef,
    'msn.com' => undef,
    'excite.com' => undef,
    'juno.com' => undef,
    'telus.net' => undef,
    'iname.com' => undef,
    'gmx.net' => undef,
    'email.com' => undef,
    'charter.net' => undef,
    'bigfoot.com' => undef,
    'earthlink.net' => undef,
    'mailcity.com' => undef,
    'mail.com' => undef,
    'bellsouth.net' => undef,
    'aol.com' => undef,
    'yume.otegami.com' => undef,
    'usa.net' => undef,
    );

    The following example filter_recipient code and email_is_forged function reject e-mail if the sender is not valid according to the outgoing server for the domain in question. The Net::DNS perl module is required to lookup the MX servers of domains listed under %forged_domains. The alarm function should probably be added, so that slow remote sites can be timed out, if they are slow to respond. Also consider Cache::Cache (among other caching or database storage methods) to cache results, so that the same e-mail address need not be looked up multiple times.

    The FIX-THIS-INVALID-HELO-ARG.com in the example code below must be changed to a valid HELO argument the server would issue, usually the canonical domain name sendmail stores as the j macro.

    use Net::DNS;
    our $dns = Net::DNS::Resolver->new;
    $dns->defnames(0); # do not search default domain

    sub filter_recipient {
    my ($recipient, $sender, $ip, $hostname, $first, $helo,
    $rcpt_mailer, $rcpt_host, $rcpt_addr) = @_;

    if (email_is_forged($sender, $ip)) {
    md_syslog('info', "MDLOG,$MsgID,forgedsender,$hostname,$ip");
    return 'REJECT', "Sender address rejected by incoming for domain";
    }

    return 'CONTINUE', 'ok';
    }

    sub email_is_forged {
    my ($sender, $ip) = @_;

    my ($domain) = $sender =~ / \@ ([\w.-]+) [>.]* $ /x;
    return 0 unless exists $forged_domains{$domain};

    my $mx = $dns->query($domain, 'MX');

    my @mxs;

    if ($mx) {
    for my $rr ($mx->answer) {
    push @mxs, $rr->exchange if $rr->type eq 'MX';
    }
    }

    # no MX (or Net::DNS error), fallback to A record for domain
    unless (@mxs) {
    md_syslog 'warning',
    "$QueueID: sender check found no MX found for $domain: $dns->errorstring";
    push @mxs, $domain;
    }

    for my $host (@mxs) {
    my ($result, $msg) =
    md_check_against_smtp_server('<>', $sender,
    'FIX-THIS-INVALID-HELO-ARG.com', $host);
    next if $result eq 'TEMPFAIL';
    return ($result eq 'REJECT' ? 1 : 0);
    }

    return 0;
    }

    The best way to set the HELO argument is to use $SendmailMacros{j}. Unfortunately, MIMEDefang does not populate %SendmailMacros during filter_recipient, so either a domain will need to be manually set in the mimedefang-filter file (which is bad for portability between multiple servers), or the sendmail macros must be loaded early so that $SendmailMacros{j} can be used.

    sub filter_recipient {
    my ($recipient, $sender, $ip, $hostname, $first, $helo,
    $rcpt_mailer, $rcpt_host, $rcpt_addr) = @_;

    load_sendmail_macros();
    ?

    ?
    for my $rr ($mx->answer) {
    next unless $rr->type eq 'MX';
    my ($result, $msg) =
    md_check_against_smtp_server('<>',
    $sender, $SendmailMacros{j}, $rr->exchange);
    next if $result eq 'TEMPFAIL';
    return ($result eq 'REJECT' ? 1 : 0);
    }
    ?