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)
This commit is contained in:
Valentin Lobstein 2026-02-11 07:14:17 +01:00
parent 8703877d7d
commit fd325c06a6
No known key found for this signature in database
GPG key ID: F053C2B15CE40815
2 changed files with 42 additions and 23 deletions

View file

@ -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

View file

@ -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()