diff --git a/CHANGELOG b/CHANGELOG index dc75fe70a..21f0d562d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o Added a new script, nfs-showmount by Patrik Karlsson, that shows nfs + exports like "showmount -e". + o Added a UDP SIPOptions probe corresponding to the TCP one thanks to the research and testing of Patrik Karlsson and Matt Selsky. diff --git a/scripts/nfs-showmount.nse b/scripts/nfs-showmount.nse new file mode 100644 index 000000000..d581bdf74 --- /dev/null +++ b/scripts/nfs-showmount.nse @@ -0,0 +1,339 @@ +description = [[ +Shows NFS exports, like the showmount -e command. +]] + +--- +-- @output +-- PORT STATE SERVICE +-- 111/tcp open rpcbind +-- +-- Host script results: +-- | nfs-showmount: +-- | /home/storage/backup 10.46.200.0/255.255.255.0 10.46.200.66/255.255.255.255 +-- |_ /home 10.46.200.0/255.255.255.0 +-- + +-- Version 0.4 + +-- Created 11/23/2009 - v0.1 - created by Patrik Karlsson +-- Revised 11/24/2009 - v0.2 - added RPC query to find mountd ports +-- Revised 11/24/2009 - v0.3 - added a hostrule instead of portrule +-- Revised 11/26/2009 - v0.4 - reduced packet sizes and documented them + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'comm' +require 'datafiles' + +hostrule = function(host) + + local port_t111 = nmap.get_port_state(host, {number=111, protocol="udp"}) + local port_u111 = nmap.get_port_state(host, {number=111, protocol="tcp"}) + + return ( port_t111 ~= nil and port_t111.state == "open") or + (port_u111 ~= nil and (port_u111.state == "open" or + port_u111.state == "open|filtered")) + +end + +-- +-- Calculates the number of fill bytes needed +-- @param length contains the length of the string +-- @return the amount of pad needed to be divideable by 4 +-- +function calc_fill_bytes(length) + + -- calculate fill bytes + if math.mod( length, 4 ) ~= 0 then + return (4 - math.mod( length, 4)) + else + return 0 + end + +end + +-- +-- extracts the group from the export list entry +-- @param data string should be start with 32-bit lenght field +-- +-- @return pos numeric new position within buffer, grp_val string the group contents +-- +function extract_group(data) + + local pos, grp_val + + -- retrieve the group length + pos, grp_len = bin.unpack( ">i", data ) + data = data:sub(pos) + + -- retrieve the group contents + grp_val = data:sub(0, grp_len) + pos = 4 + calc_fill_bytes(grp_len) + grp_len + 1 + + return pos, grp_val + +end + +-- +-- extracts the directory from the export list entry +-- @param data string should be start with 32-bit lenght field +-- +-- @return pos numeric new position within buffer, dir_name string the name of the directory +-- +function extract_directory(data) + + local pos, dir_len, dir_name + + -- retrieve the length of the directory name + pos, dir_len = bin.unpack(">i", data) + data = data:sub(pos) + + -- retrieve the directory name + dir_name = data:sub(0, dir_len) + pos = 4 + calc_fill_bytes(dir_len) + dir_len + 1 + + return pos, dir_name + +end + +-- +-- processes the response back from the mountd service +-- @param proto string should be either "udp" or "tcp" +-- @param data string contains the response recieved from the service +-- +-- @return string with exports from NFS +-- +function process_response(proto, data) + + local pos, val_follows + local header = {} + local response=" \n" + + -- if we're running over UDP skip first 4 bytes ( theres no 16-bit something + 16-bit length) + if "udp" == proto then + pos, header['xid'], header['type'], header['state'], + header['verifier'], header['accept_state'] = bin.unpack(">iiili", data) + else + pos, _, header['length'], header['xid'], header['type'], header['state'], + header['verifier'], header['accept_state'] = bin.unpack("S>Siiili", data) + end + + data = data:sub(pos) + + -- We should probably be doing a lot more verification here, but let's stick to basics + -- Was the response from the server = Reply(1) and + -- Accept state = RPC Executed succefully (0) + if header['type'] ~= 1 or header['accept_state'] ~= 0 then + return + end + + -- + -- + -- Each export list entry consists of: + -- + -- One or more Directory entries: + -- 32-bit - length + -- length - directory name + -- + -- One or more Group entries: + -- 32-bit - length + -- length - group contents + -- + -- Every group entry is separated by + -- 32-bit - value follows - if set to 1 more groups exist + -- + -- Every directory entry is separated by + -- 32-bit - value follows - if set to 1 more entries exist + -- + -- + -- Note: The length specifies the amount of characters for + -- both dir and group entries + -- + -- However, directories and groups are padded by zeroes so that + -- they are divideable by 4. Hence calc_fill_bytes + -- + + pos, val_follows = bin.unpack(">i", data) + data = data:sub(pos) + + while 1 == val_follows do + + local dir_name, exp_group + local grp_follows, grp_len, grp_val + + groups="" + + pos, dir_name = extract_directory( data ) + data = data:sub(pos) + + -- check if we have a group following + pos, grp_follows = bin.unpack(">i", data ) + data = data:sub(pos) + + while grp_follows == 1 do + + pos, grp_val = extract_group( data ) + groups = groups .. " " .. grp_val + + data = data:sub(pos) + + -- check if there's antoher group following + pos, grp_follows = bin.unpack(">i", data ) + data = data:sub(pos) + + end + + -- concatenate our dir_name and groups to the result + response = response .. dir_name .. "" .. groups .. "\n" + + -- are there any more directory entries? + pos, val_follows = bin.unpack(">i", data) + data = data:sub(pos) + + end + + return response +end + +-- +-- Ruthlessly ripped, and modified, from Sven Klemm's rpcinfo.nse script +-- +function get_rpc_port_for_service(host, svc_progname, svc_version) + + local socket = nmap.new_socket() + socket:set_timeout(1000) + local catch = function() socket:close() end + local try = nmap.new_try(catch) + local rpc_numbers = try(datafiles.parse_rpc()) + + try(socket:connect(host.ip, 111)) + + -- build rpc dump call packet + local transaction_id = math.random(0x7FFFFFFF) + local request = bin.pack('>IIIIIIILL',0x80000028,transaction_id,0,2,100000,2,4,0,0) + try(socket:send(request)) + + local answer = try(socket:receive_bytes(1)) + + local _,offset,header,length,tx_id,msg_type,reply_state,accept_state,value,payload,last_fragment + last_fragment = false; offset = 1; payload = '' + + -- extract payload from answer and try to receive more packets if + -- RPC header with last_fragment set has not been received + -- If we can't get further packets don't stop but process what we + -- got so far. + while not last_fragment do + if offset > #answer then + local status, data = socket:receive_bytes(1) + if not status then break end + answer = answer .. data + end + offset,header = bin.unpack('>I',answer,offset) + last_fragment = bit.band( header, 0x80000000 ) ~= 0 + length = bit.band( header, 0x7FFFFFFF ) + payload = payload .. answer:sub( offset, offset + length - 1 ) + offset = offset + length + end + socket:close() + + offset,tx_id,msg_type,reply_state,_,_,accept_state = bin.unpack( '>IIIIII', payload ) + + -- transaction_id matches, message type reply, reply state accepted and accept state executed successfully + if tx_id == transaction_id and msg_type == 1 and reply_state == 0 and accept_state == 0 then + local dir = { udp = {}, tcp = {}} + local protocols = {[6]='tcp',[17]='udp'} + local prog, version, proto, port + local ports = {} + offset, value = bin.unpack('>I',payload,offset) + while value == 1 and #payload - offset >= 19 do + offset,prog,version,proto,port,value = bin.unpack('>IIIII',payload,offset) + proto = protocols[proto] or tostring( proto ) + + if rpc_numbers[prog] == svc_progname and version == svc_version then + ports[proto] = port + end + + end + + return ports + + end + + return + +end + +action = function(host) + + local data = {} + + -- packet copy/pasted from wireshark, running showmount -e + + data["tcp"] = string.char( + 0x80, 0x00, 0x00, 0x28, -- Fragment Length: 44 bytes (31-bit?) + -- Last Fragment: Yes + + 0x21, 0x00, 0x46, 0x4c, -- XID: 0x2100464c + 0x00, 0x00, 0x00, 0x00, -- Message type: Call(0) + 0x00, 0x00, 0x00, 0x02, -- RPC Version: 2 + 0x00, 0x01, 0x86, 0xa5, -- Program: MOUNT(100005) + 0x00, 0x00, 0x00, 0x01, -- Program Version: 1 + 0x00, 0x00, 0x00, 0x05, -- Procedure: EXPORT(5) + + -- Credentials + 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL (0) + 0x00, 0x00, 0x00, 0x00, -- Length: 0 + + -- Verifier + 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL + 0x00, 0x00, 0x00, 0x00 -- Length: 0 + ) + + data["udp"] = string.char( + 0x21, 0x00, 0x46, 0x4c, -- XID: 0x2100464c + 0x00, 0x00, 0x00, 0x00, -- Message type: Call(0) + 0x00, 0x00, 0x00, 0x02, -- RPC Version: 2 + 0x00, 0x01, 0x86, 0xa5, -- Program: MOUNT(100005) + 0x00, 0x00, 0x00, 0x01, -- Program Version: 1 + 0x00, 0x00, 0x00, 0x05, -- Procedure: EXPORT(5) + + -- Credentials + 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL (0) + 0x00, 0x00, 0x00, 0x00, -- Length: 0 + + -- Verifier + 0x00, 0x00, 0x00, 0x00, -- Flavor: AUTH_NULL + 0x00, 0x00, 0x00, 0x00 -- Length: 0 + + ) + + + local status, result + local ports = get_rpc_port_for_service(host, "mountd", 1) + + for p in pairs(ports) do + + status, result = comm.exchange(host, ports[p], data[p], {proto=p}) + + -- Fail gracefully + if not status then + if (nmap.verbosity() >= 2 or nmap.debugging() >= 1) then + return "ERROR: TIMEOUT" + else + return + end + end + + result = process_response( p, result ) + + if result then + return result + end + + end + + return + +end diff --git a/scripts/script.db b/scripts/script.db index 8600cf24e..e01cb7a37 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -31,6 +31,7 @@ Entry { filename = "irc-info.nse", categories = { "default", "discovery", "safe" Entry { filename = "ms-sql-info.nse", categories = { "default", "discovery", "intrusive", } } Entry { filename = "mysql-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "nbstat.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "nfs-showmount.nse", categories = { "discovery", "safe", } } Entry { filename = "p2p-conficker.nse", categories = { "default", "safe", } } Entry { filename = "pjl-ready-message.nse", categories = { "intrusive", } } Entry { filename = "pop3-brute.nse", categories = { "auth", "intrusive", } }