Forged Sender Checking with MIMEDefang

Testing | Implementation

The Simple Mail Transport Protocol (SMTP) currently allows any sender to create e-mail as if from anyone, which spammers and malware abuse. The following notes discuss how to setup MIMEDefang to query the Mail Exchange (MX) servers for a domain with the sender address of an incoming e-mail to determine whether the sender is forged.

A new protocol is under development that prevents the forging of e-mail: Sender Permitted From (SPF). This should eliminate the need to do reverse SMTP connections for domains that support SPF. Under SPF, Domain Name System (DNS) queries are made to determine whether the sending server is a proper outgoing e-mail server for the domain in question. Notes on implementing SPF in MIMEDefang are also available. However, many domains have not yet implemented SPF support, so reverse SMTP connections are a poorer but workable method of determining whether a sender address is forged.

The md_check_against_smtp_server function outlined below can be used to check both remote systems 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, which could tie up your system resources. 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 be forging e-mail from a non-existent address under one of your domains.

Overview

The following checks happen 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 additional bandwidth is wasted on the headers and 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 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);
    }