diff --git a/CHANGELOG b/CHANGELOG index 138018e44..bfa4ef531 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the scripts xdmcp-discover, broadcast-xdmcp-discover and the + X Display Manager Control Protocol (xdmcp) library. The scripts discover + hosts either using unicast or broadcast and try to detect supported + authentication and authorization mechanisms. [Patrik] + o Audited the nmap-service-probes database to remove all unused captures, fixing dozens of bugs with captures either being ignored or two fields erroneously using the same capture. This was done by diff --git a/nselib/xdmcp.lua b/nselib/xdmcp.lua new file mode 100644 index 000000000..ab7405c95 --- /dev/null +++ b/nselib/xdmcp.lua @@ -0,0 +1,394 @@ +--- +-- Implementation of the XDMCP X-Windows protocol based on: +-- x http://www.xfree86.org/current/xdmcp.pdf +-- +-- @author "Patrik Karlsson " + +module(... or "xdmcp", package.seeall) + +-- Supported operations +OpCode = { + BCAST_QUERY = 1, + QUERY = 2, + WILLING = 5, + REQUEST = 7, + ACCEPT = 8, + MANAGE = 10, +} + +-- Packet class +Packet = { + + -- The cdmcp header + Header = { + + -- Creates a new instance of class + -- @param version number containing the protocol version + -- @param opcode number containing the opcode type + -- @param length number containing the length of the data + -- @return o instance of class + new = function(self, version, opcode, length) + local o = { version = version, opcode = opcode, length = length } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parses data based on which a new object is instantiated + -- @param data opaque string containing data received over the wire + -- @return hdr instance of class + parse = function(data) + local pos, hdr = nil, Packet.Header:new() + pos, hdr.version, hdr.opcode, hdr.length = bin.unpack(">SSS", data) + return hdr + end, + + -- Converts the instance to an opaque string + -- @return str string containing the instance + __tostring = function(self) + assert(self.length, "No header length was supplied") + return bin.pack(">SSS", self.version, self.opcode, self.length) + end, + }, + + [OpCode.QUERY] = { + + -- Creates a new instance of class + -- @param authnames table of strings containing authentication + -- mechanism names. + -- @return o instance of class + new = function(self, authnames) + local o = { + header = Packet.Header:new(1, OpCode.QUERY), + authnames = authnames or {}, + } + o.header.length = #o.authnames + 1 + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts the instance to an opaque string + -- @return str string containing the instance + __tostring = function(self) + local data = tostring(self.header) + data = data .. bin.pack("C", #self.authnames) + for _, name in ipairs(self.authnames) do + data = data .. bin.pack("P", name) + end + return data + end, + + }, + + [OpCode.BCAST_QUERY] = { + new = function(...) + local packet = Packet[OpCode.QUERY]:new(...) + packet.header.opcode = OpCode.BCAST_QUERY + return packet + end, + + __tostring = function(...) + return Packet[OpCode.QUERY]:__tostring(...) + end + + }, + + [OpCode.WILLING] = { + + -- Creates a new instance of class + -- @return o instance of class + new = function(self) + local o = { + header = Packet.Header:new(1, OpCode.WILLING) + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parses data based on which a new object is instantiated + -- @param data opaque string containing data received over the wire + -- @return hdr instance of class + parse = function(data) + local willing = Packet[OpCode.WILLING]:new() + willing.header = Packet.Header.parse(data) + + local pos = 7 + pos, willing.authname, willing.hostname, + willing.status = bin.unpack("ppp", data, pos) + return willing + end, + + }, + + [OpCode.REQUEST] = { + + -- The connection class + Connection = { + + IpType = { + IPv4 = 0, + IPv6 = 6, + }, + + -- Creates a new instance of class + -- @param iptype number + -- @param ip opaque string containing the ip + -- @return o instance of class + new = function(self, iptype, ip) + local o = { + iptype = iptype, + ip = ip, + } + setmetatable(o, self) + self.__index = self + return o + end, + + }, + + -- Creates a new instance of class + -- @param disp_no number containing the display name + -- @param auth_name string containing the authentication name + -- @param auth_data string containing additional authentication data + -- @param authr_names string containing authorization mechanisms + -- @param manf_id string containing the manufacturer id + -- @return o instance of class + new = function(self, disp_no, conns, auth_name, auth_data, authr_names, manf_id ) + local o = { + header = Packet.Header:new(1, OpCode.REQUEST), + disp_no = disp_no or 1, + conns = conns or {}, + auth_name = auth_name or "", + auth_data = auth_data or "", + authr_names = authr_names or {}, + manf_id = manf_id or "", + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Adds a new connection entry + -- @param conn instance of Connections + addConnection = function(self, conn) + table.insert(self.conns, conn) + end, + + -- Adds a new authorization entry + -- @param str string containing the name of the authorization mechanism + addAuthrName = function(self, str) + table.insert(self.authr_names, str) + end, + + -- Converts the instance to an opaque string + -- @return str string containing the instance + __tostring = function(self) + local data = bin.pack(">SC", self.disp_no, #self.conns) + for _, conn in ipairs(self.conns) do + data = data .. bin.pack(">S", conn.iptype) + end + data = data .. bin.pack("C", #self.conns) + for _, conn in ipairs(self.conns) do + data = data .. bin.pack(">P", ipOps.ip_to_str(conn.ip)) + end + data = data .. bin.pack(">PP", self.auth_name, self.auth_data) + data = data .. bin.pack("C", #self.authr_names) + for _, authr in ipairs(self.authr_names) do + data = data .. bin.pack(">P", authr) + end + data = data .. bin.pack(">P", self.manf_id) + self.header.length = #data + + return tostring(self.header) .. data + end, + + }, + + [OpCode.ACCEPT] = { + + -- Creates a new instance of class + -- @param session_id number containing the session id + -- @param auth_name string containing the authentication name + -- @param auth_data string containing additional authentication data + -- @param authr_name string containing the authorization mechanism name + -- @param authr_names string containing authorization mechanisms + -- @return o instance of class + new = function(self, session_id, auth_name, auth_data, authr_name, authr_data) + local o = { + header = Packet.Header:new(1, OpCode.ACCEPT), + session_id = session_id, + auth_name = auth_name, + auth_data = auth_data, + authr_name = authr_name, + authr_data = authr_data, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Parses data based on which a new object is instantiated + -- @param data opaque string containing data received over the wire + -- @return hdr instance of class + parse = function(data) + local accept = Packet[OpCode.ACCEPT]:new() + accept.header = Packet.Header.parse(data) + local pos = 7 + pos, accept.session_id, accept.auth_name, accept.auth_data, + accept.authr_name, accept.authr_data = bin.unpack(">IPPPP", data, pos) + return accept + end, + + }, + + [OpCode.MANAGE] = { + + -- Creates a new instance of class + -- @param session_id number containing the session id + -- @param disp_no number containing the display number + -- @param disp_class string containing the display class + -- @return o instance of class + new = function(self, sess_id, disp_no, disp_class) + local o = { + header = Packet.Header:new(1, OpCode.MANAGE), + session_id = sess_id, + disp_no = disp_no, + disp_class = disp_class or "" + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts the instance to an opaque string + -- @return str string containing the instance + __tostring = function(self) + local data = bin.pack(">ISP", self.session_id, self.disp_no, self.disp_class) + self.header.length = #data + return tostring(self.header) .. data + end, + + } + +} + +-- The Helper class serves as the main script interface +Helper = { + + -- Creates a new instance of Helper + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @param options table + -- @retun o new instance of Helper + new = function(self, host, port, options) + local o = { + host = host, + port = port, + options = options or {}, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- "Connects" to the server (ie. creates the socket) + -- @return status, true on success, false on failure + connect = function(self) + self.socket = nmap.new_socket("udp") + self.socket:set_timeout(self.options.timeout or 10000) + return true + end, + + -- Creates a xdmcp session + -- @param auth_name string containing the authentication name + -- @param authr_name string containing the authorization mechanism name + -- @param disp_class string containing the display class + -- @return status true on success, false on failure + -- @return response table or err string containing an error message + createSession = function(self, auth_names, authr_names, disp_no) + local info = nmap.get_interface_info(self.host.interface) + if ( not(info) ) then + return false, ("Failed to get information for interface %s"):format(host.interface) + end + + local req = xdmcp.Packet[xdmcp.OpCode.QUERY]:new(auth_names) + local status, response = self:exch(req) + if ( not(status) ) then + return false, response + elseif ( response.header.opcode ~= xdmcp.OpCode.WILLING ) then + return false, "Received unexpected response" + end + + local REQ = xdmcp.Packet[xdmcp.OpCode.REQUEST] + local iptype = REQ.Connection.IpType.IPv4 + if ( nmap.address_family() == 'inet6' ) then + iptype = REQ.Connection.IpType.IPv6 + end + + local conns = { REQ.Connection:new(iptype, info.address) } + local req = REQ:new(disp_no, conns, nil, nil, authr_names) + local status, response = self:exch(req) + if ( not(status) ) then + return false, response + elseif ( response.header.opcode ~= xdmcp.OpCode.ACCEPT ) then + return false, "Received unexpected response" + end + + -- Sending this last manage packet doesn't make any sense as we can't + -- set up a listening TCP server anyway. When we can, we could enable + -- this and wait for the incoming request and retrieve X protocol info. + + -- local manage = xdmcp.Packet[xdmcp.OpCode.MANAGE]:new(response.session_id, + -- disp_no, "MIT-unspecified") + -- local status, response = self:exch(manage) + -- if ( not(status) ) then + -- return false, response + -- end + + return true, { + session_id = response.session_id, + auth_name = response.auth_name, + auth_data = response.auth_data, + authr_name = response.authr_name, + authr_data = response.authr_data, + } + end, + + send = function(self, req) + return self.socket:sendto(self.host, self.port, tostring(req)) + end, + + recv = function(self) + local status, data = self.socket:receive() + if ( not(status) ) then + return false, data + end + local header = Packet.Header.parse(data) + if ( not(header) ) then + return false, "Failed to parse xdmcp header" + end + if ( not(Packet[header.opcode]) ) then + return false, ("No parser for opcode: %d"):format(header.opcode) + end + local resp = Packet[header.opcode].parse(data) + if ( not(resp) ) then + return false, "Failed to parse response" + end + return true, resp + end, + + -- Sends a request to the server, receives and parses a response + -- @param req instance of Packet + -- @return status true on success, false on failure + -- @return response instance of response packet + exch = function(self, req) + local status, err = self:send(req) + if ( not(status) ) then + return false, "Failed to send xdmcp request" + end + return self:recv() + end, + +} \ No newline at end of file diff --git a/scripts/broadcast-xdmcp-discover.nse b/scripts/broadcast-xdmcp-discover.nse new file mode 100644 index 000000000..527982076 --- /dev/null +++ b/scripts/broadcast-xdmcp-discover.nse @@ -0,0 +1,69 @@ +description = [[ +Discovers servers running the X Display Manager Control Protocol (XDMCP) by +sending a XDMCP broadcast request to the LAN. Display managers allowing access +are marked using the keyword Willing in the result. +]] + +--- +-- @usage +-- nmap --script broadcast-xdmcp-discover +-- +-- @output +-- Pre-scan script results: +-- | broadcast-xdmcp-discover: +-- |_ 192.168.2.162 - Willing +-- +-- @arg broadcast-xdmcp-discover.timeout socket timeout in seconds (default: 5) + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"broadcast", "safe"} + +require 'xdmcp' + +prerule = function() return true end + +local arg_timeout = stdnse.get_script_args(SCRIPT_NAME .. ".timeout") + +action = function() + + local host, port = { ip = "255.255.255.255" }, { number = 177, protocol = "udp" } + local options = { timeout = 1 } + local helper = xdmcp.Helper:new(host, port, options) + local status = helper:connect() + + local req = xdmcp.Packet[xdmcp.OpCode.BCAST_QUERY]:new(nil) + local status, err = helper:send(req) + if ( not(status) ) then + return false, response + end + + local timeout = arg_timeout or 5 + local start = os.time() + local result = {} + repeat + + local status, response = helper:recv() + if ( not(status) and response ~= "TIMEOUT" ) then + break + elseif ( status ) then + local status, _, _, rhost = helper.socket:get_info() + if ( response.header.opcode == xdmcp.OpCode.WILLING ) then + result[rhost] = true + else + result[rhost] = false + end + end + + until( os.time() - start > timeout ) + + local output = {} + for ip, res in pairs(result) do + if ( res ) then + table.insert(output, ("%s - Willing"):format(ip)) + else + table.insert(output, ("%s - Unwilling"):format(ip)) + end + end + return stdnse.format_output(true, output) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index a47b4e476..30954ad9b 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -36,6 +36,7 @@ Entry { filename = "broadcast-upnp-info.nse", categories = { "broadcast", "safe" Entry { filename = "broadcast-wake-on-lan.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-wpad-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-wsdd-discover.nse", categories = { "broadcast", "safe", } } +Entry { filename = "broadcast-xdmcp-discover.nse", categories = { "broadcast", "safe", } } Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } } Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } } Entry { filename = "citrix-enum-apps.nse", categories = { "discovery", "safe", } } @@ -313,5 +314,6 @@ Entry { filename = "wdb-version.nse", categories = { "default", "discovery", "ve Entry { filename = "whois.nse", categories = { "discovery", "external", "safe", } } Entry { filename = "wsdd-discover.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "x11-access.nse", categories = { "auth", "default", "safe", } } +Entry { filename = "xdmcp-discover.nse", categories = { "discovery", "safe", } } Entry { filename = "xmpp-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "xmpp-info.nse", categories = { "default", "discovery", "safe", "version", } } diff --git a/scripts/xdmcp-discover.nse b/scripts/xdmcp-discover.nse new file mode 100644 index 000000000..c4862dc19 --- /dev/null +++ b/scripts/xdmcp-discover.nse @@ -0,0 +1,64 @@ +description = [[ +Requests a XDMCP session and lists supported authentication and authorization mechanisms +]] + +--- +-- @usage +-- nmap -sU -p 177 --script xdmcp-discover +-- +-- @output +-- PORT STATE SERVICE +-- 177/udp open|filtered xdmcp +-- | xdmcp-discover: +-- | Session id: 0x0000703E +-- | Authorization name: MIT-MAGIC-COOKIE-1 +-- |_ Authorization data: c282137c9bf8e2af88879e6eaa922326 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"safe", "discovery"} + +require 'ipOps' +require 'shortport' +require 'xdmcp' + +portrule = shortport.port_or_service(177, "xdmcp", "udp") + +local mutex = nmap.mutex("xdmcp-discover") +local function fail(err) return ("\n ERROR: %s"):format(err or "") end + + +action = function(host, port) + + local DISPLAY_ID = 1 + local result = {} + + local helper = xdmcp.Helper:new(host, port) + local status = helper:connect() + if ( not(status) ) then + return fail("Failed to connect to server") + end + + local status, response = helper:createSession(nil, + {"MIT-MAGIC-COOKIE-1", "XDM-AUTHORIZATION-1"}, DISPLAY_ID) + + if ( not(status) ) then + return fail("Failed to create xdmcp session") + end + + table.insert(result, ("Session id: 0x%.8X"):format(response.session_id)) + if ( response.auth_name and 0 < #response.auth_name ) then + table.insert(result, ("Authentication name: %s"):format(response.auth_name)) + end + if ( response.auth_data and 0 < #response.auth_data ) then + table.insert(result, ("Authentication data: %s"):format(stdnse.tohex(response.auth_data))) + end + if ( response.authr_name and 0 < #response.authr_name ) then + table.insert(result, ("Authorization name: %s"):format(response.authr_name)) + end + if ( response.authr_data and 0 < #response.authr_data ) then + table.insert(result, ("Authorization data: %s"):format(stdnse.tohex(response.authr_data))) + end + return stdnse.format_output(true, result) +end \ No newline at end of file