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", } }