SMTP User Enumeration
Three ways to answer “does this mailbox exist?” against an SMTP server (25/465/587): the purpose-built smtp-user-enum.pl (bulk VRFY/EXPN/RCPT), nmap’s bundled smtp-enum-users script (same three methods, one-shot with the rest of a scan), and swaks for hand-crafted probes — relay testing, RCPT TO acceptance checks, and STARTTLS/auth testing that the dedicated enumerators don’t do. Start with nmap -sC -p25 $IP (the smtp-commands script) to see which of VRFY/EXPN/AUTH/STARTTLS the server even advertises before picking a method. For the full protocol-level workflow (open relay, spray, CVE-2020-7247) see Service Enumeration → SMTP and Attacking Common Services → Email.
Raw SMTP session — telnet / nc (hand verification)
Before trusting a tool, drive the protocol by hand — it shows the exact response codes the enumerators key off and lets you confirm a couple of hits without a wordlist.
$ nc -nv $IP 25
220 mail.corp.local ESMTP Sendmail 8.15.2; ...
HELO x
250 mail.corp.local
VRFY root
252 2.1.5 Cannot VRFY user; try RCPT to attempt delivery # ambiguous (calibrate below)
VRFY nonexistent
252 2.1.5 Cannot VRFY user; try RCPT to attempt delivery # identical 252 for a bad user => VRFY is neutered, use RCPT
EXPN postmaster
502 5.5.1 EXPN command disabled # EXPN is off on most modern MTAs
MAIL FROM:<probe@evil.com>
250 2.1.0 Ok
RCPT TO:<jsmith@corp.local>
250 2.1.5 Ok # 250/251 => mailbox exists
RCPT TO:<nouser@corp.local>
550 5.1.1 <nouser@corp.local>: Recipient address rejected: User unknown # 550 => no such user
QUIT
221 2.0.0 Bye
| Code | VRFY / RCPT meaning |
|---|---|
250 | Mailbox exists / recipient accepted (VRFY echoes the full address) |
251 | User not local, will forward — valid, remote mailbox |
252 | Cannot verify but will attempt delivery — ambiguous; the classic Sendmail/Exchange “cannot VRFY” reply (default Postfix instead answers definitively 250/550, or 502 when VRFY is disabled) |
550 | No such user / mailbox unavailable — invalid |
551 / 553 | User not local (try forward-path) / mailbox name not allowed |
450 / 451 / 452 | Temp failure (greylist / throttle) — not a validity signal, retry |
[!tip] Calibrate VRFY in one shot Feed one known-bad and one known-good username down the same connection and compare the two codes — no need to script a whole run to find out if VRFY is real.
printf 'HELO x\r\nVRFY nonexistent_zzz9\r\nVRFY root\r\nQUIT\r\n' | nc -w3 $IP 25 # identical codes for both => VRFY neutered (switch to -M RCPT) # different codes (e.g. 550 vs 250) => VRFY is live, enumerate with it
[!tip] Same session inside TLS On 587/465 where cleartext is refused, get the exact hand-driven session over TLS with
openssl s_client -starttls smtp -connect $IP:587(STARTTLS) oropenssl s_client -connect $IP:465(implicit TLS), then typeHELO/VRFY/RCPTas above.
smtp-user-enum.pl
Perl script (pentestmonkey), preinstalled on Kali; on macOS clone it — git clone https://github.com/pentestmonkey/smtp-user-enum. It drives raw sockets directly (use Socket; IO::Socket::INET; IO::Select; IO::Handle; Getopt::Std), so it needs no CPAN modules — not even Net::SMTP. Runs one method against a wordlist with configurable concurrency.
# VRFY (default method) — most reliable when not disabled
perl smtp-user-enum.pl -M VRFY -U users.txt -t $IP
# EXPN — expands a mailing list/alias to member addresses
perl smtp-user-enum.pl -M EXPN -U users.txt -t $IP
# RCPT — works even when VRFY/EXPN are disabled; needs a MAIL FROM domain
perl smtp-user-enum.pl -M RCPT -U users.txt -D $DOMAIN -t $IP
# single username check, custom port, more workers
perl smtp-user-enum.pl -M VRFY -u root -t $IP -p 25 -m 10
| Flag | Description | Default |
|---|---|---|
-M mode | Method: VRFY, EXPN, or RCPT | VRFY |
-u user | Single username to check | — |
-U file | File of usernames | — |
-t host | Single target host | — |
-T file | File of target hosts | — |
-D dom | Domain appended to usernames for RCPT mode (guess full addresses) | none |
-f addr | MAIL FROM address, RCPT mode only | user@example.com |
-p port | TCP port | 25 |
-m n | Max concurrent processes | 5 |
-d | Debug output | off |
-v | Verbose | off |
[!warning] Watch out
- Many MTAs (Sendmail and Exchange classically) answer VRFY with
252 Cannot VRFY userregardless of validity — calibrate first by testing one known-bad and one known-good username; if both give identical responses, VRFY is neutered and you needRCPTinstead. Default Postfix is the opposite — it answers VRFY definitively (250/550), which is why VRFY enum is classically demonstrated against it, and returns502 5.5.1 VRFY command is disabledwhen an admin turns it off (disable_vrfy_command=yes).RCPTmode is slow (~5–7 requests/sec) and the noisiest of the three since it opens a fullMAIL FROM/RCPT TOsequence per guess (noDATAsent, so no mail is actually delivered) — scope the wordlist, don’t point it at rockyou.-mconcurrency above ~10–15 gets rate-limited or blacklisted by most modern MTAs.
nmap smtp-enum-users
Same three methods, run as part of an nmap scan — convenient when you’re already scanning the port and want a same-command result, or need nmap’s userdb/passdb-style scripted output.
# default: tries RCPT, then VRFY, then EXPN, against nmap's built-in username list
nmap -p25 --script smtp-enum-users $IP
# pick methods and order explicitly
nmap -p25 --script smtp-enum-users --script-args smtp-enum-users.methods={EXPN,RCPT,VRFY} $IP
# custom username list (standard nmap unpwdb 'userdb' argument)
nmap -p25 --script smtp-enum-users --script-args userdb=/opt/users.txt $IP
# banner/capability check first — shows if VRFY/EXPN/STARTTLS/AUTH are even offered
nmap -p25 -sC -sV $IP
Stops early if the server enforces authentication, and prints whatever usernames it found before any error. Related scripts on the same target: smtp-commands (EHLO/HELP capability banner), smtp-open-relay (tries hardcoded MAIL FROM/RCPT TO combinations to detect relaying — flags authenticated servers as not-vulnerable rather than erroring), smtp-vuln-cve2010-4344 / smtp-vuln-cve2011-1720 (Exim/Postfix heap overflows).
nmap -p25 --script smtp-commands,smtp-open-relay,smtp-vuln* $IP
smtp-ntlm-info — internal hostname/domain disclosure
When a server advertises AUTH NTLM (typically Exchange message-submission on 587, sometimes 25/465), the smtp-ntlm-info NSE script sends a Type-1 NTLM token and decodes the Type-2 challenge — leaking the NetBIOS domain, NetBIOS computer name, DNS domain, FQDN and OS build with no credentials.
# hit all three SMTP ports; 587 (submission) is the usual winner on Exchange
nmap -p25,465,587 --script smtp-ntlm-info $IP
[!tip] Why 587 Perimeter/internal Exchange exposes authenticated submission on 587 with NTLM enabled far more often than on 25, so 587 is where this leak usually lands. Same primitive as
rdp-ntlm-info/ms-sql-ntlm-info— an unauthenticated pull of the AD domain and host FQDN. Feed the recovered domain straight intosmtp-user-enum -M RCPT -D <domain>orswaks --to user@<domain>.
hydra & metasploit — enumeration equivalents
The same VRFY/EXPN/RCPT primitives wrapped in tooling you may already have loaded — one framework for spray + enum, or Metasploit’s DB to store found users.
# Hydra's smtp-enum module — mode is the URL suffix (VRFY default / EXPN / RCPT)
# VRFY/EXPN ignore the password field; a dummy -p keeps hydra's parser happy on builds that demand one
hydra -L users.txt -p x smtp-enum://$IP/VRFY
hydra -L users.txt -p x smtp-enum://$IP/EXPN
# RCPT mode: -p carries the domain appended to each username
hydra -L users.txt -p $DOMAIN smtp-enum://$IP/RCPT
hydra -U smtp-enum # print the module's own options
# Metasploit — auxiliary/scanner/smtp/smtp_enum (RCPT/VRFY/EXPN, no creds needed)
msfconsole -q
use auxiliary/scanner/smtp/smtp_enum
set RHOSTS $IP
set RPORT 25
set USER_FILE /usr/share/seclists/Usernames/Names/names.txt
run
# grab the banner/version alongside it:
use auxiliary/scanner/smtp/smtp_version
| Tool | Enum invocation | Notes |
|---|---|---|
| hydra | smtp-enum://$IP/{VRFY,EXPN,RCPT} | RCPT needs -p $DOMAIN; VRFY/EXPN take a throwaway -p; ships with hydra |
| metasploit | auxiliary/scanner/smtp/smtp_enum | USER_FILE default data/wordlists/unix_users.txt; UNIXONLY true skips the MS banner check |
| smtp-user-enum | -M {VRFY,EXPN,RCPT} (above) | fastest / most controllable for large lists |
swaks — manual probing, relay & auth testing
swaks doesn’t bulk-enumerate on its own, but it’s the right tool for anything smtp-user-enum/nmap don’t cover: single-shot RCPT TO acceptance checks (useful when VRFY/EXPN are both off), open-relay testing, and exercising STARTTLS/AUTH.
# basic connectivity / banner + EHLO capabilities
swaks --to test@$DOMAIN --server $IP
# RCPT-based user check — stop right after RCPT TO, read the response code
# (accepted, 250/251 = valid mailbox; 550/551/553 = no such user)
swaks --to victim@$DOMAIN --from test@evil.com --server $IP --quit-after RCPT --hide-all; echo "exit: $?"
# same idea, scripted over a userlist (swaks exits non-zero on rejection)
for u in $(cat users.txt); do
swaks --to "$u@$DOMAIN" --server $IP --quit-after RCPT --hide-all \
&& echo "VALID: $u"
done
# open relay test — external sender AND external recipient, neither in the local domain
swaks --to outsider@external-test.com --from spoofed@some-other-domain.com --server $IP
# accepted (250) with no auth from an address outside $DOMAIN, to an address outside $DOMAIN = open relay
# force/require STARTTLS, dump the peer cert
swaks --to test@$DOMAIN --server $IP --tls --tls-get-peer-cert
# authenticated send (validate creds found via spraying/loot)
swaks --to test@$DOMAIN --from user@$DOMAIN --server $IP \
--auth LOGIN --auth-user "user@$DOMAIN" --auth-password 'Password1!'
| Flag | Description | Default |
|---|---|---|
-t, --to ADDR | Envelope recipient (only truly required option) | — |
-f, --from ADDR | Envelope sender; <> for null sender | best-guess local user@host |
-s, --server HOST[:PORT] | Target server | localhost |
--port PORT | Override port | protocol default (25) |
--protocol PROTO | SMTP, ESMTP, SSMTP, SMTPS, LMTP, … — sets port/TLS/HELO type as a side effect | ESMTP |
--quit-after STOP | End the transaction cleanly right after a stage: CONNECT, HELO/EHLO, STARTTLS, AUTH, MAIL, RCPT | full transaction |
-tls | Require STARTTLS, abort if unavailable | off |
-tlsc, --tls-on-connect | Implicit TLS on connect (port 465 style) | off |
-a, --auth [TYPE] | Require auth: LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5, NTLM | off |
-au, --auth-user / -ap, --auth-password | Credentials for --auth | prompt |
-n, --suppress-data | Don’t print the DATA section (keep output readable) | off |
-ha, --hide-all | Suppress all output — check $? instead | off |
--timeout TIME | Transaction timeout (5s/3m/1h, 0 = none) | 30s |
[!tip]
--quit-after RCPTis the whole trick This is straight from swaks’ own quick-start docs: stopping right afterRCPT TO:gets you the server’s accept/reject verdict without ever sending a message body, so it reads almost identically to whatsmtp-user-enum -M RCPTdoes internally — useful when you want to hand-verify a couple of hits, or when you need swaks’ TLS/auth handling that smtp-user-enum doesn’t have.
[!warning] Watch out
- Open-relay and unauthenticated
RCPTprobing generate real SMTP session log entries (and, on a relay hit, an actual outbound message) — scope carefully on live/production mail infrastructure, this isn’t a passive check.--protocol SMTPforcesHELOinstead ofEHLO, which some scanners use specifically to dodge servers that only rate-limit/log on ESMTP extensions.
25 / 587 / 465 with the right TLS mode
# 587 submission — explicit STARTTLS upgrade, then RCPT probe
swaks --to victim@$DOMAIN --server $IP:587 --tls --quit-after RCPT --hide-all; echo "exit: $?"
# 465 implicit TLS (SMTPS) — TLS handshake on connect, no cleartext EHLO
swaks --to victim@$DOMAIN --server $IP --port 465 --tls-on-connect --quit-after RCPT --hide-all; echo "exit: $?"
# plain 25, no TLS (MTA relay path)
swaks --to victim@$DOMAIN --server $IP:25 --quit-after RCPT --hide-all; echo "exit: $?"
[!warning] STARTTLS vs implicit TLS
--tlsperforms an in-band STARTTLS upgrade after a cleartextEHLO(ports 25/587).--tls-on-connect(-tlsc) does implicit TLS — the socket is encrypted from byte zero (port 465). Using the wrong one hangs or errors: 465 will not answer a plaintextEHLO, and most 587 listeners refuseRCPTbefore STARTTLS.
Ports & TLS — 25 vs 587 vs 465
| Port | Role | Transport | Enum relevance |
|---|---|---|---|
| 25 | MTA ↔ MTA relay (RFC 5321) | Cleartext, opportunistic STARTTLS | Classic VRFY/EXPN/RCPT + open-relay tests; client submission often ISP-blocked |
| 587 | Message submission (RFC 6409) | Cleartext EHLO → STARTTLS, AUTH required | Best smtp-ntlm-info target on Exchange; RCPT enum after STARTTLS |
| 465 | Submission over implicit TLS (SMTPS, RFC 8314) | TLS on connect, then AUTH | Same probes as 587 but wrap in --tls-on-connect / openssl s_client -connect |
[!tip] STARTTLS = opportunistic upgrade on 25/587; implicit TLS = 465 from the first byte. If VRFY/RCPT come back refused on 587, you almost certainly need to STARTTLS first (
swaks --tls, oropenssl s_client -starttls smtp -connect $IP:587).
[!warning] Microsoft 365 & Google Workspace are out of scope here Their public MX (
*.mail.protection.outlook.com,aspmx.l.google.com) accepts or generically rejects everyRCPT, so SMTP VRFY/EXPN/RCPT enumeration does not work against them. User enumeration on those platforms is a web/auth technique — Autodiscover / GetCredentialType / OWA-timing for M365, login-flow responses for Workspace — handled by tools likeo365sprayand MailSniper, not this sheet. See Attacking Common Services → Email for the M365 workflow.
Troubleshooting
| Problem | Cause & fix |
|---|---|
VRFY/EXPN always return 252 | The server accepts the verb but refuses to confirm — 252 means “cannot verify, will try to deliver”. Switch to RCPT TO enumeration, which most MTAs still answer distinctly |
| Every user returns the same code | User enumeration is disabled or the MTA replies identically for valid and invalid names. Fall back to timing analysis, or accept the vector is closed |
smtp-user-enum reports all users valid | You are hitting a catch-all / accept-all domain. Confirm with an obviously bogus name; if that “exists” too, the signal is worthless |
| Connection resets after a few probes | Rate limiting or greylisting kicked in. Slow down (-w delay), reconnect per batch, and expect the first attempt after a pause to be deferred |
RCPT TO needs MAIL FROM first | The SMTP state machine requires an envelope sender before a recipient. Send MAIL FROM:<test@test.com> once, then iterate RCPT TO on the same connection |
| STARTTLS required before any command | The MTA rejects cleartext auth/enum on 25/587. Use swaks --tls (587) or connect to 465 implicit TLS; openssl s_client -starttls smtp gives you a raw TLS session to type into |
| Works on port 25 but not 587 | 587 (submission) usually mandates AUTH and rejects anonymous VRFY/RCPT. Enumeration lives on 25; 587 is for authenticated relay testing |
See Also
- Nmap —
-p25,465,587 --script smtp-*to fingerprint the MTA before hand-enumerating. - Passive External Recon — harvest the name/email format that valid usernames follow.
- Attacking Common Services (guide) — SMTP inside the full service-attack workflow, including relay and phishing use.