diff --git a/nselib/amqp.lua b/nselib/amqp.lua new file mode 100644 index 000000000..745e41f83 --- /dev/null +++ b/nselib/amqp.lua @@ -0,0 +1,398 @@ +--- +-- The AMQP library provides some basic functionality for retrieving information +-- about an AMQP server's properties. +-- +-- Summary +-- ------- +-- The library currently supports the AMQP 0-9 and 0-8 protocol specifications. +-- +-- Overview +-- -------- +-- The library contains the following classes: +-- +-- o AMQP +-- - This class contains the core functions needed to communicate with AMQP +-- +-- o AMQPSocket +-- - This is a copy of the VNCSocket class. + +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @author "Sebastian Dragomir " + +-- Version 0.1 + +-- Created 05/04/2011 - v0.1 - created by Sebastian Dragomir + +module(... or "amqp", package.seeall) + +require "bin" +require "stdnse" + +AMQP = { + + -- protocol versions sent by the server + versions = { + [0x0800] = "0-8", + [0x0009] = "0-9" + }, + + -- version strings the client supports + client_version_strings = { + ["0-8"] = string.char(0x01) .. string.char(0x01) .. string.char(0x08) .. string.char(0x00), + ["0-9"] = string.char(0x00) .. string.char(0x00) .. string.char(0x09) .. string.char(0x00), + ["0-9-1"] = string.char(0x00) .. string.char(0x00) .. string.char(0x09) .. string.char(0x01) + }, + + new = function(self, host, port) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + o.amqpsocket = AMQPSocket:new() + o.cli_version = self.client_version_strings[nmap.registry.args['amqp.version']] or self.client_version_strings["0-9-1"] + o.protover = nil + o.server_version = nil + o.server_product = nil + o.serer_properties = nil + return o + end, + + --- Connects the AMQP socket + connect = function(self) + local data, status, msg + + status, msg = self.amqpsocket:connect(self.host, self.port, "tcp") + return status, msg + end, + + --- Disconnects the AMQP socket + disconnect = function(self) + self.amqpsocket:close() + end, + + --- Decodes a table value in the server properties field. + -- + -- @param tbl the decoded table + -- @param tsize number, the table size in bytes + -- @return status, true on success, false on failure + -- @return error string containing error message if status is false + -- @return decoded value + decodeTable = function(self, tbl, tsize) + local status, err, tmp, read, value + read = 0 + + while read < tsize do + local key, value + + status, tmp = self.amqpsocket:recv( 1 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading key length", nil + end + read = read + 1 + + tmp = select( 2, bin.unpack("C", tmp) ) + status, key = self.amqpsocket:recv( tmp ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading key", nil + end + read = read + tmp + + status, tmp = self.amqpsocket:recv( 1 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value type for " .. key, nil + end + read = read + 1 + + if ( tmp == 'F' ) then -- table type + status, tmp = self.amqpsocket:recv( 4 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading table size", nil + end + + read = read + 4 + value = {} + tmp = select( 2, bin.unpack(">I", tmp) ) + status, err, value = self:decodeTable(value, tmp) + read = read + tmp + table.insert(tbl, key .. ": ") + table.insert(tbl, value) + elseif ( tmp == 'S' ) then -- string type + status, err, value, read = self:decodeString(key, read) + if ( key == "product" ) then + self.server_product = value + elseif ( key == "version" ) then + self.server_version = value + end + table.insert(tbl, key .. ": " .. value) + elseif ( tmp == 't' ) then -- boolean type + status, err, value, read = self:decodeBoolean(key, read) + table.insert(tbl, key .. ": " .. value) + end + + if ( not(status) ) then + return status, err, nil + end + + end + + return true, nil, tbl + end, + + --- Decodes a string value in the server properties field. + -- + -- @param key string, the key being read + -- @param read number, number of bytes already read + -- @return status, true on success, false on failure + -- @return error string containing error message if status is false + -- @return decoded value + -- @return number of bytes read after decoding this value + decodeString = function(self, key, read) + local value, status, tmp + status, tmp = self.amqpsocket:recv( 4 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value size for " .. key, nil, 0 + end + + read = read + 4 + tmp = select( 2, bin.unpack(">I", tmp) ) + status, value = self.amqpsocket:recv( tmp ) + + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value for " .. key, nil, 0 + end + read = read + tmp + + return true, nil, value, read + end, + + --- Decodes a boolean value in the server properties field. + -- + -- @param key string, the key being read + -- @param read number, number of bytes already read + -- @return status, true on success, false on failure + -- @return error string containing error message if status is false + -- @return decoded value + -- @return number of bytes read after decoding this value + decodeBoolean = function(self, key, read) + local status, value + status, value = self.amqpsocket:recv( 1 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value for " .. key, nil, 0 + end + + value = select( 2, bin.unpack("C", value) ) + read = read + 1 + + return true, nil, value == 0x01 and "YES" or "NO", read + end, + + --- Performs the AMQP handshake and determines + -- o The AMQP protocol version + -- o The server properties/capabilities + -- + -- @return status, true on success, false on failure + -- @return error string containing error message if status is false + handshake = function(self) + local _, status, err, version, tmp, value, properties + + status = self.amqpsocket:send( "AMQP" .. self.cli_version ) + if ( not(status) ) then + return false, "ERROR: AMQP:handshake failed while sending client version" + end + + status, tmp = self.amqpsocket:recv( 11 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading frame header" + end + + -- check if the server rejected our proposed version + if ( #tmp ~= 11 ) then + if ( #tmp == 8 and select( 2, bin.unpack(">I", tmp) ) == 0x414D5150 ) then + local vi, vii, v1, v2, v3, v4, found + _, vi = bin.unpack(">I", tmp, 5) + found = false + + -- check if we support the server's version + for _, v in pairs( self.client_version_strings ) do + _, vii = bin.unpack(">I", v) + if ( vii == vi ) then + version = v + found = true + break + end + end + + -- try again with new version string + if ( found and version ~= self.cli_version ) then + self.cli_version = version + self:disconnect() + status, err = self:connect() + + if ( not(status) ) then + return status, err + end + + return self:handshake() + end + + -- version unsupported + _, v1, v2, v3, v4 = bin.unpack(">CCCC", tmp, 5) + return false, ("ERROR: AMQP:handshake unsupported version (%d.%d.%d.%d)"):format( v1, v2, v3, v4 ) + else + return false, ("ERROR: AMQP:handshake server might not be AMQP, received: %s"):format( tmp ) + end + end + + -- parse frame header + local frametype, chnumber, framesize, method + _, frametype, chnumber, framesize, method = bin.unpack(">CSII", tmp) + stdnse.print_debug("frametype: %d, chnumber: %d, framesize: %d, method: %d", frametype, chnumber, framesize, method) + + if (frametype ~= 1) then + return false, ("ERROR: AQMP:handshake expected header (1) frame, but was %d"):format(frametype) + end + + if (method ~= 0x000A000A) then + return false, ("ERROR: AQMP:handshake expected connection.start (0x000A000A) method, but was %x"):format(method) + end + + -- parse protocol version + status, tmp = self.amqpsocket:recv( 2 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading version" + end + version = select( 2, bin.unpack(">S", tmp) ) + self.protover = AMQP.versions[version] + + if ( not(self.protover) ) then + return false, ("ERROR: AMQP:handshake unsupported version (%x)"):format(version) + end + + -- parse server properties + status, tmp = self.amqpsocket:recv( 4 ) + if ( not(status) ) then + return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading server properties size" + end + + local tablesize = select( 2, bin.unpack(">I", tmp) ) + properties = {} + status, err, properties = self:decodeTable(properties, tablesize) + + if ( not(status) ) then + return status, err + end + + status, err, value, tmp = self:decodeString("mechanisms", 0) + if ( not(status) ) then + return status, err + end + table.insert(properties, "mechanisms: " .. value) + + status, err, value, tmp = self:decodeString("locales", 0) + if ( not(status) ) then + return status, err + end + table.insert(properties, "locales: " .. value) + + self.server_properties = properties + + return true + end, + + --- Returns the protocol version reported by the server + -- + -- @return string containing the version number + getProtocolVersion = function( self ) + return self.protover + end, + + --- Returns the product version reported by the server + -- + -- @return string containing the version number + getServerVersion = function( self ) + return self.server_version + end, + + --- Returns the product name reported by the server + -- + -- @return string containing the product name + getServerProduct = function( self ) + return self.server_product + end, + + --- Returns the properties reported by the server + -- + -- @return table containing server properties + getServerProperties = function( self ) + return self.server_properties + end, +} + +AMQPSocket = +{ + retries = 3, + + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + o.Socket = nmap.new_socket() + o.Buffer = nil + return o + end, + + --- Establishes a connection. + -- + -- @param hostid Hostname or IP address. + -- @param port Port number. + -- @param protocol "tcp", "udp", or + -- @return Status (true or false). + -- @return Error code (if status is false). + connect = function( self, hostid, port, protocol ) + return self.Socket:connect( hostid, port, protocol ) + end, + + --- Closes an open connection. + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + close = function( self ) + self.Buffer = nil + return self.Socket:close() + end, + + --- Opposed to the socket:receive_bytes function, that returns + -- at least x bytes, this function returns the amount of bytes requested. + -- + -- @param count of bytes to read + -- @return true on success, false on failure + -- @return data containing bytes read from the socket + -- err containing error message if status is false + recv = function( self, count ) + local status, data + + self.Buffer = self.Buffer or "" + + if ( #self.Buffer < count ) then + status, data = self.Socket:receive_bytes( count - #self.Buffer ) + if ( not(status) ) then + return false, data + end + self.Buffer = self.Buffer .. data + end + + data = self.Buffer:sub( 1, count ) + self.Buffer = self.Buffer:sub( count + 1) + + return true, data + end, + + --- Sends data over the socket + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + send = function( self, data ) + return self.Socket:send( data ) + end, +} diff --git a/scripts/amqp-info.nse b/scripts/amqp-info.nse new file mode 100644 index 000000000..5562eafa6 --- /dev/null +++ b/scripts/amqp-info.nse @@ -0,0 +1,61 @@ +description = [[ +Gathers information from an AMQP server. +It lists all of its server properties. + +See http://www.rabbitmq.com/extensions.html for details on the +server-properties field. +]] + +--- +-- @usage +-- nmap --script amqp-info -p5672 +--- +-- @args amqp.version Can be used to specify the client version to use (currently, 0-8, 0-9 or 0-9-1) +-- +-- @output +-- 5672/tcp open amqp +-- | amqp-info: +-- | capabilities: +-- | publisher_confirms: YES +-- | exchange_exchange_bindings: YES +-- | basic.nack: YES +-- | consumer_cancel_notify: YES +-- | copyright: Copyright (C) 2007-2011 VMware, Inc. +-- | information: Licensed under the MPL. See http://www.rabbitmq.com/ +-- | platform: Erlang/OTP +-- | product: RabbitMQ +-- | version: 2.4.0 +-- | mechanisms: PLAIN AMQPLAIN +-- |_ locales: en_US + +author = "Sebastian Dragomir" + +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" + +categories = {"default", "discovery", "safe"} + +require("stdnse") +require("shortport") +require("amqp") + +portrule = shortport.port_or_service(5672, "amqp", "tcp", "open") + +action = function(host, port) + local cli = amqp.AMQP:new( host.ip, port.number ) + + local status, data = cli:connect() + if not status then return "Unable to open connection: " .. data end + + status, data = cli:handshake() + if not status then return data end + + cli:disconnect() + + port.version.name = "amqp" + port.version.product = cli:getServerProduct() + port.version.extrainfo = cli:getProtocolVersion() + port.version.version = cli:getServerVersion() + nmap.set_port_version(host, port, "hardmatched") + + return stdnse.format_output(status, cli:getServerProperties()) +end diff --git a/scripts/script.db b/scripts/script.db index 133ef7405..75340476f 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -4,6 +4,7 @@ Entry { filename = "afp-ls.nse", categories = { "discovery", "safe", } } Entry { filename = "afp-path-vuln.nse", categories = { "exploit", "intrusive", "vuln", } } Entry { filename = "afp-serverinfo.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "afp-showmount.nse", categories = { "discovery", "safe", } } +Entry { filename = "amqp-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "asn-query.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "auth-owners.nse", categories = { "default", "safe", } } Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } }