smtp-user-enum.md (17245B)
1 --- 2 title: "SMTP User Enumeration" 3 description: "smtp-user-enum, swaks and nmap's smtp-enum-users — VRFY/EXPN/RCPT username enumeration, open-relay checks and manual SMTP probing." 4 category: enumeration 5 tags: [enumeration, smtp, email, ntlm, starttls] 6 tools: [smtp-user-enum, swaks, Nmap, Hydra, Metasploit, netcat] 7 difficulty: beginner 8 updated: "2026-09-25" 9 --- 10 11 # SMTP User Enumeration 12 13 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](/sheets/pentest-workflow/service-enumeration) and [Attacking Common Services → Email](/sheets/pentest-workflow/attacking-common-services-guide). 14 15 ## Raw SMTP session — telnet / nc (hand verification) 16 17 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. 18 19 ```text 20 $ nc -nv $IP 25 21 220 mail.corp.local ESMTP Sendmail 8.15.2; ... 22 HELO x 23 250 mail.corp.local 24 VRFY root 25 252 2.1.5 Cannot VRFY user; try RCPT to attempt delivery # ambiguous (calibrate below) 26 VRFY nonexistent 27 252 2.1.5 Cannot VRFY user; try RCPT to attempt delivery # identical 252 for a bad user => VRFY is neutered, use RCPT 28 EXPN postmaster 29 502 5.5.1 EXPN command disabled # EXPN is off on most modern MTAs 30 MAIL FROM:<probe@evil.com> 31 250 2.1.0 Ok 32 RCPT TO:<jsmith@corp.local> 33 250 2.1.5 Ok # 250/251 => mailbox exists 34 RCPT TO:<nouser@corp.local> 35 550 5.1.1 <nouser@corp.local>: Recipient address rejected: User unknown # 550 => no such user 36 QUIT 37 221 2.0.0 Bye 38 ``` 39 40 | Code | VRFY / RCPT meaning | 41 |---|---| 42 | `250` | Mailbox exists / recipient accepted (VRFY echoes the full address) | 43 | `251` | User not local, will forward — valid, remote mailbox | 44 | `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) | 45 | `550` | No such user / mailbox unavailable — invalid | 46 | `551` / `553` | User not local (try forward-path) / mailbox name not allowed | 47 | `450` / `451` / `452` | Temp failure (greylist / throttle) — not a validity signal, retry | 48 49 > [!tip] Calibrate VRFY in one shot 50 > 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. 51 > ```bash 52 > printf 'HELO x\r\nVRFY nonexistent_zzz9\r\nVRFY root\r\nQUIT\r\n' | nc -w3 $IP 25 53 > # identical codes for both => VRFY neutered (switch to -M RCPT) 54 > # different codes (e.g. 550 vs 250) => VRFY is live, enumerate with it 55 > ``` 56 57 > [!tip] Same session inside TLS 58 > 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) or `openssl s_client -connect $IP:465` (implicit TLS), then type `HELO`/`VRFY`/`RCPT` as above. 59 60 ## smtp-user-enum.pl 61 62 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. 63 64 ```bash 65 # VRFY (default method) — most reliable when not disabled 66 perl smtp-user-enum.pl -M VRFY -U users.txt -t $IP 67 68 # EXPN — expands a mailing list/alias to member addresses 69 perl smtp-user-enum.pl -M EXPN -U users.txt -t $IP 70 71 # RCPT — works even when VRFY/EXPN are disabled; needs a MAIL FROM domain 72 perl smtp-user-enum.pl -M RCPT -U users.txt -D $DOMAIN -t $IP 73 74 # single username check, custom port, more workers 75 perl smtp-user-enum.pl -M VRFY -u root -t $IP -p 25 -m 10 76 ``` 77 78 | Flag | Description | Default | 79 |---|---|---| 80 | `-M mode` | Method: `VRFY`, `EXPN`, or `RCPT` | `VRFY` | 81 | `-u user` | Single username to check | — | 82 | `-U file` | File of usernames | — | 83 | `-t host` | Single target host | — | 84 | `-T file` | File of target hosts | — | 85 | `-D dom` | Domain appended to usernames for `RCPT` mode (guess full addresses) | none | 86 | `-f addr` | `MAIL FROM` address, `RCPT` mode only | `user@example.com` | 87 | `-p port` | TCP port | 25 | 88 | `-m n` | Max concurrent processes | 5 | 89 | `-d` | Debug output | off | 90 | `-v` | Verbose | off | 91 92 > [!warning] Watch out 93 > - Many MTAs (Sendmail and Exchange classically) answer VRFY with `252 Cannot VRFY user` regardless of validity — calibrate first by testing one known-bad and one known-good username; if both give identical responses, VRFY is neutered and you need `RCPT` instead. Default Postfix is the opposite — it answers VRFY *definitively* (`250`/`550`), which is why VRFY enum is classically demonstrated against it, and returns `502 5.5.1 VRFY command is disabled` when an admin turns it off (`disable_vrfy_command=yes`). 94 > - `RCPT` mode is slow (~5–7 requests/sec) and the noisiest of the three since it opens a full `MAIL FROM`/`RCPT TO` sequence per guess (no `DATA` sent, so no mail is actually delivered) — scope the wordlist, don't point it at rockyou. 95 > - `-m` concurrency above ~10–15 gets rate-limited or blacklisted by most modern MTAs. 96 97 ## nmap `smtp-enum-users` 98 99 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. 100 101 ```bash 102 # default: tries RCPT, then VRFY, then EXPN, against nmap's built-in username list 103 nmap -p25 --script smtp-enum-users $IP 104 105 # pick methods and order explicitly 106 nmap -p25 --script smtp-enum-users --script-args smtp-enum-users.methods={EXPN,RCPT,VRFY} $IP 107 108 # custom username list (standard nmap unpwdb 'userdb' argument) 109 nmap -p25 --script smtp-enum-users --script-args userdb=/opt/users.txt $IP 110 111 # banner/capability check first — shows if VRFY/EXPN/STARTTLS/AUTH are even offered 112 nmap -p25 -sC -sV $IP 113 ``` 114 115 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). 116 117 ```bash 118 nmap -p25 --script smtp-commands,smtp-open-relay,smtp-vuln* $IP 119 ``` 120 121 ### smtp-ntlm-info — internal hostname/domain disclosure 122 123 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. 124 125 ```bash 126 # hit all three SMTP ports; 587 (submission) is the usual winner on Exchange 127 nmap -p25,465,587 --script smtp-ntlm-info $IP 128 ``` 129 130 > [!tip] Why 587 131 > 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 into `smtp-user-enum -M RCPT -D <domain>` or `swaks --to user@<domain>`. 132 133 ## hydra & metasploit — enumeration equivalents 134 135 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. 136 137 ```bash 138 # Hydra's smtp-enum module — mode is the URL suffix (VRFY default / EXPN / RCPT) 139 # VRFY/EXPN ignore the password field; a dummy -p keeps hydra's parser happy on builds that demand one 140 hydra -L users.txt -p x smtp-enum://$IP/VRFY 141 hydra -L users.txt -p x smtp-enum://$IP/EXPN 142 # RCPT mode: -p carries the domain appended to each username 143 hydra -L users.txt -p $DOMAIN smtp-enum://$IP/RCPT 144 hydra -U smtp-enum # print the module's own options 145 ``` 146 147 ```bash 148 # Metasploit — auxiliary/scanner/smtp/smtp_enum (RCPT/VRFY/EXPN, no creds needed) 149 msfconsole -q 150 use auxiliary/scanner/smtp/smtp_enum 151 set RHOSTS $IP 152 set RPORT 25 153 set USER_FILE /usr/share/seclists/Usernames/Names/names.txt 154 run 155 # grab the banner/version alongside it: 156 use auxiliary/scanner/smtp/smtp_version 157 ``` 158 159 | Tool | Enum invocation | Notes | 160 |---|---|---| 161 | hydra | `smtp-enum://$IP/{VRFY,EXPN,RCPT}` | RCPT needs `-p $DOMAIN`; VRFY/EXPN take a throwaway `-p`; ships with hydra | 162 | metasploit | `auxiliary/scanner/smtp/smtp_enum` | `USER_FILE` default `data/wordlists/unix_users.txt`; `UNIXONLY true` skips the MS banner check | 163 | smtp-user-enum | `-M {VRFY,EXPN,RCPT}` (above) | fastest / most controllable for large lists | 164 165 ## swaks — manual probing, relay & auth testing 166 167 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. 168 169 ```bash 170 # basic connectivity / banner + EHLO capabilities 171 swaks --to test@$DOMAIN --server $IP 172 173 # RCPT-based user check — stop right after RCPT TO, read the response code 174 # (accepted, 250/251 = valid mailbox; 550/551/553 = no such user) 175 swaks --to victim@$DOMAIN --from test@evil.com --server $IP --quit-after RCPT --hide-all; echo "exit: $?" 176 177 # same idea, scripted over a userlist (swaks exits non-zero on rejection) 178 for u in $(cat users.txt); do 179 swaks --to "$u@$DOMAIN" --server $IP --quit-after RCPT --hide-all \ 180 && echo "VALID: $u" 181 done 182 183 # open relay test — external sender AND external recipient, neither in the local domain 184 swaks --to outsider@external-test.com --from spoofed@some-other-domain.com --server $IP 185 # accepted (250) with no auth from an address outside $DOMAIN, to an address outside $DOMAIN = open relay 186 187 # force/require STARTTLS, dump the peer cert 188 swaks --to test@$DOMAIN --server $IP --tls --tls-get-peer-cert 189 190 # authenticated send (validate creds found via spraying/loot) 191 swaks --to test@$DOMAIN --from user@$DOMAIN --server $IP \ 192 --auth LOGIN --auth-user "user@$DOMAIN" --auth-password 'Password1!' 193 ``` 194 195 | Flag | Description | Default | 196 |---|---|---| 197 | `-t, --to ADDR` | Envelope recipient (only truly required option) | — | 198 | `-f, --from ADDR` | Envelope sender; `<>` for null sender | best-guess local user@host | 199 | `-s, --server HOST[:PORT]` | Target server | localhost | 200 | `--port PORT` | Override port | protocol default (25) | 201 | `--protocol PROTO` | `SMTP`, `ESMTP`, `SSMTP`, `SMTPS`, `LMTP`, … — sets port/TLS/HELO type as a side effect | `ESMTP` | 202 | `--quit-after STOP` | End the transaction cleanly right after a stage: `CONNECT`, `HELO`/`EHLO`, `STARTTLS`, `AUTH`, `MAIL`, `RCPT` | full transaction | 203 | `-tls` | Require STARTTLS, abort if unavailable | off | 204 | `-tlsc, --tls-on-connect` | Implicit TLS on connect (port 465 style) | off | 205 | `-a, --auth [TYPE]` | Require auth: `LOGIN`, `PLAIN`, `CRAM-MD5`, `DIGEST-MD5`, `NTLM` | off | 206 | `-au, --auth-user` / `-ap, --auth-password` | Credentials for `--auth` | prompt | 207 | `-n, --suppress-data` | Don't print the DATA section (keep output readable) | off | 208 | `-ha, --hide-all` | Suppress all output — check `$?` instead | off | 209 | `--timeout TIME` | Transaction timeout (`5s`/`3m`/`1h`, `0` = none) | 30s | 210 211 > [!tip] `--quit-after RCPT` is the whole trick 212 > This is straight from swaks' own quick-start docs: stopping right after `RCPT TO:` gets you the server's accept/reject verdict without ever sending a message body, so it reads almost identically to what `smtp-user-enum -M RCPT` does 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. 213 214 > [!warning] Watch out 215 > - Open-relay and unauthenticated `RCPT` probing 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. 216 > - `--protocol SMTP` forces `HELO` instead of `EHLO`, which some scanners use specifically to dodge servers that only rate-limit/log on ESMTP extensions. 217 218 ### 25 / 587 / 465 with the right TLS mode 219 220 ```bash 221 # 587 submission — explicit STARTTLS upgrade, then RCPT probe 222 swaks --to victim@$DOMAIN --server $IP:587 --tls --quit-after RCPT --hide-all; echo "exit: $?" 223 224 # 465 implicit TLS (SMTPS) — TLS handshake on connect, no cleartext EHLO 225 swaks --to victim@$DOMAIN --server $IP --port 465 --tls-on-connect --quit-after RCPT --hide-all; echo "exit: $?" 226 227 # plain 25, no TLS (MTA relay path) 228 swaks --to victim@$DOMAIN --server $IP:25 --quit-after RCPT --hide-all; echo "exit: $?" 229 ``` 230 231 > [!warning] STARTTLS vs implicit TLS 232 > `--tls` performs an in-band **STARTTLS** upgrade after a cleartext `EHLO` (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 plaintext `EHLO`, and most 587 listeners refuse `RCPT` before STARTTLS. 233 234 ## Ports & TLS — 25 vs 587 vs 465 235 236 | Port | Role | Transport | Enum relevance | 237 |---|---|---|---| 238 | **25** | MTA ↔ MTA relay (RFC 5321) | Cleartext, opportunistic STARTTLS | Classic VRFY/EXPN/RCPT + open-relay tests; client submission often ISP-blocked | 239 | **587** | Message submission (RFC 6409) | Cleartext EHLO → **STARTTLS**, AUTH required | Best `smtp-ntlm-info` target on Exchange; RCPT enum after STARTTLS | 240 | **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` | 241 242 > [!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`, or `openssl s_client -starttls smtp -connect $IP:587`). 243 244 > [!warning] Microsoft 365 & Google Workspace are out of scope here 245 > Their public MX (`*.mail.protection.outlook.com`, `aspmx.l.google.com`) accepts or generically rejects every `RCPT`, 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 like `o365spray` and MailSniper, not this sheet. See [Attacking Common Services → Email](/sheets/pentest-workflow/attacking-common-services-guide) for the M365 workflow. 246 247 ## Troubleshooting 248 249 | Problem | Cause & fix | 250 |---------|-------------| 251 | `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 | 252 | 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 | 253 | `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 | 254 | 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 | 255 | `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 | 256 | 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 | 257 | 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 | 258 259 ## See Also 260 261 - **[Nmap](/sheets/enumeration/nmap)** — `-p25,465,587 --script smtp-* ` to fingerprint the MTA before hand-enumerating. 262 - **[Passive External Recon](/sheets/pentest-workflow/passive-external-recon)** — harvest the name/email format that valid usernames follow. 263 - **[Attacking Common Services (guide)](/sheets/pentest-workflow/attacking-common-services-guide)** — SMTP inside the full service-attack workflow, including relay and phishing use. 264 265 ## Sources 266 267 - https://github.com/pentestmonkey/smtp-user-enum 268 - https://github.com/jetmore/swaks 269 - https://nmap.org/nsedoc/scripts/smtp-enum-users.html 270 - https://nmap.org/nsedoc/scripts/smtp-open-relay.html