diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/smb-psexec.nse | 1565 |
1 files changed, 1565 insertions, 0 deletions
diff --git a/scripts/smb-psexec.nse b/scripts/smb-psexec.nse new file mode 100644 index 0000000..9f89541 --- /dev/null +++ b/scripts/smb-psexec.nse @@ -0,0 +1,1565 @@ +local _G = require "_G" +local io = require "io" +local math = require "math" +local msrpc = require "msrpc" +local nmap = require "nmap" +local smb = require "smb" +local stdnse = require "stdnse" +local string = require "string" +local stringaux = require "stringaux" +local table = require "table" + +description = [[ +Implements remote process execution similar to the Sysinternals' psexec +tool, allowing a user to run a series of programs on a remote machine and +read the output. This is great for gathering information about servers, +running the same tool on a range of system, or even installing a backdoor on +a collection of computers. + +This script can run commands present on the remote machine, such as ping or +tracert, or it can upload a program and run it, such as pwdump6 or a +backdoor. Additionally, it can read the program's stdout/stderr and return +it to the user (works well with ping, pwdump6, etc), or it can read a file +that the process generated (fgdump, for example, generates a file), or it +can just start the process and let it run headless (a backdoor might run +like this). + +To use this, a configuration file should be created and edited. Several +configuration files are included that you can customize, or you can write +your own. This config file is placed in <code>nselib/data/psexec</code> (if +you aren't sure where that is, search your system for +<code>default.lua</code>), then is passed to Nmap as a script argument (for +example, myconfig.lua would be passed as +<code>--script-args=config=myconfig</code>. + +The configuration file consists mainly of a module list. Each module is +defined by a lua table, and contains fields for the name of the program, the +executable and arguments for the program, and a score of other options. +Modules also have an 'upload' field, which determines whether or not the +module is to be uploaded. Here is a simple example of how to run <code>net +localgroup administrators</code>, which returns a list of users in the +"administrators" group (take a look at the <code>examples.lua</code> +configuration file for these examples): + +<code> + mod = {} + mod.upload = false + mod.name = "Example 1: Membership of 'administrators'" + mod.program = "net.exe" + mod.args = "localgroup administrators" + table.insert(modules, mod) +</code> + +<code>mod.upload</code> is <code>false</code>, meaning the program should +already be present on the remote system (since 'net.exe' is on every version +of Windows, this should be the case). <code>mod.name</code> defines the name +that the program will have in the output. <code>mod.program</code> and +<code>mod.args</code> obviously define which program is going to be run. The +output for this script is this: + +<code> + | Example 1: Membership of 'administrators' + | | Alias name administrators + | | Comment Administrators have complete and unrestricted access to the computer/domain + | | + | | Members + | | + | | ------------------------------------------------------------------------------- + | | Administrator + | | ron + | | test + | | The command completed successfully. + | | + | |_ +</code> + +That works, but it's really ugly. In general, we can use +<code>mod.find</code>, <code>mod.replace</code>, <code>mod.remove</code>, +and <code>mod.noblank</code> to clean up the output. For this example, we're +going to use <code>mod.remove</code> to remove a lot of the useless lines, +and <code>mod.noblank</code> to get rid of the blank lines that we don't +want: + +<code> + mod = {} + mod.upload = false + mod.name = "Example 2: Membership of 'administrators', cleaned" + mod.program = "net.exe" + mod.args = "localgroup administrators" + mod.remove = {"The command completed", "%-%-%-%-%-%-%-%-%-%-%-", "Members", "Alias name", "Comment"} + mod.noblank = true + table.insert(modules, mod) +</code> + +We can see that the output is now much cleaner: + +<code> +| Example 2: Membership of 'administrators', cleaned +| | Administrator +| | ron +| |_test +</code> + +For our next command, we're going to run Windows' ipconfig.exe, which +outputs a significant amount of unnecessary information, and what we do want +isn't formatted very nicely. All we want is the IP address and MAC address, +and we get it using <code>mod.find</code> and <code>mod.replace</code>: + +<code> + mod = {} + mod.upload = false + mod.name = "Example 3: IP Address and MAC Address" + mod.program = "ipconfig.exe" + mod.args = "/all" + mod.maxtime = 1 + mod.find = {"IP Address", "Physical Address", "Ethernet adapter"} + mod.replace = {{"%. ", ""}, {"-", ":"}, {"Physical Address", "MAC Address"}} + table.insert(modules, mod) +</code> + +This module searches for lines that contain "IP Address", "Physical +Address", or "Ethernet adapter". In these lines, a ". " is replaced with +nothing, a "-" is replaced with a colon, and the term "Physical Address" is +replaced with "MAC Address" (arguably unnecessary). Run ipconfig /all +yourself to see what we start with, but here's the final output: + +<code> +| Example 3: IP Address and MAC Address +| | Ethernet adapter Local Area Connection: +| | MAC Address: 00:0C:29:12:E6:DB +| |_ IP Address: 192.168.1.21| Example 3: IP Address and MAC Address +</code> + +Another interesting part of this script is that variables can be used in any +script fields. There are two types of variables: built-in and user-supplied. +Built-in variables can be anything found in the <code>config</code> table, +most of which are listed below. The more interesting ones are: + +* <code>$lhost</code>: The address of the scanner +* <code>$rhost</code>: The address being scanned +* <code>$path</code>: The path where the scripts are uploaded +* <code>$share</code>: The share where the script was uploaded + +User-supplied arguments are given on the commandline, and can be controlled +by <code>mod.req_args</code> in the configuration file. Arguments are given +by the user in --script-args; for example, to set $host to '1.2.3.4', the +user would pass in --script-args=host=1.2.3.4. To ensure the user passes in +the host variable, <code>mod.req_args</code> would be set to +<code>{'host'}</code>. + +Here is a module that pings the local ip address: + +<code> + mod = {} + mod.upload = false + mod.name = "Example 4: Can the host ping our address?" + mod.program = "ping.exe" + mod.args = "$lhost" + mod.remove = {"statistics", "Packet", "Approximate", "Minimum"} + mod.noblank = true + mod.env = "SystemRoot=c:\\WINDOWS" + table.insert(modules, mod) +</code> + +And the output: +<code> +| Example 4: Can the host ping our address? +| | Pinging 192.168.1.100 with 32 bytes of data: +| | Reply from 192.168.1.100: bytes=32 time<1ms TTL=64 +| | Reply from 192.168.1.100: bytes=32 time<1ms TTL=64 +| | Reply from 192.168.1.100: bytes=32 time<1ms TTL=64 +| |_Reply from 192.168.1.100: bytes=32 time<1ms TTL=64 +</code> + +And this module pings an arbitrary address that the user is expected to +give: + +<code> + mod = {} + mod.upload = false + mod.name = "Example 5: Can the host ping $host?" + mod.program = "ping.exe" + mod.args = "$host" + mod.remove = {"statistics", "Packet", "Approximate", "Minimum"} + mod.noblank = true + mod.env = "SystemRoot=c:\\WINDOWS" + mod.req_args = {'host'} + table.insert(modules, mod) +</code> + +And the output (note that we had to up the timeout so this would complete; +we'll talk about override values later): + +<code> +$ ./nmap -n -d -p445 --script=smb-psexec --script-args=smbuser=test,smbpass=test,config=examples,host=1.2.3.4 192.168.1.21 +[...] +| Example 5: Can the host ping 1.2.3.4? +| | Pinging 1.2.3.4 with 32 bytes of data: +| | Request timed out. +| | Request timed out. +| | Request timed out. +| |_Request timed out. +</code> + +For the final example, we'll use the <code>upload</code> command to upload +<code>fgdump.exe</code>, run it, download its output file, and clean up its +logfile. You'll have to put <code>fgdump.exe</code> in the same folder as +the script for this to work: + +<code> + mod = {} + mod.upload = true + mod.name = "Example 6: FgDump" + mod.program = "fgdump.exe" + mod.args = "-c -l fgdump.log" + mod.url = "http://www.foofus.net/fizzgig/fgdump/" + mod.tempfiles = {"fgdump.log"} + mod.outfile = "127.0.0.1.pwdump" + table.insert(modules, mod) +</code> + +The <code>-l</code> argument for fgdump supplies the name of the logfile. +That file is listed in the <code>mod.tempfiles</code> field. What, exactly, +does <code>mod.tempfiles</code> do? It simply gives the service a list of +files to delete while cleaning up. The cleanup process will be discussed +later. + +<code>mod.url</code> is displayed to the user if <code>mod.program</code> +isn't found in <code>nselib/data/psexec/</code>. And finally, +<code>mod.outfile</code> is the file that is downloaded from the system. +This is required because fgdump writes to an output file instead of to +stdout (pwdump6, for example, doesn't require <code>mod.outfile</code>. + +Now that we've seen a few possible combinations of fields, I present a +complete list of all fields available and what each of them do. Many of them +will be familiar, but there are a few that aren't discussed in the examples: + +* <code>upload</code> (boolean) true if it's a local file to upload, false + if it's already on the host machine. If + <code>upload</code> is true, <code>program</code> has + to be in <code>nselib/data/psexec</code>. +* <code>name</code> (string) The name to display above the output. If this + isn't given, <code>program</code> .. <code>args</code> + are used. +* <code>program</code> (string) If <code>upload</code> is false, the name + (fully qualified or relative) of the program on the + remote system; if <code>upload</code> is true, the + name of the local file that will be uploaded (stored + in <code>nselib/data/psexec</code>). +* <code>args</code> (string) Arguments to pass to the process. +* <code>env</code> (string) Environmental variables to pass to the process, + as name=value pairs, delimited, per Microsoft's spec, by + NULL characters (<code>string.char(0)</code>). +* <code>maxtime</code> (integer) The approximate amount of time to wait for + this process to complete. The total timeout for the + script before it gives up waiting for a response is + the total of all <code>maxtime</code> fields. +* <code>extrafiles</code> (string[]) Extra file(s) to upload before running + the program. These will not be renamed (because, + presumably, if they are then the program won't be + able to find them), but they will be marked as + hidden/system/etc. This may cause a race condition + if multiple people are doing this at once, but + there isn't much we can do. The files are also + deleted afterwards as tempfiles would be. The + files have to be in the same directory as programs + (<code>nselib/data/psexec</code>), but the program + doesn't necessarily need to be an uploaded one. +* <code>tempfiles</code> (string[]) A list of temporary files that the + process is known to create (if the process does + create files, using this field is recommended + because it helps avoid making a mess on the remote + system). +* <code>find</code> (string[]) Only display lines that contain the given + string(s) (for example, if you're searching for a line + that contains "IP Address", set this to <code>{'IP + Address'}</code>. This allows Lua-style patterns, see: + http://lua-users.org/wiki/PatternsTutorial (don't forget + to escape special characters with a <code>%</code>). + Note that this is client-side only; the full output is + still returned, the rest is removed while displaying. + The line of output only needs to match one of the + strings given here. +* <code>remove</code> (string[]) Opposite of <code>find</code>; this removes + lines containing the given string(s) instead of + displaying them. Like <code>find</code>, this is + client-side only and uses Lua-style patterns. If + <code>remove</code> and <code>find</code> are in + conflict, then <code>remove</code> takes priority. +* <code>noblank</code> (boolean) Setting this to true removes all blank + lines from the output. +* <code>replace</code> (table) A table of values to replace in the strings + returned. Like <code>find</code> and + <code>replace</code>, this is client-side only and + uses Lua-style patterns. +* <code>headless</code> (boolean) If <code>headless</code> is set to true, + the program doesn't return any output; rather, it + runs detached from the service so that, when the + service ends, the program keeps going. This can be + useful for, say, a monitoring program. Or a + backdoor, if that's what you're into (a Metasploit + payload should work nicely). Not compatible with: + <code>find</code>, <code>remove</code>, + <code>noblank</code>, <code>replace</code>, + <code>maxtime</code>, <code>outfile</code>. +* <code>enabled</code> (boolean) Set to false, and optionally set + <code>disabled_message</code>, if you don't want a + module to run. Alternatively, you can comment out + the process. +* <code>disabled_message</code> (string) Displayed if the module is disabled. +* <code>url</code> (string) A module where the user can download the + uploadable file. Displayed if the uploadable file is + missing. +* <code>outfile</code> (string) If set, the specified file will be returned + instead of stdout. +* <code>req_args</code> (string[]) An array of arguments that the user must + set in <code>--script-args</code>. + + +Any field in the configuration file can contain variables, as discussed. +Here are some of the available built-in variables: + +* <code>$lhost</code>: local IP address as a string. +* <code>$lport</code>: local port (meaningless; it'll change by the time the + module is uploaded since multiple connections are + made). +* <code>$rhost</code>: remote IP address as a string. +* <code>$rport</code>: remote port. +* <code>$lmac</code>: local MAC address as a string in the + xx:xx:xx:xx:xx:xx format (note: requires root). +* <code>$path</code>: the path where the file will be uploaded to. +* <code>$service_name</code>: the name of the service that will be running + this program +* <code>$service_file</code>: the name of the executable file for the + service +* <code>$temp_output_file</code>: The (ciphered) file where the programs' + output will be written before being + renamed to $output_file +* <code>$output_file</code>: The final name of the (ciphered) output file. + When this file appears, the script downloads it + and stops the service +* <code>$timeout</code>: The total amount of time the script is going to run + before it gives up and stops the process +* <code>$share</code>: The share that everything was uploaded to +* (script args): Any value passed as a script argument will be replaced (for + example, if Nmap is run with + <code>--script-args=var3=10</code>, then <code>$var3</code> + in any field will be replaced with <code>10</code>. See the + <code>req_args</code> field above. Script argument values + take priority over config values. + +In addition to modules, the configuration file can also contain overrides. +Most of these aren't useful, so I'm not going to go into great detail. +Search <code>smb-psexec.nse</code> for any reference to the +<code>config</code> table; any value in the <code>config</code> table can be +overridden with the <code>overrides</code> table in the module. The most +useful value to override is probably <code>timeout</code>. + +Before and after scripts are run, and when there's an error, a cleanup is +performed. in the cleanup, we attempt to stop the remote processes, delete +all programs, output files, temporary files, extra files, etc. A lot of +effort was put into proper cleanup, since making a mess on remote systems is +a bad idea. + + +Now that I've talked at length about how to use this script, I'd like to +spend some time talking about how it works. + +Running a script happens in several stages: + +1. An open fileshare is found that we can write to. Finding an open + fileshare basically consists of enumerating all shares and seeing which + one(s) we have access to. +2. A "service wrapper", and all of the uploadable/extra files, are uploaded. + Before they're uploaded, the name of each file is obfuscated. The + obfuscation completely renames the file, is unique for each source system, + and doesn't change between multiple runs. This obfuscation has the benefit + of preventing filenames from overlapping if multiple people are running this + against the same computer, and also makes it more difficult to determine + their purposes. The reason for keeping them consistent for every run is to + make cleanup possible: a random filename, if the script somehow fails, will + be left on the system. +3. A new service is created and started. The new service has a random name + for the same reason the files do, and points at the 'service wrapper' + program that was uploaded. +4. The service runs the processes. One by one, the processes are run and + their output is captured. The output is obfuscated using a simple (and + highly insecure) xor algorithm, which is designed to prevent casual sniffing + (but won't deter intelligent attackers). This data is put into a temporary + output file. When all the programs have finished, the file is renamed to the + final output file +5. The output file is downloaded, and the cleanup is performced. The file + being renamed triggers the final stage of the program, where the data is + downloaded and all relevant files are deleted. +6. Output file, now decrypted, is formatted and displayed to the user. + +And that's how it works! + +Please post any questions, or suggestions for better modules, to +dev@nmap.org. + +And, as usual, since this tool can be dangerous and can easily be viewed as +a malicious tool -- use this responsibly, and don't break any laws with it. + +Some ideas for later versions (TODO): + +* Set up a better environment for scripts (<code>PATH</code>, + <code>SystemRoot</code>, etc). Without this, a lot of programs (especially + ones that deal with network traffic) behave oddly. +* Abstract the code required to run remote processes so other scripts can + use it more easily (difficult, but will ultimately be well worth it + later). (May actually not be possible. There is a lot of overhead and + specialized code in this module. We'll see, though.) +* Let user specify an output file (per-script) so they can, for example, + download binary files (don't think it's worthwhile). +* Consider running the external programs in parallel (not sure if the + benefits outweigh the drawbacks). +* Let the config request the return code from the process instead of the + output (not sure if doing this would be worth the effort). +* Check multiple shares in a single session to save packets (and see where + else we can tighten up the amount of traffic). +]] + +--- +-- @usage +-- nmap --script smb-psexec.nse --script-args=smbuser=<username>,smbpass=<password>[,config=<config>] -p445 <host> +-- sudo nmap -sU -sS --script smb-psexec.nse --script-args=smbuser=<username>,smbpass=<password>[,config=<config>] -p U:137,T:139 <host> +-- +-- @output +-- Host script results: +-- | smb-psexec: +-- | | Windows version +-- | | |_ Microsoft Windows 2000 [Version 5.00.2195] +-- | | IP Address and MAC Address from 'ipconfig.exe' +-- | | | Ethernet adapter Local Area Connection 2: +-- | | | MAC Address: 00:50:56:A1:24:C2 +-- | | | IP Address: 10.0.0.30 +-- | | | Ethernet adapter Local Area Connection: +-- | | |_ MAC Address: 00:50:56:A1:00:65 +-- | | User list from 'net user' +-- | | | Administrator TestUser3 Guest +-- | | | IUSR_RON-WIN2K-TEST IWAM_RON-WIN2K-TEST nmap +-- | | | rontest123 sshd SvcCOPSSH +-- | | |_ test1234 Testing TsInternetUser +-- | | Membership of 'administrators' from 'net localgroup administrators' +-- | | | Administrator +-- | | | SvcCOPSSH +-- | | | test1234 +-- | | |_ Testing +-- | | Can the host ping our address? +-- | | | Pinging 10.0.0.138 with 32 bytes of data: +-- | | |_ Reply from 10.0.0.138: bytes=32 time<10ms TTL=64 +-- | | Traceroute back to the scanner +-- | | |_ 1 <10 ms <10 ms <10 ms 10.0.0.138 +-- | | ARP Cache from arp.exe +-- | | | Internet Address Physical Address Type +-- | | |_ 10.0.0.138 00-50-56-a1-27-4b dynamic +-- | | List of listening and established connections (netstat -an) +-- | | | Proto Local Address Foreign Address State +-- | | | TCP 0.0.0.0:22 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:25 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:80 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:135 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:443 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:445 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:1025 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:1028 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:1029 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING +-- | | | TCP 0.0.0.0:4933 0.0.0.0:0 LISTENING +-- | | | TCP 10.0.0.30:139 0.0.0.0:0 LISTENING +-- | | | TCP 127.0.0.1:2528 127.0.0.1:2529 ESTABLISHED +-- | | | TCP 127.0.0.1:2529 127.0.0.1:2528 ESTABLISHED +-- | | | TCP 127.0.0.1:2531 127.0.0.1:2532 ESTABLISHED +-- | | | TCP 127.0.0.1:2532 127.0.0.1:2531 ESTABLISHED +-- | | | TCP 127.0.0.1:5152 0.0.0.0:0 LISTENING +-- | | | TCP 127.0.0.1:5152 127.0.0.1:2530 CLOSE_WAIT +-- | | | UDP 0.0.0.0:135 *:* +-- | | | UDP 0.0.0.0:445 *:* +-- | | | UDP 0.0.0.0:1030 *:* +-- | | | UDP 0.0.0.0:3456 *:* +-- | | | UDP 10.0.0.30:137 *:* +-- | | | UDP 10.0.0.30:138 *:* +-- | | | UDP 10.0.0.30:500 *:* +-- | | | UDP 10.0.0.30:4500 *:* +-- | | |_ UDP 127.0.0.1:1026 *:* +-- | | Full routing table from 'netstat -nr' +-- | | | =========================================================================== +-- | | | Interface List +-- | | | 0x1 ........................... MS TCP Loopback interface +-- | | | 0x2 ...00 50 56 a1 00 65 ...... VMware Accelerated AMD PCNet Adapter +-- | | | 0x1000004 ...00 50 56 a1 24 c2 ...... VMware Accelerated AMD PCNet Adapter +-- | | | =========================================================================== +-- | | | =========================================================================== +-- | | | Active Routes: +-- | | | Network Destination Netmask Gateway Interface Metric +-- | | | 10.0.0.0 255.255.255.0 10.0.0.30 10.0.0.30 1 +-- | | | 10.0.0.30 255.255.255.255 127.0.0.1 127.0.0.1 1 +-- | | | 10.255.255.255 255.255.255.255 10.0.0.30 10.0.0.30 1 +-- | | | 127.0.0.0 255.0.0.0 127.0.0.1 127.0.0.1 1 +-- | | | 224.0.0.0 224.0.0.0 10.0.0.30 10.0.0.30 1 +-- | | | 255.255.255.255 255.255.255.255 10.0.0.30 2 1 +-- | | | =========================================================================== +-- | | | Persistent Routes: +-- | | | None +-- |_ |_ |_ Route Table +-- +--@args config The config file to use (eg, default). Config files require a .lua extension, and are located in <code>nselib/data/psexec</code>. +--@args nohide Don't set the uploaded files to hidden/system/etc. +--@args cleanup Set to only clean up any mess we made (leftover files, processes, etc. on the host OS) on a previous run of the script. +-- This will attempt to delete the files from every share, not just the first one. This is done to prevent leftover +-- files if the OS changes the ordering of the shares (there's no guarantee of shares coming back in any particular +-- order) +-- Note that cleaning up is still fairly invasive, since it has to re-discover the proper share, connect to it, +-- delete files, open the services manager, etc. +--@args share Set to override the share used for uploading. This also stops shares from being enumerated, and all other shares +-- will be ignored. No checks are done to determine whether or not this is a valid share before using it. Reqires +-- <code>sharepath</code> to be set. +--@args sharepath The full path to the share (eg, <code>"c:\windows"</code>). This is required when creating a service. +--@args time The minimum amount of time, in seconds, to wait for the external module to finish (default: <code>15</code>) +-- +--@args nocleanup Set to not clean up at all; this leaves the files on the remote system and the wrapper +-- service installed. This is bad in practice, but significantly reduces the network traffic and makes analysis +-- easier. +--@args nocipher Set to disable the ciphering of the returned text (useful for debugging). +--@args key Script uses this value instead of a random encryption key (useful for debugging the crypto). +----------------------------------------------------------------------- + +author = "Ron Bowes" +copyright = "Ron Bowes" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"intrusive"} +dependencies = {"smb-brute"} + + + +-- Where we tell the user to get nmap_service.exe if it's not installed. +local NMAP_SERVICE_EXE_DOWNLOAD = "https://nmap.org/psexec/nmap_service.exe" + + +hostrule = function(host) + return smb.get_port(host) ~= nil +end + +---Get the random-ish filenames used by the service. +-- +--@param host The host table, which the names are based on. +--@return Status: true or false. +--@return Name of the remote service, or an error message if status is false. +--@return Name of the executable file that's run by the service. +--@return Name of the temporary output file. +--@return Name of the final output file. +local function get_service_files(host) + local status, service_name, service_file, temp_output_file, output_file + + -- Get the name of the service + status, service_name = smb.get_uniqueish_name(host) + if(status == false) then + return false, string.format("Error generating service name: %s", service_name) + end + stdnse.debug1("Generated static service name: %s", service_name) + + -- Get the name and service's executable file (with a .txt extension for fun) + status, service_file = smb.get_uniqueish_name(host, "txt") + if(status == false) then + return false, string.format("Error generating remote filename: %s", service_file) + end + stdnse.debug1("Generated static service name: %s", service_name) + + -- Get the temporary output file + status, temp_output_file = smb.get_uniqueish_name(host, "out.tmp") + if(status == false) then + return false, string.format("Error generating remote filename: %s", temp_output_file) + end + stdnse.debug1("Generated static service filename: %s", temp_output_file) + + -- Get the actual output file + status, output_file = smb.get_uniqueish_name(host, "out") + if(status == false) then + return false, string.format("Error generating remote output file: %s", output_file) + end + stdnse.debug1("Generated static output filename: %s", output_file) + + -- Return everything + return true, service_name, service_file, temp_output_file, output_file +end + +---Stop/delete the service and delete the service file. +-- +--@param host The host object. +--@param config The table of configuration values. +function cleanup(host, config) + local status, err + + -- Add a delay here. For some reason, calling this function too quickly causes SMB to close the connection, + -- but even a tiny delay makes that issue go away. + stdnse.sleep(.01) + + -- If the user doesn't want to clean up, don't + if(stdnse.get_script_args( "nocleanup" )) then + return + end + + stdnse.debug1("Entering cleanup() -- errors here can generally be ignored") + -- Try stopping the service + status, err = msrpc.service_stop(host, config.service_name) + if(status == false) then + stdnse.debug1("[cleanup] Couldn't stop service: %s", err) + end + + -- Try deleting the service + status, err = msrpc.service_delete(host, config.service_name) + if(status == false) then + stdnse.debug1("[cleanup] Couldn't delete service: %s", err) + end + + -- Delete the files + for _, share in ipairs(config.all_shares) do + status, err = smb.file_delete(host, share, config.all_files) + end + + stdnse.debug1("Leaving cleanup()") + + return true +end + +---Find the file on the system (checks both Nmap's directories and the current +-- directory). +-- +--@param filename The name of the file. +--@param extension The extension of the file (filename without the extension is tried first). +--@return The full filename, or nil if it couldn't be found. +local function locate_file(filename, extension) + stdnse.debug1("Attempting to find file: %s", filename) + + extension = extension or "" + + local filename_full = nmap.fetchfile(filename) or nmap.fetchfile(filename .. "." .. extension) + + if(filename_full == nil) then + local psexecfile = "nselib/data/psexec/" .. filename + filename_full = nmap.fetchfile(psexecfile) or nmap.fetchfile(psexecfile .. "." .. extension) + end + + -- check for absolute path or relative to current directory + if(filename_full == nil) then + local f, err = io.open(filename, "rb") + if f == nil then + stdnse.debug1("Error opening %s: %s", filename, err) + f, err = io.open(filename .. "." .. extension, "rb") + if f == nil then + stdnse.debug1("Error opening %s.%s: %s", filename, extension, err) + return nil -- unnecessary, but explicit + else + f:close() + return filename .. "." .. extension + end + else + f:close() + return filename + end + end + + return filename_full +end + +---Generate an array of all files that will be uploaded/created, including +-- the temporary file and the output file. This is done so the files can +-- all be deleted during the cleanup phase. +-- +--@param config The config table. +--@return The array of files. +local function get_all_files(config) + local files = {config.service_file, config.output_file, config.temp_output_file} + for _, mod in ipairs(config.enabled_modules) do + -- We're going to delete the module itself + table.insert(files, mod.upload_name) + + -- We're also going to delete any temp files... + if(mod.tempfiles) then + for _, file in ipairs(mod.tempfiles) do + table.insert(files, file) + end + end + + -- ... and any extra files we uploaded ,,, + if(mod.extrafiles) then + for _, file in ipairs(mod.extrafiles) do + table.insert(files, file) + end + end + + -- ... not to mention the output file + if(mod.outfile and mod.outfile ~= "") then + table.insert(files, mod.outfile) + end + end + + return files +end + +---Decide which share to use. Unless the user overrides it with the 'share' and 'sharepath' +-- arguments, a the first writable share is used. +-- +--@param host The host object. +--@return status true for success, false for failure +--@return share The share we're going to use, or an error message. +--@return path The path on the remote system that points to the share. +--@return shares A list of all shares on the system (used for cleaning up). +local function find_share(host) + local status, share, path, shares + + -- Determine which share to use + if(nmap.registry.args.share ~= nil) then + share = nmap.registry.args.share + shares = {share} + path = nmap.registry.args.sharepath + if(path == nil) then + return false, "Setting the 'share' script-arg requires the 'sharepath' to be set as well." + end + + stdnse.debug1("Using share chosen by the user: %s (%s)", share, path) + else + -- Try and find a share to use. + status, share, path, shares = smb.share_find_writable(host) + if(status == false) then + return false, share .. " (May not have an administrator account)" + end + if(path == nil) then + return false, string.format("Couldn't find path to writable share (we probably don't have admin access): '%s'", share) + end + stdnse.debug1("Found usable share %s (%s) (all writable shares: %s)", share, path, table.concat(shares, ", ")) + end + + return true, share, path, shares +end + +---Recursively replace all variables in the 'setting' field with string variables +-- found in the 'config' field and in the script-args passed by the user. +-- +--@param config The configuration table (used as a source of variables to replace). +--@param setting The current setting field (generally a string or a table). +--@return setting The setting with all values replaced. +local function replace_variables(config, setting) + if(type(setting) == "string") then + -- Replace module fields with variables in the script-args argument + for k, v in pairs(nmap.registry.args) do + setting = string.gsub(setting, "$"..k, v) + end + + -- Replace module fields with variables in the config file + for k, v in pairs(config) do + if((type(v) == "string" or type(v) == "boolean" or type(v) == "number") and k ~= "key") then + setting = string.gsub(setting, "$"..k, v) + end + end + elseif(type(setting) == "table") then + for k, v in pairs(setting) do + setting[k] = replace_variables(config, v) + end + end + + return setting +end + +---Takes the 'overrides' field from a module and replace any configuration variables. +-- +--@param config The config table. +--@param overrides The overrides we're replacing values with. +--@return config The new config table. +local function do_overrides(config, overrides) + if(overrides) then + if(type(overrides) == 'string') then + overrides = {overrides} + end + + for i, v in pairs(overrides) do + config[i] = v + end + end + + return config +end + +---Reads, prepares, parses, sanity checks, and pre-processes the configuration file (either the +-- default, or the file passed as a parameter). +-- +--@param host The host table. +--@param config A table to fill with configuration values. +--@return status true or false +--@return config The configuration table or an error message. +local function get_config(host, config) + local status + local filename = nmap.registry.args.config + config.enabled_modules = {} + config.disabled_modules = {} + + -- Find the config file + filename = locate_file(filename or 'default', 'lua') + if(filename == nil) then + return false, "Couldn't locate config file: file not found (make sure it has a .lua extension and is in nselib/data/psexec/)" + end + + -- Load the config file + local env = setmetatable({modules = {}; overrides = {}; module = function() stdnse.debug1("WARNING: Selected config file contains an unnecessary call to module()") end}, {__index = _G}) + stdnse.debug1("Attempting to load config file: %s", filename) + local file = loadfile(filename, "t", env) + if(not(file)) then + return false, "Couldn't load module file:\n" .. filename + end + + -- Run the config file + file() + local modules = env.modules + local overrides = env.overrides + + -- Generate a cipher key + if(stdnse.get_script_args( "nocipher" )) then + config.key = "" + elseif(nmap.registry.args.key) then + config.key = nmap.registry.args.key + else + local tmp = {} + for i = 1, 127, 1 do + tmp[i] = string.char(math.random(0x20, 0x7F)) + end + config.key = table.concat(tmp) + config.key_index = 0 + end + + -- Initialize the timeout + config.timeout = 0 + + -- Figure out which share we're using (this is the first place in the script where a lot of traffic is generated -- + -- any possible sanity checking should be done before this) + status, config.share, config.path, config.all_shares = find_share(host) + if(not(status)) then + return false, config.share + end + + -- Get information about the socket; it's a bit out of place here, but it should go before the mod loop + status, config.lhost, config.lport, config.rhost, config.rport, config.lmac = smb.get_socket_info(host) + if(status == false) then + return false, "Couldn't get socket information: " .. config.lhost + end + + -- Get the names of the files we're going to need + status, config.service_name, config.service_file, config.temp_output_file, config.output_file = get_service_files(host) + if(not(status)) then + return false, config.service_name + end + + -- Make sure the modules loaded properly + -- NOTE: If you're here because of an error that 'modules' is undefined, it's likely because your configuration file doesn't have a + -- proper modules table, or your configuration file has a module() declaration at the top. + if(not(modules) or #modules == 0) then + return false, string.format("Configuration file (%s) doesn't have a proper 'modules' table.", filename) + end + + -- Make sure we got a proper modules array + if(type(modules) ~= "table") then + return false, string.format("The chosen configuration file, %s.lua, \z + doesn't have a proper 'modules' table. If possible, it should be \z + modified to have a public array called 'modules' that contains a \z + list of all modules that will be run.", filename) + end + + -- Loop through the modules for some pre-processing + stdnse.debug1("Verifying uploadable executables exist") + for i, mod in ipairs(modules) do + local enabled = true + -- Do some sanity checking + if(mod.program == nil) then + enabled = false + if(mod.name) then + mod.disabled_message = string.format("Configuration error: '%s': module doesn't have a program", mod.name) + else + mod.disabled_message = string.format("Configuration error: Module #%d doesn't have a program", i) + end + end + + -- Set some defaults, if the user didn't specify + mod.name = mod.name or (string.format("%s %s", mod.program, mod.args or "")) + mod.maxtime = mod.maxtime or 1 + + -- Check if they forgot the uploadibility + if(mod.upload == nil) then + enabled = false + mod.disabled_message = string.format("Configuration error: '%s': 'upload' field is required", mod.name) + end + + -- Check if the upload field is set wrong + if(mod.upload ~= true and mod.upload ~= false) then + enabled = false + mod.disabled_message = string.format("Configuration error: '%s': 'upload' field has to be true or false", mod.name) + end + + -- Check for incompatible fields with 'headless' + if(mod.headless) then + if(mod.find or mod.remove or mod.noblank or mod.replace or (mod.maxtime > 1) or mod.outfile) then + enabled = false + mod.disabled_message = string.format("Configuration error: '%s': 'headless' is incompatible with find, remove, noblank, replace, and maxtime", mod.name) + end + end + + -- Check for improperly formatted 'replace' + if(mod.replace) then + if(type(mod.replace) ~= "table") then + enabled = false + mod.disabled_message = string.format("Configuration error: '%s': 'replace' has to be a table of one-element tables (eg. replace = {{'a'='b'}, {'c'='d'}})", mod.name) + end + + for _, v in ipairs(mod.replace) do + if(type(v) ~= 'table') then + enabled = false + mod.disabled_message = string.format("Configuration error: '%s': 'replace' has to be a table of one-element tables (eg. replace = {{'a'='b'}, {'c'='d'}})", mod.name) + end + end + end + + -- Set some default values + if(mod.headless == nil) then + mod.headless = false + end + if(mod.include_stderr == nil) then + mod.include_stderr = true + end + + -- Make sure required arguments are given + if(mod.req_args) then + if(type(mod.req_args) == 'string') then + mod.req_args = {mod.req_args} + end + + -- Keep a table of missing args so we can tell the user all the args they're missing at once + local missing_args = {} + for _, arg in ipairs(mod.req_args) do + if(nmap.registry.args[arg] == nil) then + table.insert(missing_args, arg) + end + end + + if(#missing_args > 0) then + enabled = false + mod.disabled_message = {} + table.insert(mod.disabled_message, string.format("Configuration error: Required argument(s) ('%s') weren't given.", table.concat('", missing_args, "'))) + table.insert(mod.disabled_message, "Please add --script-args=[arg]=[value] to your commandline to run this module") + if(#missing_args == 1) then + table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123", missing_args[1])) + else + table.insert(mod.disabled_message, string.format("For example: --script-args=%s=123,%s=456...", missing_args[1], missing_args[2])) + end + end + end + + -- Checks for the uploadable modules + if(mod.upload) then + -- Check if the module actually exists + stdnse.debug1("Looking for uploadable module: %s or %s.exe", mod.program, mod.program) + mod.filename = locate_file(mod.program, "exe") + if(mod.filename == nil) then + enabled = false + stdnse.debug1("Couldn't find uploadable module %s, disabling", mod.program) + mod.disabled_message = {string.format("Couldn't find uploadable module %s, disabling", mod.program)} + if(mod.url) then + stdnse.debug1("You can try getting it from: %s", mod.url) + table.insert(mod.disabled_message, string.format("You can try getting it from: %s", mod.url)) + table.insert(mod.disabled_message, "And placing it in Nmap's nselib/data/psexec/ directory") + end + else + -- We found it + stdnse.debug1("Found: %s", mod.filename) + + -- Generate a name to upload them as (we don't upload with the original names) + status, mod.upload_name = smb.get_uniqueish_name(host, "txt", mod.filename) + if(not(status)) then + return false, "Couldn't generate name for uploaded file: " .. mod.upload_name + end + stdnse.debug1("Will upload %s as %s", mod.filename, mod.upload_name) + end + end + + + -- Prepare extra files + if(enabled and mod.extrafiles) then + -- Make sure we have an array to help save on duplicate code + if(type(mod.extrafiles) == "string") then + mod.extrafiles = {mod.extrafiles} + end + + -- Loop through all of the extra files + mod.extrafiles_paths = {} + for i, extrafile in ipairs(mod.extrafiles) do + stdnse.debug1("Looking for extra module: %s", extrafile) + mod.extrafiles_paths[i] = locate_file(extrafile) + if(mod.extrafiles_paths[i] == nil) then + return false, string.format("Couldn't find required file to upload: %s", extrafile) + end + stdnse.debug1("Found: %s", mod.extrafiles_paths[i]) + end + end + + -- Add the timeout to the total + config.timeout = config.timeout + mod.maxtime + + -- Add the module to the appropriate list + if(enabled) then + table.insert(config.enabled_modules, mod) + else + table.insert(config.disabled_modules, mod) + end + end + + -- Make a list of *all* files (used for cleaning up) + config.all_files = get_all_files(config) + + -- Finalize the timeout + local max_timeout = nmap.registry.args.timeout or 15 + config.timeout = math.max(config.timeout, max_timeout) + stdnse.debug1("Timeout waiting for a response is %d seconds", config.timeout) + + -- Do config overrides + if(overrides) then + config = do_overrides(config, overrides) + end + + -- Replace variable values in the configuration (this has to go last) + stdnse.debug1("Replacing variables in the modules' fields") + for i, mod in ipairs(config.enabled_modules) do + for k, v in pairs(mod) do + mod[k] = replace_variables(config, v) + end + end + + return true, config +end + +---Cipher (or uncipher) a string with a weak xor-based encryption. +-- +--@args str The string go cipher/uncipher. +--@args config The config file for this host (stores the encryption key). +--@return The decrypted string. +local function cipher(str, config) + local result = {} + if(config.key == "") then + return str + end + + for i = 1, #str, 1 do + local c = string.byte(str, i) + c = string.char(c ~ string.byte(config.key, config.key_index + 1)) + + config.key_index = config.key_index + 1 + config.key_index = config.key_index % #config.key + + result[i] = c + end + + return table.concat(result) +end + +local function get_overrides() + -- Create some overrides: + -- 0x00004000 = Encrypted + -- 0x00002000 = Don't index this file + -- 0x00000100 = Temporary file + -- 0x00000800 = Compressed file + -- 0x00000002 = Hidden file + -- 0x00000004 = System file + local attr = 0x00000004 | 0x00000002 | 0x00000800 | 0x00000100 | 0x00002000 | 0x00004000 + + -- Let the user override this behaviour + if(stdnse.get_script_args( "nohide" )) then + attr = 0 + end + + -- Create the overrides + return {file_create_attributes=attr} +end + +--- Check if an nmap_service.exe file is the XOR-encoded version from the 5.21 +-- release. It works by checking the first few bytes against a known pattern. +-- Returns <code>true</code> or <code>false</code>, or else <code>nil</code> and +-- an error message. +-- @param filename the name of the file to check. +-- @return status +-- @return error message +local function service_file_is_xor_encoded(filename) + local f, bytes, msg + + f, msg = io.open(filename) + if not f then + return nil, msg + end + bytes = f:read(2) + f:close() + if not bytes or #bytes < 2 then + return nil, "Can't read from service file" + end + -- This is the XOR-inverse of "MZ". + return bytes == "\xb2\xa5" +end + +---Upload all of the uploadable files to the remote system. +-- +--@param host The host table. +--@param config The configuration table. +--@return status true or false +--@return err An error message if status is false. +local function upload_everything(host, config) + local is_xor_encoded, msg + local overrides = get_overrides() + + -- In Nmap 5.20, it was discovered that nmap_service.exe file was + -- causing false positives in antivirus software. In an effort to avoid + -- this, in version 5.21 the file was obfuscated by XORing all its bytes + -- with 0xFF. That didn't work, so now the file is not included in the + -- distribution. But it means we must check if we are dealing with the + -- original or XOR-encoded version of the file. + is_xor_encoded, msg = service_file_is_xor_encoded(config.local_service_file) + if is_xor_encoded == nil then + return nil, msg + elseif is_xor_encoded then + stdnse.debug2("%s is the XOR-encoded version from the 5.21 release.", config.local_service_file) + end + + -- Upload the service file + stdnse.debug1("Uploading: %s => \\\\%s\\%s", config.local_service_file, config.share, config.service_file) + local status, err + status, err = smb.file_upload(host, config.local_service_file, config.share, "\\" .. config.service_file, overrides, is_xor_encoded) + if(status == false) then + cleanup(host, config) + return false, string.format("Couldn't upload the service file: %s\n", err) + end + stdnse.debug1("Service file successfully uploaded!") + + -- Upload the modules and all their extras + stdnse.debug1("Attempting to upload the modules") + for _, mod in ipairs(config.enabled_modules) do + -- If it's an uploadable module, upload it + if(mod.upload) then + stdnse.debug1("Uploading: %s => \\\\%s\\%s", mod.filename, config.share, mod.upload_name) + status, err = smb.file_upload(host, mod.filename, config.share, "\\" .. mod.upload_name, overrides) + if(status == false) then + cleanup(host, config) + return false, string.format("Couldn't upload module %s: %s\n", mod.program, err) + end + end + + -- If it requires extra files, upload them, too + if(mod.extrafiles) then + -- Convert to a table, if it's a string + if(type(mod.extrafiles) == "string") then + mod.extrafiles = {mod.extrafiles} + end + + -- Loop over the files and upload them + for i, extrafile in ipairs(mod.extrafiles) do + local extrafile_local = mod.extrafiles_paths[i] + + stdnse.debug1("Uploading extra file: %s => \\\\%s\\%s", extrafile_local, config.share, extrafile) + status, err = smb.file_upload(host, extrafile_local, config.share, extrafile, overrides) + if(status == false) then + cleanup(host, config) + return false, string.format("Couldn't upload extra file %s: %s\n", extrafile_local, err) + end + end + end + end + stdnse.debug1("Modules successfully uploaded!") + + return true +end + +---Create the service on the remote system. +--@param host The host object. +--@param config The configuration table. +--@return status true or false +--@return err An error message if status is false. +local function create_service(host, config) + local status, err = msrpc.service_create(host, config.service_name, config.path .. "\\" .. config.service_file) + if(status == false) then + stdnse.debug1("Couldn't create the service: %s", err) + cleanup(host, config) + + if(string.find(err, "MARKED_FOR_DELETE")) then + return false, "Service is stuck in 'being deleted' phase on remote machine; try setting script-args=randomseed=abc for now" + else + return false, string.format("Couldn't create the service on the remote machine: %s", err) + end + end + + return true +end + +---Create the list of parameters we're using to start the service. This consists +-- of a few global params, then a group of parameters with options for each process +-- that's going to be started. +-- +--@param config The configuration table. +--@return status true or false +--@return params A table of parameters if status is true, or an error message if status is false. +local function get_params(config) + local count = 0 + + -- Build the table of parameters to pass to the service + local params = {} + table.insert(params, config.path .. "\\" .. config.output_file) + table.insert(params, config.path .. "\\" .. config.temp_output_file) + table.insert(params, tostring(#config.enabled_modules)) + table.insert(params, "0") + table.insert(params, config.key) + table.insert(params, config.path) + for _, mod in ipairs(config.enabled_modules) do + if(mod.upload) then + table.insert(params, config.path .. "\\" .. mod.upload_name .. " " .. (mod.args or "")) + else + table.insert(params, mod.program .. " " .. (mod.args or "")) + end + + table.insert(params, (mod.env or "")) + table.insert(params, tostring(mod.headless)) + table.insert(params, tostring(mod.include_stderr)) + table.insert(params, mod.outfile or "") + end + + return true, params +end + +---Start the service on the remote machine. +-- +--@param host The host object. +--@param config The configuration table. +--@param params The parameters to pass to the service, likely from the <code>get_params</code> function. +--@return status true or false +--@return err An error message if status is false. +local function start_service(host, config, params) + local status, err = msrpc.service_start(host, config.service_name, params) + if(status == false) then + stdnse.debug1("Couldn't start the service: %s", err) + return false, string.format("Couldn't start the service on the remote machine: %s", err) + end + + return true +end + +---Poll for the output file on the remote machine until either the file is created, or the timeout +-- expires. +-- +--@param host The host object. +--@param config The configuration table. +--@return status true or false +--@return result The file if status is true, or an error message if status is false. + +local function get_output_file(host, config) + stdnse.debug1("Waiting for output file to be created (timeout = %d seconds)", config.timeout) + local status, result + + local i = config.timeout + while true do + status, result = smb.file_read(host, config.share, "\\" .. config.output_file, nil, {file_create_disposition=1}) + + if(not(status) and result ~= "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + -- An unexpected error occurred + stdnse.debug1("Couldn't read the file: %s", result) + cleanup(host, config) + + return false, string.format("Couldn't read the file from the remote machine: %s", result) + end + + if(not(status) and result == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + -- An expected error occurred; if this happens, we just wait + if(i == 0) then + stdnse.debug1("Error in remote service: output file was never created!") + cleanup(host, config) + + return false, "Error in remote service: output file was never created" + end + + stdnse.debug1("Output file %s doesn't exist yet, waiting for %d more seconds", config.output_file, i) + stdnse.sleep(1) + i = i - 1 + end + + if(status) then + break + end + end + + return true, result +end + +---Decide whether or not a line should be included in the output file, based on the module's +-- find, remove, and noblank settings. +local function should_be_included(mod, line) + local removed, found + + -- Remove lines from the output, if the module requested it + removed = false + if(mod.remove and #mod.remove > 0) then + -- Make a single string into a table to save code + if(type(mod.remove) ~= 'table') then + mod.remove = {mod.remove} + end + + -- Loop through the module's find table to see if any of the lines match + for _, remove in ipairs(mod.remove) do + if(string.match(line, remove)) then + removed = true + break + end + end + end + + -- Remove blank lines if we're supposed to + if(mod.noblank and line == "") then + removed = true + end + + -- If the line wasn't removed, and we are searching for specific text, do the search + found = false + if(mod.find and #mod.find > 0 and not(removed)) then + -- Make a single string a table to save duplicate code + if(type(mod.find) ~= 'table') then + mod.find = {mod.find} + end + + -- Loop through the module's find table to see if any of the lines match + for _, find in ipairs(mod.find) do + if(string.match(line, find)) then + found = true + break + end + end + else + found = true + end + + -- Only display the line if it's found and not removed + return (found and not(removed)) +end + +---Alter a line based on the module's 'replace' setting. +local function do_replacements(mod, line) + if(mod.replace) then + for _, v in pairs(mod.replace) do + line = string.gsub(line, v[1], v[2]) + end + end + + return line +end + +---Parse the output file into a neat array. +local function parse_output(config, data) + -- Allow 'data' to be nil. This lets us skip most of the effort when all mods are disabled + data = data or "" + + -- Split the result at newlines + local lines = stringaux.strsplit("\n", data) + + local module_num = -1 + local mod = nil + local result = nil + + -- Loop through the lines and parse them into the results table + local results = {} + for _, line in ipairs(lines) do + if(line ~= "") then + local this_module_num = tonumber(string.sub(line, 1, 1)) + + -- Get the important part of the line + line = string.sub(line, 2) + + -- Remove the Windows endline (0x0a) from the string (these are left in up to this point to maintain + -- the ability to download binary files, if that ever comes up + line = string.gsub(line, "\r", "") + + -- If the module_number has changed, increment to the next module + if(this_module_num ~= (module_num % 10)) then + -- Increment our module number + if(module_num < 0) then + module_num = 0 + else + module_num = module_num + 1 + end + + + -- Go to the next module, and make sure it exists + mod = config.enabled_modules[module_num + 1] + if(mod == nil) then + stdnse.debug1("Server's response wasn't formatted properly (mod %d); if you can reproduce, place report to dev@nmap.org", module_num) + stdnse.debug1("--\n" .. string.gsub("%%", "%%", data) .. "\n--") + return false, "Server's response wasn't formatted properly; if you can reproduce, place report to dev@nmap.org" + end + + -- Save this result + if(result ~= nil) then + table.insert(results, result) + end + result = {} + result['name'] = "<no name>" + result['lines'] = {} + + if(mod.name) then + result['name'] = mod.name + else + result['name'] = string.format("'%s %s;", mod.program, (mod.args or "")) + end + end + + + local include = should_be_included(mod, line) + + -- If we're including it, do the replacements + if(include) then + line = do_replacements(mod, line) + table.insert(result, line) + end + end + end + + table.insert(results, result) + + -- Loop through the disabled modules and print them out + for _, mod in ipairs(config.disabled_modules) do + local result = {} + result['name'] = mod.name + if(mod.disabled_message == nil) then + mod.disabled_message = {"No reason for disabling the module was found"} + end + + if(type(mod.disabled_message) == 'string') then + mod.disabled_message = {mod.disabled_message} + end + + for _, message in ipairs(mod.disabled_message) do + table.insert(result, "WARNING: " .. message) + end + + table.insert(results, result) + end + + return true, results +end + +action = function(host) + local status, result, err + local key + + local i + + local params + + local config = {} + local files + + -- First check for nmap_service.exe; we can't do anything without it. + stdnse.debug1("Looking for the service file: nmap_service or nmap_service.exe") + config.local_service_file = locate_file("nmap_service", "exe") + if (config.local_service_file == nil) then + if nmap.verbosity() > 0 then + return string.format([[ +Can't find the service file: nmap_service.exe (or nmap_service). +Due to false positives in antivirus software, this module is no +longer included by default. Please download it from +%s +and place it in nselib/data/psexec/ under the Nmap DATADIR. +]], NMAP_SERVICE_EXE_DOWNLOAD) + else + return + end + end + + -- Parse the configuration file + status, config = get_config(host, config) + if(not(status)) then + return stdnse.format_output(false, config) + end + + if(#config.enabled_modules > 0) then + -- Start by cleaning up, just in case. + cleanup(host, config) + + -- If the user just wanted a cleanup, do it + if(stdnse.get_script_args( "cleanup" )) then + return stdnse.format_output(true, "Cleanup complete.") + end + + -- Check if any of the files exist + status, result, files = smb.files_exist(host, config.share, config.all_files, {}) + if(not(status)) then + return stdnse.format_output(false, "Couldn't log in to check for remote files: " .. result) + end + if(result > 0) then + local response = {} + table.insert(response, "One or more output files already exist on the host, and couldn't be removed. Try:") + table.insert(response, "* Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might help),") + table.insert(response, "* Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,") + table.insert(response, "* Changing the share and path using, for example, --script-args=share=C$,sharepath=C:, or") + table.insert(response, "* Deleting the affected file(s) off the server manually (\\\\" .. config.share .. "\\" .. table.concat(files, ", \\\\" .. config.share .. "\\") .. ")") + return stdnse.format_output(false, response) + end + + -- Upload the modules + status, err = upload_everything(host, config) + if(not(status)) then + cleanup(host, config) + return stdnse.format_output(false, err) + end + + -- Create the service + status, err = create_service(host, config) + if(not(status)) then + cleanup(host, config) + return stdnse.format_output(false, err) + end + + -- Get the table of parameters to pass to the service when we start it + status, params = get_params(config) + if(not(status)) then + cleanup(host, config) + return stdnse.format_output(false, params) + end + + -- Start the service + status, params = start_service(host, config, params) + if(not(status)) then + cleanup(host, config) + return stdnse.format_output(false, params) + end + + -- Get the result + status, result = get_output_file(host, config, config.share) + if(not(status)) then + cleanup(host, config) + return stdnse.format_output(false, result) + end + + -- Do a final cleanup + cleanup(host, config) + + -- Uncipher the file + result = cipher(result, config) + end + + -- Build the output into a nice table + local response + status, response = parse_output(config, result) + if(status == false) then + return stdnse.format_output(false, "Couldn't parse output: " .. response) + end + + -- Add a warning if nothing was enabled + if(#config.enabled_modules == 0) then + if(#response == 0) then + response = {"No modules were enabled! Please check your configuration file."} + else + table.insert(response, "No modules were enabled! Please fix any errors displayed above, or check your configuration file.") + end + end + + -- Return the string + return stdnse.format_output(true, response) +end + |