diff --git a/VERSION b/VERSION index 07a45d78..d532fd93 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.21 +1.5.22 diff --git a/engines/nmap/Dockerfile b/engines/nmap/Dockerfile index 7f8f02af..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.48" +LABEL Name="Nmap\ \(Patrowl engine\)" Version="1.4.49" # Set the working directory RUN mkdir -p /opt/patrowl-engines/nmap @@ -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"] diff --git a/engines/nmap/VERSION b/engines/nmap/VERSION index 3d558d0b..ed32bf2a 100644 --- a/engines/nmap/VERSION +++ b/engines/nmap/VERSION @@ -1 +1 @@ -1.4.48 +1.4.49 diff --git a/engines/nmap/__init__.py b/engines/nmap/__init__.py index 460c061c..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.34' +__version__ = '1.4.49' __author__ = 'Nicolas MATTIOCCO' __license__ = 'AGPLv3' __copyright__ = 'Copyright (C) 2018-2022 Nicolas Mattiocco - @MaKyOtOx' diff --git a/engines/nmap/engine-nmap.py b/engines/nmap/engine-nmap.py index 1d63384f..4bf14791 100644 --- a/engines/nmap/engine-nmap.py +++ b/engines/nmap/engine-nmap.py @@ -859,6 +859,12 @@ def _parse_report(filename, scan_id): except Exception: product = "" + script_output = "" + + #Get Result from Port Script. + for port_script in port.findall("script"): + script_output += port_script.get("output")+"\n" + port_data.update({"script_output":script_output}) res.append( deepcopy( _add_issue( @@ -897,70 +903,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() 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 + 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; diff --git a/engines/nmap/nmap.json.sample b/engines/nmap/nmap.json.sample index a846cb45..0bea308f 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", "description": "Network Scanner", "path": "/usr/bin/nmap", "allowed_asset_types": ["ip", "domain", "fqdn", "url", "ip-range", "ip-subnet"],