From 959f52c5017cc44728210276ddfcdeef80a5e86d Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:51:22 +0100 Subject: [PATCH 01/12] Create ms-sql-info.nse --- fixed_script/ms-sql-info.nse | 1 + 1 file changed, 1 insertion(+) create mode 100644 fixed_script/ms-sql-info.nse diff --git a/fixed_script/ms-sql-info.nse b/fixed_script/ms-sql-info.nse new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/fixed_script/ms-sql-info.nse @@ -0,0 +1 @@ + From 5fa9c0bca4d8c147bf20131c1e579e6ab5271f1d Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:51:35 +0100 Subject: [PATCH 02/12] Update ms-sql-info.nse --- fixed_script/ms-sql-info.nse | 265 +++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/fixed_script/ms-sql-info.nse b/fixed_script/ms-sql-info.nse index 8b137891..f920486a 100644 --- a/fixed_script/ms-sql-info.nse +++ b/fixed_script/ms-sql-info.nse @@ -1 +1,266 @@ +local mssql = require "mssql" +local nmap = require "nmap" +local smb = require "smb" +local shortport = require "shortport" +local stdnse = require "stdnse" + +-- -*- mode: lua -*- +-- vim: set filetype=lua : + +description = [[ +Attempts to determine configuration and version information for Microsoft SQL +Server instances. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). +Run criteria: +* Host script: Will always run. +* Port script: N/A + +NOTE: Unlike previous versions, this script will NOT attempt to log in to SQL +Server instances. Blank passwords can be checked using the +ms-sql-empty-password script. E.g.: +nmap -sn --script ms-sql-empty-password --script-args mssql.instance-all + +The script uses two means of getting version information for SQL Server instances: +* Querying the SQL Server Browser service, which runs by default on UDP port +1434 on servers that have SQL Server 2000 or later installed. However, this +service may be disabled without affecting the functionality of the instances. +Additionally, it provides imprecise version information. +* Sending a probe to the instance, causing the instance to respond with +information including the exact version number. This is the same method that +Nmap uses for service versioning; however, this script can also do the same for +instances accessible via Windows named pipes, and can target all of the +instances listed by the SQL Server Browser service. + +In the event that the script can connect to the SQL Server Browser service +(UDP 1434) but is unable to connect directly to the instance to obtain more +accurate version information (because ports are blocked or the mssql.scanned-ports-only +argument has been used), the script will rely only upon the version number +provided by the SQL Server Browser/Monitor, which has the following limitations: +* For SQL Server 2000 and SQL Server 7.0 instances, the RTM version number is +always given, regardless of any service packs or patches installed. +* For SQL Server 2005 and later, the version number will reflect the service +pack installed, but the script will not be able to tell whether patches have +been installed. + +Where possible, the script will determine major version numbers, service pack +levels and whether patches have been installed. However, in cases where +particular determinations can not be made, the script will report only what can +be confirmed. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. +]] +--- +-- @usage +-- nmap -p 445 --script ms-sql-info +-- nmap -p 1433 --script ms-sql-info --script-args mssql.instance-port=1433 +-- +-- @output +-- | ms-sql-info: +-- | Windows server name: WINXP +-- | 192.168.100.128\PROD: +-- | Instance name: PROD +-- | Version: +-- | name: Microsoft SQL Server 2000 SP3 +-- | number: 8.00.760 +-- | Product: Microsoft SQL Server 2000 +-- | Service pack level: SP3 +-- | Post-SP patches applied: No +-- | TCP port: 1278 +-- | Named pipe: \\192.168.100.128\pipe\MSSQL$PROD\sql\query +-- | Clustered: No +-- | 192.168.100.128\SQLFIREWALLED: +-- | Instance name: SQLFIREWALLED +-- | Version: +-- | name: Microsoft SQL Server 2008 RTM +-- | Product: Microsoft SQL Server 2008 +-- | Service pack level: RTM +-- | TCP port: 4343 +-- | Clustered: No +-- | \\192.168.100.128\pipe\sql\query: +-- | Version: +-- | name: Microsoft SQL Server 2005 SP3+ +-- | number: 9.00.4053 +-- | Product: Microsoft SQL Server 2005 +-- | Service pack level: SP3 +-- | Post-SP patches applied: Yes +-- |_ Named pipe: \\192.168.100.128\pipe\sql\query +-- +-- @xmloutput +-- WINXP +-- +-- PROD +--
+-- Microsoft SQL Server 2000 SP3 +-- 8.00.760 +-- Microsoft SQL Server 2000 +-- SP3 +-- No +--
+-- 1278 +-- \\192.168.100.128\pipe\MSSQL$PROD\sql\query +-- No +-- +-- +-- SQLFIREWALLED +--
+-- Microsoft SQL Server 2008 RTM +-- Microsoft SQL Server 2008 +-- RTM +--
+-- 4343 +-- No +-- +-- +--
+-- Microsoft SQL Server 2005 SP3+ +-- 9.00.4053 +-- Microsoft SQL Server 2005 +-- SP3 +-- Yes +--
+-- \\192.168.100.128\pipe\sql\query +-- + +-- rev 1.0 (2007-06-09) +-- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers) +-- rev 1.2 (2010-10-03 - Added Broadcast support ) +-- rev 1.3 (2010-10-10 - Added prerule and newtargets support ) +-- rev 1.4 (2011-01-24 - Revised logic in order to get version data without logging in; +-- added functionality to interpret version in terms of SP level, etc. +-- added script arg to prevent script from connecting to ports that +-- weren't in original Nmap scan ) +-- rev 1.5 (2011-02-01 - Moved discovery functionality into ms-sql-discover.nse and +-- broadcast-ms-sql-discovery.nse ) +-- rev 1.6 (2014-09-04 - Added structured output Daniel Miller) + +author = {"Chris Woodbury", "Thomas Buchanan"} + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"default", "discovery", "safe"} + +portrule = shortport.port_or_service(1433, "ms-sql-s") + + + +--- Returns formatted output for the given version data +local function create_version_output_table( versionInfo ) + local versionOutput = stdnse.output_table() + + versionOutput["name"] = versionInfo:ToString() + if ( versionInfo.source ~= "SSRP" ) then + versionOutput["number"] = versionInfo.versionNumber + end + versionOutput["Product"] = versionInfo.productName + versionOutput["Service pack level"] = versionInfo.servicePackLevel + versionOutput["Post-SP patches applied"] = versionInfo.patched + + return versionOutput +end + + +--- Returns formatted output for the given instance +local function create_instance_output_table( instance ) + + -- if we didn't get anything useful (due to errors or the port not actually + -- being SQL Server), don't report anything + if not ( instance.instanceName or instance.version ) then return nil end + + local instanceOutput = stdnse.output_table() + + instanceOutput["Instance name"] = instance.instanceName + if instance.version then + instanceOutput["Version"] = create_version_output_table( instance.version ) + end + if instance.port then instanceOutput["TCP port"] = instance.port.number end + instanceOutput["Named pipe"] = instance.pipeName + instanceOutput["Clustered"] = instance.isClustered + + return instanceOutput + +end + + +--- Processes a single instance, attempting to determine its version, etc. +local function process_instance( instance ) + + local foundVersion = false + local ssnetlibVersion + + -- If possible and allowed (see 'mssql.scanned-ports-only' argument), we'll + -- connect to the instance to get an accurate version number + if ( instance:HasNetworkProtocols() ) then + local ssnetlibVersion + foundVersion, ssnetlibVersion = mssql.Helper.GetInstanceVersion( instance ) + if ( foundVersion ) then + instance.version = ssnetlibVersion + stdnse.debug1("Retrieved SSNetLib version for %s.", instance:GetName() ) + else + stdnse.debug1("Could not retrieve SSNetLib version for %s.", instance:GetName() ) + end + end + + -- If we didn't get a version from SSNetLib, give the user some detail as to why + if ( not foundVersion ) then + if ( not instance:HasNetworkProtocols() ) then + stdnse.debug1("%s has no network protocols enabled.", instance:GetName() ) + end + if ( instance.version ) then + stdnse.debug1("Using version number from SSRP response for %s.", instance:GetName() ) + else + stdnse.debug1("Version info could not be retrieved for %s.", instance:GetName() ) + end + end + + -- Give some version info back to Nmap + if ( instance.port and instance.version ) then + instance.version:PopulateNmapPortVersion( instance.port ) + nmap.set_port_version( instance.host, instance.port) + end + +end + + +action = function( host ) + local scriptOutput = stdnse.output_table() + + local status, instanceList = mssql.Helper.GetTargetInstances( host ) + -- if no instances were targeted, then display info on all + if ( not status ) then + if ( not mssql.Helper.WasDiscoveryPerformed( host ) ) then + mssql.Helper.Discover( host ) + end + instanceList = mssql.Helper.GetDiscoveredInstances( host ) + end + + + if ( not instanceList ) then + return stdnse.format_output( false, instanceList or "" ) + else + for _, instance in ipairs( instanceList ) do + if instance.serverName then + scriptOutput["Windows server name"] = instance.serverName + break + end + end + for _, instance in pairs( instanceList ) do + process_instance( instance ) + scriptOutput[instance:GetName()] = create_instance_output_table( instance ) + end + end + + return scriptOutput +end From f9bd4fbd586d631ec7f25c3eb1fcedf4d2353581 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:52:02 +0100 Subject: [PATCH 03/12] Add files via upload --- fixed_script/msrpc-enum.nse | 111 +++ fixed_script/ntlm-methods.nse | 73 ++ fixed_script/redis-info.nse | 250 +++++++ fixed_script/smb-protocols.nse | 70 ++ fixed_script/stdnse.lua | 1233 ++++++++++++++++++++++++++++++++ 5 files changed, 1737 insertions(+) create mode 100644 fixed_script/msrpc-enum.nse create mode 100644 fixed_script/ntlm-methods.nse create mode 100644 fixed_script/redis-info.nse create mode 100644 fixed_script/smb-protocols.nse create mode 100644 fixed_script/stdnse.lua diff --git a/fixed_script/msrpc-enum.nse b/fixed_script/msrpc-enum.nse new file mode 100644 index 00000000..84d3e865 --- /dev/null +++ b/fixed_script/msrpc-enum.nse @@ -0,0 +1,111 @@ +local msrpc = require "msrpc" +local smb = require "smb" +local stdnse = require "stdnse" +local table = require "table" +local shortport = require "shortport" + +description = [[ +Queries an MSRPC endpoint mapper for a list of mapped +services and displays the gathered information. + +As it is using smb library, you can specify optional +username and password to use. + +Script works much like Microsoft's rpcdump tool +or dcedump tool from SPIKE fuzzer. +]] +--- +-- @usage nmap --script=msrpc-enum +-- +-- @output +-- PORT STATE SERVICE REASON +-- 445/tcp open microsoft-ds syn-ack +-- +-- Host script results: +-- | msrpc-enum: +-- | +-- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5 +-- | annotation: DHCP Client LRPC Endpoint +-- | ncalrpc: dhcpcsvc +-- | +-- | uuid: 12345678-1234-abcd-ef00-0123456789ab +-- | annotation: IPSec Policy agent endpoint +-- | ncalrpc: audit +-- | +-- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5 +-- | ip_addr: 0.0.0.0 +-- | annotation: DHCP Client LRPC Endpoint +-- | tcp_port: 49153 +-- | +-- +-- | +-- | uuid: 12345678-1234-abcd-ef00-0123456789ab +-- | annotation: IPSec Policy agent endpoint +-- | ncalrpc: securityevent +-- | +-- | uuid: 12345678-1234-abcd-ef00-0123456789ab +-- | annotation: IPSec Policy agent endpoint +-- |_ ncalrpc: protected_storage +-- +-- @xmloutput +-- -snip- +-- +-- c100beab-d33a-4a4b-bf23-bbef4663d017 +-- wcncsvc.wcnprpc +-- wcncsvc.wcnprpc +--
+-- +-- 6b5bdd1e-528c-422c-af8c-a4079be4fe48 +-- Remote Fw APIs +-- 49158 +-- 0.0.0.0 +--
+-- +-- 12345678-1234-abcd-ef00-0123456789ab +-- IPSec Policy agent endpoint +-- 49158 +-- 0.0.0.0 +--
+-- -snip- + +author = "Aleksandar Nikolic" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe","discovery"} + +portrule = shortport.port_or_service(445, "microsoft-ds") + +action = function(host,port) + local status, smbstate + status, smbstate = msrpc.start_smb(host,msrpc.EPMAPPER_PATH,true) + if(status == false) then + stdnse.debug1("SMB: " .. smbstate) + return false, smbstate + end + local bind_result,epresult -- bind to endpoint mapper service + status, bind_result = msrpc.bind(smbstate,msrpc.EPMAPPER_UUID, msrpc.EPMAPPER_VERSION, nil) + if(status == false) then + msrpc.stop_smb(smbstate) + stdnse.debug1("SMB: " .. bind_result) + return false, bind_result + end + local results = {} + status, epresult = msrpc.epmapper_lookup(smbstate,nil) -- get the initial handle + if not status then + stdnse.debug1("SMB: " .. epresult) + return false, epresult + + end + local handle = epresult.new_handle + epresult.new_handle = nil + table.insert(results,epresult) + + while not (epresult == nil) do + status, epresult = msrpc.epmapper_lookup(smbstate,handle) -- get next result until there are no more + if not status then + break + end + epresult.new_handle = nil + table.insert(results,epresult) + end + return results +end diff --git a/fixed_script/ntlm-methods.nse b/fixed_script/ntlm-methods.nse new file mode 100644 index 00000000..60830f12 --- /dev/null +++ b/fixed_script/ntlm-methods.nse @@ -0,0 +1,73 @@ +local http = require "http" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +description = [[ +Returns authentication methods a winrm server supports. +]] + +--- +-- @usage +-- nmap --script winrm-auth-methods -p 5985 +-- +-- @output +-- 5985/tcp open wsman +-- | winrm-auth-methods: +-- | Accepted Authentication Methods: +-- | Negotiate +-- | Basic +-- | Kerberos +-- |_ CredSSP + +author = "Evangelos Deirmentzoglou" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} + +portrule = shortport.port_or_service({5985, 5986},{'wsman','wsmans'}) + +function generate_random_string(len, charset) + local t = {} + local ascii_A = 65 + local ascii_Z = 90 + if charset then + for i=1,len do + t[i]=charset[math.random(#charset)] + end + else + for i=1,len do + t[i]=string.char(math.random(ascii_A,ascii_Z)) + end + end + return table.concat(t) +end + +action = function(host, port) + + local r = {} + local result = stdnse.output_table() + local randoms = generate_random_string(5) + local url = "/wsman" + local response = http.post( host, port, url, nil, nil, randoms ) + + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Negotiate") then + table.insert(r, "Negotiate") + end + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Basic") then + table.insert(r, "Basic") + end + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Kerberos") then + table.insert(r, "Kerberos") + end + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "CredSSP") then + table.insert(r, "CredSSP") + end + if #r > 0 then + result = r + else + result = "Server does not support authentication." + end + + return result +end diff --git a/fixed_script/redis-info.nse b/fixed_script/redis-info.nse new file mode 100644 index 00000000..fcfd4b7a --- /dev/null +++ b/fixed_script/redis-info.nse @@ -0,0 +1,250 @@ +local creds = require "creds" +local redis = require "redis" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local stringaux = require "stringaux" +local table = require "table" +local tableaux = require "tableaux" +local ipOps = require "ipOps" + +description = [[ +Retrieves information (such as version number and architecture) from a Redis key-value store. +]] + +--- +-- @usage +-- nmap -p 6379 --script redis-info +-- +-- @output +-- PORT STATE SERVICE +-- 6379/tcp open unknown +-- | redis-info: +-- | Version 2.2.11 +-- | Architecture 64 bits +-- | Process ID 17821 +-- | Used CPU (sys) 2.37 +-- | Used CPU (user) 1.02 +-- | Connected clients 1 +-- | Connected slaves 0 +-- | Used memory 780.16K +-- | Role master +-- | Bind addresses: +-- | 192.168.121.101 +-- | Active channels: +-- | testChannel +-- | bidChannel +-- | Client connections: +-- | 192.168.171.101 +-- |_ 72.14.177.105 +-- +-- + +author = {"Patrik Karlsson", "Vasiliy Kulikov"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} +dependencies = {"redis-brute"} + + +portrule = shortport.port_or_service(6379, "redis") + +local function fail(err) return stdnse.format_output(false, err) end + +local function cb_parse_version(host, port, val) + port.version.version = val + port.version.cpe = port.version.cpe or {} + table.insert(port.version.cpe, 'cpe:/a:redis:redis:' .. val) + nmap.set_port_version(host, port) + return val +end + +local function cb_parse_architecture(host, port, val) + val = ("%s bits"):format(val) + port.version.extrainfo = val + nmap.set_port_version(host, port) + return val +end + +local filter = { + + ["redis_version"] = { name = "Version", func = cb_parse_version }, + ["os"] = { name = "Operating System" }, + ["arch_bits"] = { name = "Architecture", func = cb_parse_architecture }, + ["process_id"] = { name = "Process ID"}, + ["uptime"] = { name = "Uptime", func = function(h, p, v) return ("%s seconds"):format(v) end }, + ["used_cpu_sys"]= { name = "Used CPU (sys)"}, + ["used_cpu_user"] = { name = "Used CPU (user)"}, + ["connected_clients"] = { name = "Connected clients"}, + ["connected_slaves"] = { name = "Connected slaves"}, + ["used_memory_human"] = { name = "Used memory"}, + ["role"] = { name = "Role"} + +} + +local order = { + "redis_version", "os", "arch_bits", "process_id", "used_cpu_sys", + "used_cpu_user", "connected_clients", "connected_slaves", + "used_memory_human", "role" +} + +local extras = { + { + -- https://redis.io/commands/config-get/ + "Bind addresses", {"CONFIG", "GET", "bind"}, function (data) + if data[1] ~= "bind" or not data[2] then + return nil + end + local restab = stringaux.strsplit(" ", data[2]) + for i, ip in ipairs(restab) do + if ip == '' then restab[i] = '0.0.0.0' end + end + return restab + end + }, + { + -- https://redis.io/commands/pubsub-channels/ + "Active channels", {"PUBSUB", "CHANNELS"}, function (data) + local channels = {} + local omitted = 0 + local limit = nmap.verbosity() <= 1 and 20 or false + for _, channel in ipairs(data) do + if limit and #channels >= limit then + omitted = omitted + 1 + else + table.insert(channels, channel) + end + end + + if omitted > 0 then + table.insert(channels, ("(omitted %s item(s), use verbose mode -v to show them)"):format(omitted)) + end + return #channels > 0 and channels or nil + end + }, + { + -- https://redis.io/commands/client-list/ + "Client connections", {"CLIENT", "LIST"}, function(data) + if not data then + stdnse.debug1("Failed to parse response from server") + return nil + end + + local client_ips = {} + for conn in data:gmatch("[^\n]+") do + local ip = conn:match("%f[^%s\0]addr=%[?([%x:.]+)%]?:%d+%f[%s\0]") + if ip then + local binip = ipOps.ip_to_str(ip) + if binip then + -- prepending length sorts IPv4 and IPv6 separately + client_ips[string.pack("s1", binip)] = binip + end + end + end + + local out = {} + local keys = tableaux.keys(client_ips) + table.sort(keys) + for _, packed in ipairs(keys) do + table.insert(out, ipOps.str_to_ip(client_ips[packed])) + end + return #out > 0 and out or nil + end + }, + { + -- https://redis.io/commands/cluster-nodes/ + "Cluster nodes", {"CLUSTER", "NODES"}, function(data) + if not data then + stdnse.debug1("Failed to parse response from server") + return nil + end + + local out = {} + for node in data:gmatch("[^\n]+") do + local ipport, flags = node:match("^%x+%s+([%x.:%[%]]+)@?%d*%s+(%S+)") + if ipport then + table.insert(out, ("%s (%s)"):format(ipport, flags)) + else + stdnse.debug1("Unable to parse cluster node info") + end + end + return #out > 0 and out or nil + end + }, +} + +action = function(host, port) + + local helper = redis.Helper:new(host, port) + local status = helper:connect() + if( not(status) ) then + return fail("Failed to connect to server") + end + + -- do we have a service password + local c = creds.Credentials:new(creds.ALL_DATA, host, port) + local cred = c:getCredentials(creds.State.VALID + creds.State.PARAM)() + + if ( cred and cred.pass ) then + local status, response = helper:reqCmd("AUTH", cred.pass) + if ( not(status) ) then + helper:close() + return fail(response) + end + end + + local status, response = helper:reqCmd("INFO") + if ( not(status) ) then + helper:close() + return fail(response) + end + + if ( redis.Response.Type.ERROR == response.type ) then + if ( "-ERR operation not permitted" == response.data ) or + ( "-NOAUTH Authentication required." == response.data ) then + return fail("Authentication required") + end + return fail(response.data) + end + + local restab = stringaux.strsplit("\r\n", response.data) + if ( not(restab) or 0 == #restab ) then + return fail("Failed to parse response from server") + end + + local kvs = {} + for _, item in ipairs(restab) do + local k, v = item:match("^([^:]*):(.*)$") + if k ~= nil then + kvs[k] = v + end + end + + local result = stdnse.output_table() + for _, item in ipairs(order) do + if kvs[item] then + local name = filter[item].name + local val + + if filter[item].func then + val = filter[item].func(host, port, kvs[item]) + else + val = kvs[item] + end + result[name] = val + end + end + + for i=1, #extras do + local name = extras[i][1] + local cmd = extras[i][2] + local process = extras[i][3] + + local status, response = helper:reqCmd(table.unpack(cmd)) + if status and redis.Response.Type.ERROR ~= response.type then + result[name] = process(response.data) + end + end + helper:close() + return result +end diff --git a/fixed_script/smb-protocols.nse b/fixed_script/smb-protocols.nse new file mode 100644 index 00000000..6dc71d8b --- /dev/null +++ b/fixed_script/smb-protocols.nse @@ -0,0 +1,70 @@ +local smb = require "smb" +local stdnse = require "stdnse" +local nmap = require "nmap" +local shortport = require "shortport" + +description = [[ +Attempts to list the supported protocols and dialects of a SMB server. + +The script attempts to initiate a connection using the dialects: +* NT LM 0.12 (SMBv1) +* 2.0.2 (SMBv2) +* 2.1 (SMBv2) +* 3.0 (SMBv3) +* 3.0.2 (SMBv3) +* 3.1.1 (SMBv3) + +Additionally if SMBv1 is found enabled, it will mark it as insecure. This +script is the successor to the (removed) smbv2-enabled script. +]] + +--- +-- @usage nmap -p445 --script smb-protocols +-- @usage nmap -p139 --script smb-protocols +-- +-- @output +-- | smb-protocols: +-- | dialects: +-- | NT LM 0.12 (SMBv1) [dangerous, but default] +-- | 2.0.2 +-- | 2.1 +-- | 3.0 +-- | 3.0.2 +-- |_ 3.1.1 +-- +-- @xmloutput +-- +-- NT LM 0.12 (SMBv1) [dangerous, but default] +-- 2.0.2 +-- 2.1 +-- 3.0 +-- 3.0.2 +-- 3.1.1 +--
+--- + +author = "Paulino Calderon" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe", "discovery"} + +portrule = shortport.port_or_service(445, "microsoft-ds") + +action = function(host,port) + local status, supported_dialects = smb.list_dialects(host) + if status then + for i, v in pairs(supported_dialects) do -- Mark SMBv1 as insecure + if v == "NT LM 0.12" then + supported_dialects[i] = v .. " (SMBv1) [dangerous, but default]" + end + end + if #supported_dialects > 0 then + local output = stdnse.output_table() + output.dialects = supported_dialects + return output + end + end + stdnse.debug1("No dialects were accepted") + if nmap.verbosity()>1 then + return "No dialects accepted. Something may be blocking the responses" + end +end diff --git a/fixed_script/stdnse.lua b/fixed_script/stdnse.lua new file mode 100644 index 00000000..909a9322 --- /dev/null +++ b/fixed_script/stdnse.lua @@ -0,0 +1,1233 @@ +--- +-- Standard Nmap Scripting Engine functions. This module contains various handy +-- functions that are too small to justify modules of their own. +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @class module +-- @name stdnse + +local _G = require "_G" +local coroutine = require "coroutine" +local math = require "math" +local nmap = require "nmap" +local os = require "os" +local string = require "string" +local table = require "table" +local assert = assert; +local error = error; +local getmetatable = getmetatable; +local ipairs = ipairs +local pairs = pairs +local next = next +local rawset = rawset +local require = require; +local select = select +local setmetatable = setmetatable; +local tonumber = tonumber; +local tostring = tostring; +local type = type + +local ceil = math.ceil +local max = math.max + +local format = string.format; +local rep = string.rep + +local concat = table.concat; +local insert = table.insert; +local pack = table.pack; +local unpack = table.unpack; + +local difftime = os.difftime; +local time = os.time; + +local EMPTY = {}; -- Empty constant table + +_ENV = require "strict" {}; + +--- Sleeps for a given amount of time. +-- +-- This causes the program to yield control and not regain it until the time +-- period has elapsed. The time may have a fractional part. Internally, the +-- timer provides millisecond resolution. +-- @name sleep +-- @class function +-- @param t Time to sleep, in seconds. +-- @usage stdnse.sleep(1.5) +_ENV.sleep = nmap.socket.sleep; + +--- +-- Prints a formatted debug message if the current debugging level is greater +-- than or equal to a given level. +-- +-- This is a convenience wrapper around +-- nmap.log_write. The first optional numeric +-- argument, level, is used as the debugging level necessary +-- to print the message (it defaults to 1 if omitted). All remaining arguments +-- are processed with Lua's string.format function. +-- @param level Optional debugging level. +-- @param fmt Format string. +-- @param ... Arguments to format. +print_debug = function(level, fmt, ...) + local l, d = tonumber(level), nmap.debugging(); + if l and l <= d then + nmap.log_write("stdout", format(fmt, ...)); + elseif not l and 1 <= d then + nmap.log_write("stdout", format(level, fmt, ...)); + end +end + +--- +-- Prints a formatted verbosity message if the current verbosity level is greater +-- than or equal to a given level. +-- +-- This is a convenience wrapper around +-- nmap.log_write. The first optional numeric +-- argument, level, is used as the verbosity level necessary +-- to print the message (it defaults to 1 if omitted). All remaining arguments +-- are processed with Lua's string.format function. +-- @param level Optional verbosity level. +-- @param fmt Format string. +-- @param ... Arguments to format. +print_verbose = function(level, fmt, ...) + local l, d = tonumber(level), nmap.verbosity(); + if l and l <= d then + nmap.log_write("stdout", format(fmt, ...)); + elseif not l and 1 <= d then + nmap.log_write("stdout", format(level, fmt, ...)); + end +end + + +--- Join a list of strings with a separator string. +-- +-- This is Lua's table.concat function with the parameters +-- swapped for coherence. +-- @usage +-- stdnse.strjoin(", ", {"Anna", "Bob", "Charlie", "Dolores"}) +-- --> "Anna, Bob, Charlie, Dolores" +-- @param delimiter String to delimit each element of the list. +-- @param list Array of strings to concatenate. +-- @return Concatenated string. +function strjoin(delimiter, list) + assert(type(delimiter) == "string" or type(delimiter) == nil, "delimiter is of the wrong type! (did you get the parameters backward?)") + + return concat(list, delimiter); +end + +--- Split a string at a given delimiter, which may be a pattern. +-- @usage +-- stdnse.strsplit(",%s*", "Anna, Bob, Charlie, Dolores") +-- --> { "Anna", "Bob", "Charlie", "Dolores" } +-- @param pattern Pattern that separates the desired strings. +-- @param text String to split. +-- @return Array of substrings without the separating pattern. +function strsplit(pattern, text) + local list, pos = {}, 1; + + assert(pattern ~= "", "delimiter matches empty string!"); + + while true do + local first, last, match = text:find(pattern, pos); + if first then -- found? + list[#list+1] = text:sub(pos, first-1); + pos = last+1; + else + list[#list+1] = text:sub(pos); + break; + end + end + return list; +end + +--- Generate a random string. +-- You can either provide your own charset or the function will use +-- a default one which is [A-Z]. +-- @param len Length of the string we want to generate. +-- @param charset Charset that will be used to generate the string. +-- @return A random string of length len consisting of +-- characters from charset if one was provided, otherwise +-- charset defaults to [A-Z] letters. +function generate_random_string(len, charset) + local t = {} + local ascii_A = 65 + local ascii_Z = 90 + if charset then + for i=1,len do + t[i]=charset[math.random(#charset)] + end + else + for i=1,len do + t[i]=string.char(math.random(ascii_A,ascii_Z)) + end + end + return table.concat(t) +end + +--- Return a wrapper closure around a socket that buffers socket reads into +-- chunks separated by a pattern. +-- +-- This function operates on a socket attempting to read data. It separates the +-- data by sep and, for each invocation, returns a piece of the +-- separated data. Typically this is used to iterate over the lines of data +-- received from a socket (sep = "\r?\n"). The returned string +-- does not include the separator. It will return the final data even if it is +-- not followed by the separator. Once an error or EOF is reached, it returns +-- nil, msg. msg is what is returned by +-- nmap.receive_lines. +-- @param socket Socket for the buffer. +-- @param sep Separator for the buffered reads. +-- @return Data from socket reads or nil on EOF or error. +-- @return Error message, as with receive_lines. +function make_buffer(socket, sep) + local point, left, buffer, done, msg = 1, ""; + local function self() + if done then + return nil, msg; -- must be nil for stdnse.lines (below) + elseif not buffer then + local status, str = socket:receive(); + if not status then + if #left > 0 then + done, msg = not status, str; + return left; + else + return status, str; + end + else + buffer = left..str; + return self(); + end + else + local i, j = buffer:find(sep, point); + if i then + local ret = buffer:sub(point, i-1); + point = j + 1; + return ret; + else + point, left, buffer = 1, buffer:sub(point), nil; + return self(); + end + end + end + return self; +end + +--[[ This function may be usable in Lua 5.2 +function lines(socket) + return make_buffer(socket, "\r?\n"), nil, nil; +end --]] + +do + local t = { + ["0"] = "0000", + ["1"] = "0001", + ["2"] = "0010", + ["3"] = "0011", + ["4"] = "0100", + ["5"] = "0101", + ["6"] = "0110", + ["7"] = "0111", + ["8"] = "1000", + ["9"] = "1001", + a = "1010", + b = "1011", + c = "1100", + d = "1101", + e = "1110", + f = "1111" + }; + +--- Converts the given number, n, to a string in a binary number format (12 +-- becomes "1100"). +-- @param n Number to convert. +-- @return String in binary format. + function tobinary(n) + assert(tonumber(n), "number expected"); + return (("%x"):format(n):gsub("%w", t):gsub("^0*", "")); + end +end + +--- Converts the given number, n, to a string in an octal number format (12 +-- becomes "14"). +-- @param n Number to convert. +-- @return String in octal format. +function tooctal(n) + assert(tonumber(n), "number expected"); + return ("%o"):format(n) +end + +--- Encode a string or number in hexadecimal (12 becomes "c", "AB" becomes +-- "4142"). +-- +-- An optional second argument is a table with formatting options. The possible +-- fields in this table are +-- * separator: A string to use to separate groups of digits. +-- * group: The size of each group of digits between separators. Defaults to 2, but has no effect if separator is not also given. +-- @usage +-- stdnse.tohex("abc") --> "616263" +-- stdnse.tohex("abc", {separator = ":"}) --> "61:62:63" +-- stdnse.tohex("abc", {separator = ":", group = 4}) --> "61:6263" +-- stdnse.tohex(123456) --> "1e240" +-- stdnse.tohex(123456, {separator = ":"}) --> "1:e2:40" +-- stdnse.tohex(123456, {separator = ":", group = 4}) --> "1:e240" +-- @param s String or number to be encoded. +-- @param options Table specifiying formatting options. +-- @return String in hexadecimal format. +function tohex( s, options ) + options = options or EMPTY + local separator = options.separator + local hex + + if type( s ) == "number" then + hex = ("%x"):format(s) + elseif type( s ) == 'string' then + hex = ("%02x"):rep(#s):format(s:byte(1,#s)) + else + error( "Type not supported in tohex(): " .. type(s), 2 ) + end + + -- format hex if we got a separator + if separator then + local group = options.group or 2 + local fmt_table = {} + -- split hex in group-size chunks + for i=#hex,1,-group do + -- table index must be consecutive otherwise table.concat won't work + fmt_table[ceil(i/group)] = hex:sub(max(i-group+1,1),i) + end + + hex = concat( fmt_table, separator ) + end + + return hex +end + +---Either return the string itself, or return "" (or the value of the second parameter) if the string +-- was blank or nil. +-- +--@param string The base string. +--@param blank The string to return if string was blank +--@return Either string or, if it was blank, blank +function string_or_blank(string, blank) + if(string == nil or string == "") then + if(blank == nil) then + return "" + else + return blank + end + else + return string + end +end + +--- +-- Parses a time duration specification, which is a number followed by a +-- unit, and returns a number of seconds. The unit is optional and +-- defaults to seconds. The possible units (case-insensitive) are +-- * ms: milliseconds, +-- * s: seconds, +-- * m: minutes, +-- * h: hours. +-- In case of a parsing error, the function returns nil +-- followed by an error message. +-- +-- @usage +-- parse_timespec("10") --> 10 +-- parse_timespec("10ms") --> 0.01 +-- parse_timespec("10s") --> 10 +-- parse_timespec("10m") --> 600 +-- parse_timespec("10h") --> 36000 +-- parse_timespec("10z") --> nil, "Can't parse time specification \"10z\" (bad unit \"z\")" +-- +-- @param timespec A time specification string. +-- @return A number of seconds, or nil followed by an error +-- message. +function parse_timespec(timespec) + if timespec == nil then return nil, "Can't parse nil timespec" end + local n, unit, t, m + local multipliers = {[""] = 1, s = 1, m = 60, h = 60 * 60, ms = 0.001} + + n, unit = string.match(timespec, "^([%d.]+)(.*)$") + if not n then + return nil, string.format("Can't parse time specification \"%s\"", timespec) + end + + t = tonumber(n) + if not t then + return nil, string.format("Can't parse time specification \"%s\" (bad number \"%s\")", timespec, n) + end + + m = multipliers[unit] + if not m then + return nil, string.format("Can't parse time specification \"%s\" (bad unit \"%s\")", timespec, unit) + end + + return t * m +end + +-- Find the offset in seconds between local time and UTC. That is, if we +-- interpret a UTC date table as a local date table by passing it to os.time, +-- how much must be added to the resulting integer timestamp to make it +-- correct? +local function utc_offset(t) + -- What does the calendar say locally? + local localtime = os.date("*t", t) + -- What does the calendar say in UTC? + local gmtime = os.date("!*t", t) + -- Interpret both as local calendar dates and find the difference. + return difftime(os.time(localtime), os.time(gmtime)) +end +--- Convert a date table into an integer timestamp. Unlike os.time, this does +-- not assume that the date table represents a local time. Rather, it takes an +-- optional offset number of seconds representing the time zone, and returns +-- the timestamp that would result using that time zone as local time. If the +-- offset is omitted or 0, the date table is interpreted as a UTC date. For +-- example, 4:00 UTC is the same as 5:00 UTC+1: +-- +-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}) --> 14400 +-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 0) --> 14400 +-- date_to_timestamp({year=1970,month=1,day=1,hour=5,min=0,sec=0}, 1*60*60) --> 14400 +-- +-- And 4:00 UTC+1 is an earlier time: +-- +-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 1*60*60) --> 10800 +-- +function date_to_timestamp(date, offset) + offset = offset or 0 + return os.time(date) + utc_offset(os.time(date)) - offset +end + +local function format_tz(offset) + local sign, hh, mm + + if not offset then + return "" + end + if offset < 0 then + sign = "-" + offset = -offset + else + sign = "+" + end + -- Truncate to minutes. + offset = math.floor(offset / 60) + hh = math.floor(offset / 60) + mm = math.floor(math.fmod(offset, 60)) + + return string.format("%s%02d:%02d", sign, hh, mm) +end +--- Format a date and time (and optional time zone) for structured output. +-- +-- Formatting is done according to RFC 3339 (a profile of ISO 8601), except +-- that a time zone may be omitted to signify an unspecified local time zone. +-- Time zones are given as an integer number of seconds from UTC. Use +-- 0 to mark UTC itself. Formatted strings with a time zone look +-- like this: +-- +-- format_timestamp(os.time(), 0) --> "2012-09-07T23:37:42+00:00" +-- format_timestamp(os.time(), 2*60*60) --> "2012-09-07T23:37:42+02:00" +-- +-- Without a time zone they look like this: +-- +-- format_timestamp(os.time()) --> "2012-09-07T23:37:42" +-- +-- +-- This function should be used for all dates emitted as part of NSE structured +-- output. +function format_timestamp(t, offset) + local tz_string = format_tz(offset) + offset = offset or 0 + return os.date("!%Y-%m-%dT%H:%M:%S", t + offset) .. tz_string +end + +--- Format the difference between times t2 and t1 +-- into a string in one of the forms (signs may vary): +-- * 0s +-- * -4s +-- * +2m38s +-- * -9h12m34s +-- * +5d17h05m06s +-- * -2y177d10h13m20s +-- The string shows t2 relative to t1; i.e., the +-- calculation is t2 minus t1. +function format_difftime(t2, t1) + local d, s, sign, yeardiff + + d = difftime(time(t2), time(t1)) + if d > 0 then + sign = "+" + elseif d < 0 then + sign = "-" + t2, t1 = t1, t2 + d = -d + else + sign = "" + end + -- t2 is always later than or equal to t1 here. + + -- The year is a tricky case because it's not a fixed number of days + -- the way a day is a fixed number of hours or an hour is a fixed + -- number of minutes. For example, the difference between 2008-02-10 + -- and 2009-02-10 is 366 days because 2008 was a leap year, but it + -- should be printed as 1y0d0h0m0s, not 1y1d0h0m0s. We advance t1 to be + -- the latest year such that it is still before t2, which means that its + -- year will be equal to or one less than t2's. The number of years + -- skipped is stored in yeardiff. + if t2.year > t1.year then + local tmpyear = t1.year + -- Put t1 in the same year as t2. + t1.year = t2.year + d = difftime(time(t2), time(t1)) + if d < 0 then + -- Too far. Back off one year. + t1.year = t2.year - 1 + d = difftime(time(t2), time(t1)) + end + yeardiff = t1.year - tmpyear + t1.year = tmpyear + else + yeardiff = 0 + end + + local s, sec, min + s = "" + -- Seconds (pad to two digits). + sec = d % 60 + d = math.floor(d / 60) + if d == 0 and yeardiff == 0 then + return sign .. string.format("%gs", sec) .. s + end + s = string.format("%02gs", sec) .. s + -- Minutes (pad to two digits). + min = d % 60 + d = math.floor(d / 60) + if d == 0 and yeardiff == 0 then + return sign .. string.format("%dm", min) .. s + end + s = string.format("%02dm", min) .. s + -- Hours. + s = string.format("%dh", d % 24) .. s + d = math.floor(d / 24) + if d == 0 and yeardiff == 0 then + return sign .. s + end + -- Days. + s = string.format("%dd", d) .. s + if yeardiff == 0 then return sign .. s end + -- Years. + s = string.format("%dy", yeardiff) .. s + return sign .. s +end + +--- Returns the current time in milliseconds since the epoch +-- @return The current time in milliseconds since the epoch +function clock_ms() + return nmap.clock() * 1000 +end + +--- Returns the current time in microseconds since the epoch +-- @return The current time in microseconds since the epoch +function clock_us() + return nmap.clock() * 1000000 +end + +---Get the indentation symbols at a given level. +local function format_get_indent(indent, at_end) + local str = "" + local had_continue = false + + if(not(at_end)) then + str = rep(' ', #indent) -- Was: "| " + else + for i = #indent, 1, -1 do + if(indent[i] and not(had_continue)) then + str = str .. " " -- Was: "|_ " + else + had_continue = true + str = str .. " " -- Was: "| " + end + end + end + + return str +end + +local function splitlines(s) + local result = {} + local i = 0 + + while i <= #s do + local b, e + b, e = string.find(s, "\r?\n", i) + if not b then + break + end + result[#result + 1] = string.sub(s, i, b - 1) + i = e + 1 + end + + if i <= #s then + result[#result + 1] = string.sub(s, i) + end + + return result +end + + +-- A helper for format_output (see below). +local function format_output_sub(status, data, indent) + if (#data == 0) then + return "" + end + + -- Used to put 'ERROR: ' in front of all lines on error messages + local prefix = "" + -- Initialize the output string to blank (or, if we're at the top, add a newline) + local output = {} + if(not(indent)) then + insert(output, '\n') + end + + if(not(status)) then + if(nmap.debugging() < 1) then + return nil + end + prefix = "ERROR: " + end + + -- If a string was passed, turn it into a table + if(type(data) == 'string') then + data = {data} + end + + -- Make sure we have an indent value + indent = indent or {} + + if(data['name']) then + if(data['warning'] and nmap.debugging() > 0) then + insert(output, format("%s%s%s (WARNING: %s)\n", + format_get_indent(indent), prefix, + data['name'], data['warning'])) + else + insert(output, format("%s%s%s\n", + format_get_indent(indent), prefix, + data['name'])) + end + elseif(data['warning'] and nmap.debugging() > 0) then + insert(output, format("%s%s(WARNING: %s)\n", + format_get_indent(indent), prefix, + data['warning'])) + end + + for i, value in ipairs(data) do + if(type(value) == 'table') then + -- Do a shallow copy of indent + local new_indent = {} + for _, v in ipairs(indent) do + insert(new_indent, v) + end + + if(i ~= #data) then + insert(new_indent, false) + else + insert(new_indent, true) + end + + insert(output, format_output_sub(status, value, new_indent)) + + elseif(type(value) == 'string') then + local lines = splitlines(value) + + for j, line in ipairs(lines) do + insert(output, format("%s %s%s\n", + format_get_indent(indent, i == #data and j == #lines), + prefix, line)) + end + end + end + + return concat(output) +end + +---Takes a table of output on the commandline and formats it for display to the +-- user. This is basically done by converting an array of nested tables into a +-- string. In addition to numbered array elements, each table can have a 'name' +-- and a 'warning' value. The 'name' will be displayed above the table, and +-- 'warning' will be displayed, with a 'WARNING' tag, if and only if debugging +-- is enabled. +-- +-- Here's an example of a table: +-- +-- local domains = {} +-- domains['name'] = "DOMAINS" +-- table.insert(domains, 'Domain 1') +-- table.insert(domains, 'Domain 2') +-- +-- local names = {} +-- names['name'] = "NAMES" +-- names['warning'] = "Not all names could be determined!" +-- table.insert(names, "Name 1") +-- +-- local response = {} +-- table.insert(response, "Apple pie") +-- table.insert(response, domains) +-- table.insert(response, names) +-- +-- return stdnse.format_output(true, response) +-- +-- +-- With debugging enabled, this is the output: +-- +-- Host script results: +-- | smb-enum-domains: +-- | Apple pie +-- | DOMAINS +-- | Domain 1 +-- | Domain 2 +-- | NAMES (WARNING: Not all names could be determined!) +-- |_ Name 1 +-- +-- +--@param status A boolean value dictating whether or not the script succeeded. +-- If status is false, and debugging is enabled, 'ERROR' is prepended +-- to every line. If status is false and debugging is disabled, no output +-- occurs. +--@param data The table of output. +--@param indent Used for indentation on recursive calls; should generally be set to +-- nil when callling from a script. +-- @return nil, if data is empty, otherwise a +-- multiline string. +function format_output(status, data, indent) + -- If data is nil, die with an error (I keep doing that by accident) + assert(data, "No data was passed to format_output()") + + -- Don't bother if we don't have any data + if (#data == 0) then + return nil + end + + local result = format_output_sub(status, data, indent) + + -- Check for an empty result + if(result == nil or #result == "" or result == "\n" or result == "\n") then + return nil + end + + return result +end + +-- Get the value of a script argument, or nil if the script argument was not +-- given. This works also for arguments given as top-level array values, like +-- --script-args=unsafe; for these it returns the value 1. +local function arg_value(argname) + if nmap.registry.args[argname] then + return nmap.registry.args[argname] + else + -- if scriptname.arg is not there, check "arg" + local argument_frags = strsplit("%.", argname) + if #argument_frags > 0 then + if nmap.registry.args[argument_frags[2]] then + return nmap.registry.args[argument_frags[2]] + end + end + end + + for _, v in ipairs(nmap.registry.args) do + if v == argname then + return 1 + end + end +end + +--- Parses the script arguments passed to the --script-args option. +-- +-- @usage +-- --script-args 'script.arg1=value,script.arg3,script-x.arg=value' +-- local arg1, arg2, arg3 = get_script_args('script.arg1','script.arg2','script.arg3') +-- => arg1 = value +-- => arg2 = nil +-- => arg3 = 1 +-- +-- --script-args 'displayall,unsafe,script-x.arg=value,script-y.arg=value' +-- local displayall, unsafe = get_script_args('displayall','unsafe') +-- => displayall = 1 +-- => unsafe = 1 +-- +-- --script-args 'dns-cache-snoop.mode=timed,dns-cache-snoop.domains={host1,host2}' +-- local mode, domains = get_script_args('dns-cache-snoop.mode', +-- 'dns-cache-snoop.domains') +-- => mode = 'timed' +-- => domains = {host1,host2} +-- +-- @param Arguments Script arguments to check. +-- @return Arguments values. +function get_script_args (...) + local args = {} + + for i, set in ipairs({...}) do + if type(set) == "string" then + set = {set} + end + for _, test in ipairs(set) do + local v = arg_value(test) + if v then + args[i] = v + break + end + end + end + + return unpack(args, 1, select("#", ...)) +end + +---Get the best possible hostname for the given host. This can be the target as given on +-- the commandline, the reverse dns name, or simply the ip address. +--@param host The host table (or a string that'll simply be returned). +--@return The best possible hostname, as a string. +function get_hostname(host) + if type(host) == "table" then + return host.targetname or ( host.name ~= '' and host.name ) or host.ip + else + return host + end +end + +---Retrieve an item from the registry, checking if each sub-key exists. If any key doesn't +-- exist, return nil. +function registry_get(subkeys) + local registry = nmap.registry + local i = 1 + + while(subkeys[i]) do + if(not(registry[subkeys[i]])) then + return nil + end + + registry = registry[subkeys[i]] + + i = i + 1 + end + + return registry +end + +--Check if the given element exists in the registry. If 'key' is nil, it isn't checked. +function registry_exists(subkeys, key, value) + local subkey = registry_get(subkeys) + + if(not(subkey)) then + return false + end + + for k, v in pairs(subkey) do + if((key == nil or key == k) and (v == value)) then -- TODO: if 'value' is a table, this fails + return true + end + end + + return false +end + +---Add an item to an array in the registry, creating all sub-keys if necessary. +-- For example, calling: +-- registry_add_array({'192.168.1.100', 'www', '80', 'pages'}, 'index.html') +-- Will create nmap.registry['192.168.1.100'] as a table, if necessary, then add a table +-- under the 'www' key, and so on. 'pages', finally, is treated as an array and the value +-- given is added to the end. +function registry_add_array(subkeys, value, allow_duplicates) + local registry = nmap.registry + local i = 1 + + -- Unless the user wants duplicates, make sure there aren't any + if(allow_duplicates ~= true) then + if(registry_exists(subkeys, nil, value)) then + return + end + end + + while(subkeys[i]) do + if(not(registry[subkeys[i]])) then + registry[subkeys[i]] = {} + end + registry = registry[subkeys[i]] + i = i + 1 + end + + -- Make sure the value isn't already in the table + for _, v in pairs(registry) do + if(v == value) then + return + end + end + insert(registry, value) +end + +---Similar to registry_add_array, except instead of adding a value to the +-- end of an array, it adds a key:value pair to the table. +function registry_add_table(subkeys, key, value, allow_duplicates) + local registry = nmap.registry + local i = 1 + + -- Unless the user wants duplicates, make sure there aren't any + if(allow_duplicates ~= true) then + if(registry_exists(subkeys, key, value)) then + return + end + end + + while(subkeys[i]) do + if(not(registry[subkeys[i]])) then + registry[subkeys[i]] = {} + end + registry = registry[subkeys[i]] + i = i + 1 + end + + registry[key] = value +end + + +--- This function allows you to create worker threads that may perform +-- network tasks in parallel with your script thread. +-- +-- Any network task (e.g. socket:connect(...)) will cause the +-- running thread to yield to NSE. This allows network tasks to appear to be +-- blocking while being able to run multiple network tasks at once. +-- While this is useful for running multiple separate scripts, it is +-- unfortunately difficult for a script itself to perform network tasks in +-- parallel. In order to allow scripts to also have network tasks running in +-- parallel, we provide this function, stdnse.new_thread, to +-- create a new thread that can perform its own network related tasks +-- in parallel with the script. +-- +-- The script launches the worker thread by calling the new_thread +-- function with the parameters: +-- * The main Lua function for the script to execute, similar to the script action function. +-- * The variable number of arguments to be passed to the worker's main function. +-- +-- The stdnse.new_thread function will return two results: +-- * The worker thread's base (main) coroutine (useful for tracking status). +-- * A status query function (described below). +-- +-- The status query function shall return two values: +-- * The result of coroutine.status using the worker thread base coroutine. +-- * The error object thrown that ended the worker thread or nil if no error was thrown. This is typically a string, like most Lua errors. +-- +-- Note that NSE discards all return values of the worker's main function. You +-- must use function parameters, upvalues or environments to communicate +-- results. +-- +-- You should use the condition variable (nmap.condvar) +-- and mutex (nmap.mutex) facilities to coordinate with your +-- worker threads. Keep in mind that Nmap is single threaded so there are +-- no (memory) issues in synchronization to worry about; however, there +-- is resource contention. Your resources are usually network +-- bandwidth, network sockets, etc. Condition variables are also useful if the +-- work for any single thread is dynamic. For example, a web server spider +-- script with a pool of workers will initially have a single root html +-- document. Following the retrieval of the root document, the set of +-- resources to be retrieved (the worker's work) will become very large +-- (an html document adds many new hyperlinks (resources) to fetch). +--@name new_thread +--@class function +--@param main The main function of the worker thread. +--@param ... The arguments passed to the main worker thread. +--@return co The base coroutine of the worker thread. +--@return info A query function used to obtain status information of the worker. +--@usage +--local requests = {"/", "/index.html", --[[ long list of objects ]]} +-- +--function thread_main (host, port, responses, ...) +-- local condvar = nmap.condvar(responses); +-- local what = {n = select("#", ...), ...}; +-- local allReqs = nil; +-- for i = 1, what.n do +-- allReqs = http.pGet(host, port, what[i], nil, nil, allReqs); +-- end +-- local p = assert(http.pipeline(host, port, allReqs)); +-- for i, response in ipairs(p) do responses[#responses+1] = response end +-- condvar "signal"; +--end +-- +--function many_requests (host, port) +-- local threads = {}; +-- local responses = {}; +-- local condvar = nmap.condvar(responses); +-- local i = 1; +-- repeat +-- local j = math.min(i+10, #requests); +-- local co = stdnse.new_thread(thread_main, host, port, responses, +-- table.unpack(requests, i, j)); +-- threads[co] = true; +-- i = j+1; +-- until i > #requests; +-- repeat +-- condvar "wait"; +-- for thread in pairs(threads) do +-- if coroutine.status(thread) == "dead" then threads[thread] = nil end +-- end +-- until next(threads) == nil; +-- return responses; +--end +do end -- no function here, see nse_main.lua + +--- Returns the base coroutine of the running script. +-- +-- A script may be resuming multiple coroutines to facilitate its own +-- collaborative multithreading design. Because there is a "root" or "base" +-- coroutine that lets us determine whether the script is still active +-- (that is, the script did not end, possibly due to an error), we provide +-- this stdnse.base function that will retrieve the base +-- coroutine of the script. This base coroutine is the coroutine that runs +-- the action function. +-- +-- The base coroutine is useful for many reasons but here are some common +-- uses: +-- * We want to attribute the ownership of an object (perhaps a network socket) to a script. +-- * We want to identify if the script is still alive. +--@name base +--@class function +--@return coroutine Returns the base coroutine of the running script. +do end -- no function here, see nse_main.lua + +--- The Lua Require Function with errors silenced. +-- +-- See the Lua manual for description of the require function. This modified +-- version allows the script to quietly fail at loading if a required +-- library does not exist. +-- +--@name silent_require +--@class function +--@usage stdnse.silent_require "openssl" +do end -- no function here, see nse_main.lua + + + +---Checks if the port is in the port range +-- For example, calling: +-- in_port_range({number=31337,protocol="udp"},"T:15,50-75,U:31334-31339") +-- would result in a true value +--@param port a port structure containing keys port number(number) and protocol(string) +--@param port_range a port range string in Nmap standard format (ex. "T:80,1-30,U:31337,21-25") +--@returns boolean indicating whether the port is in the port range +function in_port_range(port,port_range) + assert(port and type(port.number)=="number" and type(port.protocol)=="string" and + (port.protocol=="udp" or port.protocol=="tcp"),"Port structure missing or invalid: port={ number=, protocol= }") + assert((type(port_range)=="string" or type(port_range)=="number") and port_range~="","Incorrect port range specification.") + + -- Proto - true for TCP, false for UDP + local proto + if(port.protocol=="tcp") then proto = true else proto = false end + + --TCP flag for iteration - true for TCP, false for UDP, if not specified we presume TCP + local tcp_flag = true + + -- in case the port_range is a single number + if type(port_range)=="number" then + if proto and port_range==port.number then return true + else return false + end + end + + --clean the string a bit + port_range=port_range:gsub("%s+","") + + -- single_pr - single port range + for i, single_pr in ipairs(strsplit(",",port_range)) do + if single_pr:match("T:") then + tcp_flag = true + single_pr = single_pr:gsub("T:","") + else + if single_pr:match("U:") then + tcp_flag = false + single_pr = single_pr:gsub("U:","") + end + end + + -- compare ports only when the port's protocol is the same as + -- the current single port range + if tcp_flag == proto then + local pone = single_pr:match("^(%d+)$") + if pone then + pone = tonumber(pone) + assert(pone>-1 and pone<65536, "Port range number out of range (0-65535).") + + if pone == port.number then + return true + end + else + local pstart, pend = single_pr:match("^(%d+)%-(%d+)$") + pstart, pend = tonumber(pstart), tonumber(pend) + assert(pstart,"Incorrect port range specification.") + assert(pstart<=pend,"Incorrect port range specification, the starting port should have a smaller value than the ending port.") + assert(pstart>-1 and pstart<65536 and pend>-1 and pend<65536, "Port range number out of range (0-65535).") + + if port.number >=pstart and port.number <= pend then + return true + end + end + end + end + -- if no match is found then the port doesn't belong to the port_range + return false +end + +--- Module function that mimics some behavior of Lua 5.1 module function. +-- +-- This convenience function returns a module environment to set the _ENV +-- upvalue. The _NAME, _PACKAGE, and _M fields are set as in the Lua 5.1 +-- version of this function. Each option function (e.g. stdnse.seeall) +-- passed is run with the new environment, in order. +-- +-- @see stdnse.seeall +-- @see strict +-- @usage +-- _ENV = stdnse.module(name, stdnse.seeall, require "strict"); +-- @param name The module name. +-- @param ... Option functions which modify the environment of the module. +function module (name, ...) + local env = {}; + env._NAME = name; + env._PACKAGE = name:match("(.+)%.[^.]+$"); + env._M = env; + local mods = pack(...); + for i = 1, mods.n do + mods[i](env); + end + return env; +end + +--- Change environment to load global variables. +-- +-- Option function for use with stdnse.module. It is the same +-- as package.seeall from Lua 5.1. +-- +-- @see stdnse.module +-- @usage +-- _ENV = stdnse.module(name, stdnse.seeall); +-- @param env Environment to change. +function seeall (env) + local m = getmetatable(env) or {}; + m.__index = _G; + setmetatable(env, m); +end + +--- Return a table that keeps elements in order of insertion. +-- +-- The pairs function, called on a table returned by this function, will yield +-- elements in the order they were inserted. This function is meant to be used +-- to construct output tables returned by scripts. +-- +-- Reinserting a key that is already in the table does not change its position +-- in the order. However, removing a key by assigning to nil and +-- then doing another assignment will move the key to the end of the order. +-- +-- @return An ordered table. +function output_table () + local t = {} + local order = {} + local function iterator () + for i, key in ipairs(order) do + coroutine.yield(key, t[key]) + end + end + local mt = { + __newindex = function (_, k, v) + if t[k] == nil and v ~= nil then + -- New key? + table.insert(order, k) + elseif v == nil then + -- Deleting an existing key? + for i, key in ipairs(order) do + if key == k then + table.remove(order, i) + break + end + end + end + rawset(t, k, v) + end, + __index = function (_, k) + return t[k] + end, + __pairs = function (_) + return coroutine.wrap(iterator) + end, + __call = function (_) -- hack to mean "not_empty?" + return not not next(order) + end, + __len = function (_) + return #order + end + } + return setmetatable({}, mt) +end + +--- A pretty printer for Lua objects. +-- +-- Takes an object (usually a table) and prints it using the +-- printer function. The printer function takes a sole string +-- argument and will be called repeatedly. +-- +-- @args obj The object to pretty print. +-- @args printer The printer function. +function pretty_printer (obj, printer) + if printer == nil then printer = print end + + local function aux (obj, spacing) + local t = type(obj) + if t == "table" then + printer "{\n" + for k, v in pairs(obj) do + local spacing = spacing.."\t" + printer(spacing) + printer "[" + aux(k, spacing) + printer "] = " + aux(v, spacing) + printer ",\n" + end + printer(spacing.."}") + elseif t == "string" then + printer(format("%q", obj)) + else + printer(tostring(obj)) + end + end + + return aux(obj, "") +end + +-- This pattern must match the percent sign '%' since it is used in +-- escaping. +local FILESYSTEM_UNSAFE = "[^a-zA-Z0-9._-]" +--- +-- Escape a string to remove bytes and strings that may have meaning to +-- a filesystem, such as slashes. All bytes are escaped, except for: +-- * alphabetic a-z and A-Z, digits 0-9, . _ - +-- In addition, the strings "." and ".." have +-- their characters escaped. +-- +-- Bytes are escaped by a percent sign followed by the two-digit +-- hexadecimal representation of the byte value. +-- * filename_escape("filename.ext") --> "filename.ext" +-- * filename_escape("input/output") --> "input%2foutput" +-- * filename_escape(".") --> "%2e" +-- * filename_escape("..") --> "%2e%2e" +-- This escaping is somewhat like that of JavaScript +-- encodeURIComponent, except that fewer bytes are +-- whitelisted, and it works on bytes, not Unicode characters or UTF-16 +-- code points. +function filename_escape(s) + if s == "." then + return "%2e" + elseif s == ".." then + return "%2e%2e" + else + return (string.gsub(s, FILESYSTEM_UNSAFE, function (c) + return string.format("%%%02x", string.byte(c)) + end)) + end +end + +return _ENV; From 3508c7d530a82ce80586930f8318aef419572449 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:52:45 +0100 Subject: [PATCH 04/12] Delete fixed_script directory --- fixed_script/ms-sql-info.nse | 266 ------- fixed_script/msrpc-enum.nse | 111 --- fixed_script/ntlm-methods.nse | 73 -- fixed_script/redis-info.nse | 250 ------- fixed_script/smb-protocols.nse | 70 -- fixed_script/stdnse.lua | 1233 -------------------------------- 6 files changed, 2003 deletions(-) delete mode 100644 fixed_script/ms-sql-info.nse delete mode 100644 fixed_script/msrpc-enum.nse delete mode 100644 fixed_script/ntlm-methods.nse delete mode 100644 fixed_script/redis-info.nse delete mode 100644 fixed_script/smb-protocols.nse delete mode 100644 fixed_script/stdnse.lua diff --git a/fixed_script/ms-sql-info.nse b/fixed_script/ms-sql-info.nse deleted file mode 100644 index f920486a..00000000 --- a/fixed_script/ms-sql-info.nse +++ /dev/null @@ -1,266 +0,0 @@ -local mssql = require "mssql" -local nmap = require "nmap" -local smb = require "smb" -local shortport = require "shortport" -local stdnse = require "stdnse" - --- -*- mode: lua -*- --- vim: set filetype=lua : - -description = [[ -Attempts to determine configuration and version information for Microsoft SQL -Server instances. - -SQL Server credentials required: No (will not benefit from -mssql.username & mssql.password). -Run criteria: -* Host script: Will always run. -* Port script: N/A - -NOTE: Unlike previous versions, this script will NOT attempt to log in to SQL -Server instances. Blank passwords can be checked using the -ms-sql-empty-password script. E.g.: -nmap -sn --script ms-sql-empty-password --script-args mssql.instance-all - -The script uses two means of getting version information for SQL Server instances: -* Querying the SQL Server Browser service, which runs by default on UDP port -1434 on servers that have SQL Server 2000 or later installed. However, this -service may be disabled without affecting the functionality of the instances. -Additionally, it provides imprecise version information. -* Sending a probe to the instance, causing the instance to respond with -information including the exact version number. This is the same method that -Nmap uses for service versioning; however, this script can also do the same for -instances accessible via Windows named pipes, and can target all of the -instances listed by the SQL Server Browser service. - -In the event that the script can connect to the SQL Server Browser service -(UDP 1434) but is unable to connect directly to the instance to obtain more -accurate version information (because ports are blocked or the mssql.scanned-ports-only -argument has been used), the script will rely only upon the version number -provided by the SQL Server Browser/Monitor, which has the following limitations: -* For SQL Server 2000 and SQL Server 7.0 instances, the RTM version number is -always given, regardless of any service packs or patches installed. -* For SQL Server 2005 and later, the version number will reflect the service -pack installed, but the script will not be able to tell whether patches have -been installed. - -Where possible, the script will determine major version numbers, service pack -levels and whether patches have been installed. However, in cases where -particular determinations can not be made, the script will report only what can -be confirmed. - -NOTE: Communication with instances via named pipes depends on the smb -library. To communicate with (and possibly to discover) instances via named pipes, -the host must have at least one SMB port (e.g. TCP 445) that was scanned and -found to be open. Additionally, named pipe connections may require Windows -authentication to connect to the Windows host (via SMB) in addition to the -authentication required to connect to the SQL Server instances itself. See the -documentation and arguments for the smb library for more information. - -NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate -with ports that were not included in the port list for the Nmap scan. This can -be disabled using the mssql.scanned-ports-only script argument. -]] ---- --- @usage --- nmap -p 445 --script ms-sql-info --- nmap -p 1433 --script ms-sql-info --script-args mssql.instance-port=1433 --- --- @output --- | ms-sql-info: --- | Windows server name: WINXP --- | 192.168.100.128\PROD: --- | Instance name: PROD --- | Version: --- | name: Microsoft SQL Server 2000 SP3 --- | number: 8.00.760 --- | Product: Microsoft SQL Server 2000 --- | Service pack level: SP3 --- | Post-SP patches applied: No --- | TCP port: 1278 --- | Named pipe: \\192.168.100.128\pipe\MSSQL$PROD\sql\query --- | Clustered: No --- | 192.168.100.128\SQLFIREWALLED: --- | Instance name: SQLFIREWALLED --- | Version: --- | name: Microsoft SQL Server 2008 RTM --- | Product: Microsoft SQL Server 2008 --- | Service pack level: RTM --- | TCP port: 4343 --- | Clustered: No --- | \\192.168.100.128\pipe\sql\query: --- | Version: --- | name: Microsoft SQL Server 2005 SP3+ --- | number: 9.00.4053 --- | Product: Microsoft SQL Server 2005 --- | Service pack level: SP3 --- | Post-SP patches applied: Yes --- |_ Named pipe: \\192.168.100.128\pipe\sql\query --- --- @xmloutput --- WINXP --- --- PROD ---
--- Microsoft SQL Server 2000 SP3 --- 8.00.760 --- Microsoft SQL Server 2000 --- SP3 --- No ---
--- 1278 --- \\192.168.100.128\pipe\MSSQL$PROD\sql\query --- No --- --- --- SQLFIREWALLED ---
--- Microsoft SQL Server 2008 RTM --- Microsoft SQL Server 2008 --- RTM ---
--- 4343 --- No --- --- ---
--- Microsoft SQL Server 2005 SP3+ --- 9.00.4053 --- Microsoft SQL Server 2005 --- SP3 --- Yes ---
--- \\192.168.100.128\pipe\sql\query --- - --- rev 1.0 (2007-06-09) --- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers) --- rev 1.2 (2010-10-03 - Added Broadcast support ) --- rev 1.3 (2010-10-10 - Added prerule and newtargets support ) --- rev 1.4 (2011-01-24 - Revised logic in order to get version data without logging in; --- added functionality to interpret version in terms of SP level, etc. --- added script arg to prevent script from connecting to ports that --- weren't in original Nmap scan ) --- rev 1.5 (2011-02-01 - Moved discovery functionality into ms-sql-discover.nse and --- broadcast-ms-sql-discovery.nse ) --- rev 1.6 (2014-09-04 - Added structured output Daniel Miller) - -author = {"Chris Woodbury", "Thomas Buchanan"} - -license = "Same as Nmap--See https://nmap.org/book/man-legal.html" - -categories = {"default", "discovery", "safe"} - -portrule = shortport.port_or_service(1433, "ms-sql-s") - - - ---- Returns formatted output for the given version data -local function create_version_output_table( versionInfo ) - local versionOutput = stdnse.output_table() - - versionOutput["name"] = versionInfo:ToString() - if ( versionInfo.source ~= "SSRP" ) then - versionOutput["number"] = versionInfo.versionNumber - end - versionOutput["Product"] = versionInfo.productName - versionOutput["Service pack level"] = versionInfo.servicePackLevel - versionOutput["Post-SP patches applied"] = versionInfo.patched - - return versionOutput -end - - ---- Returns formatted output for the given instance -local function create_instance_output_table( instance ) - - -- if we didn't get anything useful (due to errors or the port not actually - -- being SQL Server), don't report anything - if not ( instance.instanceName or instance.version ) then return nil end - - local instanceOutput = stdnse.output_table() - - instanceOutput["Instance name"] = instance.instanceName - if instance.version then - instanceOutput["Version"] = create_version_output_table( instance.version ) - end - if instance.port then instanceOutput["TCP port"] = instance.port.number end - instanceOutput["Named pipe"] = instance.pipeName - instanceOutput["Clustered"] = instance.isClustered - - return instanceOutput - -end - - ---- Processes a single instance, attempting to determine its version, etc. -local function process_instance( instance ) - - local foundVersion = false - local ssnetlibVersion - - -- If possible and allowed (see 'mssql.scanned-ports-only' argument), we'll - -- connect to the instance to get an accurate version number - if ( instance:HasNetworkProtocols() ) then - local ssnetlibVersion - foundVersion, ssnetlibVersion = mssql.Helper.GetInstanceVersion( instance ) - if ( foundVersion ) then - instance.version = ssnetlibVersion - stdnse.debug1("Retrieved SSNetLib version for %s.", instance:GetName() ) - else - stdnse.debug1("Could not retrieve SSNetLib version for %s.", instance:GetName() ) - end - end - - -- If we didn't get a version from SSNetLib, give the user some detail as to why - if ( not foundVersion ) then - if ( not instance:HasNetworkProtocols() ) then - stdnse.debug1("%s has no network protocols enabled.", instance:GetName() ) - end - if ( instance.version ) then - stdnse.debug1("Using version number from SSRP response for %s.", instance:GetName() ) - else - stdnse.debug1("Version info could not be retrieved for %s.", instance:GetName() ) - end - end - - -- Give some version info back to Nmap - if ( instance.port and instance.version ) then - instance.version:PopulateNmapPortVersion( instance.port ) - nmap.set_port_version( instance.host, instance.port) - end - -end - - -action = function( host ) - local scriptOutput = stdnse.output_table() - - local status, instanceList = mssql.Helper.GetTargetInstances( host ) - -- if no instances were targeted, then display info on all - if ( not status ) then - if ( not mssql.Helper.WasDiscoveryPerformed( host ) ) then - mssql.Helper.Discover( host ) - end - instanceList = mssql.Helper.GetDiscoveredInstances( host ) - end - - - if ( not instanceList ) then - return stdnse.format_output( false, instanceList or "" ) - else - for _, instance in ipairs( instanceList ) do - if instance.serverName then - scriptOutput["Windows server name"] = instance.serverName - break - end - end - for _, instance in pairs( instanceList ) do - process_instance( instance ) - scriptOutput[instance:GetName()] = create_instance_output_table( instance ) - end - end - - return scriptOutput -end - diff --git a/fixed_script/msrpc-enum.nse b/fixed_script/msrpc-enum.nse deleted file mode 100644 index 84d3e865..00000000 --- a/fixed_script/msrpc-enum.nse +++ /dev/null @@ -1,111 +0,0 @@ -local msrpc = require "msrpc" -local smb = require "smb" -local stdnse = require "stdnse" -local table = require "table" -local shortport = require "shortport" - -description = [[ -Queries an MSRPC endpoint mapper for a list of mapped -services and displays the gathered information. - -As it is using smb library, you can specify optional -username and password to use. - -Script works much like Microsoft's rpcdump tool -or dcedump tool from SPIKE fuzzer. -]] ---- --- @usage nmap --script=msrpc-enum --- --- @output --- PORT STATE SERVICE REASON --- 445/tcp open microsoft-ds syn-ack --- --- Host script results: --- | msrpc-enum: --- | --- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5 --- | annotation: DHCP Client LRPC Endpoint --- | ncalrpc: dhcpcsvc --- | --- | uuid: 12345678-1234-abcd-ef00-0123456789ab --- | annotation: IPSec Policy agent endpoint --- | ncalrpc: audit --- | --- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5 --- | ip_addr: 0.0.0.0 --- | annotation: DHCP Client LRPC Endpoint --- | tcp_port: 49153 --- | --- --- | --- | uuid: 12345678-1234-abcd-ef00-0123456789ab --- | annotation: IPSec Policy agent endpoint --- | ncalrpc: securityevent --- | --- | uuid: 12345678-1234-abcd-ef00-0123456789ab --- | annotation: IPSec Policy agent endpoint --- |_ ncalrpc: protected_storage --- --- @xmloutput --- -snip- --- --- c100beab-d33a-4a4b-bf23-bbef4663d017 --- wcncsvc.wcnprpc --- wcncsvc.wcnprpc ---
--- --- 6b5bdd1e-528c-422c-af8c-a4079be4fe48 --- Remote Fw APIs --- 49158 --- 0.0.0.0 ---
--- --- 12345678-1234-abcd-ef00-0123456789ab --- IPSec Policy agent endpoint --- 49158 --- 0.0.0.0 ---
--- -snip- - -author = "Aleksandar Nikolic" -license = "Same as Nmap--See https://nmap.org/book/man-legal.html" -categories = {"safe","discovery"} - -portrule = shortport.port_or_service(445, "microsoft-ds") - -action = function(host,port) - local status, smbstate - status, smbstate = msrpc.start_smb(host,msrpc.EPMAPPER_PATH,true) - if(status == false) then - stdnse.debug1("SMB: " .. smbstate) - return false, smbstate - end - local bind_result,epresult -- bind to endpoint mapper service - status, bind_result = msrpc.bind(smbstate,msrpc.EPMAPPER_UUID, msrpc.EPMAPPER_VERSION, nil) - if(status == false) then - msrpc.stop_smb(smbstate) - stdnse.debug1("SMB: " .. bind_result) - return false, bind_result - end - local results = {} - status, epresult = msrpc.epmapper_lookup(smbstate,nil) -- get the initial handle - if not status then - stdnse.debug1("SMB: " .. epresult) - return false, epresult - - end - local handle = epresult.new_handle - epresult.new_handle = nil - table.insert(results,epresult) - - while not (epresult == nil) do - status, epresult = msrpc.epmapper_lookup(smbstate,handle) -- get next result until there are no more - if not status then - break - end - epresult.new_handle = nil - table.insert(results,epresult) - end - return results -end diff --git a/fixed_script/ntlm-methods.nse b/fixed_script/ntlm-methods.nse deleted file mode 100644 index 60830f12..00000000 --- a/fixed_script/ntlm-methods.nse +++ /dev/null @@ -1,73 +0,0 @@ -local http = require "http" -local shortport = require "shortport" -local stdnse = require "stdnse" -local string = require "string" -local table = require "table" - -description = [[ -Returns authentication methods a winrm server supports. -]] - ---- --- @usage --- nmap --script winrm-auth-methods -p 5985 --- --- @output --- 5985/tcp open wsman --- | winrm-auth-methods: --- | Accepted Authentication Methods: --- | Negotiate --- | Basic --- | Kerberos --- |_ CredSSP - -author = "Evangelos Deirmentzoglou" -license = "Same as Nmap--See https://nmap.org/book/man-legal.html" -categories = {"default", "discovery", "safe"} - -portrule = shortport.port_or_service({5985, 5986},{'wsman','wsmans'}) - -function generate_random_string(len, charset) - local t = {} - local ascii_A = 65 - local ascii_Z = 90 - if charset then - for i=1,len do - t[i]=charset[math.random(#charset)] - end - else - for i=1,len do - t[i]=string.char(math.random(ascii_A,ascii_Z)) - end - end - return table.concat(t) -end - -action = function(host, port) - - local r = {} - local result = stdnse.output_table() - local randoms = generate_random_string(5) - local url = "/wsman" - local response = http.post( host, port, url, nil, nil, randoms ) - - if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Negotiate") then - table.insert(r, "Negotiate") - end - if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Basic") then - table.insert(r, "Basic") - end - if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Kerberos") then - table.insert(r, "Kerberos") - end - if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "CredSSP") then - table.insert(r, "CredSSP") - end - if #r > 0 then - result = r - else - result = "Server does not support authentication." - end - - return result -end diff --git a/fixed_script/redis-info.nse b/fixed_script/redis-info.nse deleted file mode 100644 index fcfd4b7a..00000000 --- a/fixed_script/redis-info.nse +++ /dev/null @@ -1,250 +0,0 @@ -local creds = require "creds" -local redis = require "redis" -local nmap = require "nmap" -local shortport = require "shortport" -local stdnse = require "stdnse" -local string = require "string" -local stringaux = require "stringaux" -local table = require "table" -local tableaux = require "tableaux" -local ipOps = require "ipOps" - -description = [[ -Retrieves information (such as version number and architecture) from a Redis key-value store. -]] - ---- --- @usage --- nmap -p 6379 --script redis-info --- --- @output --- PORT STATE SERVICE --- 6379/tcp open unknown --- | redis-info: --- | Version 2.2.11 --- | Architecture 64 bits --- | Process ID 17821 --- | Used CPU (sys) 2.37 --- | Used CPU (user) 1.02 --- | Connected clients 1 --- | Connected slaves 0 --- | Used memory 780.16K --- | Role master --- | Bind addresses: --- | 192.168.121.101 --- | Active channels: --- | testChannel --- | bidChannel --- | Client connections: --- | 192.168.171.101 --- |_ 72.14.177.105 --- --- - -author = {"Patrik Karlsson", "Vasiliy Kulikov"} -license = "Same as Nmap--See https://nmap.org/book/man-legal.html" -categories = {"discovery", "safe"} -dependencies = {"redis-brute"} - - -portrule = shortport.port_or_service(6379, "redis") - -local function fail(err) return stdnse.format_output(false, err) end - -local function cb_parse_version(host, port, val) - port.version.version = val - port.version.cpe = port.version.cpe or {} - table.insert(port.version.cpe, 'cpe:/a:redis:redis:' .. val) - nmap.set_port_version(host, port) - return val -end - -local function cb_parse_architecture(host, port, val) - val = ("%s bits"):format(val) - port.version.extrainfo = val - nmap.set_port_version(host, port) - return val -end - -local filter = { - - ["redis_version"] = { name = "Version", func = cb_parse_version }, - ["os"] = { name = "Operating System" }, - ["arch_bits"] = { name = "Architecture", func = cb_parse_architecture }, - ["process_id"] = { name = "Process ID"}, - ["uptime"] = { name = "Uptime", func = function(h, p, v) return ("%s seconds"):format(v) end }, - ["used_cpu_sys"]= { name = "Used CPU (sys)"}, - ["used_cpu_user"] = { name = "Used CPU (user)"}, - ["connected_clients"] = { name = "Connected clients"}, - ["connected_slaves"] = { name = "Connected slaves"}, - ["used_memory_human"] = { name = "Used memory"}, - ["role"] = { name = "Role"} - -} - -local order = { - "redis_version", "os", "arch_bits", "process_id", "used_cpu_sys", - "used_cpu_user", "connected_clients", "connected_slaves", - "used_memory_human", "role" -} - -local extras = { - { - -- https://redis.io/commands/config-get/ - "Bind addresses", {"CONFIG", "GET", "bind"}, function (data) - if data[1] ~= "bind" or not data[2] then - return nil - end - local restab = stringaux.strsplit(" ", data[2]) - for i, ip in ipairs(restab) do - if ip == '' then restab[i] = '0.0.0.0' end - end - return restab - end - }, - { - -- https://redis.io/commands/pubsub-channels/ - "Active channels", {"PUBSUB", "CHANNELS"}, function (data) - local channels = {} - local omitted = 0 - local limit = nmap.verbosity() <= 1 and 20 or false - for _, channel in ipairs(data) do - if limit and #channels >= limit then - omitted = omitted + 1 - else - table.insert(channels, channel) - end - end - - if omitted > 0 then - table.insert(channels, ("(omitted %s item(s), use verbose mode -v to show them)"):format(omitted)) - end - return #channels > 0 and channels or nil - end - }, - { - -- https://redis.io/commands/client-list/ - "Client connections", {"CLIENT", "LIST"}, function(data) - if not data then - stdnse.debug1("Failed to parse response from server") - return nil - end - - local client_ips = {} - for conn in data:gmatch("[^\n]+") do - local ip = conn:match("%f[^%s\0]addr=%[?([%x:.]+)%]?:%d+%f[%s\0]") - if ip then - local binip = ipOps.ip_to_str(ip) - if binip then - -- prepending length sorts IPv4 and IPv6 separately - client_ips[string.pack("s1", binip)] = binip - end - end - end - - local out = {} - local keys = tableaux.keys(client_ips) - table.sort(keys) - for _, packed in ipairs(keys) do - table.insert(out, ipOps.str_to_ip(client_ips[packed])) - end - return #out > 0 and out or nil - end - }, - { - -- https://redis.io/commands/cluster-nodes/ - "Cluster nodes", {"CLUSTER", "NODES"}, function(data) - if not data then - stdnse.debug1("Failed to parse response from server") - return nil - end - - local out = {} - for node in data:gmatch("[^\n]+") do - local ipport, flags = node:match("^%x+%s+([%x.:%[%]]+)@?%d*%s+(%S+)") - if ipport then - table.insert(out, ("%s (%s)"):format(ipport, flags)) - else - stdnse.debug1("Unable to parse cluster node info") - end - end - return #out > 0 and out or nil - end - }, -} - -action = function(host, port) - - local helper = redis.Helper:new(host, port) - local status = helper:connect() - if( not(status) ) then - return fail("Failed to connect to server") - end - - -- do we have a service password - local c = creds.Credentials:new(creds.ALL_DATA, host, port) - local cred = c:getCredentials(creds.State.VALID + creds.State.PARAM)() - - if ( cred and cred.pass ) then - local status, response = helper:reqCmd("AUTH", cred.pass) - if ( not(status) ) then - helper:close() - return fail(response) - end - end - - local status, response = helper:reqCmd("INFO") - if ( not(status) ) then - helper:close() - return fail(response) - end - - if ( redis.Response.Type.ERROR == response.type ) then - if ( "-ERR operation not permitted" == response.data ) or - ( "-NOAUTH Authentication required." == response.data ) then - return fail("Authentication required") - end - return fail(response.data) - end - - local restab = stringaux.strsplit("\r\n", response.data) - if ( not(restab) or 0 == #restab ) then - return fail("Failed to parse response from server") - end - - local kvs = {} - for _, item in ipairs(restab) do - local k, v = item:match("^([^:]*):(.*)$") - if k ~= nil then - kvs[k] = v - end - end - - local result = stdnse.output_table() - for _, item in ipairs(order) do - if kvs[item] then - local name = filter[item].name - local val - - if filter[item].func then - val = filter[item].func(host, port, kvs[item]) - else - val = kvs[item] - end - result[name] = val - end - end - - for i=1, #extras do - local name = extras[i][1] - local cmd = extras[i][2] - local process = extras[i][3] - - local status, response = helper:reqCmd(table.unpack(cmd)) - if status and redis.Response.Type.ERROR ~= response.type then - result[name] = process(response.data) - end - end - helper:close() - return result -end diff --git a/fixed_script/smb-protocols.nse b/fixed_script/smb-protocols.nse deleted file mode 100644 index 6dc71d8b..00000000 --- a/fixed_script/smb-protocols.nse +++ /dev/null @@ -1,70 +0,0 @@ -local smb = require "smb" -local stdnse = require "stdnse" -local nmap = require "nmap" -local shortport = require "shortport" - -description = [[ -Attempts to list the supported protocols and dialects of a SMB server. - -The script attempts to initiate a connection using the dialects: -* NT LM 0.12 (SMBv1) -* 2.0.2 (SMBv2) -* 2.1 (SMBv2) -* 3.0 (SMBv3) -* 3.0.2 (SMBv3) -* 3.1.1 (SMBv3) - -Additionally if SMBv1 is found enabled, it will mark it as insecure. This -script is the successor to the (removed) smbv2-enabled script. -]] - ---- --- @usage nmap -p445 --script smb-protocols --- @usage nmap -p139 --script smb-protocols --- --- @output --- | smb-protocols: --- | dialects: --- | NT LM 0.12 (SMBv1) [dangerous, but default] --- | 2.0.2 --- | 2.1 --- | 3.0 --- | 3.0.2 --- |_ 3.1.1 --- --- @xmloutput --- --- NT LM 0.12 (SMBv1) [dangerous, but default] --- 2.0.2 --- 2.1 --- 3.0 --- 3.0.2 --- 3.1.1 ---
---- - -author = "Paulino Calderon" -license = "Same as Nmap--See https://nmap.org/book/man-legal.html" -categories = {"safe", "discovery"} - -portrule = shortport.port_or_service(445, "microsoft-ds") - -action = function(host,port) - local status, supported_dialects = smb.list_dialects(host) - if status then - for i, v in pairs(supported_dialects) do -- Mark SMBv1 as insecure - if v == "NT LM 0.12" then - supported_dialects[i] = v .. " (SMBv1) [dangerous, but default]" - end - end - if #supported_dialects > 0 then - local output = stdnse.output_table() - output.dialects = supported_dialects - return output - end - end - stdnse.debug1("No dialects were accepted") - if nmap.verbosity()>1 then - return "No dialects accepted. Something may be blocking the responses" - end -end diff --git a/fixed_script/stdnse.lua b/fixed_script/stdnse.lua deleted file mode 100644 index 909a9322..00000000 --- a/fixed_script/stdnse.lua +++ /dev/null @@ -1,1233 +0,0 @@ ---- --- Standard Nmap Scripting Engine functions. This module contains various handy --- functions that are too small to justify modules of their own. --- --- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html --- @class module --- @name stdnse - -local _G = require "_G" -local coroutine = require "coroutine" -local math = require "math" -local nmap = require "nmap" -local os = require "os" -local string = require "string" -local table = require "table" -local assert = assert; -local error = error; -local getmetatable = getmetatable; -local ipairs = ipairs -local pairs = pairs -local next = next -local rawset = rawset -local require = require; -local select = select -local setmetatable = setmetatable; -local tonumber = tonumber; -local tostring = tostring; -local type = type - -local ceil = math.ceil -local max = math.max - -local format = string.format; -local rep = string.rep - -local concat = table.concat; -local insert = table.insert; -local pack = table.pack; -local unpack = table.unpack; - -local difftime = os.difftime; -local time = os.time; - -local EMPTY = {}; -- Empty constant table - -_ENV = require "strict" {}; - ---- Sleeps for a given amount of time. --- --- This causes the program to yield control and not regain it until the time --- period has elapsed. The time may have a fractional part. Internally, the --- timer provides millisecond resolution. --- @name sleep --- @class function --- @param t Time to sleep, in seconds. --- @usage stdnse.sleep(1.5) -_ENV.sleep = nmap.socket.sleep; - ---- --- Prints a formatted debug message if the current debugging level is greater --- than or equal to a given level. --- --- This is a convenience wrapper around --- nmap.log_write. The first optional numeric --- argument, level, is used as the debugging level necessary --- to print the message (it defaults to 1 if omitted). All remaining arguments --- are processed with Lua's string.format function. --- @param level Optional debugging level. --- @param fmt Format string. --- @param ... Arguments to format. -print_debug = function(level, fmt, ...) - local l, d = tonumber(level), nmap.debugging(); - if l and l <= d then - nmap.log_write("stdout", format(fmt, ...)); - elseif not l and 1 <= d then - nmap.log_write("stdout", format(level, fmt, ...)); - end -end - ---- --- Prints a formatted verbosity message if the current verbosity level is greater --- than or equal to a given level. --- --- This is a convenience wrapper around --- nmap.log_write. The first optional numeric --- argument, level, is used as the verbosity level necessary --- to print the message (it defaults to 1 if omitted). All remaining arguments --- are processed with Lua's string.format function. --- @param level Optional verbosity level. --- @param fmt Format string. --- @param ... Arguments to format. -print_verbose = function(level, fmt, ...) - local l, d = tonumber(level), nmap.verbosity(); - if l and l <= d then - nmap.log_write("stdout", format(fmt, ...)); - elseif not l and 1 <= d then - nmap.log_write("stdout", format(level, fmt, ...)); - end -end - - ---- Join a list of strings with a separator string. --- --- This is Lua's table.concat function with the parameters --- swapped for coherence. --- @usage --- stdnse.strjoin(", ", {"Anna", "Bob", "Charlie", "Dolores"}) --- --> "Anna, Bob, Charlie, Dolores" --- @param delimiter String to delimit each element of the list. --- @param list Array of strings to concatenate. --- @return Concatenated string. -function strjoin(delimiter, list) - assert(type(delimiter) == "string" or type(delimiter) == nil, "delimiter is of the wrong type! (did you get the parameters backward?)") - - return concat(list, delimiter); -end - ---- Split a string at a given delimiter, which may be a pattern. --- @usage --- stdnse.strsplit(",%s*", "Anna, Bob, Charlie, Dolores") --- --> { "Anna", "Bob", "Charlie", "Dolores" } --- @param pattern Pattern that separates the desired strings. --- @param text String to split. --- @return Array of substrings without the separating pattern. -function strsplit(pattern, text) - local list, pos = {}, 1; - - assert(pattern ~= "", "delimiter matches empty string!"); - - while true do - local first, last, match = text:find(pattern, pos); - if first then -- found? - list[#list+1] = text:sub(pos, first-1); - pos = last+1; - else - list[#list+1] = text:sub(pos); - break; - end - end - return list; -end - ---- Generate a random string. --- You can either provide your own charset or the function will use --- a default one which is [A-Z]. --- @param len Length of the string we want to generate. --- @param charset Charset that will be used to generate the string. --- @return A random string of length len consisting of --- characters from charset if one was provided, otherwise --- charset defaults to [A-Z] letters. -function generate_random_string(len, charset) - local t = {} - local ascii_A = 65 - local ascii_Z = 90 - if charset then - for i=1,len do - t[i]=charset[math.random(#charset)] - end - else - for i=1,len do - t[i]=string.char(math.random(ascii_A,ascii_Z)) - end - end - return table.concat(t) -end - ---- Return a wrapper closure around a socket that buffers socket reads into --- chunks separated by a pattern. --- --- This function operates on a socket attempting to read data. It separates the --- data by sep and, for each invocation, returns a piece of the --- separated data. Typically this is used to iterate over the lines of data --- received from a socket (sep = "\r?\n"). The returned string --- does not include the separator. It will return the final data even if it is --- not followed by the separator. Once an error or EOF is reached, it returns --- nil, msg. msg is what is returned by --- nmap.receive_lines. --- @param socket Socket for the buffer. --- @param sep Separator for the buffered reads. --- @return Data from socket reads or nil on EOF or error. --- @return Error message, as with receive_lines. -function make_buffer(socket, sep) - local point, left, buffer, done, msg = 1, ""; - local function self() - if done then - return nil, msg; -- must be nil for stdnse.lines (below) - elseif not buffer then - local status, str = socket:receive(); - if not status then - if #left > 0 then - done, msg = not status, str; - return left; - else - return status, str; - end - else - buffer = left..str; - return self(); - end - else - local i, j = buffer:find(sep, point); - if i then - local ret = buffer:sub(point, i-1); - point = j + 1; - return ret; - else - point, left, buffer = 1, buffer:sub(point), nil; - return self(); - end - end - end - return self; -end - ---[[ This function may be usable in Lua 5.2 -function lines(socket) - return make_buffer(socket, "\r?\n"), nil, nil; -end --]] - -do - local t = { - ["0"] = "0000", - ["1"] = "0001", - ["2"] = "0010", - ["3"] = "0011", - ["4"] = "0100", - ["5"] = "0101", - ["6"] = "0110", - ["7"] = "0111", - ["8"] = "1000", - ["9"] = "1001", - a = "1010", - b = "1011", - c = "1100", - d = "1101", - e = "1110", - f = "1111" - }; - ---- Converts the given number, n, to a string in a binary number format (12 --- becomes "1100"). --- @param n Number to convert. --- @return String in binary format. - function tobinary(n) - assert(tonumber(n), "number expected"); - return (("%x"):format(n):gsub("%w", t):gsub("^0*", "")); - end -end - ---- Converts the given number, n, to a string in an octal number format (12 --- becomes "14"). --- @param n Number to convert. --- @return String in octal format. -function tooctal(n) - assert(tonumber(n), "number expected"); - return ("%o"):format(n) -end - ---- Encode a string or number in hexadecimal (12 becomes "c", "AB" becomes --- "4142"). --- --- An optional second argument is a table with formatting options. The possible --- fields in this table are --- * separator: A string to use to separate groups of digits. --- * group: The size of each group of digits between separators. Defaults to 2, but has no effect if separator is not also given. --- @usage --- stdnse.tohex("abc") --> "616263" --- stdnse.tohex("abc", {separator = ":"}) --> "61:62:63" --- stdnse.tohex("abc", {separator = ":", group = 4}) --> "61:6263" --- stdnse.tohex(123456) --> "1e240" --- stdnse.tohex(123456, {separator = ":"}) --> "1:e2:40" --- stdnse.tohex(123456, {separator = ":", group = 4}) --> "1:e240" --- @param s String or number to be encoded. --- @param options Table specifiying formatting options. --- @return String in hexadecimal format. -function tohex( s, options ) - options = options or EMPTY - local separator = options.separator - local hex - - if type( s ) == "number" then - hex = ("%x"):format(s) - elseif type( s ) == 'string' then - hex = ("%02x"):rep(#s):format(s:byte(1,#s)) - else - error( "Type not supported in tohex(): " .. type(s), 2 ) - end - - -- format hex if we got a separator - if separator then - local group = options.group or 2 - local fmt_table = {} - -- split hex in group-size chunks - for i=#hex,1,-group do - -- table index must be consecutive otherwise table.concat won't work - fmt_table[ceil(i/group)] = hex:sub(max(i-group+1,1),i) - end - - hex = concat( fmt_table, separator ) - end - - return hex -end - ----Either return the string itself, or return "" (or the value of the second parameter) if the string --- was blank or nil. --- ---@param string The base string. ---@param blank The string to return if string was blank ---@return Either string or, if it was blank, blank -function string_or_blank(string, blank) - if(string == nil or string == "") then - if(blank == nil) then - return "" - else - return blank - end - else - return string - end -end - ---- --- Parses a time duration specification, which is a number followed by a --- unit, and returns a number of seconds. The unit is optional and --- defaults to seconds. The possible units (case-insensitive) are --- * ms: milliseconds, --- * s: seconds, --- * m: minutes, --- * h: hours. --- In case of a parsing error, the function returns nil --- followed by an error message. --- --- @usage --- parse_timespec("10") --> 10 --- parse_timespec("10ms") --> 0.01 --- parse_timespec("10s") --> 10 --- parse_timespec("10m") --> 600 --- parse_timespec("10h") --> 36000 --- parse_timespec("10z") --> nil, "Can't parse time specification \"10z\" (bad unit \"z\")" --- --- @param timespec A time specification string. --- @return A number of seconds, or nil followed by an error --- message. -function parse_timespec(timespec) - if timespec == nil then return nil, "Can't parse nil timespec" end - local n, unit, t, m - local multipliers = {[""] = 1, s = 1, m = 60, h = 60 * 60, ms = 0.001} - - n, unit = string.match(timespec, "^([%d.]+)(.*)$") - if not n then - return nil, string.format("Can't parse time specification \"%s\"", timespec) - end - - t = tonumber(n) - if not t then - return nil, string.format("Can't parse time specification \"%s\" (bad number \"%s\")", timespec, n) - end - - m = multipliers[unit] - if not m then - return nil, string.format("Can't parse time specification \"%s\" (bad unit \"%s\")", timespec, unit) - end - - return t * m -end - --- Find the offset in seconds between local time and UTC. That is, if we --- interpret a UTC date table as a local date table by passing it to os.time, --- how much must be added to the resulting integer timestamp to make it --- correct? -local function utc_offset(t) - -- What does the calendar say locally? - local localtime = os.date("*t", t) - -- What does the calendar say in UTC? - local gmtime = os.date("!*t", t) - -- Interpret both as local calendar dates and find the difference. - return difftime(os.time(localtime), os.time(gmtime)) -end ---- Convert a date table into an integer timestamp. Unlike os.time, this does --- not assume that the date table represents a local time. Rather, it takes an --- optional offset number of seconds representing the time zone, and returns --- the timestamp that would result using that time zone as local time. If the --- offset is omitted or 0, the date table is interpreted as a UTC date. For --- example, 4:00 UTC is the same as 5:00 UTC+1: --- --- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}) --> 14400 --- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 0) --> 14400 --- date_to_timestamp({year=1970,month=1,day=1,hour=5,min=0,sec=0}, 1*60*60) --> 14400 --- --- And 4:00 UTC+1 is an earlier time: --- --- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 1*60*60) --> 10800 --- -function date_to_timestamp(date, offset) - offset = offset or 0 - return os.time(date) + utc_offset(os.time(date)) - offset -end - -local function format_tz(offset) - local sign, hh, mm - - if not offset then - return "" - end - if offset < 0 then - sign = "-" - offset = -offset - else - sign = "+" - end - -- Truncate to minutes. - offset = math.floor(offset / 60) - hh = math.floor(offset / 60) - mm = math.floor(math.fmod(offset, 60)) - - return string.format("%s%02d:%02d", sign, hh, mm) -end ---- Format a date and time (and optional time zone) for structured output. --- --- Formatting is done according to RFC 3339 (a profile of ISO 8601), except --- that a time zone may be omitted to signify an unspecified local time zone. --- Time zones are given as an integer number of seconds from UTC. Use --- 0 to mark UTC itself. Formatted strings with a time zone look --- like this: --- --- format_timestamp(os.time(), 0) --> "2012-09-07T23:37:42+00:00" --- format_timestamp(os.time(), 2*60*60) --> "2012-09-07T23:37:42+02:00" --- --- Without a time zone they look like this: --- --- format_timestamp(os.time()) --> "2012-09-07T23:37:42" --- --- --- This function should be used for all dates emitted as part of NSE structured --- output. -function format_timestamp(t, offset) - local tz_string = format_tz(offset) - offset = offset or 0 - return os.date("!%Y-%m-%dT%H:%M:%S", t + offset) .. tz_string -end - ---- Format the difference between times t2 and t1 --- into a string in one of the forms (signs may vary): --- * 0s --- * -4s --- * +2m38s --- * -9h12m34s --- * +5d17h05m06s --- * -2y177d10h13m20s --- The string shows t2 relative to t1; i.e., the --- calculation is t2 minus t1. -function format_difftime(t2, t1) - local d, s, sign, yeardiff - - d = difftime(time(t2), time(t1)) - if d > 0 then - sign = "+" - elseif d < 0 then - sign = "-" - t2, t1 = t1, t2 - d = -d - else - sign = "" - end - -- t2 is always later than or equal to t1 here. - - -- The year is a tricky case because it's not a fixed number of days - -- the way a day is a fixed number of hours or an hour is a fixed - -- number of minutes. For example, the difference between 2008-02-10 - -- and 2009-02-10 is 366 days because 2008 was a leap year, but it - -- should be printed as 1y0d0h0m0s, not 1y1d0h0m0s. We advance t1 to be - -- the latest year such that it is still before t2, which means that its - -- year will be equal to or one less than t2's. The number of years - -- skipped is stored in yeardiff. - if t2.year > t1.year then - local tmpyear = t1.year - -- Put t1 in the same year as t2. - t1.year = t2.year - d = difftime(time(t2), time(t1)) - if d < 0 then - -- Too far. Back off one year. - t1.year = t2.year - 1 - d = difftime(time(t2), time(t1)) - end - yeardiff = t1.year - tmpyear - t1.year = tmpyear - else - yeardiff = 0 - end - - local s, sec, min - s = "" - -- Seconds (pad to two digits). - sec = d % 60 - d = math.floor(d / 60) - if d == 0 and yeardiff == 0 then - return sign .. string.format("%gs", sec) .. s - end - s = string.format("%02gs", sec) .. s - -- Minutes (pad to two digits). - min = d % 60 - d = math.floor(d / 60) - if d == 0 and yeardiff == 0 then - return sign .. string.format("%dm", min) .. s - end - s = string.format("%02dm", min) .. s - -- Hours. - s = string.format("%dh", d % 24) .. s - d = math.floor(d / 24) - if d == 0 and yeardiff == 0 then - return sign .. s - end - -- Days. - s = string.format("%dd", d) .. s - if yeardiff == 0 then return sign .. s end - -- Years. - s = string.format("%dy", yeardiff) .. s - return sign .. s -end - ---- Returns the current time in milliseconds since the epoch --- @return The current time in milliseconds since the epoch -function clock_ms() - return nmap.clock() * 1000 -end - ---- Returns the current time in microseconds since the epoch --- @return The current time in microseconds since the epoch -function clock_us() - return nmap.clock() * 1000000 -end - ----Get the indentation symbols at a given level. -local function format_get_indent(indent, at_end) - local str = "" - local had_continue = false - - if(not(at_end)) then - str = rep(' ', #indent) -- Was: "| " - else - for i = #indent, 1, -1 do - if(indent[i] and not(had_continue)) then - str = str .. " " -- Was: "|_ " - else - had_continue = true - str = str .. " " -- Was: "| " - end - end - end - - return str -end - -local function splitlines(s) - local result = {} - local i = 0 - - while i <= #s do - local b, e - b, e = string.find(s, "\r?\n", i) - if not b then - break - end - result[#result + 1] = string.sub(s, i, b - 1) - i = e + 1 - end - - if i <= #s then - result[#result + 1] = string.sub(s, i) - end - - return result -end - - --- A helper for format_output (see below). -local function format_output_sub(status, data, indent) - if (#data == 0) then - return "" - end - - -- Used to put 'ERROR: ' in front of all lines on error messages - local prefix = "" - -- Initialize the output string to blank (or, if we're at the top, add a newline) - local output = {} - if(not(indent)) then - insert(output, '\n') - end - - if(not(status)) then - if(nmap.debugging() < 1) then - return nil - end - prefix = "ERROR: " - end - - -- If a string was passed, turn it into a table - if(type(data) == 'string') then - data = {data} - end - - -- Make sure we have an indent value - indent = indent or {} - - if(data['name']) then - if(data['warning'] and nmap.debugging() > 0) then - insert(output, format("%s%s%s (WARNING: %s)\n", - format_get_indent(indent), prefix, - data['name'], data['warning'])) - else - insert(output, format("%s%s%s\n", - format_get_indent(indent), prefix, - data['name'])) - end - elseif(data['warning'] and nmap.debugging() > 0) then - insert(output, format("%s%s(WARNING: %s)\n", - format_get_indent(indent), prefix, - data['warning'])) - end - - for i, value in ipairs(data) do - if(type(value) == 'table') then - -- Do a shallow copy of indent - local new_indent = {} - for _, v in ipairs(indent) do - insert(new_indent, v) - end - - if(i ~= #data) then - insert(new_indent, false) - else - insert(new_indent, true) - end - - insert(output, format_output_sub(status, value, new_indent)) - - elseif(type(value) == 'string') then - local lines = splitlines(value) - - for j, line in ipairs(lines) do - insert(output, format("%s %s%s\n", - format_get_indent(indent, i == #data and j == #lines), - prefix, line)) - end - end - end - - return concat(output) -end - ----Takes a table of output on the commandline and formats it for display to the --- user. This is basically done by converting an array of nested tables into a --- string. In addition to numbered array elements, each table can have a 'name' --- and a 'warning' value. The 'name' will be displayed above the table, and --- 'warning' will be displayed, with a 'WARNING' tag, if and only if debugging --- is enabled. --- --- Here's an example of a table: --- --- local domains = {} --- domains['name'] = "DOMAINS" --- table.insert(domains, 'Domain 1') --- table.insert(domains, 'Domain 2') --- --- local names = {} --- names['name'] = "NAMES" --- names['warning'] = "Not all names could be determined!" --- table.insert(names, "Name 1") --- --- local response = {} --- table.insert(response, "Apple pie") --- table.insert(response, domains) --- table.insert(response, names) --- --- return stdnse.format_output(true, response) --- --- --- With debugging enabled, this is the output: --- --- Host script results: --- | smb-enum-domains: --- | Apple pie --- | DOMAINS --- | Domain 1 --- | Domain 2 --- | NAMES (WARNING: Not all names could be determined!) --- |_ Name 1 --- --- ---@param status A boolean value dictating whether or not the script succeeded. --- If status is false, and debugging is enabled, 'ERROR' is prepended --- to every line. If status is false and debugging is disabled, no output --- occurs. ---@param data The table of output. ---@param indent Used for indentation on recursive calls; should generally be set to --- nil when callling from a script. --- @return nil, if data is empty, otherwise a --- multiline string. -function format_output(status, data, indent) - -- If data is nil, die with an error (I keep doing that by accident) - assert(data, "No data was passed to format_output()") - - -- Don't bother if we don't have any data - if (#data == 0) then - return nil - end - - local result = format_output_sub(status, data, indent) - - -- Check for an empty result - if(result == nil or #result == "" or result == "\n" or result == "\n") then - return nil - end - - return result -end - --- Get the value of a script argument, or nil if the script argument was not --- given. This works also for arguments given as top-level array values, like --- --script-args=unsafe; for these it returns the value 1. -local function arg_value(argname) - if nmap.registry.args[argname] then - return nmap.registry.args[argname] - else - -- if scriptname.arg is not there, check "arg" - local argument_frags = strsplit("%.", argname) - if #argument_frags > 0 then - if nmap.registry.args[argument_frags[2]] then - return nmap.registry.args[argument_frags[2]] - end - end - end - - for _, v in ipairs(nmap.registry.args) do - if v == argname then - return 1 - end - end -end - ---- Parses the script arguments passed to the --script-args option. --- --- @usage --- --script-args 'script.arg1=value,script.arg3,script-x.arg=value' --- local arg1, arg2, arg3 = get_script_args('script.arg1','script.arg2','script.arg3') --- => arg1 = value --- => arg2 = nil --- => arg3 = 1 --- --- --script-args 'displayall,unsafe,script-x.arg=value,script-y.arg=value' --- local displayall, unsafe = get_script_args('displayall','unsafe') --- => displayall = 1 --- => unsafe = 1 --- --- --script-args 'dns-cache-snoop.mode=timed,dns-cache-snoop.domains={host1,host2}' --- local mode, domains = get_script_args('dns-cache-snoop.mode', --- 'dns-cache-snoop.domains') --- => mode = 'timed' --- => domains = {host1,host2} --- --- @param Arguments Script arguments to check. --- @return Arguments values. -function get_script_args (...) - local args = {} - - for i, set in ipairs({...}) do - if type(set) == "string" then - set = {set} - end - for _, test in ipairs(set) do - local v = arg_value(test) - if v then - args[i] = v - break - end - end - end - - return unpack(args, 1, select("#", ...)) -end - ----Get the best possible hostname for the given host. This can be the target as given on --- the commandline, the reverse dns name, or simply the ip address. ---@param host The host table (or a string that'll simply be returned). ---@return The best possible hostname, as a string. -function get_hostname(host) - if type(host) == "table" then - return host.targetname or ( host.name ~= '' and host.name ) or host.ip - else - return host - end -end - ----Retrieve an item from the registry, checking if each sub-key exists. If any key doesn't --- exist, return nil. -function registry_get(subkeys) - local registry = nmap.registry - local i = 1 - - while(subkeys[i]) do - if(not(registry[subkeys[i]])) then - return nil - end - - registry = registry[subkeys[i]] - - i = i + 1 - end - - return registry -end - ---Check if the given element exists in the registry. If 'key' is nil, it isn't checked. -function registry_exists(subkeys, key, value) - local subkey = registry_get(subkeys) - - if(not(subkey)) then - return false - end - - for k, v in pairs(subkey) do - if((key == nil or key == k) and (v == value)) then -- TODO: if 'value' is a table, this fails - return true - end - end - - return false -end - ----Add an item to an array in the registry, creating all sub-keys if necessary. --- For example, calling: --- registry_add_array({'192.168.1.100', 'www', '80', 'pages'}, 'index.html') --- Will create nmap.registry['192.168.1.100'] as a table, if necessary, then add a table --- under the 'www' key, and so on. 'pages', finally, is treated as an array and the value --- given is added to the end. -function registry_add_array(subkeys, value, allow_duplicates) - local registry = nmap.registry - local i = 1 - - -- Unless the user wants duplicates, make sure there aren't any - if(allow_duplicates ~= true) then - if(registry_exists(subkeys, nil, value)) then - return - end - end - - while(subkeys[i]) do - if(not(registry[subkeys[i]])) then - registry[subkeys[i]] = {} - end - registry = registry[subkeys[i]] - i = i + 1 - end - - -- Make sure the value isn't already in the table - for _, v in pairs(registry) do - if(v == value) then - return - end - end - insert(registry, value) -end - ----Similar to registry_add_array, except instead of adding a value to the --- end of an array, it adds a key:value pair to the table. -function registry_add_table(subkeys, key, value, allow_duplicates) - local registry = nmap.registry - local i = 1 - - -- Unless the user wants duplicates, make sure there aren't any - if(allow_duplicates ~= true) then - if(registry_exists(subkeys, key, value)) then - return - end - end - - while(subkeys[i]) do - if(not(registry[subkeys[i]])) then - registry[subkeys[i]] = {} - end - registry = registry[subkeys[i]] - i = i + 1 - end - - registry[key] = value -end - - ---- This function allows you to create worker threads that may perform --- network tasks in parallel with your script thread. --- --- Any network task (e.g. socket:connect(...)) will cause the --- running thread to yield to NSE. This allows network tasks to appear to be --- blocking while being able to run multiple network tasks at once. --- While this is useful for running multiple separate scripts, it is --- unfortunately difficult for a script itself to perform network tasks in --- parallel. In order to allow scripts to also have network tasks running in --- parallel, we provide this function, stdnse.new_thread, to --- create a new thread that can perform its own network related tasks --- in parallel with the script. --- --- The script launches the worker thread by calling the new_thread --- function with the parameters: --- * The main Lua function for the script to execute, similar to the script action function. --- * The variable number of arguments to be passed to the worker's main function. --- --- The stdnse.new_thread function will return two results: --- * The worker thread's base (main) coroutine (useful for tracking status). --- * A status query function (described below). --- --- The status query function shall return two values: --- * The result of coroutine.status using the worker thread base coroutine. --- * The error object thrown that ended the worker thread or nil if no error was thrown. This is typically a string, like most Lua errors. --- --- Note that NSE discards all return values of the worker's main function. You --- must use function parameters, upvalues or environments to communicate --- results. --- --- You should use the condition variable (nmap.condvar) --- and mutex (nmap.mutex) facilities to coordinate with your --- worker threads. Keep in mind that Nmap is single threaded so there are --- no (memory) issues in synchronization to worry about; however, there --- is resource contention. Your resources are usually network --- bandwidth, network sockets, etc. Condition variables are also useful if the --- work for any single thread is dynamic. For example, a web server spider --- script with a pool of workers will initially have a single root html --- document. Following the retrieval of the root document, the set of --- resources to be retrieved (the worker's work) will become very large --- (an html document adds many new hyperlinks (resources) to fetch). ---@name new_thread ---@class function ---@param main The main function of the worker thread. ---@param ... The arguments passed to the main worker thread. ---@return co The base coroutine of the worker thread. ---@return info A query function used to obtain status information of the worker. ---@usage ---local requests = {"/", "/index.html", --[[ long list of objects ]]} --- ---function thread_main (host, port, responses, ...) --- local condvar = nmap.condvar(responses); --- local what = {n = select("#", ...), ...}; --- local allReqs = nil; --- for i = 1, what.n do --- allReqs = http.pGet(host, port, what[i], nil, nil, allReqs); --- end --- local p = assert(http.pipeline(host, port, allReqs)); --- for i, response in ipairs(p) do responses[#responses+1] = response end --- condvar "signal"; ---end --- ---function many_requests (host, port) --- local threads = {}; --- local responses = {}; --- local condvar = nmap.condvar(responses); --- local i = 1; --- repeat --- local j = math.min(i+10, #requests); --- local co = stdnse.new_thread(thread_main, host, port, responses, --- table.unpack(requests, i, j)); --- threads[co] = true; --- i = j+1; --- until i > #requests; --- repeat --- condvar "wait"; --- for thread in pairs(threads) do --- if coroutine.status(thread) == "dead" then threads[thread] = nil end --- end --- until next(threads) == nil; --- return responses; ---end -do end -- no function here, see nse_main.lua - ---- Returns the base coroutine of the running script. --- --- A script may be resuming multiple coroutines to facilitate its own --- collaborative multithreading design. Because there is a "root" or "base" --- coroutine that lets us determine whether the script is still active --- (that is, the script did not end, possibly due to an error), we provide --- this stdnse.base function that will retrieve the base --- coroutine of the script. This base coroutine is the coroutine that runs --- the action function. --- --- The base coroutine is useful for many reasons but here are some common --- uses: --- * We want to attribute the ownership of an object (perhaps a network socket) to a script. --- * We want to identify if the script is still alive. ---@name base ---@class function ---@return coroutine Returns the base coroutine of the running script. -do end -- no function here, see nse_main.lua - ---- The Lua Require Function with errors silenced. --- --- See the Lua manual for description of the require function. This modified --- version allows the script to quietly fail at loading if a required --- library does not exist. --- ---@name silent_require ---@class function ---@usage stdnse.silent_require "openssl" -do end -- no function here, see nse_main.lua - - - ----Checks if the port is in the port range --- For example, calling: --- in_port_range({number=31337,protocol="udp"},"T:15,50-75,U:31334-31339") --- would result in a true value ---@param port a port structure containing keys port number(number) and protocol(string) ---@param port_range a port range string in Nmap standard format (ex. "T:80,1-30,U:31337,21-25") ---@returns boolean indicating whether the port is in the port range -function in_port_range(port,port_range) - assert(port and type(port.number)=="number" and type(port.protocol)=="string" and - (port.protocol=="udp" or port.protocol=="tcp"),"Port structure missing or invalid: port={ number=, protocol= }") - assert((type(port_range)=="string" or type(port_range)=="number") and port_range~="","Incorrect port range specification.") - - -- Proto - true for TCP, false for UDP - local proto - if(port.protocol=="tcp") then proto = true else proto = false end - - --TCP flag for iteration - true for TCP, false for UDP, if not specified we presume TCP - local tcp_flag = true - - -- in case the port_range is a single number - if type(port_range)=="number" then - if proto and port_range==port.number then return true - else return false - end - end - - --clean the string a bit - port_range=port_range:gsub("%s+","") - - -- single_pr - single port range - for i, single_pr in ipairs(strsplit(",",port_range)) do - if single_pr:match("T:") then - tcp_flag = true - single_pr = single_pr:gsub("T:","") - else - if single_pr:match("U:") then - tcp_flag = false - single_pr = single_pr:gsub("U:","") - end - end - - -- compare ports only when the port's protocol is the same as - -- the current single port range - if tcp_flag == proto then - local pone = single_pr:match("^(%d+)$") - if pone then - pone = tonumber(pone) - assert(pone>-1 and pone<65536, "Port range number out of range (0-65535).") - - if pone == port.number then - return true - end - else - local pstart, pend = single_pr:match("^(%d+)%-(%d+)$") - pstart, pend = tonumber(pstart), tonumber(pend) - assert(pstart,"Incorrect port range specification.") - assert(pstart<=pend,"Incorrect port range specification, the starting port should have a smaller value than the ending port.") - assert(pstart>-1 and pstart<65536 and pend>-1 and pend<65536, "Port range number out of range (0-65535).") - - if port.number >=pstart and port.number <= pend then - return true - end - end - end - end - -- if no match is found then the port doesn't belong to the port_range - return false -end - ---- Module function that mimics some behavior of Lua 5.1 module function. --- --- This convenience function returns a module environment to set the _ENV --- upvalue. The _NAME, _PACKAGE, and _M fields are set as in the Lua 5.1 --- version of this function. Each option function (e.g. stdnse.seeall) --- passed is run with the new environment, in order. --- --- @see stdnse.seeall --- @see strict --- @usage --- _ENV = stdnse.module(name, stdnse.seeall, require "strict"); --- @param name The module name. --- @param ... Option functions which modify the environment of the module. -function module (name, ...) - local env = {}; - env._NAME = name; - env._PACKAGE = name:match("(.+)%.[^.]+$"); - env._M = env; - local mods = pack(...); - for i = 1, mods.n do - mods[i](env); - end - return env; -end - ---- Change environment to load global variables. --- --- Option function for use with stdnse.module. It is the same --- as package.seeall from Lua 5.1. --- --- @see stdnse.module --- @usage --- _ENV = stdnse.module(name, stdnse.seeall); --- @param env Environment to change. -function seeall (env) - local m = getmetatable(env) or {}; - m.__index = _G; - setmetatable(env, m); -end - ---- Return a table that keeps elements in order of insertion. --- --- The pairs function, called on a table returned by this function, will yield --- elements in the order they were inserted. This function is meant to be used --- to construct output tables returned by scripts. --- --- Reinserting a key that is already in the table does not change its position --- in the order. However, removing a key by assigning to nil and --- then doing another assignment will move the key to the end of the order. --- --- @return An ordered table. -function output_table () - local t = {} - local order = {} - local function iterator () - for i, key in ipairs(order) do - coroutine.yield(key, t[key]) - end - end - local mt = { - __newindex = function (_, k, v) - if t[k] == nil and v ~= nil then - -- New key? - table.insert(order, k) - elseif v == nil then - -- Deleting an existing key? - for i, key in ipairs(order) do - if key == k then - table.remove(order, i) - break - end - end - end - rawset(t, k, v) - end, - __index = function (_, k) - return t[k] - end, - __pairs = function (_) - return coroutine.wrap(iterator) - end, - __call = function (_) -- hack to mean "not_empty?" - return not not next(order) - end, - __len = function (_) - return #order - end - } - return setmetatable({}, mt) -end - ---- A pretty printer for Lua objects. --- --- Takes an object (usually a table) and prints it using the --- printer function. The printer function takes a sole string --- argument and will be called repeatedly. --- --- @args obj The object to pretty print. --- @args printer The printer function. -function pretty_printer (obj, printer) - if printer == nil then printer = print end - - local function aux (obj, spacing) - local t = type(obj) - if t == "table" then - printer "{\n" - for k, v in pairs(obj) do - local spacing = spacing.."\t" - printer(spacing) - printer "[" - aux(k, spacing) - printer "] = " - aux(v, spacing) - printer ",\n" - end - printer(spacing.."}") - elseif t == "string" then - printer(format("%q", obj)) - else - printer(tostring(obj)) - end - end - - return aux(obj, "") -end - --- This pattern must match the percent sign '%' since it is used in --- escaping. -local FILESYSTEM_UNSAFE = "[^a-zA-Z0-9._-]" ---- --- Escape a string to remove bytes and strings that may have meaning to --- a filesystem, such as slashes. All bytes are escaped, except for: --- * alphabetic a-z and A-Z, digits 0-9, . _ - --- In addition, the strings "." and ".." have --- their characters escaped. --- --- Bytes are escaped by a percent sign followed by the two-digit --- hexadecimal representation of the byte value. --- * filename_escape("filename.ext") --> "filename.ext" --- * filename_escape("input/output") --> "input%2foutput" --- * filename_escape(".") --> "%2e" --- * filename_escape("..") --> "%2e%2e" --- This escaping is somewhat like that of JavaScript --- encodeURIComponent, except that fewer bytes are --- whitelisted, and it works on bytes, not Unicode characters or UTF-16 --- code points. -function filename_escape(s) - if s == "." then - return "%2e" - elseif s == ".." then - return "%2e%2e" - else - return (string.gsub(s, FILESYSTEM_UNSAFE, function (c) - return string.format("%%%02x", string.byte(c)) - end)) - end -end - -return _ENV; From 2e28dba607f085e8d401dbf2ff05fc6435144f7a Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:53:32 +0100 Subject: [PATCH 05/12] Create ms-sql-info.nse --- engines/nmap/fixed_script/ms-sql-info.nse | 266 ++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 engines/nmap/fixed_script/ms-sql-info.nse diff --git a/engines/nmap/fixed_script/ms-sql-info.nse b/engines/nmap/fixed_script/ms-sql-info.nse new file mode 100644 index 00000000..f920486a --- /dev/null +++ b/engines/nmap/fixed_script/ms-sql-info.nse @@ -0,0 +1,266 @@ +local mssql = require "mssql" +local nmap = require "nmap" +local smb = require "smb" +local shortport = require "shortport" +local stdnse = require "stdnse" + +-- -*- mode: lua -*- +-- vim: set filetype=lua : + +description = [[ +Attempts to determine configuration and version information for Microsoft SQL +Server instances. + +SQL Server credentials required: No (will not benefit from +mssql.username & mssql.password). +Run criteria: +* Host script: Will always run. +* Port script: N/A + +NOTE: Unlike previous versions, this script will NOT attempt to log in to SQL +Server instances. Blank passwords can be checked using the +ms-sql-empty-password script. E.g.: +nmap -sn --script ms-sql-empty-password --script-args mssql.instance-all + +The script uses two means of getting version information for SQL Server instances: +* Querying the SQL Server Browser service, which runs by default on UDP port +1434 on servers that have SQL Server 2000 or later installed. However, this +service may be disabled without affecting the functionality of the instances. +Additionally, it provides imprecise version information. +* Sending a probe to the instance, causing the instance to respond with +information including the exact version number. This is the same method that +Nmap uses for service versioning; however, this script can also do the same for +instances accessible via Windows named pipes, and can target all of the +instances listed by the SQL Server Browser service. + +In the event that the script can connect to the SQL Server Browser service +(UDP 1434) but is unable to connect directly to the instance to obtain more +accurate version information (because ports are blocked or the mssql.scanned-ports-only +argument has been used), the script will rely only upon the version number +provided by the SQL Server Browser/Monitor, which has the following limitations: +* For SQL Server 2000 and SQL Server 7.0 instances, the RTM version number is +always given, regardless of any service packs or patches installed. +* For SQL Server 2005 and later, the version number will reflect the service +pack installed, but the script will not be able to tell whether patches have +been installed. + +Where possible, the script will determine major version numbers, service pack +levels and whether patches have been installed. However, in cases where +particular determinations can not be made, the script will report only what can +be confirmed. + +NOTE: Communication with instances via named pipes depends on the smb +library. To communicate with (and possibly to discover) instances via named pipes, +the host must have at least one SMB port (e.g. TCP 445) that was scanned and +found to be open. Additionally, named pipe connections may require Windows +authentication to connect to the Windows host (via SMB) in addition to the +authentication required to connect to the SQL Server instances itself. See the +documentation and arguments for the smb library for more information. + +NOTE: By default, the ms-sql-* scripts may attempt to connect to and communicate +with ports that were not included in the port list for the Nmap scan. This can +be disabled using the mssql.scanned-ports-only script argument. +]] +--- +-- @usage +-- nmap -p 445 --script ms-sql-info +-- nmap -p 1433 --script ms-sql-info --script-args mssql.instance-port=1433 +-- +-- @output +-- | ms-sql-info: +-- | Windows server name: WINXP +-- | 192.168.100.128\PROD: +-- | Instance name: PROD +-- | Version: +-- | name: Microsoft SQL Server 2000 SP3 +-- | number: 8.00.760 +-- | Product: Microsoft SQL Server 2000 +-- | Service pack level: SP3 +-- | Post-SP patches applied: No +-- | TCP port: 1278 +-- | Named pipe: \\192.168.100.128\pipe\MSSQL$PROD\sql\query +-- | Clustered: No +-- | 192.168.100.128\SQLFIREWALLED: +-- | Instance name: SQLFIREWALLED +-- | Version: +-- | name: Microsoft SQL Server 2008 RTM +-- | Product: Microsoft SQL Server 2008 +-- | Service pack level: RTM +-- | TCP port: 4343 +-- | Clustered: No +-- | \\192.168.100.128\pipe\sql\query: +-- | Version: +-- | name: Microsoft SQL Server 2005 SP3+ +-- | number: 9.00.4053 +-- | Product: Microsoft SQL Server 2005 +-- | Service pack level: SP3 +-- | Post-SP patches applied: Yes +-- |_ Named pipe: \\192.168.100.128\pipe\sql\query +-- +-- @xmloutput +-- WINXP +-- +-- PROD +--
+-- Microsoft SQL Server 2000 SP3 +-- 8.00.760 +-- Microsoft SQL Server 2000 +-- SP3 +-- No +--
+-- 1278 +-- \\192.168.100.128\pipe\MSSQL$PROD\sql\query +-- No +-- +-- +-- SQLFIREWALLED +--
+-- Microsoft SQL Server 2008 RTM +-- Microsoft SQL Server 2008 +-- RTM +--
+-- 4343 +-- No +-- +-- +--
+-- Microsoft SQL Server 2005 SP3+ +-- 9.00.4053 +-- Microsoft SQL Server 2005 +-- SP3 +-- Yes +--
+-- \\192.168.100.128\pipe\sql\query +-- + +-- rev 1.0 (2007-06-09) +-- rev 1.1 (2009-12-06 - Added SQL 2008 identification T Sellers) +-- rev 1.2 (2010-10-03 - Added Broadcast support ) +-- rev 1.3 (2010-10-10 - Added prerule and newtargets support ) +-- rev 1.4 (2011-01-24 - Revised logic in order to get version data without logging in; +-- added functionality to interpret version in terms of SP level, etc. +-- added script arg to prevent script from connecting to ports that +-- weren't in original Nmap scan ) +-- rev 1.5 (2011-02-01 - Moved discovery functionality into ms-sql-discover.nse and +-- broadcast-ms-sql-discovery.nse ) +-- rev 1.6 (2014-09-04 - Added structured output Daniel Miller) + +author = {"Chris Woodbury", "Thomas Buchanan"} + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"default", "discovery", "safe"} + +portrule = shortport.port_or_service(1433, "ms-sql-s") + + + +--- Returns formatted output for the given version data +local function create_version_output_table( versionInfo ) + local versionOutput = stdnse.output_table() + + versionOutput["name"] = versionInfo:ToString() + if ( versionInfo.source ~= "SSRP" ) then + versionOutput["number"] = versionInfo.versionNumber + end + versionOutput["Product"] = versionInfo.productName + versionOutput["Service pack level"] = versionInfo.servicePackLevel + versionOutput["Post-SP patches applied"] = versionInfo.patched + + return versionOutput +end + + +--- Returns formatted output for the given instance +local function create_instance_output_table( instance ) + + -- if we didn't get anything useful (due to errors or the port not actually + -- being SQL Server), don't report anything + if not ( instance.instanceName or instance.version ) then return nil end + + local instanceOutput = stdnse.output_table() + + instanceOutput["Instance name"] = instance.instanceName + if instance.version then + instanceOutput["Version"] = create_version_output_table( instance.version ) + end + if instance.port then instanceOutput["TCP port"] = instance.port.number end + instanceOutput["Named pipe"] = instance.pipeName + instanceOutput["Clustered"] = instance.isClustered + + return instanceOutput + +end + + +--- Processes a single instance, attempting to determine its version, etc. +local function process_instance( instance ) + + local foundVersion = false + local ssnetlibVersion + + -- If possible and allowed (see 'mssql.scanned-ports-only' argument), we'll + -- connect to the instance to get an accurate version number + if ( instance:HasNetworkProtocols() ) then + local ssnetlibVersion + foundVersion, ssnetlibVersion = mssql.Helper.GetInstanceVersion( instance ) + if ( foundVersion ) then + instance.version = ssnetlibVersion + stdnse.debug1("Retrieved SSNetLib version for %s.", instance:GetName() ) + else + stdnse.debug1("Could not retrieve SSNetLib version for %s.", instance:GetName() ) + end + end + + -- If we didn't get a version from SSNetLib, give the user some detail as to why + if ( not foundVersion ) then + if ( not instance:HasNetworkProtocols() ) then + stdnse.debug1("%s has no network protocols enabled.", instance:GetName() ) + end + if ( instance.version ) then + stdnse.debug1("Using version number from SSRP response for %s.", instance:GetName() ) + else + stdnse.debug1("Version info could not be retrieved for %s.", instance:GetName() ) + end + end + + -- Give some version info back to Nmap + if ( instance.port and instance.version ) then + instance.version:PopulateNmapPortVersion( instance.port ) + nmap.set_port_version( instance.host, instance.port) + end + +end + + +action = function( host ) + local scriptOutput = stdnse.output_table() + + local status, instanceList = mssql.Helper.GetTargetInstances( host ) + -- if no instances were targeted, then display info on all + if ( not status ) then + if ( not mssql.Helper.WasDiscoveryPerformed( host ) ) then + mssql.Helper.Discover( host ) + end + instanceList = mssql.Helper.GetDiscoveredInstances( host ) + end + + + if ( not instanceList ) then + return stdnse.format_output( false, instanceList or "" ) + else + for _, instance in ipairs( instanceList ) do + if instance.serverName then + scriptOutput["Windows server name"] = instance.serverName + break + end + end + for _, instance in pairs( instanceList ) do + process_instance( instance ) + scriptOutput[instance:GetName()] = create_instance_output_table( instance ) + end + end + + return scriptOutput +end + From 7f43dbc79857528f1586fe38bc0d6aad1fb5ee50 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:53:48 +0100 Subject: [PATCH 06/12] Add files via upload --- engines/nmap/fixed_script/msrpc-enum.nse | 111 ++ engines/nmap/fixed_script/ntlm-methods.nse | 73 ++ engines/nmap/fixed_script/redis-info.nse | 250 ++++ engines/nmap/fixed_script/smb-protocols.nse | 70 ++ engines/nmap/fixed_script/stdnse.lua | 1233 +++++++++++++++++++ 5 files changed, 1737 insertions(+) create mode 100644 engines/nmap/fixed_script/msrpc-enum.nse create mode 100644 engines/nmap/fixed_script/ntlm-methods.nse create mode 100644 engines/nmap/fixed_script/redis-info.nse create mode 100644 engines/nmap/fixed_script/smb-protocols.nse create mode 100644 engines/nmap/fixed_script/stdnse.lua diff --git a/engines/nmap/fixed_script/msrpc-enum.nse b/engines/nmap/fixed_script/msrpc-enum.nse new file mode 100644 index 00000000..84d3e865 --- /dev/null +++ b/engines/nmap/fixed_script/msrpc-enum.nse @@ -0,0 +1,111 @@ +local msrpc = require "msrpc" +local smb = require "smb" +local stdnse = require "stdnse" +local table = require "table" +local shortport = require "shortport" + +description = [[ +Queries an MSRPC endpoint mapper for a list of mapped +services and displays the gathered information. + +As it is using smb library, you can specify optional +username and password to use. + +Script works much like Microsoft's rpcdump tool +or dcedump tool from SPIKE fuzzer. +]] +--- +-- @usage nmap --script=msrpc-enum +-- +-- @output +-- PORT STATE SERVICE REASON +-- 445/tcp open microsoft-ds syn-ack +-- +-- Host script results: +-- | msrpc-enum: +-- | +-- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5 +-- | annotation: DHCP Client LRPC Endpoint +-- | ncalrpc: dhcpcsvc +-- | +-- | uuid: 12345678-1234-abcd-ef00-0123456789ab +-- | annotation: IPSec Policy agent endpoint +-- | ncalrpc: audit +-- | +-- | uuid: 3c4728c5-f0ab-448b-bda1-6ce01eb0a6d5 +-- | ip_addr: 0.0.0.0 +-- | annotation: DHCP Client LRPC Endpoint +-- | tcp_port: 49153 +-- | +-- +-- | +-- | uuid: 12345678-1234-abcd-ef00-0123456789ab +-- | annotation: IPSec Policy agent endpoint +-- | ncalrpc: securityevent +-- | +-- | uuid: 12345678-1234-abcd-ef00-0123456789ab +-- | annotation: IPSec Policy agent endpoint +-- |_ ncalrpc: protected_storage +-- +-- @xmloutput +-- -snip- +-- +-- c100beab-d33a-4a4b-bf23-bbef4663d017 +-- wcncsvc.wcnprpc +-- wcncsvc.wcnprpc +--
+-- +-- 6b5bdd1e-528c-422c-af8c-a4079be4fe48 +-- Remote Fw APIs +-- 49158 +-- 0.0.0.0 +--
+-- +-- 12345678-1234-abcd-ef00-0123456789ab +-- IPSec Policy agent endpoint +-- 49158 +-- 0.0.0.0 +--
+-- -snip- + +author = "Aleksandar Nikolic" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe","discovery"} + +portrule = shortport.port_or_service(445, "microsoft-ds") + +action = function(host,port) + local status, smbstate + status, smbstate = msrpc.start_smb(host,msrpc.EPMAPPER_PATH,true) + if(status == false) then + stdnse.debug1("SMB: " .. smbstate) + return false, smbstate + end + local bind_result,epresult -- bind to endpoint mapper service + status, bind_result = msrpc.bind(smbstate,msrpc.EPMAPPER_UUID, msrpc.EPMAPPER_VERSION, nil) + if(status == false) then + msrpc.stop_smb(smbstate) + stdnse.debug1("SMB: " .. bind_result) + return false, bind_result + end + local results = {} + status, epresult = msrpc.epmapper_lookup(smbstate,nil) -- get the initial handle + if not status then + stdnse.debug1("SMB: " .. epresult) + return false, epresult + + end + local handle = epresult.new_handle + epresult.new_handle = nil + table.insert(results,epresult) + + while not (epresult == nil) do + status, epresult = msrpc.epmapper_lookup(smbstate,handle) -- get next result until there are no more + if not status then + break + end + epresult.new_handle = nil + table.insert(results,epresult) + end + return results +end diff --git a/engines/nmap/fixed_script/ntlm-methods.nse b/engines/nmap/fixed_script/ntlm-methods.nse new file mode 100644 index 00000000..60830f12 --- /dev/null +++ b/engines/nmap/fixed_script/ntlm-methods.nse @@ -0,0 +1,73 @@ +local http = require "http" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +description = [[ +Returns authentication methods a winrm server supports. +]] + +--- +-- @usage +-- nmap --script winrm-auth-methods -p 5985 +-- +-- @output +-- 5985/tcp open wsman +-- | winrm-auth-methods: +-- | Accepted Authentication Methods: +-- | Negotiate +-- | Basic +-- | Kerberos +-- |_ CredSSP + +author = "Evangelos Deirmentzoglou" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} + +portrule = shortport.port_or_service({5985, 5986},{'wsman','wsmans'}) + +function generate_random_string(len, charset) + local t = {} + local ascii_A = 65 + local ascii_Z = 90 + if charset then + for i=1,len do + t[i]=charset[math.random(#charset)] + end + else + for i=1,len do + t[i]=string.char(math.random(ascii_A,ascii_Z)) + end + end + return table.concat(t) +end + +action = function(host, port) + + local r = {} + local result = stdnse.output_table() + local randoms = generate_random_string(5) + local url = "/wsman" + local response = http.post( host, port, url, nil, nil, randoms ) + + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Negotiate") then + table.insert(r, "Negotiate") + end + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Basic") then + table.insert(r, "Basic") + end + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "Kerberos") then + table.insert(r, "Kerberos") + end + if response.header["www-authenticate"] and string.match(response.header["www-authenticate"], "CredSSP") then + table.insert(r, "CredSSP") + end + if #r > 0 then + result = r + else + result = "Server does not support authentication." + end + + return result +end diff --git a/engines/nmap/fixed_script/redis-info.nse b/engines/nmap/fixed_script/redis-info.nse new file mode 100644 index 00000000..fcfd4b7a --- /dev/null +++ b/engines/nmap/fixed_script/redis-info.nse @@ -0,0 +1,250 @@ +local creds = require "creds" +local redis = require "redis" +local nmap = require "nmap" +local shortport = require "shortport" +local stdnse = require "stdnse" +local string = require "string" +local stringaux = require "stringaux" +local table = require "table" +local tableaux = require "tableaux" +local ipOps = require "ipOps" + +description = [[ +Retrieves information (such as version number and architecture) from a Redis key-value store. +]] + +--- +-- @usage +-- nmap -p 6379 --script redis-info +-- +-- @output +-- PORT STATE SERVICE +-- 6379/tcp open unknown +-- | redis-info: +-- | Version 2.2.11 +-- | Architecture 64 bits +-- | Process ID 17821 +-- | Used CPU (sys) 2.37 +-- | Used CPU (user) 1.02 +-- | Connected clients 1 +-- | Connected slaves 0 +-- | Used memory 780.16K +-- | Role master +-- | Bind addresses: +-- | 192.168.121.101 +-- | Active channels: +-- | testChannel +-- | bidChannel +-- | Client connections: +-- | 192.168.171.101 +-- |_ 72.14.177.105 +-- +-- + +author = {"Patrik Karlsson", "Vasiliy Kulikov"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} +dependencies = {"redis-brute"} + + +portrule = shortport.port_or_service(6379, "redis") + +local function fail(err) return stdnse.format_output(false, err) end + +local function cb_parse_version(host, port, val) + port.version.version = val + port.version.cpe = port.version.cpe or {} + table.insert(port.version.cpe, 'cpe:/a:redis:redis:' .. val) + nmap.set_port_version(host, port) + return val +end + +local function cb_parse_architecture(host, port, val) + val = ("%s bits"):format(val) + port.version.extrainfo = val + nmap.set_port_version(host, port) + return val +end + +local filter = { + + ["redis_version"] = { name = "Version", func = cb_parse_version }, + ["os"] = { name = "Operating System" }, + ["arch_bits"] = { name = "Architecture", func = cb_parse_architecture }, + ["process_id"] = { name = "Process ID"}, + ["uptime"] = { name = "Uptime", func = function(h, p, v) return ("%s seconds"):format(v) end }, + ["used_cpu_sys"]= { name = "Used CPU (sys)"}, + ["used_cpu_user"] = { name = "Used CPU (user)"}, + ["connected_clients"] = { name = "Connected clients"}, + ["connected_slaves"] = { name = "Connected slaves"}, + ["used_memory_human"] = { name = "Used memory"}, + ["role"] = { name = "Role"} + +} + +local order = { + "redis_version", "os", "arch_bits", "process_id", "used_cpu_sys", + "used_cpu_user", "connected_clients", "connected_slaves", + "used_memory_human", "role" +} + +local extras = { + { + -- https://redis.io/commands/config-get/ + "Bind addresses", {"CONFIG", "GET", "bind"}, function (data) + if data[1] ~= "bind" or not data[2] then + return nil + end + local restab = stringaux.strsplit(" ", data[2]) + for i, ip in ipairs(restab) do + if ip == '' then restab[i] = '0.0.0.0' end + end + return restab + end + }, + { + -- https://redis.io/commands/pubsub-channels/ + "Active channels", {"PUBSUB", "CHANNELS"}, function (data) + local channels = {} + local omitted = 0 + local limit = nmap.verbosity() <= 1 and 20 or false + for _, channel in ipairs(data) do + if limit and #channels >= limit then + omitted = omitted + 1 + else + table.insert(channels, channel) + end + end + + if omitted > 0 then + table.insert(channels, ("(omitted %s item(s), use verbose mode -v to show them)"):format(omitted)) + end + return #channels > 0 and channels or nil + end + }, + { + -- https://redis.io/commands/client-list/ + "Client connections", {"CLIENT", "LIST"}, function(data) + if not data then + stdnse.debug1("Failed to parse response from server") + return nil + end + + local client_ips = {} + for conn in data:gmatch("[^\n]+") do + local ip = conn:match("%f[^%s\0]addr=%[?([%x:.]+)%]?:%d+%f[%s\0]") + if ip then + local binip = ipOps.ip_to_str(ip) + if binip then + -- prepending length sorts IPv4 and IPv6 separately + client_ips[string.pack("s1", binip)] = binip + end + end + end + + local out = {} + local keys = tableaux.keys(client_ips) + table.sort(keys) + for _, packed in ipairs(keys) do + table.insert(out, ipOps.str_to_ip(client_ips[packed])) + end + return #out > 0 and out or nil + end + }, + { + -- https://redis.io/commands/cluster-nodes/ + "Cluster nodes", {"CLUSTER", "NODES"}, function(data) + if not data then + stdnse.debug1("Failed to parse response from server") + return nil + end + + local out = {} + for node in data:gmatch("[^\n]+") do + local ipport, flags = node:match("^%x+%s+([%x.:%[%]]+)@?%d*%s+(%S+)") + if ipport then + table.insert(out, ("%s (%s)"):format(ipport, flags)) + else + stdnse.debug1("Unable to parse cluster node info") + end + end + return #out > 0 and out or nil + end + }, +} + +action = function(host, port) + + local helper = redis.Helper:new(host, port) + local status = helper:connect() + if( not(status) ) then + return fail("Failed to connect to server") + end + + -- do we have a service password + local c = creds.Credentials:new(creds.ALL_DATA, host, port) + local cred = c:getCredentials(creds.State.VALID + creds.State.PARAM)() + + if ( cred and cred.pass ) then + local status, response = helper:reqCmd("AUTH", cred.pass) + if ( not(status) ) then + helper:close() + return fail(response) + end + end + + local status, response = helper:reqCmd("INFO") + if ( not(status) ) then + helper:close() + return fail(response) + end + + if ( redis.Response.Type.ERROR == response.type ) then + if ( "-ERR operation not permitted" == response.data ) or + ( "-NOAUTH Authentication required." == response.data ) then + return fail("Authentication required") + end + return fail(response.data) + end + + local restab = stringaux.strsplit("\r\n", response.data) + if ( not(restab) or 0 == #restab ) then + return fail("Failed to parse response from server") + end + + local kvs = {} + for _, item in ipairs(restab) do + local k, v = item:match("^([^:]*):(.*)$") + if k ~= nil then + kvs[k] = v + end + end + + local result = stdnse.output_table() + for _, item in ipairs(order) do + if kvs[item] then + local name = filter[item].name + local val + + if filter[item].func then + val = filter[item].func(host, port, kvs[item]) + else + val = kvs[item] + end + result[name] = val + end + end + + for i=1, #extras do + local name = extras[i][1] + local cmd = extras[i][2] + local process = extras[i][3] + + local status, response = helper:reqCmd(table.unpack(cmd)) + if status and redis.Response.Type.ERROR ~= response.type then + result[name] = process(response.data) + end + end + helper:close() + return result +end diff --git a/engines/nmap/fixed_script/smb-protocols.nse b/engines/nmap/fixed_script/smb-protocols.nse new file mode 100644 index 00000000..6dc71d8b --- /dev/null +++ b/engines/nmap/fixed_script/smb-protocols.nse @@ -0,0 +1,70 @@ +local smb = require "smb" +local stdnse = require "stdnse" +local nmap = require "nmap" +local shortport = require "shortport" + +description = [[ +Attempts to list the supported protocols and dialects of a SMB server. + +The script attempts to initiate a connection using the dialects: +* NT LM 0.12 (SMBv1) +* 2.0.2 (SMBv2) +* 2.1 (SMBv2) +* 3.0 (SMBv3) +* 3.0.2 (SMBv3) +* 3.1.1 (SMBv3) + +Additionally if SMBv1 is found enabled, it will mark it as insecure. This +script is the successor to the (removed) smbv2-enabled script. +]] + +--- +-- @usage nmap -p445 --script smb-protocols +-- @usage nmap -p139 --script smb-protocols +-- +-- @output +-- | smb-protocols: +-- | dialects: +-- | NT LM 0.12 (SMBv1) [dangerous, but default] +-- | 2.0.2 +-- | 2.1 +-- | 3.0 +-- | 3.0.2 +-- |_ 3.1.1 +-- +-- @xmloutput +-- +-- NT LM 0.12 (SMBv1) [dangerous, but default] +-- 2.0.2 +-- 2.1 +-- 3.0 +-- 3.0.2 +-- 3.1.1 +--
+--- + +author = "Paulino Calderon" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"safe", "discovery"} + +portrule = shortport.port_or_service(445, "microsoft-ds") + +action = function(host,port) + local status, supported_dialects = smb.list_dialects(host) + if status then + for i, v in pairs(supported_dialects) do -- Mark SMBv1 as insecure + if v == "NT LM 0.12" then + supported_dialects[i] = v .. " (SMBv1) [dangerous, but default]" + end + end + if #supported_dialects > 0 then + local output = stdnse.output_table() + output.dialects = supported_dialects + return output + end + end + stdnse.debug1("No dialects were accepted") + if nmap.verbosity()>1 then + return "No dialects accepted. Something may be blocking the responses" + end +end diff --git a/engines/nmap/fixed_script/stdnse.lua b/engines/nmap/fixed_script/stdnse.lua new file mode 100644 index 00000000..909a9322 --- /dev/null +++ b/engines/nmap/fixed_script/stdnse.lua @@ -0,0 +1,1233 @@ +--- +-- Standard Nmap Scripting Engine functions. This module contains various handy +-- functions that are too small to justify modules of their own. +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html +-- @class module +-- @name stdnse + +local _G = require "_G" +local coroutine = require "coroutine" +local math = require "math" +local nmap = require "nmap" +local os = require "os" +local string = require "string" +local table = require "table" +local assert = assert; +local error = error; +local getmetatable = getmetatable; +local ipairs = ipairs +local pairs = pairs +local next = next +local rawset = rawset +local require = require; +local select = select +local setmetatable = setmetatable; +local tonumber = tonumber; +local tostring = tostring; +local type = type + +local ceil = math.ceil +local max = math.max + +local format = string.format; +local rep = string.rep + +local concat = table.concat; +local insert = table.insert; +local pack = table.pack; +local unpack = table.unpack; + +local difftime = os.difftime; +local time = os.time; + +local EMPTY = {}; -- Empty constant table + +_ENV = require "strict" {}; + +--- Sleeps for a given amount of time. +-- +-- This causes the program to yield control and not regain it until the time +-- period has elapsed. The time may have a fractional part. Internally, the +-- timer provides millisecond resolution. +-- @name sleep +-- @class function +-- @param t Time to sleep, in seconds. +-- @usage stdnse.sleep(1.5) +_ENV.sleep = nmap.socket.sleep; + +--- +-- Prints a formatted debug message if the current debugging level is greater +-- than or equal to a given level. +-- +-- This is a convenience wrapper around +-- nmap.log_write. The first optional numeric +-- argument, level, is used as the debugging level necessary +-- to print the message (it defaults to 1 if omitted). All remaining arguments +-- are processed with Lua's string.format function. +-- @param level Optional debugging level. +-- @param fmt Format string. +-- @param ... Arguments to format. +print_debug = function(level, fmt, ...) + local l, d = tonumber(level), nmap.debugging(); + if l and l <= d then + nmap.log_write("stdout", format(fmt, ...)); + elseif not l and 1 <= d then + nmap.log_write("stdout", format(level, fmt, ...)); + end +end + +--- +-- Prints a formatted verbosity message if the current verbosity level is greater +-- than or equal to a given level. +-- +-- This is a convenience wrapper around +-- nmap.log_write. The first optional numeric +-- argument, level, is used as the verbosity level necessary +-- to print the message (it defaults to 1 if omitted). All remaining arguments +-- are processed with Lua's string.format function. +-- @param level Optional verbosity level. +-- @param fmt Format string. +-- @param ... Arguments to format. +print_verbose = function(level, fmt, ...) + local l, d = tonumber(level), nmap.verbosity(); + if l and l <= d then + nmap.log_write("stdout", format(fmt, ...)); + elseif not l and 1 <= d then + nmap.log_write("stdout", format(level, fmt, ...)); + end +end + + +--- Join a list of strings with a separator string. +-- +-- This is Lua's table.concat function with the parameters +-- swapped for coherence. +-- @usage +-- stdnse.strjoin(", ", {"Anna", "Bob", "Charlie", "Dolores"}) +-- --> "Anna, Bob, Charlie, Dolores" +-- @param delimiter String to delimit each element of the list. +-- @param list Array of strings to concatenate. +-- @return Concatenated string. +function strjoin(delimiter, list) + assert(type(delimiter) == "string" or type(delimiter) == nil, "delimiter is of the wrong type! (did you get the parameters backward?)") + + return concat(list, delimiter); +end + +--- Split a string at a given delimiter, which may be a pattern. +-- @usage +-- stdnse.strsplit(",%s*", "Anna, Bob, Charlie, Dolores") +-- --> { "Anna", "Bob", "Charlie", "Dolores" } +-- @param pattern Pattern that separates the desired strings. +-- @param text String to split. +-- @return Array of substrings without the separating pattern. +function strsplit(pattern, text) + local list, pos = {}, 1; + + assert(pattern ~= "", "delimiter matches empty string!"); + + while true do + local first, last, match = text:find(pattern, pos); + if first then -- found? + list[#list+1] = text:sub(pos, first-1); + pos = last+1; + else + list[#list+1] = text:sub(pos); + break; + end + end + return list; +end + +--- Generate a random string. +-- You can either provide your own charset or the function will use +-- a default one which is [A-Z]. +-- @param len Length of the string we want to generate. +-- @param charset Charset that will be used to generate the string. +-- @return A random string of length len consisting of +-- characters from charset if one was provided, otherwise +-- charset defaults to [A-Z] letters. +function generate_random_string(len, charset) + local t = {} + local ascii_A = 65 + local ascii_Z = 90 + if charset then + for i=1,len do + t[i]=charset[math.random(#charset)] + end + else + for i=1,len do + t[i]=string.char(math.random(ascii_A,ascii_Z)) + end + end + return table.concat(t) +end + +--- Return a wrapper closure around a socket that buffers socket reads into +-- chunks separated by a pattern. +-- +-- This function operates on a socket attempting to read data. It separates the +-- data by sep and, for each invocation, returns a piece of the +-- separated data. Typically this is used to iterate over the lines of data +-- received from a socket (sep = "\r?\n"). The returned string +-- does not include the separator. It will return the final data even if it is +-- not followed by the separator. Once an error or EOF is reached, it returns +-- nil, msg. msg is what is returned by +-- nmap.receive_lines. +-- @param socket Socket for the buffer. +-- @param sep Separator for the buffered reads. +-- @return Data from socket reads or nil on EOF or error. +-- @return Error message, as with receive_lines. +function make_buffer(socket, sep) + local point, left, buffer, done, msg = 1, ""; + local function self() + if done then + return nil, msg; -- must be nil for stdnse.lines (below) + elseif not buffer then + local status, str = socket:receive(); + if not status then + if #left > 0 then + done, msg = not status, str; + return left; + else + return status, str; + end + else + buffer = left..str; + return self(); + end + else + local i, j = buffer:find(sep, point); + if i then + local ret = buffer:sub(point, i-1); + point = j + 1; + return ret; + else + point, left, buffer = 1, buffer:sub(point), nil; + return self(); + end + end + end + return self; +end + +--[[ This function may be usable in Lua 5.2 +function lines(socket) + return make_buffer(socket, "\r?\n"), nil, nil; +end --]] + +do + local t = { + ["0"] = "0000", + ["1"] = "0001", + ["2"] = "0010", + ["3"] = "0011", + ["4"] = "0100", + ["5"] = "0101", + ["6"] = "0110", + ["7"] = "0111", + ["8"] = "1000", + ["9"] = "1001", + a = "1010", + b = "1011", + c = "1100", + d = "1101", + e = "1110", + f = "1111" + }; + +--- Converts the given number, n, to a string in a binary number format (12 +-- becomes "1100"). +-- @param n Number to convert. +-- @return String in binary format. + function tobinary(n) + assert(tonumber(n), "number expected"); + return (("%x"):format(n):gsub("%w", t):gsub("^0*", "")); + end +end + +--- Converts the given number, n, to a string in an octal number format (12 +-- becomes "14"). +-- @param n Number to convert. +-- @return String in octal format. +function tooctal(n) + assert(tonumber(n), "number expected"); + return ("%o"):format(n) +end + +--- Encode a string or number in hexadecimal (12 becomes "c", "AB" becomes +-- "4142"). +-- +-- An optional second argument is a table with formatting options. The possible +-- fields in this table are +-- * separator: A string to use to separate groups of digits. +-- * group: The size of each group of digits between separators. Defaults to 2, but has no effect if separator is not also given. +-- @usage +-- stdnse.tohex("abc") --> "616263" +-- stdnse.tohex("abc", {separator = ":"}) --> "61:62:63" +-- stdnse.tohex("abc", {separator = ":", group = 4}) --> "61:6263" +-- stdnse.tohex(123456) --> "1e240" +-- stdnse.tohex(123456, {separator = ":"}) --> "1:e2:40" +-- stdnse.tohex(123456, {separator = ":", group = 4}) --> "1:e240" +-- @param s String or number to be encoded. +-- @param options Table specifiying formatting options. +-- @return String in hexadecimal format. +function tohex( s, options ) + options = options or EMPTY + local separator = options.separator + local hex + + if type( s ) == "number" then + hex = ("%x"):format(s) + elseif type( s ) == 'string' then + hex = ("%02x"):rep(#s):format(s:byte(1,#s)) + else + error( "Type not supported in tohex(): " .. type(s), 2 ) + end + + -- format hex if we got a separator + if separator then + local group = options.group or 2 + local fmt_table = {} + -- split hex in group-size chunks + for i=#hex,1,-group do + -- table index must be consecutive otherwise table.concat won't work + fmt_table[ceil(i/group)] = hex:sub(max(i-group+1,1),i) + end + + hex = concat( fmt_table, separator ) + end + + return hex +end + +---Either return the string itself, or return "" (or the value of the second parameter) if the string +-- was blank or nil. +-- +--@param string The base string. +--@param blank The string to return if string was blank +--@return Either string or, if it was blank, blank +function string_or_blank(string, blank) + if(string == nil or string == "") then + if(blank == nil) then + return "" + else + return blank + end + else + return string + end +end + +--- +-- Parses a time duration specification, which is a number followed by a +-- unit, and returns a number of seconds. The unit is optional and +-- defaults to seconds. The possible units (case-insensitive) are +-- * ms: milliseconds, +-- * s: seconds, +-- * m: minutes, +-- * h: hours. +-- In case of a parsing error, the function returns nil +-- followed by an error message. +-- +-- @usage +-- parse_timespec("10") --> 10 +-- parse_timespec("10ms") --> 0.01 +-- parse_timespec("10s") --> 10 +-- parse_timespec("10m") --> 600 +-- parse_timespec("10h") --> 36000 +-- parse_timespec("10z") --> nil, "Can't parse time specification \"10z\" (bad unit \"z\")" +-- +-- @param timespec A time specification string. +-- @return A number of seconds, or nil followed by an error +-- message. +function parse_timespec(timespec) + if timespec == nil then return nil, "Can't parse nil timespec" end + local n, unit, t, m + local multipliers = {[""] = 1, s = 1, m = 60, h = 60 * 60, ms = 0.001} + + n, unit = string.match(timespec, "^([%d.]+)(.*)$") + if not n then + return nil, string.format("Can't parse time specification \"%s\"", timespec) + end + + t = tonumber(n) + if not t then + return nil, string.format("Can't parse time specification \"%s\" (bad number \"%s\")", timespec, n) + end + + m = multipliers[unit] + if not m then + return nil, string.format("Can't parse time specification \"%s\" (bad unit \"%s\")", timespec, unit) + end + + return t * m +end + +-- Find the offset in seconds between local time and UTC. That is, if we +-- interpret a UTC date table as a local date table by passing it to os.time, +-- how much must be added to the resulting integer timestamp to make it +-- correct? +local function utc_offset(t) + -- What does the calendar say locally? + local localtime = os.date("*t", t) + -- What does the calendar say in UTC? + local gmtime = os.date("!*t", t) + -- Interpret both as local calendar dates and find the difference. + return difftime(os.time(localtime), os.time(gmtime)) +end +--- Convert a date table into an integer timestamp. Unlike os.time, this does +-- not assume that the date table represents a local time. Rather, it takes an +-- optional offset number of seconds representing the time zone, and returns +-- the timestamp that would result using that time zone as local time. If the +-- offset is omitted or 0, the date table is interpreted as a UTC date. For +-- example, 4:00 UTC is the same as 5:00 UTC+1: +-- +-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}) --> 14400 +-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 0) --> 14400 +-- date_to_timestamp({year=1970,month=1,day=1,hour=5,min=0,sec=0}, 1*60*60) --> 14400 +-- +-- And 4:00 UTC+1 is an earlier time: +-- +-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 1*60*60) --> 10800 +-- +function date_to_timestamp(date, offset) + offset = offset or 0 + return os.time(date) + utc_offset(os.time(date)) - offset +end + +local function format_tz(offset) + local sign, hh, mm + + if not offset then + return "" + end + if offset < 0 then + sign = "-" + offset = -offset + else + sign = "+" + end + -- Truncate to minutes. + offset = math.floor(offset / 60) + hh = math.floor(offset / 60) + mm = math.floor(math.fmod(offset, 60)) + + return string.format("%s%02d:%02d", sign, hh, mm) +end +--- Format a date and time (and optional time zone) for structured output. +-- +-- Formatting is done according to RFC 3339 (a profile of ISO 8601), except +-- that a time zone may be omitted to signify an unspecified local time zone. +-- Time zones are given as an integer number of seconds from UTC. Use +-- 0 to mark UTC itself. Formatted strings with a time zone look +-- like this: +-- +-- format_timestamp(os.time(), 0) --> "2012-09-07T23:37:42+00:00" +-- format_timestamp(os.time(), 2*60*60) --> "2012-09-07T23:37:42+02:00" +-- +-- Without a time zone they look like this: +-- +-- format_timestamp(os.time()) --> "2012-09-07T23:37:42" +-- +-- +-- This function should be used for all dates emitted as part of NSE structured +-- output. +function format_timestamp(t, offset) + local tz_string = format_tz(offset) + offset = offset or 0 + return os.date("!%Y-%m-%dT%H:%M:%S", t + offset) .. tz_string +end + +--- Format the difference between times t2 and t1 +-- into a string in one of the forms (signs may vary): +-- * 0s +-- * -4s +-- * +2m38s +-- * -9h12m34s +-- * +5d17h05m06s +-- * -2y177d10h13m20s +-- The string shows t2 relative to t1; i.e., the +-- calculation is t2 minus t1. +function format_difftime(t2, t1) + local d, s, sign, yeardiff + + d = difftime(time(t2), time(t1)) + if d > 0 then + sign = "+" + elseif d < 0 then + sign = "-" + t2, t1 = t1, t2 + d = -d + else + sign = "" + end + -- t2 is always later than or equal to t1 here. + + -- The year is a tricky case because it's not a fixed number of days + -- the way a day is a fixed number of hours or an hour is a fixed + -- number of minutes. For example, the difference between 2008-02-10 + -- and 2009-02-10 is 366 days because 2008 was a leap year, but it + -- should be printed as 1y0d0h0m0s, not 1y1d0h0m0s. We advance t1 to be + -- the latest year such that it is still before t2, which means that its + -- year will be equal to or one less than t2's. The number of years + -- skipped is stored in yeardiff. + if t2.year > t1.year then + local tmpyear = t1.year + -- Put t1 in the same year as t2. + t1.year = t2.year + d = difftime(time(t2), time(t1)) + if d < 0 then + -- Too far. Back off one year. + t1.year = t2.year - 1 + d = difftime(time(t2), time(t1)) + end + yeardiff = t1.year - tmpyear + t1.year = tmpyear + else + yeardiff = 0 + end + + local s, sec, min + s = "" + -- Seconds (pad to two digits). + sec = d % 60 + d = math.floor(d / 60) + if d == 0 and yeardiff == 0 then + return sign .. string.format("%gs", sec) .. s + end + s = string.format("%02gs", sec) .. s + -- Minutes (pad to two digits). + min = d % 60 + d = math.floor(d / 60) + if d == 0 and yeardiff == 0 then + return sign .. string.format("%dm", min) .. s + end + s = string.format("%02dm", min) .. s + -- Hours. + s = string.format("%dh", d % 24) .. s + d = math.floor(d / 24) + if d == 0 and yeardiff == 0 then + return sign .. s + end + -- Days. + s = string.format("%dd", d) .. s + if yeardiff == 0 then return sign .. s end + -- Years. + s = string.format("%dy", yeardiff) .. s + return sign .. s +end + +--- Returns the current time in milliseconds since the epoch +-- @return The current time in milliseconds since the epoch +function clock_ms() + return nmap.clock() * 1000 +end + +--- Returns the current time in microseconds since the epoch +-- @return The current time in microseconds since the epoch +function clock_us() + return nmap.clock() * 1000000 +end + +---Get the indentation symbols at a given level. +local function format_get_indent(indent, at_end) + local str = "" + local had_continue = false + + if(not(at_end)) then + str = rep(' ', #indent) -- Was: "| " + else + for i = #indent, 1, -1 do + if(indent[i] and not(had_continue)) then + str = str .. " " -- Was: "|_ " + else + had_continue = true + str = str .. " " -- Was: "| " + end + end + end + + return str +end + +local function splitlines(s) + local result = {} + local i = 0 + + while i <= #s do + local b, e + b, e = string.find(s, "\r?\n", i) + if not b then + break + end + result[#result + 1] = string.sub(s, i, b - 1) + i = e + 1 + end + + if i <= #s then + result[#result + 1] = string.sub(s, i) + end + + return result +end + + +-- A helper for format_output (see below). +local function format_output_sub(status, data, indent) + if (#data == 0) then + return "" + end + + -- Used to put 'ERROR: ' in front of all lines on error messages + local prefix = "" + -- Initialize the output string to blank (or, if we're at the top, add a newline) + local output = {} + if(not(indent)) then + insert(output, '\n') + end + + if(not(status)) then + if(nmap.debugging() < 1) then + return nil + end + prefix = "ERROR: " + end + + -- If a string was passed, turn it into a table + if(type(data) == 'string') then + data = {data} + end + + -- Make sure we have an indent value + indent = indent or {} + + if(data['name']) then + if(data['warning'] and nmap.debugging() > 0) then + insert(output, format("%s%s%s (WARNING: %s)\n", + format_get_indent(indent), prefix, + data['name'], data['warning'])) + else + insert(output, format("%s%s%s\n", + format_get_indent(indent), prefix, + data['name'])) + end + elseif(data['warning'] and nmap.debugging() > 0) then + insert(output, format("%s%s(WARNING: %s)\n", + format_get_indent(indent), prefix, + data['warning'])) + end + + for i, value in ipairs(data) do + if(type(value) == 'table') then + -- Do a shallow copy of indent + local new_indent = {} + for _, v in ipairs(indent) do + insert(new_indent, v) + end + + if(i ~= #data) then + insert(new_indent, false) + else + insert(new_indent, true) + end + + insert(output, format_output_sub(status, value, new_indent)) + + elseif(type(value) == 'string') then + local lines = splitlines(value) + + for j, line in ipairs(lines) do + insert(output, format("%s %s%s\n", + format_get_indent(indent, i == #data and j == #lines), + prefix, line)) + end + end + end + + return concat(output) +end + +---Takes a table of output on the commandline and formats it for display to the +-- user. This is basically done by converting an array of nested tables into a +-- string. In addition to numbered array elements, each table can have a 'name' +-- and a 'warning' value. The 'name' will be displayed above the table, and +-- 'warning' will be displayed, with a 'WARNING' tag, if and only if debugging +-- is enabled. +-- +-- Here's an example of a table: +-- +-- local domains = {} +-- domains['name'] = "DOMAINS" +-- table.insert(domains, 'Domain 1') +-- table.insert(domains, 'Domain 2') +-- +-- local names = {} +-- names['name'] = "NAMES" +-- names['warning'] = "Not all names could be determined!" +-- table.insert(names, "Name 1") +-- +-- local response = {} +-- table.insert(response, "Apple pie") +-- table.insert(response, domains) +-- table.insert(response, names) +-- +-- return stdnse.format_output(true, response) +-- +-- +-- With debugging enabled, this is the output: +-- +-- Host script results: +-- | smb-enum-domains: +-- | Apple pie +-- | DOMAINS +-- | Domain 1 +-- | Domain 2 +-- | NAMES (WARNING: Not all names could be determined!) +-- |_ Name 1 +-- +-- +--@param status A boolean value dictating whether or not the script succeeded. +-- If status is false, and debugging is enabled, 'ERROR' is prepended +-- to every line. If status is false and debugging is disabled, no output +-- occurs. +--@param data The table of output. +--@param indent Used for indentation on recursive calls; should generally be set to +-- nil when callling from a script. +-- @return nil, if data is empty, otherwise a +-- multiline string. +function format_output(status, data, indent) + -- If data is nil, die with an error (I keep doing that by accident) + assert(data, "No data was passed to format_output()") + + -- Don't bother if we don't have any data + if (#data == 0) then + return nil + end + + local result = format_output_sub(status, data, indent) + + -- Check for an empty result + if(result == nil or #result == "" or result == "\n" or result == "\n") then + return nil + end + + return result +end + +-- Get the value of a script argument, or nil if the script argument was not +-- given. This works also for arguments given as top-level array values, like +-- --script-args=unsafe; for these it returns the value 1. +local function arg_value(argname) + if nmap.registry.args[argname] then + return nmap.registry.args[argname] + else + -- if scriptname.arg is not there, check "arg" + local argument_frags = strsplit("%.", argname) + if #argument_frags > 0 then + if nmap.registry.args[argument_frags[2]] then + return nmap.registry.args[argument_frags[2]] + end + end + end + + for _, v in ipairs(nmap.registry.args) do + if v == argname then + return 1 + end + end +end + +--- Parses the script arguments passed to the --script-args option. +-- +-- @usage +-- --script-args 'script.arg1=value,script.arg3,script-x.arg=value' +-- local arg1, arg2, arg3 = get_script_args('script.arg1','script.arg2','script.arg3') +-- => arg1 = value +-- => arg2 = nil +-- => arg3 = 1 +-- +-- --script-args 'displayall,unsafe,script-x.arg=value,script-y.arg=value' +-- local displayall, unsafe = get_script_args('displayall','unsafe') +-- => displayall = 1 +-- => unsafe = 1 +-- +-- --script-args 'dns-cache-snoop.mode=timed,dns-cache-snoop.domains={host1,host2}' +-- local mode, domains = get_script_args('dns-cache-snoop.mode', +-- 'dns-cache-snoop.domains') +-- => mode = 'timed' +-- => domains = {host1,host2} +-- +-- @param Arguments Script arguments to check. +-- @return Arguments values. +function get_script_args (...) + local args = {} + + for i, set in ipairs({...}) do + if type(set) == "string" then + set = {set} + end + for _, test in ipairs(set) do + local v = arg_value(test) + if v then + args[i] = v + break + end + end + end + + return unpack(args, 1, select("#", ...)) +end + +---Get the best possible hostname for the given host. This can be the target as given on +-- the commandline, the reverse dns name, or simply the ip address. +--@param host The host table (or a string that'll simply be returned). +--@return The best possible hostname, as a string. +function get_hostname(host) + if type(host) == "table" then + return host.targetname or ( host.name ~= '' and host.name ) or host.ip + else + return host + end +end + +---Retrieve an item from the registry, checking if each sub-key exists. If any key doesn't +-- exist, return nil. +function registry_get(subkeys) + local registry = nmap.registry + local i = 1 + + while(subkeys[i]) do + if(not(registry[subkeys[i]])) then + return nil + end + + registry = registry[subkeys[i]] + + i = i + 1 + end + + return registry +end + +--Check if the given element exists in the registry. If 'key' is nil, it isn't checked. +function registry_exists(subkeys, key, value) + local subkey = registry_get(subkeys) + + if(not(subkey)) then + return false + end + + for k, v in pairs(subkey) do + if((key == nil or key == k) and (v == value)) then -- TODO: if 'value' is a table, this fails + return true + end + end + + return false +end + +---Add an item to an array in the registry, creating all sub-keys if necessary. +-- For example, calling: +-- registry_add_array({'192.168.1.100', 'www', '80', 'pages'}, 'index.html') +-- Will create nmap.registry['192.168.1.100'] as a table, if necessary, then add a table +-- under the 'www' key, and so on. 'pages', finally, is treated as an array and the value +-- given is added to the end. +function registry_add_array(subkeys, value, allow_duplicates) + local registry = nmap.registry + local i = 1 + + -- Unless the user wants duplicates, make sure there aren't any + if(allow_duplicates ~= true) then + if(registry_exists(subkeys, nil, value)) then + return + end + end + + while(subkeys[i]) do + if(not(registry[subkeys[i]])) then + registry[subkeys[i]] = {} + end + registry = registry[subkeys[i]] + i = i + 1 + end + + -- Make sure the value isn't already in the table + for _, v in pairs(registry) do + if(v == value) then + return + end + end + insert(registry, value) +end + +---Similar to registry_add_array, except instead of adding a value to the +-- end of an array, it adds a key:value pair to the table. +function registry_add_table(subkeys, key, value, allow_duplicates) + local registry = nmap.registry + local i = 1 + + -- Unless the user wants duplicates, make sure there aren't any + if(allow_duplicates ~= true) then + if(registry_exists(subkeys, key, value)) then + return + end + end + + while(subkeys[i]) do + if(not(registry[subkeys[i]])) then + registry[subkeys[i]] = {} + end + registry = registry[subkeys[i]] + i = i + 1 + end + + registry[key] = value +end + + +--- This function allows you to create worker threads that may perform +-- network tasks in parallel with your script thread. +-- +-- Any network task (e.g. socket:connect(...)) will cause the +-- running thread to yield to NSE. This allows network tasks to appear to be +-- blocking while being able to run multiple network tasks at once. +-- While this is useful for running multiple separate scripts, it is +-- unfortunately difficult for a script itself to perform network tasks in +-- parallel. In order to allow scripts to also have network tasks running in +-- parallel, we provide this function, stdnse.new_thread, to +-- create a new thread that can perform its own network related tasks +-- in parallel with the script. +-- +-- The script launches the worker thread by calling the new_thread +-- function with the parameters: +-- * The main Lua function for the script to execute, similar to the script action function. +-- * The variable number of arguments to be passed to the worker's main function. +-- +-- The stdnse.new_thread function will return two results: +-- * The worker thread's base (main) coroutine (useful for tracking status). +-- * A status query function (described below). +-- +-- The status query function shall return two values: +-- * The result of coroutine.status using the worker thread base coroutine. +-- * The error object thrown that ended the worker thread or nil if no error was thrown. This is typically a string, like most Lua errors. +-- +-- Note that NSE discards all return values of the worker's main function. You +-- must use function parameters, upvalues or environments to communicate +-- results. +-- +-- You should use the condition variable (nmap.condvar) +-- and mutex (nmap.mutex) facilities to coordinate with your +-- worker threads. Keep in mind that Nmap is single threaded so there are +-- no (memory) issues in synchronization to worry about; however, there +-- is resource contention. Your resources are usually network +-- bandwidth, network sockets, etc. Condition variables are also useful if the +-- work for any single thread is dynamic. For example, a web server spider +-- script with a pool of workers will initially have a single root html +-- document. Following the retrieval of the root document, the set of +-- resources to be retrieved (the worker's work) will become very large +-- (an html document adds many new hyperlinks (resources) to fetch). +--@name new_thread +--@class function +--@param main The main function of the worker thread. +--@param ... The arguments passed to the main worker thread. +--@return co The base coroutine of the worker thread. +--@return info A query function used to obtain status information of the worker. +--@usage +--local requests = {"/", "/index.html", --[[ long list of objects ]]} +-- +--function thread_main (host, port, responses, ...) +-- local condvar = nmap.condvar(responses); +-- local what = {n = select("#", ...), ...}; +-- local allReqs = nil; +-- for i = 1, what.n do +-- allReqs = http.pGet(host, port, what[i], nil, nil, allReqs); +-- end +-- local p = assert(http.pipeline(host, port, allReqs)); +-- for i, response in ipairs(p) do responses[#responses+1] = response end +-- condvar "signal"; +--end +-- +--function many_requests (host, port) +-- local threads = {}; +-- local responses = {}; +-- local condvar = nmap.condvar(responses); +-- local i = 1; +-- repeat +-- local j = math.min(i+10, #requests); +-- local co = stdnse.new_thread(thread_main, host, port, responses, +-- table.unpack(requests, i, j)); +-- threads[co] = true; +-- i = j+1; +-- until i > #requests; +-- repeat +-- condvar "wait"; +-- for thread in pairs(threads) do +-- if coroutine.status(thread) == "dead" then threads[thread] = nil end +-- end +-- until next(threads) == nil; +-- return responses; +--end +do end -- no function here, see nse_main.lua + +--- Returns the base coroutine of the running script. +-- +-- A script may be resuming multiple coroutines to facilitate its own +-- collaborative multithreading design. Because there is a "root" or "base" +-- coroutine that lets us determine whether the script is still active +-- (that is, the script did not end, possibly due to an error), we provide +-- this stdnse.base function that will retrieve the base +-- coroutine of the script. This base coroutine is the coroutine that runs +-- the action function. +-- +-- The base coroutine is useful for many reasons but here are some common +-- uses: +-- * We want to attribute the ownership of an object (perhaps a network socket) to a script. +-- * We want to identify if the script is still alive. +--@name base +--@class function +--@return coroutine Returns the base coroutine of the running script. +do end -- no function here, see nse_main.lua + +--- The Lua Require Function with errors silenced. +-- +-- See the Lua manual for description of the require function. This modified +-- version allows the script to quietly fail at loading if a required +-- library does not exist. +-- +--@name silent_require +--@class function +--@usage stdnse.silent_require "openssl" +do end -- no function here, see nse_main.lua + + + +---Checks if the port is in the port range +-- For example, calling: +-- in_port_range({number=31337,protocol="udp"},"T:15,50-75,U:31334-31339") +-- would result in a true value +--@param port a port structure containing keys port number(number) and protocol(string) +--@param port_range a port range string in Nmap standard format (ex. "T:80,1-30,U:31337,21-25") +--@returns boolean indicating whether the port is in the port range +function in_port_range(port,port_range) + assert(port and type(port.number)=="number" and type(port.protocol)=="string" and + (port.protocol=="udp" or port.protocol=="tcp"),"Port structure missing or invalid: port={ number=, protocol= }") + assert((type(port_range)=="string" or type(port_range)=="number") and port_range~="","Incorrect port range specification.") + + -- Proto - true for TCP, false for UDP + local proto + if(port.protocol=="tcp") then proto = true else proto = false end + + --TCP flag for iteration - true for TCP, false for UDP, if not specified we presume TCP + local tcp_flag = true + + -- in case the port_range is a single number + if type(port_range)=="number" then + if proto and port_range==port.number then return true + else return false + end + end + + --clean the string a bit + port_range=port_range:gsub("%s+","") + + -- single_pr - single port range + for i, single_pr in ipairs(strsplit(",",port_range)) do + if single_pr:match("T:") then + tcp_flag = true + single_pr = single_pr:gsub("T:","") + else + if single_pr:match("U:") then + tcp_flag = false + single_pr = single_pr:gsub("U:","") + end + end + + -- compare ports only when the port's protocol is the same as + -- the current single port range + if tcp_flag == proto then + local pone = single_pr:match("^(%d+)$") + if pone then + pone = tonumber(pone) + assert(pone>-1 and pone<65536, "Port range number out of range (0-65535).") + + if pone == port.number then + return true + end + else + local pstart, pend = single_pr:match("^(%d+)%-(%d+)$") + pstart, pend = tonumber(pstart), tonumber(pend) + assert(pstart,"Incorrect port range specification.") + assert(pstart<=pend,"Incorrect port range specification, the starting port should have a smaller value than the ending port.") + assert(pstart>-1 and pstart<65536 and pend>-1 and pend<65536, "Port range number out of range (0-65535).") + + if port.number >=pstart and port.number <= pend then + return true + end + end + end + end + -- if no match is found then the port doesn't belong to the port_range + return false +end + +--- Module function that mimics some behavior of Lua 5.1 module function. +-- +-- This convenience function returns a module environment to set the _ENV +-- upvalue. The _NAME, _PACKAGE, and _M fields are set as in the Lua 5.1 +-- version of this function. Each option function (e.g. stdnse.seeall) +-- passed is run with the new environment, in order. +-- +-- @see stdnse.seeall +-- @see strict +-- @usage +-- _ENV = stdnse.module(name, stdnse.seeall, require "strict"); +-- @param name The module name. +-- @param ... Option functions which modify the environment of the module. +function module (name, ...) + local env = {}; + env._NAME = name; + env._PACKAGE = name:match("(.+)%.[^.]+$"); + env._M = env; + local mods = pack(...); + for i = 1, mods.n do + mods[i](env); + end + return env; +end + +--- Change environment to load global variables. +-- +-- Option function for use with stdnse.module. It is the same +-- as package.seeall from Lua 5.1. +-- +-- @see stdnse.module +-- @usage +-- _ENV = stdnse.module(name, stdnse.seeall); +-- @param env Environment to change. +function seeall (env) + local m = getmetatable(env) or {}; + m.__index = _G; + setmetatable(env, m); +end + +--- Return a table that keeps elements in order of insertion. +-- +-- The pairs function, called on a table returned by this function, will yield +-- elements in the order they were inserted. This function is meant to be used +-- to construct output tables returned by scripts. +-- +-- Reinserting a key that is already in the table does not change its position +-- in the order. However, removing a key by assigning to nil and +-- then doing another assignment will move the key to the end of the order. +-- +-- @return An ordered table. +function output_table () + local t = {} + local order = {} + local function iterator () + for i, key in ipairs(order) do + coroutine.yield(key, t[key]) + end + end + local mt = { + __newindex = function (_, k, v) + if t[k] == nil and v ~= nil then + -- New key? + table.insert(order, k) + elseif v == nil then + -- Deleting an existing key? + for i, key in ipairs(order) do + if key == k then + table.remove(order, i) + break + end + end + end + rawset(t, k, v) + end, + __index = function (_, k) + return t[k] + end, + __pairs = function (_) + return coroutine.wrap(iterator) + end, + __call = function (_) -- hack to mean "not_empty?" + return not not next(order) + end, + __len = function (_) + return #order + end + } + return setmetatable({}, mt) +end + +--- A pretty printer for Lua objects. +-- +-- Takes an object (usually a table) and prints it using the +-- printer function. The printer function takes a sole string +-- argument and will be called repeatedly. +-- +-- @args obj The object to pretty print. +-- @args printer The printer function. +function pretty_printer (obj, printer) + if printer == nil then printer = print end + + local function aux (obj, spacing) + local t = type(obj) + if t == "table" then + printer "{\n" + for k, v in pairs(obj) do + local spacing = spacing.."\t" + printer(spacing) + printer "[" + aux(k, spacing) + printer "] = " + aux(v, spacing) + printer ",\n" + end + printer(spacing.."}") + elseif t == "string" then + printer(format("%q", obj)) + else + printer(tostring(obj)) + end + end + + return aux(obj, "") +end + +-- This pattern must match the percent sign '%' since it is used in +-- escaping. +local FILESYSTEM_UNSAFE = "[^a-zA-Z0-9._-]" +--- +-- Escape a string to remove bytes and strings that may have meaning to +-- a filesystem, such as slashes. All bytes are escaped, except for: +-- * alphabetic a-z and A-Z, digits 0-9, . _ - +-- In addition, the strings "." and ".." have +-- their characters escaped. +-- +-- Bytes are escaped by a percent sign followed by the two-digit +-- hexadecimal representation of the byte value. +-- * filename_escape("filename.ext") --> "filename.ext" +-- * filename_escape("input/output") --> "input%2foutput" +-- * filename_escape(".") --> "%2e" +-- * filename_escape("..") --> "%2e%2e" +-- This escaping is somewhat like that of JavaScript +-- encodeURIComponent, except that fewer bytes are +-- whitelisted, and it works on bytes, not Unicode characters or UTF-16 +-- code points. +function filename_escape(s) + if s == "." then + return "%2e" + elseif s == ".." then + return "%2e%2e" + else + return (string.gsub(s, FILESYSTEM_UNSAFE, function (c) + return string.format("%%%02x", string.byte(c)) + end)) + end +end + +return _ENV; From 8502440ca9f62baaef24ad026edd574f678fa5dd Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:54:24 +0100 Subject: [PATCH 07/12] Update Dockerfile --- engines/nmap/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engines/nmap/Dockerfile b/engines/nmap/Dockerfile index 56336cd1..7ba89075 100644 --- a/engines/nmap/Dockerfile +++ b/engines/nmap/Dockerfile @@ -40,5 +40,7 @@ RUN pip3 install --trusted-host pypi.python.org -r requirements.txt EXPOSE 5001 #USER alpine #Can't set properly env vars from Docker because it sets root env only +COPY fixed_script/* /usr/share/nmap/scripts/ + # Run app when the container launches CMD ["gunicorn", "engine-nmap:app", "-b", "0.0.0.0:5001", "--access-logfile", "-", "-k", "gevent"] From 0f8658f6b162641e0e9a0b4dbba0e4da266bf057 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 6 Feb 2024 17:54:42 +0100 Subject: [PATCH 08/12] Update engine-nmap.py --- engines/nmap/engine-nmap.py | 133 +++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/engines/nmap/engine-nmap.py b/engines/nmap/engine-nmap.py index a4841d0a..429dc8e4 100644 --- a/engines/nmap/engine-nmap.py +++ b/engines/nmap/engine-nmap.py @@ -857,6 +857,75 @@ def _parse_report(filename, scan_id): except Exception: product = "" + script_output = "" + + #Get Result from Port Script. + for port_script in port.findall("script"): + #script_id = port_script.get("id") + script_output += port_script.get("output")+"\n" + # Disable hash for some script_id + # if script_id in ["fingerprint-strings"]: + # script_hash = "None" + # else: + # script_hash = hashlib.sha1(str(script_output).encode('utf-8')).hexdigest()[:6] + + # if script_id == "vulners": + # ( + # port_max_cvss, + # port_cve_list, + # port_cve_links, + # port_cpe, + # ) = _get_vulners_findings(script_output) + + # port_severity = "info" + # if port_max_cvss >= 7.5: + # port_severity = "high" + # elif port_max_cvss >= 5.0 and port_max_cvss < 7.5: + # port_severity = "medium" + # elif port_max_cvss >= 3.0 and port_max_cvss < 5.0: + # port_severity = "low" + + # res.append( + # deepcopy( + # _add_issue( + # scan_id, + # target, + # ts, + # "Nmap script '{}' detected findings on port {}/{}".format( + # script_id, proto, portid + # ), + # "The script '{}' detected following findings:\n{}".format( + # script_id, script_output + # ), + # severity=port_severity, + # type="port_script", + # tags=[script_id], + # risk={"cvss_base_score": port_max_cvss}, + # vuln_refs={"CVE": port_cve_list, "CPE": port_cpe}, + # links=port_cve_links, + # ) + # ) + # ) + # else: + # res.append( + # deepcopy( + # _add_issue( + # scan_id, + # target, + # ts, + # "Nmap script '{}' detected findings on port {}/{}".format( + # script_id, proto, portid + # ), + # "The script '{}' detected following findings:\n{}".format( + # script_id, script_output + # ), + # type="port_script", + # tags=[script_id], + # ) + # ) + # ) + + port_data.update({"script_output":script_output}) res.append( deepcopy( _add_issue( @@ -895,70 +964,6 @@ def _parse_report(filename, scan_id): ) ) - for port_script in port.findall("script"): - script_id = port_script.get("id") - script_output = port_script.get("output") - # Disable hash for some script_id - # if script_id in ["fingerprint-strings"]: - # script_hash = "None" - # else: - # script_hash = hashlib.sha1(str(script_output).encode('utf-8')).hexdigest()[:6] - - if script_id == "vulners": - ( - port_max_cvss, - port_cve_list, - port_cve_links, - port_cpe, - ) = _get_vulners_findings(script_output) - - port_severity = "info" - if port_max_cvss >= 7.5: - port_severity = "high" - elif port_max_cvss >= 5.0 and port_max_cvss < 7.5: - port_severity = "medium" - elif port_max_cvss >= 3.0 and port_max_cvss < 5.0: - port_severity = "low" - - res.append( - deepcopy( - _add_issue( - scan_id, - target, - ts, - "Nmap script '{}' detected findings on port {}/{}".format( - script_id, proto, portid - ), - "The script '{}' detected following findings:\n{}".format( - script_id, script_output - ), - severity=port_severity, - type="port_script", - tags=[script_id], - risk={"cvss_base_score": port_max_cvss}, - vuln_refs={"CVE": port_cve_list, "CPE": port_cpe}, - links=port_cve_links, - ) - ) - ) - else: - res.append( - deepcopy( - _add_issue( - scan_id, - target, - ts, - "Nmap script '{}' detected findings on port {}/{}".format( - script_id, proto, portid - ), - "The script '{}' detected following findings:\n{}".format( - script_id, script_output - ), - type="port_script", - tags=[script_id], - ) - ) - ) if ( not openports and "ports" in this.scans[scan_id]["options"].keys() From 301260ff6c65b48da6fd1dccac51b89330591569 Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 20 Feb 2024 17:11:31 +0100 Subject: [PATCH 09/12] Clean --- engines/nmap/engine-nmap.py | 63 ------------------------------------- 1 file changed, 63 deletions(-) diff --git a/engines/nmap/engine-nmap.py b/engines/nmap/engine-nmap.py index 429dc8e4..0ed5f23d 100644 --- a/engines/nmap/engine-nmap.py +++ b/engines/nmap/engine-nmap.py @@ -861,70 +861,7 @@ def _parse_report(filename, scan_id): #Get Result from Port Script. for port_script in port.findall("script"): - #script_id = port_script.get("id") script_output += port_script.get("output")+"\n" - # Disable hash for some script_id - # if script_id in ["fingerprint-strings"]: - # script_hash = "None" - # else: - # script_hash = hashlib.sha1(str(script_output).encode('utf-8')).hexdigest()[:6] - - # if script_id == "vulners": - # ( - # port_max_cvss, - # port_cve_list, - # port_cve_links, - # port_cpe, - # ) = _get_vulners_findings(script_output) - - # port_severity = "info" - # if port_max_cvss >= 7.5: - # port_severity = "high" - # elif port_max_cvss >= 5.0 and port_max_cvss < 7.5: - # port_severity = "medium" - # elif port_max_cvss >= 3.0 and port_max_cvss < 5.0: - # port_severity = "low" - - # res.append( - # deepcopy( - # _add_issue( - # scan_id, - # target, - # ts, - # "Nmap script '{}' detected findings on port {}/{}".format( - # script_id, proto, portid - # ), - # "The script '{}' detected following findings:\n{}".format( - # script_id, script_output - # ), - # severity=port_severity, - # type="port_script", - # tags=[script_id], - # risk={"cvss_base_score": port_max_cvss}, - # vuln_refs={"CVE": port_cve_list, "CPE": port_cpe}, - # links=port_cve_links, - # ) - # ) - # ) - # else: - # res.append( - # deepcopy( - # _add_issue( - # scan_id, - # target, - # ts, - # "Nmap script '{}' detected findings on port {}/{}".format( - # script_id, proto, portid - # ), - # "The script '{}' detected following findings:\n{}".format( - # script_id, script_output - # ), - # type="port_script", - # tags=[script_id], - # ) - # ) - # ) - port_data.update({"script_output":script_output}) res.append( deepcopy( From 31f80d6fc77b5ac91dffb469f1052b1cd5d2ab11 Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 20 Feb 2024 17:14:42 +0100 Subject: [PATCH 10/12] Updated VERSION (nmap) --- engines/nmap/Dockerfile | 2 +- engines/nmap/VERSION | 2 +- engines/nmap/__init__.py | 2 +- engines/nmap/nmap.json.sample | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/engines/nmap/Dockerfile b/engines/nmap/Dockerfile index ef23978c..d9414350 100644 --- a/engines/nmap/Dockerfile +++ b/engines/nmap/Dockerfile @@ -1,5 +1,5 @@ FROM alpine:3.16.3 -LABEL Name="Nmap\ \(Patrowl engine\)" Version="1.4.48" +LABEL Name="Nmap\ \(Patrowl engine\)" Version="1.4.49-rc1" # Set the working directory RUN mkdir -p /opt/patrowl-engines/nmap diff --git a/engines/nmap/VERSION b/engines/nmap/VERSION index 3d558d0b..3d529614 100644 --- a/engines/nmap/VERSION +++ b/engines/nmap/VERSION @@ -1 +1 @@ -1.4.48 +1.4.49-rc1 diff --git a/engines/nmap/__init__.py b/engines/nmap/__init__.py index 460c061c..5ec9942c 100644 --- a/engines/nmap/__init__.py +++ b/engines/nmap/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- __title__ = 'patrowl_engine_nmap' -__version__ = '1.4.34' +__version__ = '1.4.49-rc1' __author__ = 'Nicolas MATTIOCCO' __license__ = 'AGPLv3' __copyright__ = 'Copyright (C) 2018-2022 Nicolas Mattiocco - @MaKyOtOx' diff --git a/engines/nmap/nmap.json.sample b/engines/nmap/nmap.json.sample index a846cb45..27da3b5a 100644 --- a/engines/nmap/nmap.json.sample +++ b/engines/nmap/nmap.json.sample @@ -1,6 +1,6 @@ { "name": "Nmap", - "version": "1.4.34", + "version": "1.4.49-rc1", "description": "Network Scanner", "path": "/usr/bin/nmap", "allowed_asset_types": ["ip", "domain", "fqdn", "url", "ip-range", "ip-subnet"], From bf0acd2355c640f0996a48a354f08ba469065581 Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 20 Feb 2024 17:27:02 +0100 Subject: [PATCH 11/12] Updated VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 07a45d78..58d4a285 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.21 +1.5.22-rc1 From 22e73b1d1b22befda4528720e4f44ec1d87eeb1b Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 22 Feb 2024 18:34:10 +0100 Subject: [PATCH 12/12] Updated VERSION (nmap) --- VERSION | 2 +- engines/nmap/Dockerfile | 2 +- engines/nmap/VERSION | 2 +- engines/nmap/__init__.py | 2 +- engines/nmap/nmap.json.sample | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index 58d4a285..d532fd93 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.22-rc1 +1.5.22 diff --git a/engines/nmap/Dockerfile b/engines/nmap/Dockerfile index d9414350..6d0d5e96 100644 --- a/engines/nmap/Dockerfile +++ b/engines/nmap/Dockerfile @@ -1,5 +1,5 @@ FROM alpine:3.16.3 -LABEL Name="Nmap\ \(Patrowl engine\)" Version="1.4.49-rc1" +LABEL Name="Nmap\ \(Patrowl engine\)" Version="1.4.49" # Set the working directory RUN mkdir -p /opt/patrowl-engines/nmap diff --git a/engines/nmap/VERSION b/engines/nmap/VERSION index 3d529614..ed32bf2a 100644 --- a/engines/nmap/VERSION +++ b/engines/nmap/VERSION @@ -1 +1 @@ -1.4.49-rc1 +1.4.49 diff --git a/engines/nmap/__init__.py b/engines/nmap/__init__.py index 5ec9942c..b11a9d2d 100644 --- a/engines/nmap/__init__.py +++ b/engines/nmap/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- __title__ = 'patrowl_engine_nmap' -__version__ = '1.4.49-rc1' +__version__ = '1.4.49' __author__ = 'Nicolas MATTIOCCO' __license__ = 'AGPLv3' __copyright__ = 'Copyright (C) 2018-2022 Nicolas Mattiocco - @MaKyOtOx' diff --git a/engines/nmap/nmap.json.sample b/engines/nmap/nmap.json.sample index 27da3b5a..0bea308f 100644 --- a/engines/nmap/nmap.json.sample +++ b/engines/nmap/nmap.json.sample @@ -1,6 +1,6 @@ { "name": "Nmap", - "version": "1.4.49-rc1", + "version": "1.4.49", "description": "Network Scanner", "path": "/usr/bin/nmap", "allowed_asset_types": ["ip", "domain", "fqdn", "url", "ip-range", "ip-subnet"],