Exploring FTP with Python3

This post explains basic interactions with FTP using Python3 from a discovery/reconnaissance perspective. I was tasked with scanning a /16 network, and couldn’t find an existing tool that did what I wanted. So I wrote one, ftp_discover.py. Ftp_discover.py is a multithreaded program that scans one or more systems to check if FTP is running, perform banner grabbing, attempt an anonymous login, perform a directory listing of a specified depth, and output to a CSV file.

While there are several options for scanning for systems running FTP, or testing for anonymous login on an FTP server, I really needed the directory listing and CSV output functionality (I was testing ~200 FTP servers that allowed anonymous login). Shortly after I wrote ftp_discover.py I found out that the Nmap NSE script, ftp-anon, does perform a directory listing, however it does not traverse past a depth of one (so at least my script has that added feature). Nmap also doesn’t provide CSV as an output option, however you can use my Nmap XML parser (discussed here) to convert the XML output to CSV.

Python ships with ftplib to interact with FTP. I’ll show how to connect to an FTP service and perform banner grabbing, login (anonymously and with credentials), and list and change directories.

Connect to an FTP server with Python and grab the banner

Connecting to an FTP server is pretty simple. Here’s what it looks like in the Python interpreter:

>>> from ftplib import FTP
>>> ftp = FTP()
>>> server = 'speedtest.tele2.net'
>>> port = 21
>>> banner = ftp.connect(server, port)
>>> banner
'220 (vsFTPd 2.3.5)'

Login to an FTP server

First, anonymously:

>>> ftp.login()
'230 Login successful.'

Next with creds (and a different server):

>>> server = 'test.rebex.net'
>>> ftp.connect(server, port)
'220 Microsoft FTP Service'
>>> username = 'demo'
>>> password = 'password'
>>> ftp.login(username, password)
'230 User logged in.'

Perform FTP directory listing

There are a couple ways to list directories in FTP with Python. The first is using the retrlines function, passing the ‘LIST’ command. This provides a descriptive listing, showing whether the items are a file or a directory. The disadvantage of this command is the return value is not the directory listing, so it is difficult to parse the listing:

>>> dirlist = ftp.retrlines('LIST')
10-27-15  04:46PM       <DIR>          pub
04-08-14  04:09PM                  403 readme.txt
>>> dirlist
'226 Transfer complete.'

Another way to list the contents of a directory is using the nlst() function. This function returns a list, however has the downside of not showing whether the item is a file or directory:

>>> dirlist = ftp.nlst()
>>> dirlist
['pub', 'readme.txt']

To determine whether the item is a file or a directory, you can run the nlst() function, passing the name of the item to the function. If it returns the item name, it is not a directory:

>>> ftp.nlst()
['pub', 'readme.txt']
>>> ftp.nlst('readme.txt')
>>> ftp.nlst('pub')

Change directories and print working directory

Navigating directories in FTP is similar to other operating systems.

To print the present working directory:

>>> ftp.pwd()

Change current working directory to ‘/pub’:

>>> ftp.cwd('pub')
'250 CWD command successful.'
>>> ftp.pwd()

Navigate back to the root directory:

>>> ftp.cwd('..')
'250 CDUP command successful.'
>>> ftp.pwd()


Interacting with FTP using Python isn’t too complicated, so I won’t go over the innerworkings of my script, but I will go over some basic usage:

Scan a single hostname or IP address (-i) and attempt an anonymous login (-a):

python3 ftp_discover.py -i speedtest.tele2.net -a

You can also specify a CIDR or Nmap style range of addresses to scan:

python3 ftp_discover.py -r 10.10.0-10.0-255 -a

Scan all hosts in a file, perform anonymous login and list directories (-l) (one deep by default):

python3 ftp_discover.py -f anon_ftp_hosts.txt -a -l

Do the same things, but list directories three deep:

python3 ftp_discover.py -f anon_ftp_hosts.txt -a -l 3

All results are written to a CSV file by default.

When reviewing the script output of the directory listing, the items are shown as a Python dictionary. If the value of the the key is None, then the item is a file. If item is a directory then the items will appear as nested dictionaries. If items exist beyond the depth of the specified depth, then the value will display that depth > [specified number].

For example, see the highlighted value of upload in the output:

python3 ftp_discover.py -i speedtest.tele2.net -a -l
[+]  Loaded 1 FTP server addresses to test
[+] speedtest.tele2.net:21 - Connection established
[+] speedtest.tele2.net:21 - Anonyomous login established
[+] speedtest.tele2.net:21 - Directory Listing Received
{'1000GB.zip': None,
'5MB.zip': None,
'upload': ['depth > 1']}

However when a greater depth is specified:

python3 ftp_discover.py -i speedtest.tele2.net -a -l 2

[+]  Loaded 1 FTP server addresses to test

[+] speedtest.tele2.net:21 - Connection established
[+] speedtest.tele2.net:21 - Anonyomous login established
[+] speedtest.tele2.net:21 - Directory Listing Received
{'1000GB.zip': None,

 '5MB.zip': None,
 'upload': {'100MB.zip': None,
            '11j7cd1nqie6ipjromnimfeekp.txt': None,
            'Ghost.In.The.Shell.1995.German.DTS.ML.1080p.BluRay.REPACK.x264.mkv': None,
            'kvb9563leh17mttcnojamh8euv.txt': None,
            'sow-bigblue.1080p.mkv': None,
            'upload_file.txt': None}}

The code is available on my GitHub, and as always feedback is welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *