1
0
mirror of https://github.com/nmap/nmap.git synced 2025-12-21 15:09:02 +00:00

nbd-info script and nbd.lua. Closes #609

This commit is contained in:
dmiller
2018-03-09 20:37:19 +00:00
parent 807b66480a
commit 262d425843
4 changed files with 796 additions and 0 deletions

View File

@@ -1,5 +1,8 @@
#Nmap Changelog ($Id$); -*-text-*- #Nmap Changelog ($Id$); -*-text-*-
o [NSE][GH#609] New script nbd-info uses the new nbd.lua library to query
Network Block Devices for protocol and file export information. [Mak Kolybabi]
o [NSE][GH#1111] Fix a script crash in ftp.lua when PASV connection timed out. o [NSE][GH#1111] Fix a script crash in ftp.lua when PASV connection timed out.
[Aniket Pandey] [Aniket Pandey]

596
nselib/nbd.lua Normal file
View File

@@ -0,0 +1,596 @@
local comm = require "comm"
local match = require "match"
local stdnse = require "stdnse"
local string = require "string"
_ENV = stdnse.module("nbd", stdnse.seeall)
---
-- An implementation of the Network Block Device protocol.
-- https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
--
-- @author "Mak Kolybabi <mak@kolybabi.com>"
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
NBD = {
magic = {
init_passwd = string.char(0x4E, 0x42, 0x44, 0x4d, 0x41, 0x47, 0x49, 0x43),
cliserv_magic_old = string.char(0x00, 0x00, 0x42, 0x02, 0x81, 0x86, 0x12, 0x53),
cliserv_magic_new = string.char(0x49, 0x48, 0x41, 0x56, 0x45, 0x4F, 0x50, 0x54),
option = string.char(0x00, 0x03, 0xE8, 0x89, 0x04, 0x55, 0x65, 0xA9),
cmd_request = string.char(0x25, 0x60, 0x95, 0x13),
cmd_reply = string.char(0x67, 0x44, 0x66, 0x98),
},
handshake_flags = {
FIXED_NEWSTYLE = 0x0001,
NO_ZEROES = 0x0002,
},
client_flags = {
C_FIXED_NEWSTYLE = 0x00000001,
C_NO_ZEROES = 0x00000002,
},
transmission_flags = {
HAS_FLAGS = 0x0001,
READ_ONLY = 0x0002,
SEND_FLUSH = 0x0004,
SEND_FUA = 0x0008,
ROTATIONAL = 0x0010,
SEND_TRIM = 0x0020,
SEND_WRITE_ZEROES = 0x0040, -- WRITE_ZEROES Extension
SEND_DF = 0x0080, -- STRUCTURED_REPLY Extension
},
opt_req_types = {
EXPORT_NAME = 0x00000001,
ABORT = 0x00000002,
LIST = 0x00000003,
PEEK_EXPORT = 0x00000004, -- PEEK_EXPORT Extension
STARTTLS = 0x00000005,
INFO = 0x00000006, -- INFO Extension
GO = 0x00000007, -- INFO Extension
STRUCTURED_REPLY = 0x00000008, -- STRUCTURED_REPLY Extension
BLOCK_SIZE = 0x00000009, -- INFO Extension
},
opt_rep_types = {
ACK = 0x00000001,
SERVER = 0x00000002,
INFO = 0x00000003, -- INFO Extension
ERR_UNSUP = 0x80000001,
ERR_POLICY = 0x80000002,
ERR_INVALID = 0x80000003,
ERR_PLATFORM = 0x80000004,
ERR_TLS_REQD = 0x80000005,
ERR_UNKNOWN = 0x80000006, -- INFO Extension
ERR_SHUTDOWN = 0x80000007,
ERR_BLOCK_SIZE_REQD = 0x80000008, -- INFO Extension
},
opt_rep_ext_types = {
info = {
EXPORT = 0x0000,
NAME = 0x0001,
DESCRIPTION = 0x0002,
BLOCK_SIZE = 0x0003,
},
},
cmd_req_flags = {
FUA = 0x0001,
NO_HOLE = 0x0002, -- WRITE_ZEROES Extension
DF = 0x0004, -- STRUCTURED_REPLY Extension
},
cmd_req_types = {
READ = 0x0000,
WRITE = 0x0001,
DISC = 0x0002,
FLUSH = 0x0003,
TRIM = 0x0004,
CACHE = 0x0005, -- XNBD custom request types
WRITE_ZEROES = 0x0006, -- WRITE_ZEROES Extension
},
errors = {
EPERM = 0x00000001,
EIO = 0x00000005,
ENOMEM = 0x0000000C,
EINVAL = 0x00000016,
ENOSPC = 0x0000001C,
EOVERFLOW = 0x0000004B,
ESHUTDOWN = 0x0000006C,
},
}
Comm = {
--- Creates a new client instance.
--
-- @name Comm.new
--
-- @param host Table as received by the action method.
-- @param port Table as received by the action method.
--
-- @return o Instance of Client.
new = function(self, host, port)
local o = {host = host, port = port, exports = {}}
setmetatable(o, self)
self.__index = self
return o
end,
--- Connects to the NBD server.
--
-- @name Comm.connect
--
-- @return status true on success, false on failure.
connect = function(self)
-- NBD servers send a response when we connect. We are using
-- tryssl here as a precaution since there are several
-- implementations of the protocol and no reason it can't be
-- wrapped. IANA has rejected assigning another port for NBD over
-- TLS.
local sd, err, proto, rep = comm.tryssl(self.host, self.port, "", {recv_before = true})
if not sd then
return false, err
end
-- The socket connected successfully over whichever protocol.
-- Store the connection information.
self.socket = sd
self.protocol = {ssl_tls = (proto == "ssl")}
if #rep < 8 then
stdnse.debug1("Failed to receive first 64 bits of magic from server: %s", rep)
self:close()
return false
end
-- We may have received 8-100+ bytes of data, depending on timing. To make
-- the code simpler, we will seed a buffer to be used by this object's
-- receive function until empty.
self.receive_buffer = rep:sub(9)
rep = rep:sub(1, 8)
if rep ~= NBD.magic.init_passwd then
stdnse.debug1("First 64 bits from server don't match expected magic: %s", stdnse.tohex(rep, {separator = ":"}))
self:close()
return false
end
local status, rep = self:receive(8)
if not status then
stdnse.debug1("Failed to receive second 64 bits of magic from server: %s", rep)
return false
end
if rep == NBD.magic.cliserv_magic_new then
self.protocol.negotiation = "newstyle"
return self:connect_new()
end
if rep == NBD.magic.cliserv_magic_old then
self.protocol.negotiation = "oldstyle"
return self:connect_old()
end
self.protocol.negotiation = "unrecognized"
stdnse.debug1("Second 64 bits from server don't match any known protocol magic: %s", stdnse.tohex(rep, {separator = ":"}))
self:close()
return true
end,
--- Cycles the connection to the server.
--
-- @name Comm.reconnect
--
-- @return status true on success, false on failure.
reconnect = function(self)
self:close()
return self:connect(self.connect_options)
end,
--- Attaches to an named share on the server.
--
-- @name Comm.attach
--
-- @return status true on success, false on failure.
attach = function(self, name)
assert(self.protocol.negotiation == "newstyle" or self.protocol.negotiation == "fixed newstyle")
assert(type(name) == "string")
local req = self:build_opt_req("EXPORT_NAME", {export_name = name})
local status, err = self:send(req)
if not status then
stdnse.debug1("Failed to send attach request for '%s': %s", name, err)
self:close()
return
end
local status, size = self:receive(8)
if not status then
stdnse.debug1("Failed to receive response to attach request for '%s': %s", name, size)
self:close()
return
end
local size, pos = (">I8"):unpack(size)
if pos ~= 9 then
stdnse.debug1("Failed to unpack size of exported block device from server.")
self:close()
return false
end
local status, tflags = self:receive(2)
if not status then
stdnse.debug1("Failed to receive transmission flags from server while attaching to export: %s", tflags)
self:close()
return false
end
local tflags, pos = (">I2"):unpack(tflags)
if pos ~= 3 then
stdnse.debug1("Failed to unpack transmission flags from server.")
self:close()
return false
end
tflags = self:parse_transmission_flags(tflags)
if self.protocol.zero_pad == "required" then
local status, err = self:receive(124)
if not status then
stdnse.debug1("Failed to receive zero pad from server while attaching to export: %s", err)
self:close()
return false
end
end
self.exports[name] = {
size = size,
tflags = tflags,
}
return true
end,
--- Sends data to the server
--
-- @name Comm.send
--
-- @param pkt String containing the bytes to send.
--
-- @return status true on success, false on failure.
-- @return err string containing the error message on failure.
send = function(self, data)
assert(type(data) == "string")
return self.socket:send(data)
end,
--- Receives data from the server.
--
-- @name Comm.receive
--
-- @param len Number of bytes to receive.
--
-- @return status True on success, false on failure.
-- @return response String representing bytes received on success,
-- string containing the error message on failure.
receive = function(self, len)
assert(type(len) == "number")
-- Try to answer this request from the buffer.
if #self.receive_buffer >= len then
local rep = self.receive_buffer:sub(1, len)
self.receive_buffer = self.receive_buffer:sub(len + 1)
return true, rep
end
return self.socket:receive_buf(match.numbytes(len), true)
end,
--- Disconnects from the server.
--
-- @name Comm.close
close = function(self)
assert(self.socket)
self.socket:close()
self.socket = nil
end,
--- Continue in-progress newstyle handshake with server.
--
-- @name Comm.connect_new
--
-- @param len Number of bytes to receive.
--
-- @return status True on success, false on failure.
-- @return response String representing bytes received on success,
-- string containing the error message on failure.
connect_new = function(self)
local status, flags = self:receive(2)
if not status then
stdnse.debug1("Failed to receive handshake flags from server: %s", flags)
self:close()
return false
end
-- Receive and parse the handshake flags from the server, and use
-- them to build the client flags.
local hflags, pos = (">I2"):unpack(flags)
if pos ~= 3 then
stdnse.debug1("Failed to unpack handshake flags from server.")
self:close()
return false
end
local cflags = 0x0000
if hflags & NBD.handshake_flags.FIXED_NEWSTYLE then
cflags = cflags | NBD.client_flags.C_FIXED_NEWSTYLE
self.protocol.negotiation = "fixed newstyle"
end
self.protocol.zero_pad = "required"
if hflags & NBD.handshake_flags.NO_ZEROES then
cflags = cflags | NBD.client_flags.C_NO_ZEROES
self.protocol.zero_pad = "optional"
end
-- Send the client flags to the server.
local req = (">I4"):pack(cflags)
local status, err = self:send(req)
if not status then
stdnse.debug1("Failed to send client flags: %s", err)
self:close()
return false
end
return true
end,
--- Continue in-progress oldstyle handshake with server.
--
-- @name Comm.connect_old
--
-- @return response String representing bytes received on success,
-- string containing the error message on failure.
connect_old = function(self)
local status, size = self:receive(8)
if not status then
stdnse.debug1("Failed to receive size of exported block device from server: %s", size)
self:close()
return false
end
local size, pos = (">I8"):unpack(size)
if pos ~= 9 then
stdnse.debug1("Failed to unpack size of exported block device from server.")
self:close()
return false
end
local status, hflags = self:receive(4)
if not status then
stdnse.debug1("Failed to receive handshake flags from server: %s", hflags)
self:close()
return false
end
local hflags, pos = (">I4"):unpack(hflags)
if pos ~= 5 then
stdnse.debug1("Failed to unpack handshake flags from server.")
self:close()
return false
end
local status, pad = self:receive(124)
if not status then
stdnse.debug1("Failed to receive zero pad from server: %s", pad)
self:close()
return false
end
self.exports["(default)"] = {
size = size,
hflags = hflags,
}
return true
end,
--- Receives an option reply.
--
-- @name Comm.receive_opt_rep
--
-- @return reply Table representing option reply on success, false
-- on failure.
receive_opt_rep = function(self)
-- Receive the static header of the option.
local status, hdr = self:receive(20)
if not status then
stdnse.debug1("Failed to receive option reply header: %s", hdr)
return false
end
local len, pos = (">I4"):unpack(hdr, 17)
if pos ~= 21 then
stdnse.debug1("Failed to parse option reply header during receive.")
return false
end
local magic = hdr:sub(1, 8)
if magic ~= NBD.magic.option then
stdnse.debug1("First 64 bits of option reply don't match expected magic: %s", stdnse.tohex(magic, {separator = ":"}))
return false
end
if len == 0 then
return self:parse_opt_rep(hdr)
end
-- Receive the variable body of the option.
local status, body = self:receive(len)
if not status then
stdnse.debug1("Failed to receive option reply: %s", body)
return false
end
return self:parse_opt_rep(hdr .. body)
end,
--- Builds an option request.
--
-- @name Comm.build_opt_req
--
-- @param name String naming the option type.
-- @param options Table containing options.
--
-- @return req String representing the option request.
build_opt_req = function(self, name, options)
assert(type(name) == "string")
if not options then
options = {}
end
assert(type(options) == "table")
local otype = NBD.opt_req_types[name]
assert(otype)
local payload = ""
if name == "EXPORT_NAME" then
assert(options.export_name)
payload = options.export_name
end
return NBD.magic.cliserv_magic_new .. (">I4s4"):pack(otype, payload)
end,
--- Parses an option reply.
--
-- @name Comm.parse_opt_rep
--
-- @param buf String to be parsed.
-- @param rep Table representing the fields of the reply that have
-- already been parsed by the caller.
--
-- @return reply Table representing option reply on success, false
-- on failure.
parse_opt_rep = function(self, buf)
assert(type(buf) == "string")
if 20 - 1 > #buf then
stdnse.debug1("Buffer is too short to be parsed as an option reply.")
return false
end
local magic, otype, rtype, rlen, pos = (">c8I4I4I4"):unpack(buf)
if magic ~= NBD.magic.option then
stdnse.debug1("First 64 bits of option reply don't match expected magic: %s", stdnse.tohex(magic, {separator = ":"}))
return false
end
local otype_name = find_key(NBD.opt_req_types, otype)
local rtype_name = find_key(NBD.opt_rep_types, rtype)
local rep = {
otype = otype,
otype_name = otype_name,
rtype = rtype,
rtype_name = rtype_name,
}
if pos + rlen - 1 > #buf then
stdnse.debug1("Option reply payload length extends past end of buffer.")
return false
end
if rtype_name == "ACK" then
return rep
end
if rtype_name == "SERVER" then
if rlen < 4 then
stdnse.debug1("SERVER option reply payload length must be 4 or greater, but is %d.", rlen)
return false
end
local nlen, pos = (">I4"):unpack(buf, pos)
if pos + nlen - 1 > #buf then
stdnse.debug1("SERVER option reply payload name length extends past end of buffer.")
return false
end
-- An empty name represents the default export.
local name = ""
if nlen > 0 then
name = buf:sub(pos, pos + nlen - 1)
pos = pos + nlen
end
rep.export_name = name
return rep
end
return rep
end,
--- Parses the transmission flags describing an export.
--
-- @name Comm.parse_transmission_flags
--
-- @param flags Transmission flags sent by server.
--
-- @return Table of parsed flags as keys.
parse_transmission_flags = function(self, flags)
assert(type(flags) == "number")
-- This flag must always be set according to the standard.
if (flags & NBD.transmission_flags.HAS_FLAGS) == 0 then
stdnse.debug1("Transmission flags were not in a valid format, skipping.")
return {}
end
local tbl = {}
for k, v in pairs(NBD.transmission_flags) do
if (flags & v) ~= 0 then
tbl[k] = true
end
end
return tbl
end,
}
--- Finds a key corresponding with a value.
--
-- @name find_key
--
-- @param tbl Table in which to search.
-- @param val Value to search for.
--
-- @return key String on success, nil on failure
find_key = function(tbl, val)
assert(type(tbl) == "table")
assert(val ~= nil)
for k, v in pairs(tbl) do
if v == val then
return k
end
end
return nil
end
return _ENV;

196
scripts/nbd-info.nse Normal file
View File

@@ -0,0 +1,196 @@
local nbd = require "nbd"
local shortport = require "shortport"
local stdnse = require "stdnse"
local table = require "table"
description = [[
Displays protocol and block device information from NBD servers.
The Network Block Device protocol is used to publish block devices
over TCP. This script connects to an NBD server and attempts to pull
down a list of exported block devices and their details
For additional information:
* https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
]]
---
-- @usage nmap -p 10809 --script nbd-info <target>
--
-- @output
-- PORT STATE SERVICE REASON
-- 10809/tcp open nbd syn-ack
-- | nbd-info:
-- | Protocol:
-- | Negotiation: fixed newstyle
-- | SSL/TLS Wrapped: false
-- | Exported Block Devices:
-- | foo:
-- | Size: 1048576 bytes
-- | Transmission Flags:
-- | SEND_FLUSH
-- | READ_ONLY
-- | SEND_FUA
-- | bar:
-- | Size: 1048576 bytes
-- | Transmission Flags:
-- | READ_ONLY
-- |_ ROTATIONAL
--
-- @args nbd-info.export_names Either a single name, or a table of
-- names to about which to request information from the server.
author = "Mak Kolybabi <mak@kolybabi.com>"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "intrusive"}
portrule = shortport.version_port_or_service(10809, "nbd", "tcp")
local enumerate_options = function(comm)
-- Run the LIST command and store the responses.
local req = comm:build_opt_req("LIST")
if not req then
return
end
local status, err = comm:send(req)
if not status then
stdnse.debug1("Failed to send option request: %s", err)
return nil
end
while true do
local rep = comm:receive_opt_rep()
if not rep or rep.rtype_name ~= "SERVER" then
break
end
comm.exports[rep.export_name] = {}
end
end
local newstyle_connection = function(comm, args)
local names = {}
for _, name in ipairs(args.export_name) do
table.insert(names, name)
end
for name, _ in pairs(comm.exports) do
table.insert(names, name)
end
for i, name in ipairs(names) do
if i ~= 1 then
local status = comm:reconnect()
if not status then
return
end
end
comm:attach(name)
end
end
local function parse_args()
local args = {}
local arg = stdnse.get_script_args(SCRIPT_NAME .. ".export-names")
if not arg then
-- An empty string for an export name indicates to the server that
-- we wish to attach to the default export.
arg = {}
elseif type(arg) ~= table then
arg = {arg}
end
args.export_name = arg
return args
end
action = function(host, port)
local args = parse_args()
local comm = nbd.Comm:new(host, port)
local status = comm:connect(args)
if not status then
return nil
end
-- If the service supports an unrecognized negotiation, or the
-- oldstyle negotiation, there's no more information to be had.
if comm.protocol.negotiation == "unrecognized" or comm.protocol.negotiation == "oldstyle" then
-- Nothing to do.
comm:close()
-- If the service supports the (non-fixed) newstyle negotiation,
-- which should be very rare, we can only send a single option. That
-- option is the name of the export to which we'd like to attach.
elseif comm.protocol.negotiation == "newstyle" then
newstyle_connection(comm, args)
-- If the service supports the fixed newstyle negotiation, then we
-- can perform option haggling to wring additional information from
-- it.
elseif comm.protocol.negotiation == "fixed newstyle" then
enumerate_options(comm)
newstyle_connection(comm, args)
-- Otherwise, we've got a mismatch between the library and this script.
else
assert(false, "NBD library supports more negotiation styles than this script.")
end
-- Master output table.
local output = stdnse.output_table()
-- Format protocol information.
local protocol = stdnse.output_table()
if comm.protocol.negotiation == "oldstyle" and comm.exports["(default)"] then
if comm.exports["(default)"].hflags & nbd.NBD.handshake_flags.FIXED_NEWSTYLE then
protocol["Fixed Newstyle Negotiation"] = "Supported by service, but not on this port."
end
end
protocol["Negotiation"] = comm.protocol.negotiation
protocol["SSL/TLS Wrapped"] = comm.protocol.ssl_tls
output["Protocol"] = protocol
-- Format exported block device information.
local exports = stdnse.output_table()
local no_shares = true
local names = stdnse.keys(comm.exports)
-- keep exports in stable order
table.sort(names)
for _, name in ipairs(names) do
local info = comm.exports[name]
local exp = {}
if type(info.size) == "number" then
exp["Size"] = info.size .. " bytes"
end
if type(info.tflags) == "table" then
local keys = {}
for k, _ in pairs(info.tflags) do
if k ~= "HAS_FLAGS" then
table.insert(keys, k)
end
end
-- sort by bitfield flag value
table.sort(keys, function(a, b)
return nbd.NBD.transmission_flags[a] < nbd.NBD.transmission_flags[b]
end)
exp["Transmission Flags"] = keys
end
no_shares = false
exports[name] = exp
end
if not no_shares then
output["Exported Block Devices"] = exports
end
return output
end

View File

@@ -373,6 +373,7 @@ Entry { filename = "mysql-variables.nse", categories = { "discovery", "intrusive
Entry { filename = "mysql-vuln-cve2012-2122.nse", categories = { "discovery", "intrusive", "vuln", } } Entry { filename = "mysql-vuln-cve2012-2122.nse", categories = { "discovery", "intrusive", "vuln", } }
Entry { filename = "nat-pmp-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "nat-pmp-info.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "nat-pmp-mapport.nse", categories = { "discovery", "safe", } } Entry { filename = "nat-pmp-mapport.nse", categories = { "discovery", "safe", } }
Entry { filename = "nbd-info.nse", categories = { "discovery", } }
Entry { filename = "nbstat.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "nbstat.nse", categories = { "default", "discovery", "safe", } }
Entry { filename = "ncp-enum-users.nse", categories = { "auth", "safe", } } Entry { filename = "ncp-enum-users.nse", categories = { "auth", "safe", } }
Entry { filename = "ncp-serverinfo.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "ncp-serverinfo.nse", categories = { "default", "discovery", "safe", } }