ProFTPD Help probe: hard prefix match on incomplete TCP read drops version
Summary
The TCP Help probe in nmap-service-probes has a hard match on only the first line of a ProFTPD HELP reply (214-The following commands are recognized (* =>'s unimplemented):).
Because version detection applies match lines against the TCP buffer as data arrives, that prefix match can fire before the rest of the HELP table is read. Nmap then stops. The later, more specific rule that assigns v/1.2.10/ never sees a complete payload.
Observed result (same daemon, same port, seconds apart):
| Recv buffer | Rule that fires | Output |
|---|---|---|
| Full HELP command table (~600 bytes) | ProFTPD 1.2.10 match |
ftp ProFTPD 1.2.10 |
Banner + first 214- line only |
prefix match (no v/) |
ftp ProFTPD (no version) |
This is not a flapping service. The 220 banner has no version string (custom welcome), so version depends entirely on the HELP fingerprint completing.
Still present in Nmap 7.991 (nmap-service-probes, Probe TCP Help).
Fingerprint lines (7.991)
Probe TCP Help (q|HELP\r\n|, totalwaitms 7500):
# specific — assigns version
match ftp m|^220 .*\r\n214-The following commands are recognized \(\* =>'s unimplemented\):\r\n CWD XCWD ... STOR STOU \r\n|s p/ProFTPD/ v/1.2.10/ ...
# shorter table — product only
match ftp m|^220 .*\r\n214-The following commands are recognized \(\* =>'s unimplemented\):\r\n CWD ... HELP \r\n|s p/ProFTPD/ ...
# prefix only — product only, too broad, HARD match
match ftp m|^220[ -].*\r\n214-The following commands are recognized \(\* =>'s unimplemented\):\r\n|s p/ProFTPD/ ...In current 7.991 these are around lines 13346, 13348, and 13350.
Line 13350 is a prefix of 13346. Any time the first TCP read ends after the 214- header, 13350 wins and version detection is finished.
Reproduce locally (no external host)
Save as proftpd_help_split.py and run python3 proftpd_help_split.py. It listens on 127.0.0.1:2121, sends a generic 220 banner (no version), then splits the 1.2.10 HELP table after the 214- header.
#!/usr/bin/env python3
import socket
import threading
import time
BANNER = b"220 FTP server ready\r\n"
HELP_HEAD = (
b"214-The following commands are recognized (* =>'s unimplemented):\r\n"
)
HELP_REST = (
b" CWD XCWD CDUP XCUP SMNT* QUIT PORT PASV \r\n"
b" EPRT EPSV ALLO* RNFR RNTO DELE MDTM RMD \r\n"
b" XRMD MKD XMKD PWD XPWD SIZE SYST HELP \r\n"
b" NOOP FEAT OPTS AUTH* CCC* CONF* ENC* MIC* \r\n"
b" PBSZ* PROT* TYPE STRU MODE RETR STOR STOU \r\n"
b" APPE REST ABOR USER PASS ACCT* REIN* LIST \r\n"
b" NLST STAT SITE MLSD MLST \r\n"
b"214 Direct comments to [email protected]\r\n"
)
SPLIT_SEC = 0.4 # 0 = one write (version present); 0.4 = prefix match (no version)
def handle(conn):
try:
conn.sendall(BANNER)
data = b""
conn.settimeout(5)
while b"\n" not in data:
chunk = conn.recv(1024)
if not chunk:
return
data += chunk
if data.upper().startswith(b"HELP"):
conn.sendall(HELP_HEAD)
if SPLIT_SEC:
time.sleep(SPLIT_SEC)
conn.sendall(HELP_REST)
time.sleep(0.2)
except OSError:
pass
finally:
conn.close()
def main():
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 2121))
s.listen(5)
print("listening on 127.0.0.1:2121")
while True:
c, _ = s.accept()
threading.Thread(target=handle, args=(c,), daemon=True).start()
if __name__ == "__main__":
main()Then:
nmap -Pn -sV -T4 -p2121 127.0.0.1With SPLIT_SEC = 0.4 (and -T4), service detection often reports ProFTPD with no version. With SPLIT_SEC = 0, the same table is reported as ProFTPD 1.2.10. Adding --version-trace -d shows which Help match line won.
Suggested fix
The prefix rule (current line 13350) should not be a hard match. Options:
- Change it to
softmatchso Nmap keeps reading until idle /totalwaitmsand can still hit thev/1.2.10/rule. - Same for the intermediate product-only table match (line 13348), which has the same “stop early, no version” shape.
- Alternatively, do not apply hard
matchlines until the214multiline HELP reply is complete (ended by a214final line) or the socket goes idle.
I am happy to send a probes patch if that is preferred.
Environment
- Nmap 7.991 (also observed on older 7.x service-probes with the same three Help lines)
- macOS / Linux
- Command:
nmap -Pn -sV -T4
Source: nmap/nmap