Acceso en modo pasivo a un servidor FTP que no conoce su propia IP pública

Recientemente he estado participando en un proyecto que emplea un servidor FTP [1] detrás de un NAT. El NAT tiene un puerto abierto redirigido hacia el servidor FTP interno, pero este está mal configurado y responde a los intentos de conexión en modo pasivo con su dirección IP interna, que no es válida en internet.

[1] El protocolo FTP tiene muchos inconvenientes y se ha quedado anticuado. Un sistema moderno debería utilizar un protocolo más acorde con los tiempos, como WebDAV, preferiblemente con cifrado de datos.

No quiero entrar en detalles finos sobre cómo funciona FTP, pero lo que nos interesa es que a la hora de transferir datos se puede usar un modo activo y un modo pasivo. En el modo activo, el servidor FTP intentaría conectarse a nuestra máquina, algo que hoy en día no va a funcionar fuera de una red local debido a los NAT y cortafuegos. En el modo pasivo, solicitamos al servidor de FTP detalles de conexión para conectarnos nosotros a él. El servidor FTP responderá con una dirección IP y un puerto al que conectarse.

El problema, en este caso concreto, es que la dirección IP que nos da el servidor FTP es privada y solo es válida dentro de su red local.

Maravillado ante el hecho de que el resto de compañeros no tengan problemas con esto, averiguo que todos utilizan un cliente FTP llamado FileZilla. Este cliente hace magia: si el servidor FTP responde a una petición de conexión en modo pasivo con una dirección IP privada, FileZilla la ignorará e intentará conectar a la dirección IP pública del servidor FTP. Es decir, a la dirección IP del cortafuegos o NAT en la frontera de su red.

Si dicho cortafuegos o NAT tiene mapeado el puerto hacia en servidor FTP interno, el proceso funcionará.

¿Cómo cambiar mis programas en Python para implementar algo similar?

Revisando el código fuente de la biblioteca ftplib de Python, puedo ver que los objetos FTP tienen un método makepasv() que es exactamente lo que me interesa.

class FTP(ftplib.FTP):
    def makepasv(self):
        host, port = super().makepasv()
        # host es una IP, pero devolvemos un nombre. No importa.
        return self.host, port

self.host es el nombre o dirección IP al que nos hemos conectado, en este caso el cortafuegos o NAT de frontera del servidor FTP. El código debería comprobar que los datos devueltos por el servidor FTP son inválidos antes de reemplazarlos, pero he sido vago. Por convención el modo pasivo indica una dirección IP, pero a la hora de reemplazarla la sustituyo por los datos de conexión que se hayan usado al instanciar la clase, sea un nombre o una dirección IP. El objeto FTP funciona igual, así que he vuelto a ser vago.

¿Cómo funcionan otros servidores FTP con un cortafuegos o NAT delante? Hay dos posibilidades:

  1. El servidor FTP usa un puerto dinámico para el modo pasivo y envía sus datos al cliente. Esos datos son inválidos fuera de la red local del servidor FTP, pero el cortafuegos o NAT de frontera realiza una supervisión activa de la conexión FTP, reconoce esta respuesta y la modifica "al vuelo" para que apunte a su dirección IP y a un puerto que esa pasarela va a abrir específicamente y redirigir de forma transparente al servidor FTP interno.

    Este análisis de contenidos es mucho más común de lo que pudiera parecer, incluso en routers domésticos, si bien normalmente está desactivado por rendimiento y seguridad. Por ejemplo, un virus podría enviar un mensaje simulando ser una respuesta de modo pasivo de un servidor FTP para que el router abra un puerto de entrada y permitir el acceso malicioso desde el exterior.

  2. El cortafuegos o NAT tiene un puerto estático configurado para que todas las conexiones entrantes hacia él se encaminen internamente a un puerto determinado del servidor FTP.

    Esto requiere que el modo pasivo del FTP utilice un único puerto o un pequeño número de puertos de entrada.

    Esto es problemático y permite que un atacante controle el contenido (pero no el nombre o dónde se guarda) de un fichero. Incluso puede ser motivo de funcionamientos erráticos y corrupción de ficheros cuando el servidor FTP tiene actividad legítima concurrente.

    En el caso del servidor FTP que me preocupa, solo admite la espera de una conexión pasiva de forma simultánea y así no puede haber confusión si hay varios usuarios concurrentes. Un segundo intento simultáneo recibe un error 421 Could not create socket..

    El tiempo de exclusión es pequeño, desde el momento en que pedimos modo pasivo hasta que nos conectamos al servidor. Una vez que nos hemos conectado, otros clientes pueden iniciar sus propias conexiones pasivas.

    Esto tiene dos efectos:

    1. Un cliente legítimo puede recibir un error 421 Could not create socket.. Un cliente FTP podría reintentar la orden automáticamente, pero lo normal es que el cliente prefiera desconectarse e informar al usuario. Reintentando a mano, entrará.

    2. Un atacante sigue teniendo la posibilidad de controlar el contenido del fichero que un cliente legítimo intenta subir, simplemente intentando abrir una conexión en modo pasivo al servidor FTP constantemente. Si no hay un cliente legítimo, esa conexión en modo pasivo fallará, pero en cuanto un cliente solicite una conexión en modo pasivo al servidor FTP y este la active, el atacante podría ganar la carrera y conectarse al servidor FTP antes que el cliente legítimo, subiendo el contenido que desee.

      Este problema es insoluble con el protocolo FTP actual y es trivial si el puerto de modo pasivo es fijo (como es este caso), aunque también funcionaría en servidores FTP con puertos dinámicos, si hay cierto nivel de actividad.

      Este y otros muchos motivos recomiendan evitar el uso de FTP y adoptar protocolos más modernos como WebDAV.