daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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