From fd325c06a6f90d4a3807aa438554cdf8b6524908 Mon Sep 17 00:00:00 2001 From: Valentin Lobstein Date: Wed, 11 Feb 2026 07:14:17 +0100 Subject: [PATCH] Fix: Handle ZMTP 2.0, CURVE/PLAIN, and identity padding in detection - Add 's' flag to service probe regex (dot must match 0x0A in signature padding when identity length == 10) - Remove '$' anchor from NULL probe ZMTP match to catch 2.0 responses longer than 10 bytes (revision + socket-type + identity frame) - Handle partial greetings from CURVE/PLAIN servers (only 10-byte signature before mechanism mismatch closure) - Tested against 19 servers: 7 NULL socket types, 2 NULL+identity, 2 CURVE, 2 PLAIN, 6 raw ZMTP 2.0 (with/without identity) --- nmap-service-probes | 10 +++++--- scripts/zmtp-info.nse | 55 +++++++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/nmap-service-probes b/nmap-service-probes index 39eb3c5ac..317ae6751 100644 --- a/nmap-service-probes +++ b/nmap-service-probes @@ -5277,8 +5277,11 @@ match zeo-monitor m|^ZEO monitor server version ([\w._-]+)\n.*\n\nStorage: \d+\n match zos-commserver m|^EZY1315E \d\d/\d\d/\d\d \d\d:\d\d:\d\d INVALID TRANID=\r\n\r\n PARTNER INET ADDR=[\d.]+ PORT= \d+ | p|IBM z/OS Communications Server| o|z/OS| cpe:/o:ibm:z%2fos/ # http://rfc.zeromq.org/spec:15 -# This is a backwards-compatible handshake -match zmtp m|^\xff\0\0\0\0\0\0\0\x01\x7f$| p/ZeroMQ ZMTP 2.0/ +# ZMTP signature: 0xFF + 8 padding bytes + 0x7F. Padding may contain identity +# length for backward compat (can be 0x0A = \n, so 's' flag is required). +# Original match required exactly 10 bytes ($), but 2.0 servers send more +# (revision + socket-type + identity frame). Use 's' flag for \n in padding. +match zmtp m|^\xff.{8}\x7f|s p/ZeroMQ ZMTP/ ##############################NEXT PROBE############################## # ZMTP 3.x (ZeroMQ Message Transport Protocol) @@ -5291,7 +5294,8 @@ ports 41459,50051,56441,30000-65535 # ZMTP response: 64-byte greeting starting with signature 0xFF...0x7F # Catches all ZMTP versions (2.0, 3.0, 3.1+). NSE script handles detailed parsing. -match zmtp m|^\xff.{8}\x7f| p/ZeroMQ ZMTP/ +# 's' flag: padding bytes may contain 0x0A (\n) when identity length == 10. +match zmtp m|^\xff.{8}\x7f|s p/ZeroMQ ZMTP/ # http://www.space-walrus.com/games/Minebuilder # Very general, so leaving it here at the end diff --git a/scripts/zmtp-info.nse b/scripts/zmtp-info.nse index 3b05abc65..58dea0c6a 100644 --- a/scripts/zmtp-info.nse +++ b/scripts/zmtp-info.nse @@ -296,12 +296,13 @@ action = function(host, port) return nil end - -- Receive at least the 10-byte signature. - -- For ZMTP 3.x we typically get 64+ bytes, for 2.0 we get ~14 bytes. + -- Receive response. We ask for at least 10 bytes (the signature). + -- ZMTP 3.x servers typically send 64+ bytes, 2.0 servers send ~14 bytes. + -- CURVE/PLAIN servers may send only 10 bytes initially (partial greeting). local response status, response = socket:receive_bytes(10) - if not status or #response < 11 then + if not status or #response < 10 then socket:close() stdnse.debug1("No valid response received") return nil @@ -317,27 +318,41 @@ action = function(host, port) return nil end + -- If we only got the 10-byte signature (partial greeting from CURVE/PLAIN + -- servers), try to receive more bytes for version info. + if #response == 10 then + stdnse.debug1("Got only signature (10 bytes), waiting for version...") + local s, more = socket:receive_bytes(1) + if s and more then + response = response .. more + end + end + local output = stdnse.output_table() output.protocol = "ZMTP" - local major = string.byte(response, 11) - - if major >= 3 then - -- ZMTP 3.x: ensure we have the full 64-byte greeting - if #response < 64 then - local s, more = socket:receive_bytes(64 - #response) - if s and more then - response = response .. more - end - end - parse_zmtp3(response, socket, output) - elseif major == 1 or major == 2 then - -- ZMTP 2.0 (revision byte 0x01) or ZMTP 2.1 (0x02) - parse_zmtp2(response, output) + if #response < 11 then + -- Only got the signature, no version byte. Report as ZMTP detected. + stdnse.debug1("ZMTP signature confirmed but no version received (%d bytes)", #response) else - -- Unknown version, report what we know - output.version = string.format("%d.0", major) - stdnse.debug1("Unknown ZMTP major version: %d", major) + local major = string.byte(response, 11) + + if major >= 3 then + -- ZMTP 3.x: try to get the full 64-byte greeting + if #response < 64 then + local s, more = socket:receive_bytes(1) + if s and more then + response = response .. more + end + end + parse_zmtp3(response, socket, output) + elseif major == 1 or major == 2 then + -- ZMTP 2.0 (revision byte 0x01) or ZMTP 2.1 (0x02) + parse_zmtp2(response, output) + else + output.version = string.format("%d.0", major) + stdnse.debug1("Unknown ZMTP major version: %d", major) + end end socket:close()