During penetration testing assessments, FTP services (such as vsftpd or ProFTPD) are sometimes configured to disable directory listings (LIST or NLST commands return 550 Access is denied). While users cannot browse the file hierarchy interactively, read and traversal permissions may still allow direct path navigation (CWD) and file retrieval (RETR).
In such “blind” scenarios, dictionary-based brute forcing reveals hidden folders and downloadable assets.
1. Directory Fuzzing via CWD
This Python script tests candidate directory names against the FTP server using the CWD (Change Working Directory) command. If the directory exists and permissions permit traversal, the server returns a positive status code rather than error_perm (550).
#!/usr/bin/env python3
import ftplib
import sys
def brute_directories(host, wordlist_file, user="anonymous", password="guest@example.com"):
try:
ftp = ftplib.FTP(host, timeout=10)
ftp.login(user, password)
print(f"[*] Authenticated to {host} as {user}")
except Exception as e:
print(f"[-] Authentication failed: {e}")
return
with open(wordlist_file, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
directory = line.strip()
if not directory:
continue
try:
ftp.cwd(directory)
print(f"[+] Discovered valid directory: /{directory}")
ftp.cwd("/") # Return to root for next test
except ftplib.error_perm:
continue
except Exception as e:
print(f"[!] Error testing {directory}: {e}")
ftp.quit()
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python ftp_dir_brute.py <host> <wordlist.txt>")
sys.exit(1)
brute_directories(sys.argv[1], sys.argv[2])
2. File Enumeration and Download via RETR
When probing for sensitive backups, configuration dumps, or logs, we can attempt retrieval directly using the RETR command:
#!/usr/bin/env python3
import ftplib
import os
import sys
def brute_files(host, wordlist_file, output_dir="./downloaded", user="anonymous", password="guest@example.com"):
os.makedirs(output_dir, exist_ok=True)
try:
ftp = ftplib.FTP(host, timeout=10)
ftp.login(user, password)
except Exception as e:
print(f"[-] Connection failed: {e}")
return
with open(wordlist_file, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
filename = line.strip()
if not filename:
continue
local_path = os.path.join(output_dir, filename)
try:
with open(local_path, "wb") as f_out:
ftp.retrbinary(f"RETR {filename}", f_out.write)
print(f"[+] Downloaded: {filename}")
except ftplib.error_perm:
if os.path.exists(local_path):
os.remove(local_path)
continue
ftp.quit()
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python ftp_file_brute.py <host> <wordlist.txt>")
sys.exit(1)
brute_files(sys.argv[1], sys.argv[2])
[!TIP] Run file retrieval checks inside a designated temporary directory to prevent overwriting existing local files during automated transfers.