From fce517d4b8fec1cf8c8952233bdcbe72f0ba925a Mon Sep 17 00:00:00 2001 From: patrik Date: Sun, 5 Aug 2012 18:55:40 +0000 Subject: [PATCH] add bjnp library and the scripts bjnp-discover and broadcast-bjnp-discover commit d41a28813e4e4d26aeaab300ad30ad7c4116e37d Merge: a45e4e2 23fc8f1 Author: Patrik Karlsson Date: Sun Aug 5 20:53:04 2012 +0200 Merge branch 'master' into bjnp Conflicts: CHANGELOG commit a45e4e2fd0c2579afc8d5b162bb5484327494b72 Author: Patrik Karlsson Date: Sun Aug 5 20:44:19 2012 +0200 add bjnp library and the scripts bjnp-discover and broadcast-bjnp-discover --- CHANGELOG | 3 + nselib/bjnp.lua | 363 ++++++++++++++++++++++++++++ scripts/bjnp-discover.nse | 49 ++++ scripts/broadcast-bjnp-discover.nse | 172 +++++++++++++ scripts/script.db | 2 + 5 files changed, 589 insertions(+) create mode 100644 nselib/bjnp.lua create mode 100644 scripts/bjnp-discover.nse create mode 100644 scripts/broadcast-bjnp-discover.nse diff --git a/CHANGELOG b/CHANGELOG index e805e9f49..f8dd0de65 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a BJNP library and the scripts broadcast-bjnp-discover and + bjnp-discover. [Patrik Karlsson] + o [NPING] Nping now prints out an error and exists when the user tries to use the -p flag for a scan option where that is meaningless. [Sean Rivera] diff --git a/nselib/bjnp.lua b/nselib/bjnp.lua new file mode 100644 index 000000000..517554e44 --- /dev/null +++ b/nselib/bjnp.lua @@ -0,0 +1,363 @@ +--- +-- An implementation of the Canon BJNP protocol used to discover and query +-- Canon network printers and scanner devices. +-- +-- The implementation is pretty much based on Wireshark decoded messages +-- the cups-bjnp implementation and the usual guesswork. +-- +-- @author "Patrik Karlsson " +-- + +local bin = require("bin") +local bit = require("bin") +local nmap = require("nmap") +local os = require("os") +local stdnse = require("stdnse") +local string = require("string") +local table = require("table") + +_ENV = stdnse.module("bjnp", stdnse.seeall) + +BJNP = { + + -- The common BJNP header + Header = { + + new = function(self, o) + o = o or {} + o = { + id = o.id or "BJNP", + type = o.type or 1, + code = o.code, + seq = o.seq or 1, + session = o.session or 0, + length = o.length or 0, + } + assert(o.code, "code argument required") + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local hdr = BJNP.Header:new({ code = -1 }) + local pos + + pos, hdr.id, hdr.type, hdr.code, + hdr.seq, hdr.session, hdr.length = bin.unpack(">A4CCISI", data) + return hdr + end, + + __tostring = function(self) + return bin.pack(">ACCISI", + self.id, + self.type, + self.code, + self.seq, + self.session, + self.length + ) + end + }, + + -- Scanner related code + Scanner = { + + Code = { + DISCOVER = 1, + IDENTITY = 48, + }, + + Request = { + + Discover = { + + new = function(self) + local o = { header = BJNP.Header:new( { type = 2, code = BJNP.Scanner.Code.DISCOVER }) } + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + return tostring(self.header) + end, + }, + + + Identity = { + + new = function(self) + local o = { header = BJNP.Header:new( { type = 2, code = BJNP.Scanner.Code.IDENTITY, length = 4 }), data = 0 } + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + return tostring(self.header) .. bin.pack(">I", self.data) + end, + } + + }, + + Response = { + + Identity = { + + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local identity = BJNP.Scanner.Response.Identity:new() + identity.header = BJNP.Header.parse(data) + + local pos = #tostring(identity.header) + 1 + local pos, len = bin.unpack(">S", data, pos) + if ( len ) then + pos, identity.data = bin.unpack("A" .. len - 2, data, pos) + return identity + end + end, + + + } + + } + + }, + + -- Printer related code + Printer = { + + Code = { + DISCOVER = 1, + IDENTITY = 48, + }, + + Request = { + + Discover = { + new = function(self) + local o = { header = BJNP.Header:new( { code = BJNP.Printer.Code.DISCOVER }) } + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + return tostring(self.header) + end, + }, + + Identity = { + + new = function(self) + local o = { header = BJNP.Header:new( { code = BJNP.Printer.Code.IDENTITY }) } + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + return tostring(self.header) + end, + } + + }, + + Response = { + + Identity = { + + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local identity = BJNP.Printer.Response.Identity:new() + identity.header = BJNP.Header.parse(data) + + local pos = #tostring(identity.header) + 1 + local pos, len = bin.unpack(">S", data, pos) + if ( len ) then + pos, identity.data = bin.unpack("A" .. len - 2, data, pos) + return identity + end + end, + + + } + + }, + + } + +} + +-- Helper class, the main script writer interface +Helper = { + + -- Creates a new Helper instance + -- @param host table + -- @param port table + -- @param options table containing one or more of the following fields; + -- timeout - the timeout in milliseconds for socket communication + -- bcast - instructs the library that the host is a broadcast + -- address + -- @return o new instance of Helper + new = function(self, host, port, options) + local o = { + host = host, port = port, options = options or {} + } + o.options.timeout = o.options.timeout or 5000 + setmetatable(o, self) + self.__index = self + return o + end, + + -- Connects the socket to the device + -- This should always be called, regardless if the broadcast option is set + -- or not. + -- + -- @return status, true on success, false on failure + -- @return err string containing the error message if status is false + connect = function(self) + self.socket = nmap.new_socket(( self.options.bcast and "udp" )) + self.socket:set_timeout(self.options.timeout) + if ( not(self.options.bcast) ) then + return self.socket:connect(self.host, self.port) + end + return true + end, + + -- Discover network devices using either broadcast or unicast + -- @param packet discovery packet (printer or scanner) + -- @return status, true on success, false on failure + -- @return devices table containing discovered devices when status is true + -- errmsg string containing the error message when status is false + discoverDevice = function(self, packet) + if ( not(self.options.bcast) ) then + if ( not(self.socket:send(tostring(packet))) ) then + return false, "Failed to send request to server" + end + else + if ( not(self.socket:sendto(self.host, self.port, tostring(packet))) ) then + return false, "Failed to send request to server" + end + end + -- discover run in loop + local devices, tmp = {}, {} + local start = os.time() + while( true ) do + local status, data = self.socket:receive() + if ( not(status) or ( os.time() - start > ( self.options.timeout/1000 - 1 ) )) then + break + end + local status, _, _, rhost = self.socket:get_info() + tmp[rhost] = true + end + for host in pairs(tmp) do table.insert(devices, host) end + return true, ( self.options.bcast and devices or ( #devices > 0 and devices[1] )) + end, + + -- Discover BJNP supporting scanners + discoverScanner = function(self) + return self:discoverDevice(BJNP.Scanner.Request.Discover:new()) + end, + + -- Discover BJNP supporting printers + discoverPrinter = function(self) + return self:discoverDevice(BJNP.Printer.Request.Discover:new()) + end, + + -- Gets a printer identity (additional information) + -- @param devtype string containing either the string printer or scanner + -- @return status, true on success, false on failure + -- @return attribs table containing device attributes when status is true + -- errmsg string containing the error message when status is false + getDeviceIdentity = function(self, devtype) + -- Were currenlty only decoding this as I don't know what the other cruft is + local attrib_names = { + ["scanner"] = { + { ['MFG'] = "Manufacturer" }, + { ['MDL'] = "Model" }, + { ['DES'] = "Description" }, + { ['CMD'] = "Command" }, + }, + ["printer"] = { + { ['MFG'] = "Manufacturer" }, + { ['MDL'] = "Model" }, + { ['DES'] = "Description" }, + { ['VER'] = "Firmware version" }, + { ['CMD'] = "Command" }, + } + } + local identity + if ( "printer" == devtype ) then + identity = BJNP.Printer.Request.Identity:new() + elseif ( "scanner" == devtype ) then + identity = BJNP.Scanner.Request.Identity:new() + end + assert(not(self.options.bcast), "getIdentity is not supported for broadcast") + if ( not(self.socket:send(tostring(identity))) ) then + return false, "Failed to send request to server" + end + local status, data = self.socket:receive() + if ( not(status) ) then + return false, "Failed to receive response from server" + end + + local identity + if ( "printer" == devtype ) then + identity = BJNP.Printer.Response.Identity.parse(data) + elseif ( "scanner" == devtype ) then + identity = BJNP.Scanner.Response.Identity.parse(data) + end + if ( not(identity) ) then + return false, "Failed to parse identity" + end + local attrs, kvps = {}, {} + + for k, v in ipairs(stdnse.strsplit(";", identity.data)) do + local nm, val = v:match("^([^:]*):(.*)$") + if ( nm ) then kvps[nm] = val end + end + + for _, attrib in ipairs(attrib_names[devtype]) do + local short, long = next(attrib) + if ( kvps[short] ) then + table.insert(attrs, ("%s: %s"):format(long, kvps[short])) + end + end + + return true, attrs + end, + + -- Retrieves information related to the printer + getPrinterIdentity = function(self) + return self:getDeviceIdentity("printer") + end, + + -- Retrieves information related to the scanner + getScannerIdentity = function(self) + return self:getDeviceIdentity("scanner") + end, + + -- Closes the connection + -- @return status, true on success, false on failure + -- @return errmsg string containing the error message when status is false + close = function(self) + return self.socket:close() + end + +} + +return _ENV; diff --git a/scripts/bjnp-discover.nse b/scripts/bjnp-discover.nse new file mode 100644 index 000000000..0184428de --- /dev/null +++ b/scripts/bjnp-discover.nse @@ -0,0 +1,49 @@ +description = [[ +Retrievs printer or scanner information from a remote device supporting the +BJNP protocol. The protocol is known to be supported by network based Canon +devices. +]] + +--- +-- @usage +-- sudo nmap -sU -p 8611,8612 --script bjnp-discover +-- +-- @output +-- PORT STATE SERVICE +-- 8611/udp open canon-bjnp1 +-- | bjnp-discover: +-- | Manufacturer: Canon +-- | Model: MG5200 series +-- | Description: Canon MG5200 series +-- | Firmware version: 1.050 +-- |_ Command: BJL,BJRaster3,BSCCe,NCCe,IVEC,IVECPLI +-- 8612/udp open canon-bjnp2 +-- | bjnp-discover: +-- | Manufacturer: Canon +-- | Model: MG5200 series +-- | Description: Canon MG5200 series +-- |_ Command: MultiPass 2.1,IVEC +-- + +categories = {"safe", "discovery"} + +local bjnp = require("bjnp") +local shortport = require("shortport") +local stdnse = require("stdnse") + +portrule = shortport.portnumber({8611, 8612}, "udp") + +action = function(host, port) + local helper = bjnp.Helper:new(host, port) + if ( not(helper:connect()) ) then + return "\n ERROR: Failed to connect to server" + end + local status, attrs + if ( port.number == 8611 ) then + status, attrs = helper:getPrinterIdentity() + else + status, attrs = helper:getScannerIdentity() + end + helper:close() + return stdnse.format_output(true, attrs) +end \ No newline at end of file diff --git a/scripts/broadcast-bjnp-discover.nse b/scripts/broadcast-bjnp-discover.nse new file mode 100644 index 000000000..9ad099fb9 --- /dev/null +++ b/scripts/broadcast-bjnp-discover.nse @@ -0,0 +1,172 @@ +description = [[ +Attempts to discover Canon devices (Printers/Scanners) supporting the BJNP +protocol. Discovery is performed by sending BJNP Discover requests to the +network broadcast address for both ports associated with the protocol. + +The script then attempts to retrieve the model, version and some additional +information for all discovered devices. +]] + +--- +-- @usage +-- nmap --script broadcast-bjnp-discover +-- +-- @output +-- | broadcast-bjnp-discover: +-- | 192.168.0.10 +-- | Printer +-- | Manufacturer: Canon +-- | Model: MG5200 series +-- | Description: Canon MG5200 series +-- | Firmware version: 1.050 +-- | Command: BJL,BJRaster3,BSCCe,NCCe,IVEC,IVECPLI +-- | Scanner +-- | Manufacturer: Canon +-- | Model: MG5200 series +-- | Description: Canon MG5200 series +-- |_ Command: MultiPass 2.1,IVEC +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"safe", "broadcast"} + +local bjnp = require("bjnp") +local stdnse = require("stdnse") +local coroutine = require("coroutine") +local nmap = require("nmap") +local table = require("table") + +local printer_port = { number = 8611, protocol = "udp"} +local scanner_port = { number = 8612, protocol = "udp"} +local arg_timeout = tonumber(stdnse.get_script_args(SCRIPT_NAME .. ".timeout")) + +prerule = function() + if ( nmap.address_family() ~= 'inet' ) then + stdnse.print_debug("%s is IPv4 compatible only.", SCRIPT_NAME) + return false + end + return true +end + +local function identifyDevices(devices, devtype) + local result + local port = ( "printers" == devtype and printer_port or scanner_port ) + for _, ip in ipairs(devices or {}) do + local helper = bjnp.Helper:new({ ip = ip }, port) + if ( helper:connect() ) then + local status, attrs + if ( "printers" == devtype ) then + status, attrs = helper:getPrinterIdentity() + end + if ( "scanners" == devtype ) then + status, attrs = helper:getScannerIdentity() + end + if ( status ) then + result = result or {} + result[ip] = attrs + end + end + helper:close() + end + return result +end + +local function identifyScanners(scanners) + return identifyDevices(scanners, "scanners") +end + +local function identifyPrinters(printers) + return identifyDevices(printers, "printers") +end + +local function getKeys(devices) + local dupes = {} + local function iter() + for k, _ in pairs(devices) do + for k2, _ in pairs(devices[k]) do + if ( not(dupes[k2]) ) then + dupes[k2] = true + coroutine.yield(k2) + end + end + end + coroutine.yield(nil) + end + return coroutine.wrap(iter) +end + +local function getPrinters(devices) + local condvar = nmap.condvar(devices) + local helper = bjnp.Helper:new( { ip = "255.255.255.255" }, printer_port, { bcast = true, timeout = arg_timeout } ) + if ( not(helper:connect()) ) then + condvar "signal" + return + end + local status, printers = helper:discoverPrinter() + helper:close() + if ( status ) then + devices["printers"] = identifyPrinters(printers) + end + condvar "signal" +end + +local function getScanners(devices) + local condvar = nmap.condvar(devices) + local helper = bjnp.Helper:new( { ip = "255.255.255.255" }, scanner_port, { bcast = true, timeout = arg_timeout } ) + if ( not(helper:connect()) ) then + condvar "signal" + return + end + local status, scanners = helper:discoverScanner() + helper:close() + if ( status ) then + devices["scanners"] = identifyScanners(scanners) + end + condvar "signal" +end + + +action = function() + arg_timeout = ( arg_timeout and arg_timeout * 1000 or 5000) + local devices, result, threads = {}, {}, {} + local condvar = nmap.condvar(devices) + + local co = stdnse.new_thread(getPrinters, devices) + threads[co] = true + + co = stdnse.new_thread(getScanners, devices) + threads[co] = true + + while(next(threads)) do + for t in pairs(threads) do + threads[t] = ( coroutine.status(t) ~= "dead" ) and true or nil + end + if ( next(threads) ) then + condvar "wait" + end + end + + for ip in getKeys(devices) do + local result_part = {} + local printer = ( devices["printers"] and devices["printers"][ip] ) + local scanner = ( devices["scanners"] and devices["scanners"][ip] ) + + if ( printer ) then + printer.name = "Printer" + table.insert(result_part, printer) + end + if ( scanner ) then + scanner.name = "Scanner" + table.insert(result_part, scanner) + end + if ( #result_part > 0 ) then + result_part.name = ip + table.insert(result, result_part) + end + end + + if ( result ) then + return stdnse.format_output(true, result) + end +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 8ad1af29c..f0196c285 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -21,8 +21,10 @@ Entry { filename = "bitcoin-getaddr.nse", categories = { "discovery", "safe", } Entry { filename = "bitcoin-info.nse", categories = { "discovery", "safe", } } Entry { filename = "bitcoinrpc-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "bittorrent-discovery.nse", categories = { "discovery", "safe", } } +Entry { filename = "bjnp-discover.nse", categories = { "discovery", "safe", } } Entry { filename = "broadcast-ataoe-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-avahi-dos.nse", categories = { "broadcast", "dos", "intrusive", "vuln", } } +Entry { filename = "broadcast-bjnp-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-db2-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dhcp-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dhcp6-discover.nse", categories = { "broadcast", "safe", } }