0

I am trying to download a file to a device over FTPS. I am using curl (7.88.1, OpenSSL/1.1.1u) as the client on the remote machine and pyftpdlib (1.5.7) to implement the FTP server on the local machine. While attempting to download my_file.txt, curl outputs: curl: (13) Bad PASV/EPSV response: 200. I have run the same setup against OpenSSL 1.1.1q & 3.1.2, LibreSSL 3.3.6, where I did not encounter this issue.

In Wireshark I can see that when the client attempts to establish a TLS connection on the passive port, the pyftpdlib server responds to the "client hello" with a RST, ACK packet.

Here is the basic implementation of my FTP server:

import os
import threading

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import TLS_FTPHandler
from pyftpdlib.servers import ThreadedFTPServer


CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__),
                                        "certificate.pem"))
KEYFILE = os.path.abspath(os.path.join(os.path.dirname(__file__),
                                        "private_key.pem"))

class FtpSrv:
        def __init__(self):
                self.server = None
                self.server_thread = None

        def start_ftp_server(self, port=2121):
                server_address = ('0.0.0.0', port)
                authorizer = DummyAuthorizer()
                authorizer.add_anonymous(os.getcwd())

                handler = TLS_FTPHandler
                handler.authorizer = authorizer
                handler.banner = "pyftpdlib based ftpd ready."
                handler.passive_ports = [1280]
                handler.certfile = CERTFILE
                handler.keyfile = KEYFILE

                self.server = ThreadedFTPServer(server_address, handler)

                self.server.max_cons = 256
                self.server.max_cons_per_ip = 5

                self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
                self.server_thread.start()

server = FtpSrv()
server.start_ftp_server(port=2128)

On the remote Linux machine I then run this curl command:

curl --ssl-reqd ftp://xxx.xxx.4.233:2128/myfile.txt -o my_file.txt -k -v

... and see this output returned: curl: (13) Bad PASV/EPSV response: 200

The verbose curl logging on the remote machine logs the following:

curl --ssl-reqd ftp://xxx.xxx.4.233:2128/myfile.txt -o my_file.txt  -k -v
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying xxx.xxx.4.233:2128...
* Connected to xxx.xxx.4.233 (xxx.xxx.4.233) port 2128 (#0)
< 220 pyftpdlib based ftpd ready.
> AUTH SSL
< 234 AUTH SSL successful.
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [6 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [930 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [264 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate:
*  subject: C=XX; ST=Some-State; L=xxx; O=xxx; CN=xxx.xxx.4.233
*  start date: Aug  1 23:59:00 2023 GMT
*  expire date: Jul 31 23:59:00 2024 GMT
*  issuer: C=xx; ST=Some-State; L=xxx; O=xxx; CN=xxx.xxx.4.233
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
} [5 bytes data]
> USER anonymous
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [217 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [217 bytes data]
* old SSL session ID is stale, removing
{ [5 bytes data]
< 331 Username ok, send password.
} [5 bytes data]
> PASS ftp@example.com
{ [5 bytes data]
< 230 Login successful.
} [5 bytes data]
> PBSZ 0
{ [5 bytes data]
< 200 PBSZ=0 successful.
} [5 bytes data]
> PROT P
{ [5 bytes data]
< 200 Protection set to Private
} [5 bytes data]
> PWD
{ [5 bytes data]
< 257 "/" is the current directory.
* Entry path is '/'
* Request has same path as previous transfer
} [5 bytes data]
> EPSV
* Connect data stream passively
* ftp_perform ends with SECONDARY: 0
{ [5 bytes data]
< 229 Entering extended passive mode (|||1280|).
* Connecting to xxx.xxx.4.233 (xxx.xxx.4.233) port 1280
*   Trying xxx.xxx.4.233:1280...
* Connected to xxx.xxx.4.233 (xxx.xxx.4.233) port 2128 (#0)
* SSL re-using session ID
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [673 bytes data]
> TYPE I
* Recv failure: Connection reset by peer
* OpenSSL SSL_connect: Connection reset by peer in connection to xxx.xxx.4.233:2128
* Failed EPSV attempt. Disabling EPSV
} [5 bytes data]
> PASV
{ [5 bytes data]
< 200 Type set to: Binary.
* Bad PASV/EPSV response: 200
* Remembering we are in dir ""
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Connection #0 to host xxx.xxx.4.233 left intact
curl: (13) Bad PASV/EPSV response: 200

And debug logging on the pyftpdlib server outputs the following:

concurrency model: multi-thread
masquerade (NAT) address: None
passive ports: 1280->1280
poller: 'pyftpdlib.ioloop.Select'
authorizer: 'pyftpdlib.authorizers.DummyAuthorizer'
handler: 'pyftpdlib.handlers.type'
max connections: 256
max connections per ip: 5
timeout: 300
banner: 'pyftpdlib based ftpd ready.'
max login attempts: 3
SSL certfile: '.\\certificate.pem'
SSL keyfile: '.\\private_key.pem'
xxx.xxx.4.233:10700-[] FTP session opened (connect)
xxx.xxx.4.233:10700-[] -> 220 pyftpdlib based ftpd ready.
xxx.xxx.4.233:10700-[] <- AUTH SSL
xxx.xxx.4.233:10700-[] -> 234 AUTH SSL successful.
[debug] securing SSL connection (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700')>)
[debug] call: _do_ssl_handshake, err: ssl-want-read (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True)>)
[debug] SSL connection established (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True)>)
xxx.xxx.4.233:10700-[] <- USER anonymous
xxx.xxx.4.233:10700-[] -> 331 Username ok, send password.
xxx.xxx.4.233:10700-[anonymous] <- PASS ******
xxx.xxx.4.233:10700-[anonymous] -> 230 Login successful.
xxx.xxx.4.233:10700-[anonymous] USER 'anonymous' logged in.
xxx.xxx.4.233:10700-[anonymous] <- PBSZ 0
xxx.xxx.4.233:10700-[anonymous] -> 200 PBSZ=0 successful.
xxx.xxx.4.233:10700-[anonymous] <- PROT P
xxx.xxx.4.233:10700-[anonymous] -> 200 Protection set to Private
xxx.xxx.4.233:10700-[anonymous] <- PWD
xxx.xxx.4.233:10700-[anonymous] -> 257 "/" is the current directory.
xxx.xxx.4.233:10700-[anonymous] <- EPSV
xxx.xxx.4.233:10700-[anonymous] -> 229 Entering extended passive mode (|||1280|).
[debug] call: close() (<pyftpdlib.handlers.PassiveDTP listening xxx.xxx.4.233:1280 at 0x1efe97835d0>)
[debug] securing SSL connection (<TLS_DTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous')>)
[debug] call: close() (<pyftpdlib.handlers.PassiveDTP xxx.xxx.4.233:1280 at 0x1efe97835d0>)
xxx.xxx.4.233:10700-[anonymous] <- TYPE I
xxx.xxx.4.233:10700-[anonymous] -> 200 Type set to: Binary.
[debug] call: close() (<TLS_DTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous', ssl-data=True)>)
xxx.xxx.4.233:10700-[anonymous] <- PASV
xxx.xxx.4.233:10700-[anonymous] -> 227 Entering passive mode (xxx,xxx,4,233,5,0).
xxx.xxx.4.233:10700-[anonymous] <- QUIT
xxx.xxx.4.233:10700-[anonymous] -> 221 Goodbye.
[debug] call: close() (<pyftpdlib.handlers.PassiveDTP listening xxx.xxx.4.233:1280 at 0x1efe9783710>)
[debug] call: _do_ssl_shutdown() -> shutdown(), err: zero-return (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous')>)
[debug] call: close() (<TLS_FTPHandler(id=2129885457744, addr='xxx.xxx.4.233:10700', ssl=True, user='anonymous')>)
xxx.xxx.4.233:10700-[anonymous] FTP session closed (disconnect).
[debug] closing IOLoop (<pyftpdlib.ioloop.Select (fds=0, tasks=0) at 0x1efe9783050>)

NOTE: Configuring a Filezilla FTP server to 'Require explicit FTP over TLS' using the same certificate and key as well as passive port (1280). The file transfer just works.

Blue_Wolf13
  • 15
  • 1
  • 2
  • 6

1 Answers1

0

Regarding the curl: (13) Bad PASV/EPSV response: 200 error you mentioned, it's important to note that this error, although it might seem unexpected, can actually be logically normal in certain scenarios. The 200 response you're encountering is actually a "TYPE I" reply: < 200 Type set to: Binary.

> PASV
{ [5 bytes data]
< 200 Type set to: Binary.
curl: (13) Bad PASV/EPSV response: 200 

According to the RFC959 specifications, the supported responses for PASV are typically 227, 500, 501, 502, 421, and 530 and NOT 200

However, the main issue that arises is when the client initiates TLS negotiations after the establishment of the extended passive mode (EPSV) over 1280 : TLSv1.3 (OUT), TLS handshake, Client hello (1) In this case, the client attempts to re-use the session ID, and it's this specific action that the server blocks.

as a proposition Create an SSL context with session caching enabled.

import ssl
class FtpSrv:
    def __init__(self):
            self.server = None
            self.server_thread = None

    def start_ftp_server(self, port=2121):
            server_address = ('0.0.0.0', port)
            authorizer = DummyAuthorizer()
            authorizer.add_anonymous(os.getcwd())

            handler = TLS_FTPHandler
            handler.authorizer = authorizer
            handler.banner = "pyftpdlib based ftpd ready."
            handler.passive_ports = [1280]
            handler.certfile = CERTFILE
            handler.keyfile = KEYFILE

            

            ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
            ssl_context.load_cert_chain(certfile=CERTFILE, keyfile=KEYFILE)
            ssl_context.set_session_cache_mode(ssl.SESS_CACHE_BOTH)
            handler.tls_context = ssl_context

            self.server = ThreadedFTPServer(server_address, handler)

            self.server.max_cons = 256
            self.server.max_cons_per_ip = 5

            self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True)
            self.server_thread.start()

and after we can see if the error still occurs the same.

  • I setup the handler to use an SSL context and tried to configure session caching by going: ssl_context = SSL.Context(SSL.TLSv1_2_METHOD) ssl_context.use_certificate_file(certfile=CERTFILE) ssl_context.use_privatekey_file(keyfile=KEYFILE) ssl_context.set_options(SSL.SESS_CACHE_BOTH) handler.ssl_context = ssl_context However, still run into the same outcome. – Blue_Wolf13 Aug 21 '23 at 22:40