diff --git a/CHANGELOG b/CHANGELOG index fd1d369fa..0d1899ad0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,11 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added two new scripts broadcast-netbios-master-browser and smb-mbenum: + - broadcast-netbios-master-browser attempts to discover master browsers in + the broadcast domain + - smb-mbenum lists servers registered with the master browser + [Patrik] + o [NSE] Added credential storage library (creds.lua) and modified the brute library and scripts to make use of it. [Patrik] diff --git a/nselib/dns.lua b/nselib/dns.lua index 7dc1b1a90..e3c02a8e3 100644 --- a/nselib/dns.lua +++ b/nselib/dns.lua @@ -239,8 +239,10 @@ end -- * tries: How often should query try to contact another server (for non-recursive queries). -- * retAll: Return all answers, not just the first. -- * retPkt: Return the packet instead of using the answer-fetching mechanism. --- * norecurse If true, do not set the recursion (RD) flag. --- * multiple If true, expects multiple hosts to respond to multicast request +-- * norecurse: If true, do not set the recursion (RD) flag. +-- * multiple: If true, expects multiple hosts to respond to multicast request +-- * flags: numeric value to set flags in the DNS query to a specific value +-- * id: numeric value to use for the DNS transaction id -- @return true if a dns response was received and contained an answer of the requested type, -- or the decoded dns response was requested (retPkt) and is being returned - or false otherwise. -- @return String answer of the requested type, table of answers or a String error message of one of the following: @@ -284,6 +286,9 @@ function query(dname, options) addOPT(pkt, {DO = true}) end + if ( options.flags ) then pkt.flags.raw = options.flags end + if ( options.id ) then pkt.id = options.id end + local data = encode(pkt) local status, response = sendPackets(data, host, port, options.timeout, options.sendCount, options.multiple) @@ -824,7 +829,12 @@ function encode(pkt) aorulen = #pkt.updates end - local encStr = bin.pack(">SBS4", pkt.id, encFlags, qorzlen, aorplen, aorulen, #pkt.additional) .. data .. additional + local encStr + if ( pkt.flags.raw ) then + encStr = bin.pack(">SSS4", pkt.id, pkt.flags.raw, qorzlen, aorplen, aorulen, #pkt.additional) .. data .. additional + else + encStr = bin.pack(">SBS4", pkt.id, encFlags, qorzlen, aorplen, aorulen, #pkt.additional) .. data .. additional + end return encStr end diff --git a/nselib/msrpc.lua b/nselib/msrpc.lua index a0b40981d..178d8f6d7 100644 --- a/nselib/msrpc.lua +++ b/nselib/msrpc.lua @@ -397,59 +397,86 @@ function call_function(smbstate, opnum, arguments) end ---LANMAN API calls use different conventions than everything else, so make a separate function for them. -function call_lanmanapi(smbstate, opnum, server_type) +function call_lanmanapi(smbstate, opnum, paramdesc, datadesc, data) local status, result local parameters = "" - local data - local convert, entry_count, available_entries - local entries = {} local pos - parameters = bin.pack("bind has been called). +-- @param domain (optional) string containing the domain name to query +-- @param server_type number containing a server bit mask to use to filter responses +-- @param detail_level number containing either 0 or 1 +function rap_netserverenum2(smbstate, domain, server_type, detail_level) + + local NETSERVERENUM2 = 0x0068 + local paramdesc = (domain and "WrLehDz" or "WrLehDO") + assert( detail_level > 0 and detail_level < 2, "detail_level must be either 0 or 1") + local datadesc = ( detail_level == 0 and "B16" or "B16BBDz") + local data = bin.pack(" 0 ) then + local comment_offset, _ + server.version = {} + pos, server.version.major, server.version.minor, + server.type, comment_offset, _ = bin.unpack(" 0 ) then + for _, resp in ipairs(response) do + assert( options.id == resp.output.id, "Received packet with invalid transaction ID" ) + if ( not(resp.output.answers) or #resp.output.answers < 1 ) then + return false, "ERROR: Response contained no answers" + end + local dname = string.char(#resp.output.answers[1].dname) .. resp.output.answers[1].dname + table.insert( results, { peer = resp.peer, name = name_decode(dname) } ) + end + return true, results + else + local dname = string.char(#response.answers[1].dname) .. response.answers[1].dname + return true, { { peer = host.ip, name = name_decode(dname) } } + end +end + ---Convert the 16-bit flags field to a string. --@param flags The 16-bit flags field --@return A string representing the flags diff --git a/scripts/broadcast-netbios-master-browser.nse b/scripts/broadcast-netbios-master-browser.nse new file mode 100644 index 000000000..7bd57ef3f --- /dev/null +++ b/scripts/broadcast-netbios-master-browser.nse @@ -0,0 +1,65 @@ +description = [[ +Attempts to discover master browsers and the domains they manage +]] + +--- +-- @usage +-- nmap --script=broadcast-netbios-master-browser +-- +-- @output +-- | broadcast-netbios-master-browser: +-- | ip server domain +-- |_10.0.200.156 WIN2K3-EPI-1 WORKGROUP +-- + +-- Version 0.1 +-- Created 06/14/2011 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"broadcast", "safe"} + +require 'netbios' +require 'tab' +require 'bit' + +prerule = function() return true end + +local function isGroup(flags) return ( bit.band(flags, 0x8000) == 0x8000 ) end + +action = function() + + -- NBNS only works over ipv4 + if ( nmap.address_family() == "inet6") then return end + + local MASTER_BROWSER_DOMAIN = 0x1D + local STD_WORKSTATION_SERVICE = 0x00 + local NBNAME = "\1\2__MSBROWSE__\2\1" + local BROADCAST_ADDR = "255.255.255.255" + + local status, result = netbios.nbquery( { ip = BROADCAST_ADDR }, NBNAME, { multiple = true }) + if ( not(status) ) then return end + + local outtab = tab.new(3) + tab.addrow(outtab, 'ip', 'server', 'domain') + + for _, v in ipairs(result) do + local status, names, _ = netbios.do_nbstat(v.peer) + local srv_name, domain_name + if (status) then + for _, item in ipairs(names) do + if ( item.suffix == MASTER_BROWSER_DOMAIN and not(isGroup(item.flags)) ) then + domain_name = item.name + elseif ( item.suffix == STD_WORKSTATION_SERVICE and not(isGroup(item.flags)) ) then + srv_name = item.name + end + end + if ( srv_name and domain_name ) then + tab.addrow(outtab, v.peer, srv_name, domain_name) + else + stdnse.print_debug(3, "No server name or domain name was found") + end + end + end + return "\n" .. tab.dump(outtab) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 2a3c8b49a..709d310df 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -13,6 +13,7 @@ Entry { filename = "broadcast-avahi-dos.nse", categories = { "broadcast", "dos", Entry { filename = "broadcast-dns-service-discovery.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-dropbox-listener.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-ms-sql-discover.nse", categories = { "broadcast", "discovery", "safe", } } +Entry { filename = "broadcast-netbios-master-browser.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-novell-locate.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-upnp-info.nse", categories = { "broadcast", "safe", } } Entry { filename = "broadcast-wsdd-discover.nse", categories = { "broadcast", "safe", } } @@ -161,6 +162,7 @@ Entry { filename = "smb-enum-sessions.nse", categories = { "discovery", "intrusi Entry { filename = "smb-enum-shares.nse", categories = { "discovery", "intrusive", } } Entry { filename = "smb-enum-users.nse", categories = { "discovery", "intrusive", } } Entry { filename = "smb-flood.nse", categories = { "dos", "intrusive", } } +Entry { filename = "smb-mbenum.nse", categories = { "discovery", "safe", } } Entry { filename = "smb-os-discovery.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "smb-psexec.nse", categories = { "intrusive", } } Entry { filename = "smb-security-mode.nse", categories = { "discovery", "safe", } } diff --git a/scripts/smb-mbenum.nse b/scripts/smb-mbenum.nse new file mode 100644 index 000000000..3a18fd733 --- /dev/null +++ b/scripts/smb-mbenum.nse @@ -0,0 +1,230 @@ +description=[[ +Queries information managed by the Windows Master Browser. +]] + +--- +-- @usage +-- nmap -p 445 --script smb-mbenum +-- +-- @output +-- | smb-mbenum: +-- | Backup Browser +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | DFS Root +-- | WIN2K3-1 5.2 MSSQL Server backend +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | Master Browser +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | SQL Server +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | Server +-- | TIME-CAPSULE 4.32 Time Capsule +-- | WIN2K3-1 5.2 MSSQL Server backend +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | Server service +-- | TIME-CAPSULE 4.32 Time Capsule +-- | WIN2K3-1 5.2 MSSQL Server backend +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | Windows NT/2000/XP/2003 server +-- | TIME-CAPSULE 4.32 Time Capsule +-- | WIN2K3-1 5.2 MSSQL Server backend +-- | WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- | Workstation +-- | TIME-CAPSULE 4.32 Time Capsule +-- | WIN2K3-1 5.2 MSSQL Server backend +-- |_ WIN2K3-EPI-1 5.2 EPiServer 2003 frontend server +-- +-- @args smb-mbenum.format (optional) if set, changes the format of the result +-- returned by the script. There are three possible formats: +-- 1. Ordered by type horizontally +-- 2. Ordered by type vertically +-- 3. Ordered by type vertically with details (default) +-- +-- @args smb-mbenum.filter (optional) if set, queries the browser for a +-- specific type of server (@see ServerTypes) +-- +-- @args smb-mbenum.domain (optional) if not specified, lists the domain of the queried browser +-- + +-- +-- Version 0.1 +-- Created 06/11/2011 - v0.1 - created by Patrik Karlsson + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'smb' +require 'msrpc' +require 'shortport' +require 'tab' + +hostrule = function(host) return smb.get_port(host) ~= nil end +prerule = function() return true end + +local function log(msg) stdnse.print_debug(3, msg) end + +ServerTypes = { + SV_TYPE_WORKSTATION = 0x00000001, + SV_TYPE_SERVER = 0x00000002, + SV_TYPE_SQLSERVER = 0x00000004, + SV_TYPE_DOMAIN_CTRL = 0x00000008, + SV_TYPE_DOMAIN_BAKCTRL = 0x00000010, + SV_TYPE_TIME_SOURCE = 0x00000020, + SV_TYPE_AFP = 0x00000040, + SV_TYPE_NOVELL = 0x00000080, + SV_TYPE_DOMAIN_MEMBER = 0x00000100, + SV_TYPE_PRINTQ_SERVER = 0x00000200, + SV_TYPE_DIALIN_SERVER = 0x00000400, + SV_TYPE_SERVER_UNIX = 0x00000800, + SV_TYPE_NT = 0x00001000, + SV_TYPE_WFW = 0x00002000, + SV_TYPE_SERVER_MFPN = 0x00004000, + SV_TYPE_SERVER_NT = 0x00008000, + SV_TYPE_POTENTIAL_BROWSER = 0x00010000, + SV_TYPE_BACKUP_BROWSER = 0x00020000, + SV_TYPE_MASTER_BROWSER = 0x00040000, + SV_TYPE_DOMAIN_MASTER = 0x00080000, + SV_TYPE_WINDOWS = 0x00400000, + SV_TYPE_DFS = 0x00800000, + SV_TYPE_CLUSTER_NT = 0x01000000, + SV_TYPE_TERMINALSERVER = 0x02000000, + SV_TYPE_CLUSTER_VS_NT = 0x04000000, + SV_TYPE_DCE = 0x10000000, + SV_TYPE_ALTERNATE_XPORT = 0x20000000, + SV_TYPE_LOCAL_LIST_ONLY = 0x40000000, + SV_TYPE_DOMAIN_ENUM = 0x80000000, + SV_TYPE_ALL = 0xFFFFFFFF +} + +TypeNames = { + SV_TYPE_WORKSTATION = { long = "Workstation", short = "WKS" }, + SV_TYPE_SERVER = { long = "Server service", short = "SRVSVC" }, + SV_TYPE_SQLSERVER = { long = "SQL Server", short = "MSSQL" }, + SV_TYPE_DOMAIN_CTRL = { long = "Domain Controller", short = "DC" }, + SV_TYPE_DOMAIN_BAKCTRL = { long = "Backup Domain Controller", short = "BDC" }, + SV_TYPE_TIME_SOURCE = { long = "Time Source", short = "TIME" }, + SV_TYPE_AFP = { long = "Apple File Protocol Server", short = "AFP" }, + SV_TYPE_NOVELL = { long = "Novell Server", short = "NOVELL" }, + SV_TYPE_DOMAIN_MEMBER = { long = "LAN Manager Domain Member", short = "MEMB" }, + SV_TYPE_PRINTQ_SERVER = { long = "Print server", short = "PRINT" }, + SV_TYPE_DIALIN_SERVER = { long = "Dial-in server", short = "DIALIN" }, + SV_TYPE_SERVER_UNIX = { long = "Unix server", short = "UNIX" }, + SV_TYPE_NT = { long = "Windows NT/2000/XP/2003 server", short = "NT" }, + SV_TYPE_WFW = { long = "Windows for workgroups", short = "WFW" }, + SV_TYPE_SERVER_MFPN = { long = "Microsoft File and Print for Netware", short="MFPN" }, + SV_TYPE_SERVER_NT = { long = "Server", short = "SRV" }, + SV_TYPE_POTENTIAL_BROWSER = { long = "Potential Browser", short = "POTBRWS" }, + SV_TYPE_BACKUP_BROWSER = { long = "Backup Browser", short = "BCKBRWS"}, + SV_TYPE_MASTER_BROWSER = { long = "Master Browser", short = "MBRWS"}, + SV_TYPE_DOMAIN_MASTER = { long = "Domain Master Browser", short = "DOMBRWS"}, + SV_TYPE_WINDOWS = { long = "Windows 95/98/ME", short="WIN95"}, + SV_TYPE_DFS = { long = "DFS Root", short = "DFS"}, +} + +OutputFormat = { + BY_TYPE_H = 1, + BY_TYPE_V = 2, + BY_TYPE_V_DETAILED = 3, +} + + +action = function(host, port) + + local status, smbstate = smb.start(host) + local err, entries + local path = ("\\\\%s\\IPC$"):format(host.ip) + local detail_level = 1 + local format = stdnse.get_script_args("smb-mbenum.format") or OutputFormat.BY_TYPE_V_DETAILED + local filter = stdnse.get_script_args("smb-mbenum.filter") or ServerTypes.SV_TYPE_ALL + local domain = stdnse.get_script_args("smb-mbenum.domain") + + filter = tonumber(filter) or ServerTypes[filter] + format = tonumber(format) + + if ( not(filter) ) then + return "\n The argument smb-mbenum.filter contained an invalid value." + end + + if ( not(format) ) then + return "\n The argument smb-mbenum.format contained an invalid value." + end + + status, err = smb.negotiate_protocol(smbstate, {}) + if ( not(status) ) then + log("ERROR: smb.negotiate_protocol failed") + return "\n ERROR: Failed to connect to browser service" + end + + status, err = smb.start_session(smbstate, {}) + if ( not(status) ) then + log("ERROR: smb.negotiate_protocol failed") + return "\n ERROR: Failed to connect to browser service" + end + + status, err = smb.tree_connect(smbstate, path, {}) + if ( not(status) ) then + log("ERROR: smb.negotiate_protocol failed") + return "\n ERROR: Failed to connect to browser service" + end + + status, entries = msrpc.rap_netserverenum2(smbstate, domain, filter, detail_level) + if ( not(status) ) then + log("ERROR: msrpc.call_lanmanapi failed") + return "\n ERROR: Failed to retrieve list of servers from browser service" + end + + status, err = smb.tree_disconnect(smbstate) + if ( not(status) ) then log("ERROR: smb.tree_disconnect failed") end + + status, err = smb.logoff(smbstate) + if ( not(status) ) then log("ERROR: smb.logoff failed") end + + status, err = smb.stop(smbstate) + if ( not(status) ) then log("ERROR: smb.stop failed") end + + local results, output = {}, {} + for k, _ in pairs(ServerTypes) do + for _, server in ipairs(entries) do + if ( bit.band(server.type,ServerTypes[k]) == ServerTypes[k] ) then + results[TypeNames[k].long] = results[TypeNames[k].long] or {} + if ( format == OutputFormat.BY_TYPE_V_DETAILED ) then + table.insert(results[TypeNames[k].long], server) + else + table.insert(results[TypeNames[k].long], server.name) + end + end + end + end + + if ( format == OutputFormat.BY_TYPE_H ) then + for k, v in pairs(results) do + local row = ("%s: %s"):format( k, stdnse.strjoin(",", v) ) + table.insert(output, row) + end + table.sort(output) + elseif( format == OutputFormat.BY_TYPE_V ) then + for k, v in pairs(results) do + v.name = k + table.insert(output, v) + end + table.sort(output, function(a,b) return a.name < b.name end) + elseif( format == OutputFormat.BY_TYPE_V_DETAILED ) then + for k, v in pairs(results) do + local cat_tab = tab.new(3) + table.sort(v, function(a,b) return a.name < b.name end ) + for _, server in pairs(v) do + tab.addrow( + cat_tab, + server.name, + ("%d.%d"):format(server.version.major,server.version.minor), + server.comment + ) + end + table.insert(output, { name = k, tab.dump(cat_tab) } ) + end + table.sort(output, function(a,b) return a.name < b.name end) + end + + return stdnse.format_output(true, output) +end \ No newline at end of file