From 7d67b08e66c65ce6426af59feeff972199cfeaa6 Mon Sep 17 00:00:00 2001 From: ron Date: Sun, 8 Nov 2009 21:31:06 +0000 Subject: [PATCH] Merged in my changes from nmap-smb. The primary changes are: * Updated the way authentication works on smb -- it's significantly cleaner now * smb-enum-shares.nse gives significantly better output now (it checks if shares are writable) * Added a script that checks if smbv2 is enabled on a server * Added smb-psexec, a script for executing commands on a remote Windows server. I also included some default scripts, a compiled .exe to run everything, and a ton of documentation (in the form of NSEDoc) * Added 'override' parameters to some of the functions in smb.lua, which lets the programmer override any field in an outgoing SMB packet without modifying smb.lua. * Lots of random code cleanups in the smb-* scripts/libraries --- nselib/data/psexec/backdoor.lua | 28 + nselib/data/psexec/default.lua | 144 +++ nselib/data/psexec/drives.lua | 50 + nselib/data/psexec/examples.lua | 69 ++ nselib/data/psexec/experimental.lua | 25 + nselib/data/psexec/network.lua | 114 ++ nselib/data/psexec/nmap_service.c | 380 +++++++ nselib/data/psexec/nmap_service.exe | Bin 0 -> 67072 bytes nselib/data/psexec/nmap_service.vcproj | 194 ++++ nselib/data/psexec/pwdump.lua | 53 + nselib/msrpc.lua | 16 +- nselib/msrpctypes.lua | 10 + nselib/nsedebug.lua | 4 +- nselib/smb.lua | 1118 ++++++++++++++++--- nselib/smbauth.lua | 400 ++++--- scripts/script.db | 4 +- scripts/smb-brute.nse | 18 +- scripts/smb-enum-shares.nse | 296 ++---- scripts/smb-psexec.nse | 1360 ++++++++++++++++++++++++ scripts/smb-pwdump.nse | 78 +- scripts/smb-security-mode.nse | 13 +- scripts/smbv2-enabled.nse | 66 ++ 22 files changed, 3875 insertions(+), 565 deletions(-) create mode 100644 nselib/data/psexec/backdoor.lua create mode 100644 nselib/data/psexec/default.lua create mode 100644 nselib/data/psexec/drives.lua create mode 100644 nselib/data/psexec/examples.lua create mode 100644 nselib/data/psexec/experimental.lua create mode 100644 nselib/data/psexec/network.lua create mode 100644 nselib/data/psexec/nmap_service.c create mode 100644 nselib/data/psexec/nmap_service.exe create mode 100644 nselib/data/psexec/nmap_service.vcproj create mode 100644 nselib/data/psexec/pwdump.lua create mode 100644 scripts/smb-psexec.nse create mode 100644 scripts/smbv2-enabled.nse diff --git a/nselib/data/psexec/backdoor.lua b/nselib/data/psexec/backdoor.lua new file mode 100644 index 000000000..72885a77b --- /dev/null +++ b/nselib/data/psexec/backdoor.lua @@ -0,0 +1,28 @@ +module(... or "backdoor", package.seeall) +---This config file is designed for adding a backdoor to the system. It has a few +-- options by default, only one enabled by default. I suggest +-- +-- Note that none of these modules are included with Nmap by default. + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +--overrides.timeout = 40 + +modules = {} +local mod + +-- TODO: allow the user to specify parameters +--Note: password can't be longer than 14-characters, otherwise the program pauses for +-- a response +mod = {} +mod.upload = false +mod.name = "Adding a user account: $username/$password" +mod.program = "net" +mod.args = "user $username $password /add" +mod.maxtime = 2 +mod.noblank = true +mod.req_args = {'username','password'} +table.insert(modules, mod) + diff --git a/nselib/data/psexec/default.lua b/nselib/data/psexec/default.lua new file mode 100644 index 000000000..f7e6ed69f --- /dev/null +++ b/nselib/data/psexec/default.lua @@ -0,0 +1,144 @@ +module(... or "network", package.seeall) +---This is the default configuration file. It simply runs some built-in Window +-- programs to gather information about the remote system. It's intended to be +-- simple, demonstrate some of the concepts, and not break/alte anything. + + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +--overrides.timeout = 40 + +modules = {} +local mod + +-- Get the Windows version. For some reason we can't run this directly, but it works ok +-- if we run it through cmd.exe. +mod = {} +mod.upload = false +mod.name = "Windows version" +mod.program = "cmd.exe" +mod.args = "/c \"ver\"" +mod.maxtime = 1 +mod.noblank = true +table.insert(modules, mod) + +-- Grab the ip and mac address(es) from ipconfig. The output requires quite a bit of cleanup +-- to end up being usable and pretty. +mod = {} +mod.upload = false +mod.name = "IP Address and MAC Address from 'ipconfig.exe'" +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) + +-- Grab the user list from 'net user', and make it look nice. Note that getting the groups +-- list (with 'net localgroup') doesn't work without a proper login shell +mod = {} +mod.upload = false +mod.name = "User list from 'net user'" +mod.program = "net.exe" +mod.args = "user" +mod.maxtime = 1 +mod.remove = {"User accounts for", "The command completed", "%-%-%-%-%-%-%-%-%-%-%-"} +mod.noblank = true +table.insert(modules, mod) + +-- Get the list of accounts in the 'administrators' group. +mod = {} +mod.upload = false +mod.name = "Membership of 'administrators' from 'net localgroup administrators'" +mod.program = "net.exe" +mod.args = "localgroup administrators" +mod.maxtime = 1 +mod.remove = {"The command completed", "%-%-%-%-%-%-%-%-%-%-%-", "Members", "Alias name", "Comment"} +mod.noblank = true +table.insert(modules, mod) + +-- Try and ping back to our host. This helps check if there's a firewall in the way for connecting backwards. +-- Interestingly, in my tests against Windows 2003, ping gives weird output (but still, more or less, worked) +-- when the SystemRoot environmental variable wasn't set. +mod = {} +mod.upload = false +mod.name = "Can the host ping our address?" +mod.program = "ping" +mod.args = "-n 1 $lhost" +mod.maxtime = 5 +mod.remove = {"statistics", "Packet", "Approximate", "Minimum"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Try a traceroute back to our host. I limited it to the first 5 hops in the interest of saving time. +-- Like ping, if the SystemRoot variable isn't set, the output is a bit strange (but still works) +mod = {} +mod.upload = false +mod.name = "Traceroute back to the scanner" +mod.program = "tracert" +mod.args = "-d -h 5 $lhost" +mod.maxtime = 20 +mod.remove = {"Tracing route", "Trace complete"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Dump the arp cache of the system. +mod = {} +mod.name = "ARP Cache from arp.exe" +mod.program = 'arp.exe' +mod.upload = false +mod.args = '-a' +mod.remove = "Interface" +mod.noblank = true +table.insert(modules, mod) + +-- Get the listening/connected ports +mod = {} +mod.upload = false +mod.name = "List of listening and established connections (netstat -an)" +mod.program = "netstat" +mod.args = "-an" +mod.maxtime = 1 +mod.remove = {"Active"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Get the routing table. +-- +-- Like 'ver', this has to be run through cmd.exe. This also requires the 'PATH' variable to be +-- set properly, so it isn't going to work against systems with odd paths. +mod = {} +mod.upload = false +mod.name = "Full routing table from 'netstat -nr'" +mod.program = "cmd.exe" +mod.args = "/c \"netstat -nr\"" +mod.env = "PATH=C:\\WINDOWS\\system32;C:\\WINDOWS;C:\\WINNT;C:\\WINNT\\system32" +mod.maxtime = 1 +mod.noblank = true +table.insert(modules, mod) + +-- Boot configuration +mod = {} +mod.upload = false +mod.name = "Boot configuration" +mod.program = "bootcfg" +mod.args = "/query" +mod.maxtime = 5 +table.insert(modules, mod) + +-- Get the drive configuration. For same (insane?) reason, it uses NULL characters instead of spaces +-- for the response, so we have to do a replaceent. +mod = {} +mod.upload = false +mod.name = "Drive list (for more info, try adding --script-args=config=drives,drive=C:)" +mod.program = "fsutil" +mod.args = "fsinfo drives" +mod.replace = {{string.char(0), " "}} +mod.maxtime = 1 +table.insert(modules, mod) + diff --git a/nselib/data/psexec/drives.lua b/nselib/data/psexec/drives.lua new file mode 100644 index 000000000..b749f679d --- /dev/null +++ b/nselib/data/psexec/drives.lua @@ -0,0 +1,50 @@ +module(... or "drive", package.seeall) +---This configuration file pulls info about a given harddrive + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +--overrides.timeout = 40 + +modules = {} +local mod + +mod = {} +mod.upload = false +mod.name = "Drive type" +mod.program = "fsutil" +mod.args = "fsinfo drivetype $drive" +mod.req_args = {"drive"} +mod.maxtime = 1 +table.insert(modules, mod) + +mod = {} +mod.upload = false +mod.name = "Drive info" +mod.program = "fsutil" +mod.args = "fsinfo ntfsinfo $drive" +mod.req_args = {"drive"} +mod.replace = {{" :",":"}} +mod.maxtime = 1 +table.insert(modules, mod) + +mod = {} +mod.upload = false +mod.name = "Drive type" +mod.program = "fsutil" +mod.args = "fsinfo statistics $drive" +mod.req_args = {"drive"} +mod.replace = {{" :",":"}} +mod.maxtime = 1 +table.insert(modules, mod) + +mod = {} +mod.upload = false +mod.name = "Drive quota" +mod.program = "fsutil" +mod.args = "quota query $drive" +mod.req_args = {"drive"} +mod.maxtime = 1 +table.insert(modules, mod) + diff --git a/nselib/data/psexec/examples.lua b/nselib/data/psexec/examples.lua new file mode 100644 index 000000000..9b23dbc94 --- /dev/null +++ b/nselib/data/psexec/examples.lua @@ -0,0 +1,69 @@ +module(... or "default", package.seeall) +---This configuration file contains the examples given in smb-psexec.nse. + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +overrides.timeout = 40 + +modules = {} +local mod + +mod = {} +mod.upload = false +mod.name = "Membership of 'administrators' from 'net localgroup administrators'" +mod.program = "net.exe" +mod.args = "localgroup administrators" +table.insert(modules, mod) + +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) + +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) + +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) + +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) + +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) + diff --git a/nselib/data/psexec/experimental.lua b/nselib/data/psexec/experimental.lua new file mode 100644 index 000000000..005a22b4c --- /dev/null +++ b/nselib/data/psexec/experimental.lua @@ -0,0 +1,25 @@ +module(... or "experimental", package.seeall) +---This is the configuration file for modules that aren't quite ready for prime +-- time yet. + + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +--overrides.timeout = 40 + +modules = {} +local mod + + +-- I can't get fport to work for me, so I'm going to leave this one in 'experimental' for now +--mod = {} +--mod.upload = true +--mod.name = "Fport" +--mod.program = "Fport.exe" +--mod.url = "http://www.foundstone.com/us/resources/proddesc/fport.htm" +--mod.maxtime = 1 +--mod.noblank = true +--table.insert(modules, mod) + diff --git a/nselib/data/psexec/network.lua b/nselib/data/psexec/network.lua new file mode 100644 index 000000000..0affb8890 --- /dev/null +++ b/nselib/data/psexec/network.lua @@ -0,0 +1,114 @@ +module(... or "default", package.seeall) +---More verbose network scripts + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +--overrides.timeout = 40 + +modules = {} +local mod + +-- Grab the ip and mac address(es) from ipconfig. The output requires quite a bit of cleanup +-- to end up being usable and pretty. +mod = {} +mod.upload = false +mod.name = "IP Address and MAC Address from 'ipconfig.exe'" +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) + +-- Dump the arp cache of the system. +mod = {} +mod.name = "ARP Cache from arp.exe" +mod.program = 'arp.exe' +mod.upload = false +mod.args = '-a' +mod.remove = "Interface" +mod.noblank = true +table.insert(modules, mod) + +-- Get the listening/connected ports +mod = {} +mod.upload = false +mod.name = "List of listening and established connections (netstat -an)" +mod.program = "netstat" +mod.args = "-anb" +mod.maxtime = 1 +mod.remove = {"Active"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Get the routing table. +-- +-- Like 'ver', this has to be run through cmd.exe. This also requires the 'PATH' variable to be +-- set properly, so it isn't going to work against systems with odd paths. +mod = {} +mod.upload = false +mod.name = "Full routing table from 'netstat -nr'" +mod.program = "cmd.exe" +mod.args = "/c \"netstat -nr\"" +mod.env = "PATH=C:\\WINDOWS\\system32;C:\\WINDOWS;C:\\WINNT;C:\\WINNT\\system32" +mod.maxtime = 1 +mod.noblank = true +table.insert(modules, mod) + +-- Try and ping back to our host. This helps check if there's a firewall in the way for connecting backwards. +-- Interestingly, in my tests against Windows 2003, ping gives weird output (but still, more or less, worked) +-- when the SystemRoot environmental variable wasn't set. +mod = {} +mod.upload = false +mod.name = "Can the host ping our address?" +mod.program = "ping" +mod.args = "-n 1 $lhost" +mod.maxtime = 5 +mod.remove = {"statistics", "Packet", "Approximate", "Minimum"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Try a traceroute back to our host. I limited it to the first 5 hops in the interest of saving time. +-- Like ping, if the SystemRoot variable isn't set, the output is a bit strange (but still works) +mod = {} +mod.upload = false +mod.name = "Traceroute back to the scanner" +mod.program = "tracert" +mod.args = "-d -h 5 $lhost" +mod.maxtime = 20 +mod.remove = {"Tracing route", "Trace complete"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Ping an arbitrary address given by the user +mod = {} +mod.upload = false +mod.name = "Can the host ping $address?" +mod.program = "ping" +mod.args = "-n 1 $address" +mod.req_args = {'address'} +mod.maxtime = 5 +mod.remove = {"statistics", "Packet", "Approximate", "Minimum"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + +-- Try a traceroute to an address given by the user +mod = {} +mod.upload = false +mod.name = "Traceroute to $address (5 hops or less)" +mod.program = "tracert" +mod.args = "-d -h 5 $address" +mod.req_args = {'address'} +mod.maxtime = 20 +mod.remove = {"Tracing route", "Trace complete"} +mod.noblank = true +mod.env = "SystemRoot=c:\\WINDOWS" +table.insert(modules, mod) + + diff --git a/nselib/data/psexec/nmap_service.c b/nselib/data/psexec/nmap_service.c new file mode 100644 index 000000000..23fd2f8f7 --- /dev/null +++ b/nselib/data/psexec/nmap_service.c @@ -0,0 +1,380 @@ +/**This is the program that's uploaded to a Windows machine when psexec is run. It acts as a Windows + * service, since that's what Windows expects. When it is started, it's passed a list of programs to + * run. These programs are all expected to be at the indicated path (whether they were uploaded or + * they were always present makes no difference). + * + * After running the programs, the output from each of them is ciphered with a simple xor encryption + * (the encryption key is passed as a parameter; because it crosses the wire, it isn't really a + * security feature, more of validation/obfuscation to prevent sniffers from grabbing the output. This + * output is placed in a temp file. When the cipher is complete, the output is moved into a new file. + * When Nmap detects the presence of this new file, it is downloaded, then all files, temp files, and + * the service (this program) is deleted. + * + * One interesting note is that executable files don't require a specific extension to be used by this + * program. By default, at the time of this writing, Nmap appends a .txt extension to the file. + * + * @args argv[1] The final filename where the ciphered output will go. + * @args argv[2] The temporary file where output is sent before being renamed; this is sent as a parameter + * so we can delete it later (if, say, the script fails). + * @args argv[3] The number of programs that are going to be run. + * @args argv[4] Logging: a boolean value (1 to enable logging, 0 to disable). + * @args argv[5] An 'encryption' key for simple 'xor' encryption. This string can be as long or as short + * as you want, but a longer string will be more secure (although this algorithm should + * *never* really be considered secure). + * @args Remaining There are two arguments for each program to run: a path (including arguments) and + * environmental variables. + * + * @auther Ron Bowes + * @copyright Ron Bowes + * @license "Same as Nmap--See http://nmap.org/book/man-legal.html" + */ + +#include +#include + +FILE *outfile; + +SERVICE_STATUS ServiceStatus; +SERVICE_STATUS_HANDLE hStatus; + +static char *enc_key; +static int enc_key_loc; + +static void log_message(char *format, ...) +{ + static int enabled = 1; + + if(!format) + { + enabled = 0; + DeleteFile("c:\\nmap-log.txt"); + } + + + if(enabled) + { + va_list argp; + FILE *file; + + fopen_s(&file, "c:\\nmap-log.txt", "a"); + + if(file != NULL) + { + va_start(argp, format); + vfprintf(file, format, argp); + va_end(argp); + fprintf(file, "\n"); + fclose(file); + } + } +} + +static char cipher(char c) +{ + if(strlen(enc_key) == 0) + return c; + + c = c ^ enc_key[enc_key_loc]; + enc_key_loc = (enc_key_loc + 1) % strlen(enc_key); + + return c; +} + +static void output(int num, char *str, int length) +{ + int i; + + if(length == -1) + length = strlen(str); + + for(i = 0; i < length; i++) + { + if(str[i] == '\n') + { + fprintf(outfile, "%c", cipher('\n')); + fprintf(outfile, "%c", cipher('0' + (num % 10))); + } + else + { + fprintf(outfile, "%c", cipher(str[i])); + } + } +} + +static void go(int num, char *lpAppPath, char *env, int headless, int include_stderr, char *readfile) +{ + STARTUPINFO startupInfo; + PROCESS_INFORMATION processInformation; + SECURITY_ATTRIBUTES sa; + HANDLE stdout_read, stdout_write; + DWORD creation_flags; + + int bytes_read; + char buffer[1024]; + + /* Create a security attributes structure. This is required to inherit handles. */ + ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = NULL; + + if(!headless) + sa.bInheritHandle = TRUE; + + /* Create a pipe that'll be used for stdout and stderr. */ + if(!headless) + CreatePipe(&stdout_read, &stdout_write, &sa, 1); + + /* Initialize the startup info struct. The most important part is setting the stdout/stderr handle to our pipe. */ + ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); + startupInfo.cb = sizeof(STARTUPINFO); + + if(!headless) + { + startupInfo.dwFlags = STARTF_USESTDHANDLES; + startupInfo.hStdOutput = stdout_write; + if(include_stderr) + startupInfo.hStdError = stdout_write; + } + + /* Log a couple messages. */ + log_message("Attempting to load the program: %s", lpAppPath); + + /* Initialize the PROCESS_INFORMATION structure. */ + ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION)); + + /* To divide the output from one program to the next */ + output(num, "\n", -1); + + /* Decide on the creation flags */ + creation_flags = CREATE_NO_WINDOW; + if(headless) + creation_flags = DETACHED_PROCESS; + + /* Create the actual process with an overly-complicated CreateProcess function. */ + if(!CreateProcess(NULL, lpAppPath, 0, &sa, sa.bInheritHandle, CREATE_NO_WINDOW, env, 0, &startupInfo, &processInformation)) + { + output(num, "Failed to create the process", -1); + + if(!headless) + { + CloseHandle(stdout_write); + CloseHandle(stdout_read); + } + } + else + { + log_message("Successfully created the process!"); + + /* Read the pipe, if it isn't headless */ + if(!headless) + { + /* Close the write handle -- if we don't do this, the ReadFile() coming up gets stuck. */ + CloseHandle(stdout_write); + + /* Read from the pipe. */ + log_message("Attempting to read from the pipe"); + while(ReadFile(stdout_read, buffer, 1023, &bytes_read, NULL)) + { + if(strlen(readfile) == 0) + output(num, buffer, bytes_read); + } + CloseHandle(stdout_read); + + /* If we're reading an output file instead of stdout, do it here. */ + if(strlen(readfile) > 0) + { + FILE *read; + errno_t err; + + log_message("Trying to open output file: %s", readfile); + err = fopen_s(&read, readfile, "rb"); + + if(err) + { + log_message("Couldn't open the readfile: %d", err); + output(num, "Couldn't open the output file", -1); + } + else + { + char buf[1024]; + int count; + + count = fread(buf, 1, 1024, read); + while(count) + { + output(num, buf, count); + count = fread(buf, 1, 1024, read); + } + + fclose(read); + } + } + } + else + { + output(num, "Process has been created", -1); + } + + log_message("Done!"); + } +} + +// Control handler function +static void ControlHandler(DWORD request) +{ + switch(request) + { + case SERVICE_CONTROL_STOP: + + ServiceStatus.dwWin32ExitCode = 0; + ServiceStatus.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus (hStatus, &ServiceStatus); + return; + + case SERVICE_CONTROL_SHUTDOWN: + + ServiceStatus.dwWin32ExitCode = 0; + ServiceStatus.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus (hStatus, &ServiceStatus); + return; + + default: + break; + } + + SetServiceStatus(hStatus, &ServiceStatus); +} + + + +static void die(int err) +{ + // Not enough arguments + ServiceStatus.dwCurrentState = SERVICE_STOPPED; + ServiceStatus.dwWin32ExitCode = err; + SetServiceStatus(hStatus, &ServiceStatus); +} + +static void ServiceMain(int argc, char** argv) +{ + char *outfile_name; + char *tempfile_name; + int count; + int logging; + int result; + int i; + char *current_directory; + + /* Make sure we got the minimum number of arguments. */ + if(argc < 6) + return; + + /* Read the arguments. */ + outfile_name = argv[1]; + tempfile_name = argv[2]; + count = atoi(argv[3]); + logging = atoi(argv[4]); + enc_key = argv[5]; + enc_key_loc = 0; + current_directory = argv[6]; + + /* If they didn't turn on logging, disable it. */ + if(logging != 1) + log_message(NULL); + + /* Log the state. */ + log_message(""); + log_message("-----------------------"); + log_message("STARTING"); + + /* Log all the arguments. */ + log_message("Arguments: %d\n", argc); + for(i = 0; i < argc; i++) + log_message("Argument %d: %s", i, argv[i]); + + /* Set up the service. Likely unnecessary for what we're doing, but it doesn't hurt. */ + ServiceStatus.dwServiceType = SERVICE_WIN32; + ServiceStatus.dwCurrentState = SERVICE_RUNNING; + ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; + ServiceStatus.dwWin32ExitCode = 0; + ServiceStatus.dwServiceSpecificExitCode = 0; + ServiceStatus.dwCheckPoint = 0; + ServiceStatus.dwWaitHint = 0; + hStatus = RegisterServiceCtrlHandler("", (LPHANDLER_FUNCTION)ControlHandler); + SetServiceStatus(hStatus, &ServiceStatus); + + /* Registering Control Handler failed (this is a bit late, but eh?) */ + if(hStatus == (SERVICE_STATUS_HANDLE)0) + { + log_message("Service failed to start"); + die(-1); + return; + } + + /* Set the current directory. */ + SetCurrentDirectory(current_directory); + + /* Open the output file. */ + log_message("Opening temporary output file: %s", tempfile_name); + + /* Open the outfile. */ + if(result = fopen_s(&outfile, tempfile_name, "wb")) + { + log_message("Couldn't open output file: %d", result); + die(-1); + } + else + { + /* Run the programs we were given. */ + for(i = 0; i < count; i++) + { + char *program = argv[(i*5) + 7]; + char *env = argv[(i*5) + 8]; + char *headless = argv[(i*5) + 9]; + char *include_stderr = argv[(i*5) + 10]; + char *read_file = argv[(i*5) + 11]; + + go(i, program, env, !strcmp(headless, "true"), !strcmp(include_stderr, "true"), read_file); + } + + /* Close the output file. */ + if(fclose(outfile)) + log_message("Couldn't close the file: %d", errno); + + /* Rename the output file (this is what tells Nmap we're done. */ + log_message("Renaming file %s => %s", tempfile_name, outfile_name); + + /* I noticed that sometimes, programs inherit the handle to the file (or something), so I can't change it right + * away. For this reason, allow about 10 seconds to move it. */ + for(i = 0; i < 10; i++) + { + if(rename(tempfile_name, outfile_name)) + { + log_message("Couldn't rename file: %d (will try %d more times)", errno, 10 - i - 1); + } + else + { + log_message("File successfully renamed!"); + break; + } + + Sleep(1000); + } + + /* Clean up and stop the service. */ + die(0); + } +} + +int main(int argc, char *argv[]) +{ + SERVICE_TABLE_ENTRY ServiceTable[2]; + ServiceTable[0].lpServiceName = ""; + ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain; + + ServiceTable[1].lpServiceName = NULL; + ServiceTable[1].lpServiceProc = NULL; + // Start the control dispatcher thread for our service + StartServiceCtrlDispatcher(ServiceTable); +} + diff --git a/nselib/data/psexec/nmap_service.exe b/nselib/data/psexec/nmap_service.exe new file mode 100644 index 0000000000000000000000000000000000000000..7b4ac3102759884dd513f336abbbff07f0a6ed85 GIT binary patch literal 67072 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�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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nselib/data/psexec/pwdump.lua b/nselib/data/psexec/pwdump.lua new file mode 100644 index 000000000..23f0c040d --- /dev/null +++ b/nselib/data/psexec/pwdump.lua @@ -0,0 +1,53 @@ +module(... or "pwdump", package.seeall) +---This config file is designed for running password-dumping scripts. So far, +-- it supports pwdump6 2.0.0 and fgdump. +-- +-- Note that none of these modules are included with Nmap by default. + +-- Any variable in the 'config' table in smb-psexec.nse can be overriden in the +-- 'overrides' table. Most of them are not really recommended, such as the host, +-- key, etc. +overrides = {} +--overrides.timeout = 40 + +modules = {} +local mod + +--mod = {} +--mod.upload = true +--mod.name = "PwDump6 2.0.0" +--mod.program = "PwDump.exe" +--mod.args = "localhost" +--mod.maxtime = 10 +--mod.include_stderr = false +--mod.url = "http://www.foofus.net/fizzgig/pwdump/" +--table.insert(modules, mod) + +---Uncomment if you'd like to use PwDump6 1.7.2 (considered obsolete, but still works). +-- Note that for some reason, this and 'fgdump' don't get along (fgdump only produces a blank +-- file if these are run together) +--mod = {} +--mod.upload = true +--mod.name = "PwDump6 1.7.2" +--mod.program = "PwDump-1.7.2.exe" +--mod.args = "localhost" +--mod.maxtime = 10 +--mod.include_stderr = false +--mod.extrafiles = {"servpw.exe", "lsremora.dll"} +--mod.url = "http://www.foofus.net/fizzgig/pwdump/" +--table.insert(modules, mod) + +-- Warning: the danger of using fgdump is that it always write the output to the harddrive unencrypted; +-- this makes it more obvious that an attack has occurred. +mod = {} +mod.upload = true +mod.name = "FgDump" +mod.program = "fgdump.exe" +mod.args = "-c -l fgdump.log" +mod.maxtime = 10 +mod.url = "http://www.foofus.net/fizzgig/fgdump/" +mod.tempfiles = {"fgdump.log"} +mod.outfile = "127.0.0.1.pwdump" +table.insert(modules, mod) + + diff --git a/nselib/msrpc.lua b/nselib/msrpc.lua index a85fa8a1e..0e45d2fa5 100644 --- a/nselib/msrpc.lua +++ b/nselib/msrpc.lua @@ -122,10 +122,12 @@ local LSA_MINEMPTY = 10 --@param host The host object. --@param path The path to the named pipe; for example, msrpc.SAMR_PATH or msrpc.SRVSVC_PATH. --@param disable_extended [optional] If set to 'true', disables extended security negotiations. +--@param overrides [optional] Overrides variables in all the SMB functions. --@return (status, smbstate) if status is false, smbstate is an error message. Otherwise, smbstate is -- required for all further calls. -function start_smb(host, path, disable_extended) - return smb.start_ex(host, true, true, "IPC$", path, disable_extended) +function start_smb(host, path, disable_extended, overrides) + overrides = overrides or {} + return smb.start_ex(host, true, true, "IPC$", path, disable_extended, overrides) end --- A wrapper around the smb.stop function. I only created it to add symmetry, so client code @@ -3272,15 +3274,16 @@ function service_start(host, servicename, args) return false, start_result end - -- Wait for it to start - stdnse.print_debug(2, "Waiting for the service to start") + -- Wait for it to start (TODO: Check the query result better) + stdnse.print_debug(1, "Waiting for the service to start") repeat status, query_result = svcctl_queryservicestatus(smbstate, open_service_result['handle']) if(status == false) then smb.stop(smbstate) return false, query_result end - until query_result['service_status']['controls_accepted'][1] == "SERVICE_CONTROL_STOP" + stdnse.sleep(.5) + until query_result['service_status']['controls_accepted'][1] == "SERVICE_CONTROL_STOP" or query_result['service_status']['state'][1] == "SERVICE_STATE_ACTIVE" -- Close the handle to the service status, close_result = svcctl_closeservicehandle(smbstate, open_service_result['handle']) @@ -3353,7 +3356,7 @@ function service_stop(host, servicename) return false, control_result end - -- Wait for it to stop (TODO: Make this better) + -- Wait for it to stop (TODO: Check the query result better) stdnse.print_debug(2, "Waiting for the service to stop") repeat status, query_result = svcctl_queryservicestatus(smbstate, open_service_result['handle']) @@ -3361,6 +3364,7 @@ function service_stop(host, servicename) smb.stop(smbstate) return false, query_result end + stdnse.sleep(.5) until query_result['service_status']['controls_accepted'][1] == nil -- Close the handle to the service diff --git a/nselib/msrpctypes.lua b/nselib/msrpctypes.lua index ea4a7c92f..d164b11b3 100644 --- a/nselib/msrpctypes.lua +++ b/nselib/msrpctypes.lua @@ -130,6 +130,16 @@ function string_to_unicode(string, do_null) do_null = false end + -- Try converting the value to a string + if(type(string) ~= 'string') then + string = tostring(string) + end + + if(string == nil) then + stdnse.print_debug(1, "MSRPC: WARNING: couldn't convert value to string in string_to_unicode()") + end + + -- Loop through the string, adding each character followed by a char(0) for i = 1, string.len(string), 1 do result = result .. string.sub(string, i, i) .. string.char(0) diff --git a/nselib/nsedebug.lua b/nselib/nsedebug.lua index fb5632a67..f0e753498 100644 --- a/nselib/nsedebug.lua +++ b/nselib/nsedebug.lua @@ -34,9 +34,9 @@ function tostr(data, indent) str = str .. (" "):rep(indent) .. data .. "\n" elseif(type(data) == "boolean") then if(data == true) then - str = str .. "true" + str = str .. "true\n" else - str = str .. "false" + str = str .. "false\n" end elseif(type(data) == "table") then local i, v diff --git a/nselib/smb.lua b/nselib/smb.lua index 053de699b..050d380ee 100644 --- a/nselib/smb.lua +++ b/nselib/smb.lua @@ -41,9 +41,9 @@ -- -- -- status, smbstate = smb.start(host) --- status, err = smb.negotiate_protocol(smbstate) --- status, err = smb.start_session(smbstate) --- status, err = smb.tree_connect(smbstate, path) +-- status, err = smb.negotiate_protocol(smbstate, {}) +-- status, err = smb.start_session(smbstate, {}) +-- status, err = smb.tree_connect(smbstate, path, {}) -- ... -- status, err = smb.tree_disconnect(smbstate) -- status, err = smb.logoff(smbstate) @@ -112,6 +112,7 @@ --@args smbport Override the default port choice. If smbport is open, it's used. It's assumed -- to be the same protocol as port 445, not port 139. Since it probably isn't possible to change -- Windows' ports normally, this is mostly useful if you're bouncing through a relay or something. +--@args randomseed Set to a value to change the filenames/service names that are randomly generated. -- --@author Ron Bowes --@copyright Same as Nmap--See http://nmap.org/book/man-legal.html @@ -133,7 +134,45 @@ status_names = {} local mutexes = setmetatable({}, {__mode = "k"}); --local debug_mutex = nmap.mutex("SMB-DEBUG") -local TIMEOUT = 20000 +local TIMEOUT = 10000 + +---Wrapper around smbauth.add_account. +function add_account(host, username, domain, password, password_hash, hash_type, is_admin) + smbauth.add_account(host, username, domain, password, password_hash, hash_type, is_admin) +end + +---Wrapper around smbauth.get_account. +function get_account(host) + return smbauth.get_account(host) +end +---Create an 'overrides' table +function get_overrides(username, domain, password, password_hash, hash_type, overrides) + if(not(overrides)) then + return {username=username, domain=domain, password=password, password_hash=password_hash, hash_type=hash_type} + else + overrides['username'] = username + overrides['domain'] = domain + overrides['password'] = password + overrides['password_hash'] = password_hash + overrides['hash_type'] = hash_type + end +end + +---Get an 'overrides' table for the anonymous user +-- +--@param overrides [optional] A base table of overrides. The appropriate fields will be added. +function get_overrides_anonymous(overrides) + if(not(overrides)) then + return {username='', domain='', password='', password_hash=nil, hash_type='none'} + else + overrides['username'] = '' + overrides['domain'] = '' + overrides['password'] = '' + overrides['password_hash'] = '' + overrides['hash_type'] = 'none' + end +end + ---Returns the mutex that should be used by the current connection. This mutex attempts -- to use the name, first, then falls back to the IP if no name was returned. @@ -204,7 +243,7 @@ end function get_status_name(status) if(status_names[status] == nil) then - -- If the name wasn't found in the array, do a linear search on it (TODO: Why is this happening??) (XXX: I think I fixed this) + -- If the name wasn't found in the array, do a linear search on it for i, v in pairs(status_names) do if(v == status) then return i @@ -265,74 +304,12 @@ function disable_extended(smb) smb['extended_security'] = false end ----Writes the given account to the registry. There are several places where accounts are stored: --- * registry['usernames'][username] => true --- * registry['smbaccounts'][username] => password --- * registry[ip]['smbaccounts'][username] => password --- * registry[ip]['smbaccount']['username'] => username --- * registry[ip]['smbaccount']['password'] => password --- * registry[ip]['smbaccount']['is_admin'] => true or false --- --- The final place, 'smbaccount', is reserved for the "best" account. This is an administrator --- account, if one's found; otherwise, it's the first account discovered that isn't guest. --- --- This has to be called while no SMB connections are made, since it potentially makes its own connection. --- ---@param ip The ip address of the host ---@param username The username to add. ---@param password The password to add. -function add_account(host, username, password) - local best_account = false - - -- Save the username in a global list - if(nmap.registry.usernames == nil) then - nmap.registry.usernames = {} - end - nmap.registry.usernames[username] = true - - -- Save the username/password pair in a global list - if(nmap.registry.smbaccounts == nil) then - nmap.registry.smbaccounts = {} - end - nmap.registry.smbaccounts[username] = password - - -- Save the username/password pair for the server with the others - if(nmap.registry[host.ip] == nil) then - nmap.registry[host.ip] = {} - end - if(nmap.registry[host.ip]['smbaccounts'] == nil) then - nmap.registry[host.ip]['smbaccounts'] = {} - end - nmap.registry[host.ip]['smbaccounts'][username] = password - - -- Don't bother saving if our account is guest or blank (those are tried anyways) - if(string.lower(username) ~= "guest" and string.lower(username) ~= "") then - -- Save the new account if this is our first one, or our other account isn't an admin - if(nmap.registry[host.ip]['smbaccount'] == nil or nmap.registry[host.ip]['smbaccount']['is_admin'] == false) then - local result, _ - - nmap.registry[host.ip]['smbaccount'] = {} - nmap.registry[host.ip]['smbaccount']['username'] = username - nmap.registry[host.ip]['smbaccount']['password'] = password - - -- Try getting information about "IPC$". This determines whether or not the user is administrator - -- since only admins can get share info. - nmap.registry[host.ip]['smbaccount']['is_admin'], _ = msrpc.get_share_info(host, "IPC$") - - if(nmap.registry[host.ip]['smbaccount']['is_admin'] == true) then - stdnse.print_debug(1, "SMB: Saved an administrative account: %s", username) - else - stdnse.print_debug(1, "SMB: Saved a non-administrative account: %s", username) - end - end - end -end - --- Begins a SMB session, automatically determining the best way to connect. Also starts a mutex -- with mutex_id. This prevents multiple threads from making queries at the same time (which breaks -- SMB). -- -- @param host The host object +-- @param overrides [optional] Overrides for various fields -- @return (status, smb) if the status is true, result is the newly crated smb object; -- otherwise, socket is the error message. function start(host) @@ -342,6 +319,7 @@ function start(host) state['uid'] = 0 state['tid'] = 0 + state['host'] = host state['ip'] = host.ip state['sequence'] = -1 @@ -364,6 +342,9 @@ function start(host) return false, "SMB: Couldn't find a valid port to check" end + -- Initialize the accounts for logging on (note: this has to be outside the mutex, or things break) + smbauth.init_account(host) + lock_mutex(state, "start(1)") if(port ~= 139) then @@ -406,11 +387,16 @@ end -- packet isn't sent. --@param create_file [optional] The path and name of the file (or pipe) that's created, if given. If -- not given, packet isn't sent. +--@param overrides [optional] A table of overrides (for, for example, username, password, etc.) to pass +-- to all functions. --@param disable_extended [optional] If set to true, disables extended security negotiations. -function start_ex(host, negotiate_protocol, start_session, tree_connect, create_file, disable_extended) +function start_ex(host, negotiate_protocol, start_session, tree_connect, create_file, disable_extended, overrides) local smbstate local status, err + -- Make sure we have overrides + overrides = overrides or {} + -- Begin the SMB session status, smbstate = smb.start(host) if(status == false) then @@ -424,7 +410,7 @@ function start_ex(host, negotiate_protocol, start_session, tree_connect, create_ if(negotiate_protocol == true) then -- Negotiate the protocol - status, err = smb.negotiate_protocol(smbstate) + status, err = smb.negotiate_protocol(smbstate, overrides) if(status == false) then smb.stop(smbstate) return false, err @@ -432,7 +418,7 @@ function start_ex(host, negotiate_protocol, start_session, tree_connect, create_ if(start_session == true) then -- Start up a session - status, err = smb.start_session(smbstate) + status, err = smb.start_session(smbstate, overrides) if(status == false) then smb.stop(smbstate) return false, err @@ -440,7 +426,7 @@ function start_ex(host, negotiate_protocol, start_session, tree_connect, create_ if(tree_connect ~= nil) then -- Connect to share - status, err = smb.tree_connect(smbstate, tree_connect) + status, err = smb.tree_connect(smbstate, tree_connect, overrides) if(status == false) then smb.stop(smbstate) return false, err @@ -448,7 +434,7 @@ function start_ex(host, negotiate_protocol, start_session, tree_connect, create_ if(create_file ~= nil) then -- Try to connect to requested pipe - status, err = smb.create_file(smbstate, create_file) + status, err = smb.create_file(smbstate, create_file, overrides) if(status == false) then smb.stop(smbstate) return false, err @@ -841,14 +827,26 @@ function smb_send(smb, header, parameters, data) local encoded_parameters = smb_encode_parameters(parameters) local encoded_data = smb_encode_data(data) local body = header .. encoded_parameters .. encoded_data + local attempts = 5 + local status, err -- Calculate the message signature body = message_sign(smb, body) local out = bin.pack(">Ismb: -- * 'security_mode' Whether or not to use cleartext passwords, message signatures, etc. @@ -984,18 +993,29 @@ end -- * 'server_challenge' A random string used for challenge/response -- * 'domain' The server's primary domain -- * 'server' The server's name -function negotiate_protocol(smb) +function negotiate_protocol(smb, overrides) local header, parameters, data local pos local header1, header2, header3, ehader4, command, status, flags, flags2, pid_high, signature, unused, pid, mid header = smb_encode_header(smb, command_codes['SMB_COM_NEGOTIATE']) + -- Make sure we have overrides + overrides = overrides or {} + -- Parameters are blank parameters = "" -- Data is a list of strings, terminated by a blank one. - data = bin.pack(""\\servername\C$") +--@param smb The SMB object associated with the connection +--@param path The path to connect (eg, "\\servername\C$") +--@param overrides [optional] Overrides for various fields --@return (status, result) If status is false, result is an error message. Otherwise, result is a -- table with the following elements: -- * 'tid' The TreeID for the session -function tree_connect(smb, path) +function tree_connect(smb, path, overrides) local header, parameters, data, err, result local pos local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid local andx_command, andx_reserved, andx_offset, action + -- Make sure we have overrides + overrides = overrides or {} + header = smb_encode_header(smb, command_codes['SMB_COM_TREE_CONNECT_ANDX']) parameters = bin.pack(" #data) then + return false, "SMB: Data returned runs off the end of the packet" + end + + -- Pull the data string out of the data + response['data'] = string.sub(data, data_offset + 1, data_offset + response['data_length']) end - -- Figure out the offset into the data section - data_offset = data_offset - data_start - - -- Make sure we don't run off the edge of the packet - if(data_offset + response['data_length'] > #data) then - return false, "SMB: Data returned runs off the end of the packet" - end - - -- Pull the data string out of the data - response['data'] = string.sub(data, data_offset + 1, data_offset + response['data_length']) - return true, response end @@ -1862,7 +1931,7 @@ function delete_file(smb, path) header = smb_encode_header(smb, command_codes['SMB_COM_DELETE']) parameters = bin.pack("file_upload, except the +-- data is given as a string, not a file. +-- +--@param host The host object +--@param share The share to upload it to (eg, C$). +--@param remotefile The remote file on the machine. It is relative to the share's root. +--@param use_anonymous [optional] If set to 'true', test is done by the anonymous user rather than the current user. +--@return (status, err) If status is false, err is an error message. Otherwise, err is undefined. +function file_write(host, data, share, remotefile, use_anonymous) + local status, err, smbstate + local chunk = 1024 + local overrides = nil + + -- If anonymous is being used, create some overrides + if(use_anonymous) then + overrides = get_overrides_anonymous() + end + + -- Create the SMB sessioan + status, smbstate = smb.start_ex(host, true, true, share, remotefile, nil, overrides) + + if(status == false) then + return false, smbstate + end + + local i = 1 + while(i <= #data) do + local chunkdata = string.sub(data, i, i + chunk - 1) + status, err = smb.write_file(smbstate, chunkdata, i - 1) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + i = i + chunk + end + + status, err = smb.close_file(smbstate) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Stop the session + smb.stop(smbstate) + + return true +end + +---Write given data to the remote machine on the given share. This is similar to file_upload, except the +-- data is given as a string, not a file. +-- +--@param host The host object +--@param share The share to read it from (eg, C$). +--@param remotefile The remote file on the machine. It is relative to the share's root. +--@param use_anonymous [optional] If set to 'true', test is done by the anonymous user rather than the current user. +--@param overrides [optional] Override various fields in the SMB packets. +--@return (status, err) If status is false, err is an error message. Otherwise, err is undefined. +function file_read(host, share, remotefile, use_anonymous, overrides) + local status, err, smbstate + local result + local chunk = 1024 + local read = "" + + -- Make sure we got overrides + overrides = overrides or {} + + -- If anonymous is being used, create some overrides + if(use_anonymous) then + overrides = get_overrides_anonymous(overrides) + end + + -- Create the SMB sessioan + status, smbstate = smb.start_ex(host, true, true, share, remotefile, nil, overrides) + + if(status == false) then + return false, smbstate + end + + local i = 1 + while true do + status, result = smb.read_file(smbstate, i - 1, chunk) + if(status == false) then + smb.stop(smbstate) + return false, result + end + + if(result['data_length'] == 0) then + break + end + + read = read .. result['data'] + i = i + chunk + end + + status, err = smb.close_file(smbstate) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Stop the session + smb.stop(smbstate) + return true, read +end + +---Check how many files, in a given list, exist on the given share. +-- +--@param host The host object +--@param share The share to read it from (eg, C$). +--@param fo;es A list of files to look for; it is relative to the share's root. +--@param overrides [optional] Override various fields in the SMB packets. +--@return status: A true/false value indicating success +--@return count: The number of files that existed, or an error message if status is 'false' +--@return files: A list of the files that existed. +function files_exist(host, share, files, overrides) + local status, smbstate, result, err + + -- Make sure we got overrides + overrides = overrides or {} + + -- We don't wan to be creating the files + overrides['file_create_disposition'] = 1 + + -- Create the SMB sessioan + status, smbstate = smb.start_ex(host, true, true, share, nil, nil, overrides) + + if(status == false) then + return false, smbstate + end + + local exist = 0 + local list = {} + + for _, file in ipairs(files) do + -- Try and open the file + status, result = create_file(smbstate, file, overrides) + + -- If there was an error other than 'file already exists', return an error + if(not(status) and result ~= 'NT_STATUS_OBJECT_NAME_NOT_FOUND') then + return false, result + end + + -- If the file existed, count it and close it + if(status) then + exist = exist + 1 + table.insert(list, file) + status, err = smb.close_file(smbstate) + if(status == false) then + smb.stop(smbstate) + return false, err + end + end + end + + -- Stop the session + smb.stop(smbstate) + return true, exist, list +end + ---Delete a file from the remote machine -- --@param host The host object --@param share The share to upload it to (eg, C$). ---@param remotefile The remote file on the machine. It is relative to the share's root. +--@param remotefile The remote file on the machine. It is relative to the share's root. It can be a string, or an array. --@return (status, err) If status is false, err is an error message. Otherwise, err is undefined. function file_delete(host, share, remotefile) local status, smbstate, err @@ -2143,10 +2372,21 @@ function file_delete(host, share, remotefile) return false, smbstate end - status, err = smb.delete_file(smbstate, remotefile) - if(status == false) then - smb.stop(smbstate) - return false, err + -- Make sure the remotefile is always a table, to save on duplicate code + if(type(remotefile) ~= "table") then + remotefile = {remotefile} + end + + + for _, file in ipairs(remotefile) do + status, err = smb.delete_file(smbstate, file) + if(status == false) then + stdnse.print_debug(1, "SMB: Couldn't delete %s\\%s: %s", share, file, err) + if(err ~= 'NT_STATUS_OBJECT_NAME_NOT_FOUND') then + smb.stop(smbstate) + return false, err + end + end end -- Stop the session @@ -2155,6 +2395,422 @@ function file_delete(host, share, remotefile) return true end +---Determine whether or not the anonymous user has write access on the share. This is done by creating then +-- deleting a file. +-- +--@param host The host object +--@param share The share to test +--@return (status, result) If status is false, result is an error message. The error message 'NT_STATUS_OBJECT_NAME_NOT_FOUND' +-- should be handled gracefully; it indicates that the share isn't a fileshare. Otherwise, result is a boolean value: +-- true if the file was successfully written, false if it was not. +function share_anonymous_can_write(host, share) + local filename, status, err + + -- First, choose a filename. This should be random. + filename = "nmap-test-file" + + -- Next, attempt to write to that file + status, err = file_write(host, string.rep("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 10), share, filename, true) + if(status == false) then + if(err == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + return false, err + end + + if(err == "NT_STATUS_ACCESS_DENIED") then + return true, false + end + + return false, "Error writing test file to disk as anonymous: " .. err + end + + -- Now the important part: delete it + status, err = file_delete(host, share, filename) + if(status == false) then + return false, "Error deleting test file as anonymous: " .. err + end + + return true, true +end + + +---Determine whether or not the current user has read or read/write access on the share. This is done by creating then +-- deleting a file. +-- +--@param host The host object +--@param share The share to test +--@return (status, result) If status is false, result is an error message. The error message 'NT_STATUS_OBJECT_NAME_NOT_FOUND' +-- should be handled gracefully; it indicates that the share isn't a fileshare. Otherwise, result is a boolean value: +-- true if the file was successfully written, false if it was not. +function share_user_can_write(host, share) + + local filename, status, err + + -- First, choose a filename. This should be random. + filename = "nmap-test-file" + + -- Next, attempt to write to that file + status, err = file_write(host, string.rep("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 10), share, filename) + if(status == false) then + if(err == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + return false, err + end + + if(err == "NT_STATUS_ACCESS_DENIED") then + return true, false + end + + return false, "Error writing test file to disk as user: " .. err + end + + -- Now the important part: delete it + status, err = file_delete(host, share, filename) + if(status == false) then + return false, "Error deleting test file as user: " .. err + end + + return true, true +end + +---Check whether or not a share is accessible by the anonymous user. Assumes that share_host_returns_proper_error +-- has been called and returns true. +-- +--@param host The host object +--@param share The share to test +--@return (status, result) If status is false, result is an error message. Otherwise, result is a boolean value: +-- true if anonymous access is permitted, false otherwise. +function share_anonymous_can_read(host, share) + local status, smbstate, err + local overrides = get_overrides_anonymous() + + -- Begin the SMB session + status, smbstate = smb.start(host) + if(status == false) then + return false, smbstate + end + + -- Negotiate the protocol + status, err = smb.negotiate_protocol(smbstate, overrides) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Start up a null session + status, err = smb.start_session(smbstate, overrides) + + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Attempt a connection to the share + status, err = smb.tree_connect(smbstate, share, overrides) + if(status == false) then + + -- Stop the session + smb.stop(smbstate) + + -- ACCESS_DENIED is the expected error: it tells us that the connection failed + if(err == 0xc0000022 or err == 'NT_STATUS_ACCESS_DENIED') then + return true, false + else + return false, err + end + end + + + + smb.stop(smbstate) + return true, true +end + +---Check whether or not a share is accessible by the current user. Assumes that share_host_returns_proper_error +-- has been called and returns true. +-- +--@param host The host object +--@param share The share to test +--@return (status, result) If status is false, result is an error message. Otherwise, result is a boolean value: +-- true if anonymous access is permitted, false otherwise. +function share_user_can_read(host, share) + local status, smbstate, err + local overrides = {} + + -- Begin the SMB session + status, smbstate = smb.start(host) + if(status == false) then + return false, smbstate + end + + -- Negotiate the protocol + status, err = smb.negotiate_protocol(smbstate, overrides) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Start up a null session + status, err = smb.start_session(smbstate, overrides) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Attempt a connection to the share + status, err = smb.tree_connect(smbstate, share, overrides) + if(status == false) then + + -- Stop the session + smb.stop(smbstate) + + -- ACCESS_DENIED is the expected error: it tells us that the connection failed + if(err == 0xc0000022 or err == 'NT_STATUS_ACCESS_DENIED') then + return true, false + else + return false, err + end + end + + smb.stop(smbstate) + return true, true +end + +---Determine whether or not a host will accept any share name (I've seen this on certain systems; it's +-- bad, because it means we cannot tell whether or not a share exists). +-- +--@param host The host object +--@param share The share to test +--@return (status, result) If status is false, result is an error message. Otherwise, result is a boolean value: +-- true if the file was successfully written, false if it was not. +function share_host_returns_proper_error(host) + local status, smbstate, err + local share = "nmap-share-test" + local overrides = get_overrides_anonymous() + + -- Begin the SMB session + status, smbstate = smb.start(host) + if(status == false) then + return false, smbstate + end + + -- Negotiate the protocol + status, err = smb.negotiate_protocol(smbstate, overrides) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Start up a null session + status, err = smb.start_session(smbstate, overrides) + if(status == false) then + smb.stop(smbstate) + return false, err + end + + -- Connect to the share + stdnse.print_debug(1, "SMB: Trying a random share to see if server responds properly: %s", share) + status, err = smb.tree_connect(smbstate, share, overrides) + + if(status == false) then + -- If the error is NT_STATUS_ACCESS_DENIED (0xc0000022), that's bad -- we don't want non-existent shares + -- showing up as 'access denied'. Any other error is ok. + if(err == 0xc0000022 or err == 'NT_STATUS_ACCESS_DENIED') then + stdnse.print_debug(1, "SMB: Server doesn't return proper value for non-existent shares (returns ACCESS_DENIED)") + smb.stop(smbstate) + return true, false + end + else + -- If we were actually able to connect to this share, then there's probably a serious issue + stdnse.print_debug(1, "SMB: Server doesn't return proper value for non-existent shares (accepts the connection)") + smb.stop(smbstate) + return true, false + end + + smb.stop(smbstate) + return true, true +end + +---Get all the details we can about the share. These details are stored in a table and returned. +-- +--@param host The host object. +--@param shares An array of shares to check. +--@return (status, result) If status is false, result is an error message. Otherwise, result is a boolean value: +-- true if the file was successfully written, false if it was not. +function share_get_details(host, share) + local smbstate, status, result + local i + local details = {} + + -- Save the name + details['name'] = share + + -- Ensure that the server returns the proper error message + status, result = share_host_returns_proper_error(host) + if(status == false) then + return false, result + end + if(status == true and result == false) then + return false, "Server doesn't return proper value for non-existent shares" + end + + -- Check if the current user can read the share + stdnse.print_debug(1, "SMB: Checking if share %s can be read by the current user", share) + status, result = share_user_can_read(host, share) + if(status == false) then + return false, result + end + details['user_can_read'] = result + + -- Check if the anonymous reader can read the share + stdnse.print_debug(1, "SMB: Checking if share %s can be read by the anonymous user", share) + status, result = share_anonymous_can_read(host, share) + if(status == false) then + return false, result + end + details['anonymous_can_read'] = result + + -- Check if the current user can write to the share + stdnse.print_debug(1, "SMB: Checking if share %s can be written by the current user", share) + status, result = share_user_can_write(host, share) + if(status == false) then + if(result == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + details['user_can_write'] = "NT_STATUS_OBJECT_NAME_NOT_FOUND" + else + return false, result + end + end + details['user_can_write'] = result + + -- Check if the anonymous user can write to the share + stdnse.print_debug(1, "SMB: Checking if share %s can be written by the anonymous user", share) + status, result = share_anonymous_can_write(host, share) + if(status == false) then + if(result == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + details['anonymous_can_write'] = "NT_STATUS_OBJECT_NAME_NOT_FOUND" + else + return false, result + end + end + details['anonymous_can_write'] = result + + + -- Try and get full details about the share + status, result = msrpc.get_share_info(host, share) + if(status == false) then + -- We don't stop for this error (it's pretty common since administive privileges are required here) + stdnse.print_debug(1, "SMB: Failed to get share info for %s: %s", share, result) + details['details'] = result + else + -- Process the result a bit + result = result['info'] + if(result['max_users'] == 4294967295) then + result['max_users'] = "" + end + details['details'] = result + end + + return true, details +end + +---Retrieve a list of fileshares, along with any details that could be pulled. This is the core of smb-enum-shares.nse, but +-- can also be used by any script that needs to find an open share. +-- +-- In the best care, the shares are determined by calling msrpc.enum_shares, and information is gathered by calling +-- msrpc.get_share_info. These require a certain level of access, though, so as a fallback, a pre-programmed list of +-- shares is used, and these are verified by attempting a connection. +-- +--@param host The host object. +--@return (status, result, extra) If status is false, result is an error message. Otherwise, result is an array of shares with as much +-- detail as we could get. If extra isn't nil, it is set to extra information that should be displayed (such as a warning). +function share_get_list(host) + + local enum_status + local extra = "" + local shares = {} + local share_details = {} + + -- Try and do this the good way, make a MSRPC call to get the shares + stdnse.print_debug(1, "SMB: Attempting to log into the system to enumerate shares") + enum_status, shares = msrpc.enum_shares(host) + + -- If that failed, try doing it with brute force. This almost certainly won't find everything, but it's the + -- best we can do. + if(enum_status == false) then + stdnse.print_debug(1, "SMB: Enumerating shares failed, guessing at common ones (%s)", shares) + extra = string.format("ERROR: Enumerating shares failed, guessing at common ones (%s)", shares) + + -- Take some common share names I've seen (thanks to Brandon Enright for most of these, except the last few) + shares = {"IPC$", "ADMIN$", "TEST", "TEST$", "HOME", "HOME$", "PUBLIC", "PRINT", "PRINT$", "GROUPS", "USERS", "MEDIA", "SOFTWARE", "XSERVE", "NETLOGON", "INFO", "PROGRAMS", "FILES", "WWW", "STMP", "TMP", "DATA", "BACKUP", "DOCS", "HD", "WEBSERVER", "WEB DOCUMENTS", "SHARED", "DESKTOP", "MY DOCUMENTS", "PORN", "PRON", "PR0N"} + + -- Try every alphabetic share, with and without a trailing '$' + for i = string.byte("A", 1), string.byte("Z", 1), 1 do + shares[#shares + 1] = string.char(i) + shares[#shares + 1] = string.char(i) .. "$" + end + else + stdnse.print_debug(1, "SMB: Found %d shares, will attempt to find more information", #shares) + end + + -- Sort the shares + table.sort(shares) + + -- Get more information on each share + for i = 1, #shares, 1 do + local status, result + stdnse.print_debug(1, "SMB: Getting information for share: %s", shares[i]) + status, result = smb.share_get_details(host, shares[i]) + if(status == false and result == 'NT_STATUS_BAD_NETWORK_NAME') then + stdnse.print_debug(1, "SMB: Share doesn't exist: %s", shares[i]) + elseif(status == false) then + stdnse.print_debug(1, "SMB: Error while getting share details: %s", result) + return false, result + else + -- Save the share details + table.insert(share_details, result) + end + end + + return true, share_details, extra +end + +---Find a share that the current user can write to. Return it, along with its path. If no share could be found, +-- an error is returned. If the path cannot be determined, the returned path is nil. +-- +--@param host The host object. +--@return (status, name, path, names) If status is false, result is an error message. Otherwise, name is the name of the share, +-- path is its path, if it could be determined, and names is a list of all writable shares. +function share_find_writable(host) + local i + local status, shares + local main_name, main_path + local names = {} + local writable = {} + + status, shares = share_get_list(host) + if(status == false) then + return false, shares + end + + for i = 1, #shares, 1 do + if(shares[i]['user_can_write'] == true) then + if(main_name == nil) then + main_name = shares[i]['name'] + + if(shares[i]['details'] ~= nil) then + main_path = shares[i]['details']['path'] + end + end + + table.insert(names, shares[i]['name']) + end + end + + if(main_name == nil) then + return false, "Couldn't find a writable share!" + else + return true, main_name, main_path, names + end +end + --- Converts numbered Windows version strings ("Windows 5.0", "Windows 5.1") to names ("Windows 2000", "Windows XP"). --@param os The numbered OS version. --@return The actual name of the OS (or the same as the os parameter if no match was found). @@ -2206,6 +2862,152 @@ function get_os(host) return true, response end +---Basically a wrapper around socket:get_info, except that it also makes a SMB connection before calling the +-- get_info function. Returns the mac address as well, for convenience. +-- +--@param host The host object +--@return status: true for successful, false otherwise. +--@return If status is true, the local ip address; otherwise, an error message. +--@return The local port (not really meaningful, since it'll change next time). +--@return The remote ip address. +--@return The report port. +--@return The mac address, if possible; nil otherwise. +function get_socket_info(host) + local status, lhost, lport, rhost, rport + local smbstate, socket + + -- Start SMB (we need a socket to get the proper local ip + status, smbstate = smb.start_ex(host) + if(status == false) then + return false, smbstate + end + + socket = smbstate['socket'] + status, lhost, lport, rhost, rport = socket:get_info() + if(status == false) then + return false, lhost + end + + -- Stop SMB + smb.stop(smbstate) + + -- Get the mac in hex format, if possible + local lmac = nil + if(host.mac_addr_src) then + lmac = stdnse.tohex(host.mac_addr_src, {separator = ":"}) + end + + return true, lhost, lport, rhost, rport, lmac +end + +---Generate a string that's somewhat unique, but is based on factors that won't change on a host. At the moment, this is a very simple +-- hash based on the IP address. This hash is *very* likely to have collisions, and that's by design -- while it should be somewhat unique, +-- I don't want it to be trivial to uniquely determine who it originated from. +-- +-- TODO: At some point, I should re-do this function properly, with a method of hashing that's somewhat proven. +-- +--@param host The host object +--@param extension [optional] The extension to add on the end of the file. Default: none. +--@param seed [optional] Some randomness on which to base the name. If you want to do multiple files, each with its +-- own uniqueish name, this can be used. +--@return (status, data) If status is true, data is a table of values; otherwise, data is an error message. Can be any kind of string. +function get_uniqueish_name(host, extension, seed) + + local status + local lhost, lport, rhost, rport + if(type(host) == "table") then + status, lhost = get_socket_info(host) + else + lhost = host + end + + -- Create our ultra-weak hash by using a simple xor/shift algorithm + -- I tested this, and in 255 tests, there were roughly 10 collisions. That's about what I'm looking for. + local hash = 0 + local i + local str = lhost .. (seed or "") .. (extension or "") .. (nmap.registry.args.randomseed or "") + + for i = 1, #str, 1 do + local chr = string.byte(string.sub(str, i, i), 1) + hash = bit.bxor(hash, chr) + hash = bit.bor(bit.lshift(hash, 3), bit.rshift(hash, 29)) + hash = bit.bxor(hash, 3) + hash = bit.band(hash, 0xFFFFFFFF) + end + + local response + if(extension) then + response = string.format("%x.%s", hash, extension) + else + response = string.format("%x", hash) + end + + return true, response +end + +---Determines, as accurately as possible, whether or not an account is an administrator. If there is an error, +-- 'false' is simply returned. +function is_admin(host, username, domain, password, password_hash, hash_type) + local status, smbstate, err, result + local overrides = get_overrides(username, domain, password, password_hash, hash_type) + + stdnse.print_debug("SMB: Checking if %s is an administrator", username) + + status, smbstate = smb.start(host) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Failed to start SMB: %s [%s]", smbstate, username) + smb.stop(smbstate) + return false + end + + status, err = smb.negotiate_protocol(smbstate, overrides) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Failed to negotiatie protocol: %s [%s]", err, username) + smb.stop(smbstate) + return false + end + + status, err = smb.start_session(smbstate, overrides) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Failed to start session %s [%s]", err, username) + smb.stop(smbstate) + return false + end + + status, err = smb.tree_connect(smbstate, "IPC$", overrides) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Failed to connect tree: %s [%s]", err, username) + smb.stop(smbstate) + return false + end + + status, err = smb.create_file(smbstate, msrpc.SRVSVC_PATH, overrides) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Failed to create file: %s [%s]", err, username) + smb.stop(smbstate) + return false + end + + status, err = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Failed to bind: %s [%s]", err, username) + smb.stop(smbstate) + return false + end + + -- Call netservergetstatistics for 'server' + status, err = msrpc.srvsvc_netservergetstatistics(smbstate, host.ip) + if(status == false) then + stdnse.print_debug("SMB; is_admin: Couldn't get server stats (may be normal): %s [%s]", err, username) + smb.stop(smbstate) + return false + end + + smb.stop(smbstate) + + return true +end + command_codes = { SMB_COM_CREATE_DIRECTORY = 0x00, diff --git a/nselib/smbauth.lua b/nselib/smbauth.lua index c0cd53ae7..23acc3e40 100644 --- a/nselib/smbauth.lua +++ b/nselib/smbauth.lua @@ -94,6 +94,219 @@ local NTLMSSP_AUTH = 0x00000003 local session_key = string.rep(string.char(0x00), 16) +-- Types of accounts (ordered by how useful they are +local ACCOUNT_TYPES = { + ANONYMOUS = 0, + GUEST = 1, + USER = 2, + ADMIN = 3 +} + +local function account_exists(host, username, domain) + if(nmap.registry[host.ip] == nil or nmap.registry[host.ip]['smbaccounts'] == nil) then + return false + end + + for i, j in pairs(nmap.registry[host.ip]['smbaccounts']) do + if(j['username'] == username and j['domain'] == domain) then + return true + end + end + + return false +end + +function next_account(host, num) + if(num == nil) then + if(nmap.registry[host.ip]['smbindex'] == nil) then + nmap.registry[host.ip]['smbindex'] = 1 + else + nmap.registry[host.ip]['smbindex'] = nmap.registry[host.ip]['smbindex'] + 1 + end + else + nmap.registry[host.ip]['smbindex'] = num + end +end + +---Writes the given account to the registry. There are several places where accounts are stored: +-- * registry['usernames'][username] => true +-- * registry['smbaccounts'][username] => password +-- * registry[ip]['smbaccounts'] => array of table containing 'username', 'password', and 'is_admin' +-- +-- The final place, 'smbaccount', is reserved for the "best" account. This is an administrator +-- account, if one's found; otherwise, it's the first account discovered that isn't guest. +-- +-- This has to be called while no SMB connections are made, since it potentially makes its own connection. +-- +--@param host The host object. +--@param username The username to add. +--@param domain The domain to add. +--@param password The password to add. +--@param password_hash The password hash to add. +--@param hash_type The hash type to use. +--@param is_admin [optional] Set to 'true' the account is known to be an administrator. +function add_account(host, username, domain, password, password_hash, hash_type, is_admin) + -- Save the username in a global list -- TODO: restore this +-- if(nmap.registry.usernames == nil) then +-- nmap.registry.usernames = {} +-- end +-- nmap.registry.usernames[username] = true +-- +-- -- Save the username/password pair in a global list +-- if(nmap.registry.smbaccounts == nil) then +-- nmap.registry.smbaccounts = {} +-- end +-- nmap.registry.smbaccounts[username] = password + + -- Check if we've already recorded this account + if(account_exists(host, username, domain)) then + return + end + + if(nmap.registry[host.ip] == nil) then + nmap.registry[host.ip] = {} + end + if(nmap.registry[host.ip]['smbaccounts'] == nil) then + nmap.registry[host.ip]['smbaccounts'] = {} + end + + -- Determine the type of account, if it wasn't given + local account_type = nil + if(is_admin) then + account_type = ACCOUNT_TYPES.ADMIN + else + if(username == '') then + -- Anonymous account + account_type = ACCOUNT_TYPES.ANONYMOUS + elseif(string.lower(username) == 'guest') then + -- Guest account + account_type = ACCOUNT_TYPES.GUEST + else + -- We have to assume it's a user-level account (we just can't call any SMB functions from inside here) + account_type = ACCOUNT_TYPES.USER + end + end + + -- Set some defaults + if(hash_type == nil) then + hash_type = 'ntlm' + end + + -- Save the new account if this is our first one, or our other account isn't an admin + local new_entry = {} + new_entry['username'] = username + new_entry['domain'] = domain + new_entry['password'] = password + new_entry['password_hash'] = password_hash + new_entry['hash_type'] = string.lower(hash_type) + new_entry['account_type'] = account_type + + -- Insert the new entry into the table + table.insert(nmap.registry[host.ip]['smbaccounts'], new_entry) + + -- Sort the table based on the account type (we want anonymous at the end, administrator at the front) + table.sort(nmap.registry[host.ip]['smbaccounts'], function(a,b) return a['account_type'] > b['account_type'] end) + + -- Print a debug message + stdnse.print_debug(1, "SMB: Added account '%s' to account list", username) + + -- Reset the credentials + next_account(host, 1) + +-- io.write("\n\n" .. nsedebug.tostr(nmap.registry[host.ip]['smbaccounts']) .. "\n\n") +end + +---Retrieve the current set of credentials set in the registry. If these fail, next_credentials should be +-- called. +-- +--@param host The host object. +--@return (result, username, domain, password, password_hash, hash_type) If result is false, username is an error message. Otherwise, username and password are +-- the current username and password that should be used. +function get_account(host) + if(nmap.registry[host.ip]['smbindex'] == nil) then + nmap.registry[host.ip]['smbindex'] = 1 + end + + local index = nmap.registry[host.ip]['smbindex'] + local account = nmap.registry[host.ip]['smbaccounts'][index] + + if(account == nil) then + return false, "No accounts left to try" + end + + return true, account['username'], account['domain'], account['password'], account['password_hash'], account['hash_type'] +end + +---Create the account table with the anonymous and guest users, as well as the user given in the script's +-- arguments, if there is one. +-- +--@param host The host object. +function init_account(host) + -- Create the key if it exists + if(nmap.registry[host.ip] == nil) then + nmap.registry[host.ip] = {} + end + + -- Don't run this more than once for each host + if(nmap.registry[host.ip]['smbaccounts'] ~= nil) then + return + end + + -- Create the list + nmap.registry[host.ip]['smbaccounts'] = {} + + -- Add the anonymous/guest accounts + add_account(host, '', '', '', nil, 'none') + add_account(host, 'guest', '', '', nil, 'ntlm') + + -- Add the account given on the commandline (TODO: allow more than one?) + local args = nmap.registry.args + local username = nil + local domain = '' + local password = nil + local password_hash = nil + local hash_type = 'ntlm' + + -- Do the username first + if(args.smbusername ~= nil) then + username = args.smbusername + elseif(args.smbuser ~= nil) then + username = args.smbuser + end + + -- If the username exists, do everything else + if(username ~= nil) then + -- Domain + if(args.smbdomain ~= nil) then + domain = args.smbdomain + end + + -- Type + if(args.smbtype ~= nil) then + hash_type = args.smbtype + end + + -- Do the password + if(args.smbpassword ~= nil) then + password = args.smbpassword + elseif(args.smbpass ~= nil) then + password = args.smbpass + end + + -- Only use the hash if there's no password + if(password == nil) then + password_hash = args.smbhash + end + + -- Add the account, if we got a password + if(password == nil and password_hash == nil) then + stdnse.print_debug(1, "SMB: Either smbpass, smbpassword, or smbhash have to be passed as script arguments to use an account") + else + add_account(host, username, domain, password, password_hash, hash_type) + end + end +end + local function to_unicode(str) local unicode = "" @@ -310,82 +523,6 @@ function ntlmv2_create_response(ntlm, username, domain, challenge, client_challe return true, openssl.hmac("MD5", ntlmv2_hash, challenge .. client_challenge) .. client_challenge end ----Determines which hash type is going to be used, based on the function parameters and --- the nmap arguments (in that order). --- ---@param hash_type [optional] The function parameter version, which will override all others if set. ---@return The highest priority hash type that's set. -local function get_hash_type(hash_type) - - if(hash_type ~= nil) then - stdnse.print_debug(2, "SMB: Using logon type passed as a parameter: %s", hash_type) - else - if(nmap.registry.args.smbtype ~= nil) then - hash_type = nmap.registry.args.smbtype - stdnse.print_debug(2, "SMB: Using logon type passed as an nmap parameter: %s", hash_type) - else - hash_type = "ntlm" - stdnse.print_debug(2, "SMB: Using default logon type: %s", hash_type) - end - end - - return string.lower(hash_type) -end - - ----Determines which username is going to be used, based on the function parameters, the nmap arguments, --- and the registry (in that order). --- ---@param ip The ip address, used when reading from the registry ---@param username [optional] The function parameter version, which will override all others if set. ---@return The highest priority username that's set. -local function get_username(ip, username) - - if(username ~= nil) then - stdnse.print_debug(2, "SMB: Using username passed as a parameter: %s", username) - else - if(nmap.registry.args.smbusername ~= nil) then - username = nmap.registry.args.smbusername - stdnse.print_debug(2, "SMB: Using username passed as an nmap parameter (smbusername): %s", username) - elseif(nmap.registry.args.smbuser ~= nil) then - username = nmap.registry.args.smbuser - stdnse.print_debug(2, "SMB: Using username passed as an nmap parameter (smbuser): %s", username) - elseif(nmap.registry[ip] ~= nil and nmap.registry[ip]['smbaccount'] ~= nil and nmap.registry[ip]['smbaccount']['username'] ~= nil) then - username = nmap.registry[ip]['smbaccount']['username'] - stdnse.print_debug(2, "SMB: Using username found in the registry: %s", username) - else - username = nil - stdnse.print_debug(2, "SMB: Couldn't find a username to use, not logging in") - end - end - - return username -end - ----Determines which domain is going to be used, based on the function parameters and --- the nmap arguments (in that order). --- --- [TODO] registry --- ---@param domain [optional] The function parameter version, which will override all others if set. ---@return The highest priority domain that's set. -local function get_domain(ip, domain) - - if(domain ~= nil) then - stdnse.print_debug(2, "SMB: Using domain passed as a parameter: %s", domain) - else - if(nmap.registry.args.smbdomain ~= nil) then - domain = nmap.registry.args.smbdomain - stdnse.print_debug(2, "SMB: Using domain passed as an nmap parameter: %s", domain) - else - domain = "" - stdnse.print_debug(2, "SMB: Couldn't find domain to use, using blank") - end - end - - return domain -end - ---Generate the Lanman and NTLM password hashes. The password itself is taken from the function parameters, -- the nmap arguments, and the registry (in that order). If no password is set, then the password hash -- is used (which is read from all the usual places). If neither is set, then a blank password is used. @@ -403,51 +540,24 @@ end -- message-signing key to be generated properly). --@return (lm_response, ntlm_response, mac_key) The two strings that can be sent directly back to the server, -- and the mac_key, which is used for message signing. -local function get_password_response(ip, username, domain, password, password_hash, challenge, hash_type, is_extended) - +function get_password_response(ip, username, domain, password, password_hash, hash_type, challenge, is_extended) local status local lm_hash = nil local ntlm_hash = nil local mac_key = nil local lm_response, ntlm_response - -- Check if there's a password or hash set. This is a little tricky, because in all places (except the one passed - -- as a parameter), it's based on whether or not the username was stored. This lets us use blank passwords by not - -- specifying one. - if(password ~= nil) then - stdnse.print_debug(2, "SMB: Using password/hash passed as a parameter (username = '%s')", username) - - elseif(nmap.registry.args.smbusername ~= nil or nmap.registry.args.smbuser ~= nil) then - stdnse.print_debug(2, "SMB: Using password/hash passed as an nmap parameter") - - if(nmap.registry.args.smbpassword ~= nil) then - password = nmap.registry.args.smbpassword - elseif(nmap.registry.args.smbpass ~= nil) then - password = nmap.registry.args.smbpass - elseif(nmap.registry.args.smbhash ~= nil) then - password_hash = nmap.registry.args.smbhash - end - - elseif(nmap.registry[ip] ~= nil and nmap.registry[ip]['smbaccount'] ~= nil and nmap.registry[ip]['smbaccount']['username'] ~= nil) then - stdnse.print_debug(2, "SMB: Using password/hash found in the registry") - - if(nmap.registry[ip]['smbaccount']['password'] ~= nil) then - password = nmap.registry[ip]['smbaccount']['password'] - elseif(nmap.registry[ip]['smbaccount']['hash'] ~= nil) then - password_hash = nmap.registry[ip]['smbaccount']['password'] - end - - else - password = nil - password_hash = nil - end - -- Check for a blank password if(password == nil and password_hash == nil) then stdnse.print_debug(2, "SMB: Couldn't find password or hash to use (assuming blank)") password = "" end + -- The anonymous user requires a single 0-byte instead of a LANMAN hash (don't ask me why, but it doesn't work without) + if(hash_type == 'none') then + return string.char(0), '', nil + end + -- If we got a password, hash it if(password ~= nil) then status, lm_hash = lm_create_hash(password) @@ -523,7 +633,12 @@ local function get_password_response(ip, username, domain, password, password_ha else -- Default to NTLMv1 - stdnse.print_debug(1, "SMB: Invalid login type specified, using default (NTLM)") + if(hash_type ~= nil) then + stdnse.print_debug(1, "SMB: Invalid login type specified ('%s'), using default (NTLM)", hash_type) + else + stdnse.print_debug(1, "SMB: No login type specified, using default (NTLM)") + end + status, lm_response = ntlm_create_response(ntlm_hash, challenge) status, ntlm_response = ntlm_create_response(ntlm_hash, challenge) @@ -535,68 +650,7 @@ local function get_password_response(ip, username, domain, password, password_ha return lm_response, ntlm_response, mac_key end ----Get the list of accounts to use to log in. TODO: More description -function get_accounts(ip, overrides, use_defaults) - local results = {} - -- Just so we can index into it - if(overrides == nil) then - overrides = {} - end - -- By default, use defaults - if(use_defaults == nil) then - use_defaults = true - end - - -- If we don't have OpenSSL, don't bother with any of this because we aren't going to - -- be able to hash the password - if(have_ssl == true) then - local result = {} - - -- Get the "real" information - result['username'] = get_username(ip, overrides['username']) - result['domain'] = get_domain(ip, overrides['domain']) - result['hash_type'] = get_hash_type(overrides['hash_type']) - - if(result['username'] ~= nil) then - results[#results + 1] = result - end - - -- Do the "guest" account, if use_defaults is set - if(use_defaults) then - result = {} - result['username'] = "guest" - result['domain'] = "" - result['hash_type'] = get_hash_type(overrides['hash_type']) - results[#results + 1] = result - end - end - - -- Do the "anonymous" account - if(use_defaults) then - local result = {} - result['username'] = "" - result['domain'] = "" - results[#results + 1] = result - end - - return results -end - -function get_password_hashes(ip, username, domain, hash_type, overrides, challenge, is_extended) - if(overrides == nil) then - overrides = {} - end - - if(username == "") then - return string.char(0), '', nil - elseif(username == "guest") then - return get_password_response(ip, username, domain, "", nil, challenge, hash_type, is_extended) - else - return get_password_response(ip, username, domain, overrides['password'], overrides['password_hash'], challenge, hash_type, is_extended) - end -end - -function get_security_blob(security_blob, ip, username, domain, hash_type, overrides, use_default) +function get_security_blob(security_blob, ip, username, domain, password, password_hash, hash_type) local pos = 1 local new_blob local flags = 0x00008211 -- (NEGOTIATE_SIGN_ALWAYS | NEGOTIATE_NTLM | NEGOTIATE_SIGN | NEGOTIATE_UNICODE) @@ -619,7 +673,7 @@ function get_security_blob(security_blob, ip, username, domain, hash_type, overr pos, identifier, message_type, domain_length, domain_max, domain_offset, server_flags, challenge, reserved = bin.unpack("results constants. local function check_login(hostinfo, username, password, logintype) local result - local domain + local domain = "" local smbstate = hostinfo['smbstate'] if(logintype == nil) then logintype = get_type(hostinfo) end ---io.write(string.format("Trying %s:%s\n", username, password)) + -- Determine if we have a password hash or a password if(#password == 32 or #password == 64 or #password == 65) then ---io.write("Hash\n") -- It's a hash (note: we always use NTLM hashes) - status, err = smb.start_session(smbstate, username, domain, nil, password, "ntlm", false, true) + status, err = smb.start_session(smbstate, smb.get_overrides(username, domain, nil, password, "ntlm"), false) else - status, err = smb.start_session(smbstate, username, domain, password, nil, logintype, false, false) + status, err = smb.start_session(smbstate, smb.get_overrides(username, domain, password, nil, logintype), false) end if(status == true) then @@ -850,7 +849,14 @@ function found_account(hostinfo, username, password, result) return false, err end - smb.add_account(hostinfo['host'], username, password) + -- Check if we have an 'admin' account + -- Try getting information about "IPC$". This determines whether or not the user is administrator + -- since only admins can get share info. Note that on Vista and up, unless UAC is disabled, all + -- accounts are non-admin. + local is_admin = smb.is_admin(hostinfo['host'], username, '', password, nil, nil) + + -- Add the account + smb.add_account(hostinfo['host'], username, '', password, nil, nil, is_admin) -- If we haven't retrieved the real user list yet, do so if(hostinfo['have_user_list'] == false) then diff --git a/scripts/smb-enum-shares.nse b/scripts/smb-enum-shares.nse index 2f72f81fc..5a2fcfacf 100644 --- a/scripts/smb-enum-shares.nse +++ b/scripts/smb-enum-shares.nse @@ -28,36 +28,37 @@ for shares that require a user account. -- sudo nmap -sU -sS --script smb-enum-shares.nse -p U:137,T:139 -- --@output --- Standard: --- | smb-enum-shares: --- | Anonymous shares: IPC$ --- |_ Restricted shares: F$, ADMIN$, C$ --- --- Verbose: -- Host script results: --- | smb-enum-shares: --- | Anonymous shares: --- | IPC$ --- | |_ Type: STYPE_IPC_HIDDEN --- | |_ Comment: Remote IPC --- | |_ Users: 1, Max: --- | |_ Path: --- | test --- | |_ Type: STYPE_DISKTREE --- | |_ Comment: This is a test share, with a maximum of 7 users --- | |_ Users: 0, Max: 7 --- | |_ Path: C:\Documents and Settings\Ron\Desktop\test --- | Restricted shares: --- | ADMIN$ --- | |_ Type: STYPE_DISKTREE_HIDDEN --- | |_ Comment: Remote Admin --- | |_ Users: 0, Max: --- | |_ Path: C:\WINNT --- | C$ --- | |_ Type: STYPE_DISKTREE_HIDDEN --- | |_ Comment: Default share --- | |_ Users: 0, Max: --- |_ |_ Path: C:\ +-- | smb-enum-shares: +-- | ADMIN$ +-- | |_ Type: STYPE_DISKTREE_HIDDEN +-- | |_ Comment: Remote Admin +-- | |_ Users: 0, Max: +-- | |_ Path: C:\WINNT +-- | |_ Anonymous access: +-- | |_ Current user ('test') access: READ/WRITE +-- | C$ +-- | |_ Type: STYPE_DISKTREE_HIDDEN +-- | |_ Comment: Default share +-- | |_ Users: 0, Max: +-- | |_ Path: C:\ +-- | |_ Anonymous access: +-- | |_ Current user ('test') access: READ +-- | IPC$ +-- | |_ Type: STYPE_IPC_HIDDEN +-- | |_ Comment: Remote IPC +-- | |_ Users: 1, Max: +-- | |_ Path: +-- | |_ Anonymous access: READ +-- | |_ Current user ('test') access: READ +-- | test +-- | |_ Type: STYPE_DISKTREE +-- | |_ Comment: This is a test share, with a maximum of 7 users +-- | |_ Users: 0, Max: 7 +-- | |_ Path: C:\Documents and Settings\Ron\Desktop\test +-- | |_ Anonymous access: +-- |_ |_ Current user ('test') access: READ/WRITE + ----------------------------------------------------------------------- author = "Ron Bowes" @@ -73,194 +74,107 @@ hostrule = function(host) return smb.get_port(host) ~= nil end ----Attempts to connect to a list of shares as the anonymous user, returning which ones --- it has and doesn't have access to. --- ---@param host The host object. ---@param shares An array of shares to check. ---@return List of shares we're allowed to access. ---@return List of shares that exist but are denied to us. -function check_shares(host, shares) - local smbstate - local i - local allowed_shares = {} - local denied_shares = {} +local function go(host) + local status, shares, extra + local response = " \n" - -- Begin the SMB session - status, smbstate = smb.start(host) + -- Get the list of shares + status, shares, extra = smb.share_get_list(host) if(status == false) then - return false, smbstate + return false, string.format("Couldn't enumerate shares: %s", shares) end - -- Negotiate the protocol - status, err = smb.negotiate_protocol(smbstate) - if(status == false) then - smb.stop(smbstate) - return false, err + -- Find out who the current user is + local result, username, domain = smb.get_account(host) + if(result == false) then + username = "" + domain = "" end - -- Start up a null session - status, err = smb.start_session(smbstate, "", "", "", "", "LM") - if(status == false) then - smb.stop(smbstate) - return false, err + if(extra ~= nil) then + response = response .. extra .. "\n" end - -- Check for hosts that accept any share by generating a totally random name (we don't use a set - -- name because then hosts could potentially fool us. Perhaps I'm in a paranoid mood today) - local set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" - local share = "" - math.randomseed(os.time()) - for i = 1, 16, 1 do - local random = math.random(#set) - share = share .. string.sub(set, random, random) - end - - share = string.format("%s", share) - stdnse.print_debug(2, "EnumShares: Trying a random share to see if server responds properly: %s", share) - status, err = smb.tree_connect(smbstate, share) - if(status == false) then - if(err == 0xc0000022 or err == 'NT_STATUS_ACCESS_DENIED') then - return false, "Server doesn't return proper value for non-existent shares (returns ACCESS_DENIED)" - end - else - -- If we were actually able to connect to this share, then there's probably a serious issue - smb.tree_disconnect(smbstate) - return false, "Server doesn't return proper value for non-existent shares (accepts the connection)" - end - - -- Connect to the shares - stdnse.print_debug(2, "EnumShares: Testing %d shares", #shares) for i = 1, #shares, 1 do + local share = shares[i] - -- Change the share to the '\\ip\share' format - local share = string.format("%s", shares[i]) + -- Start generating a human-readable string + response = response .. share['name'] .. "\n" + + if(type(share['details']) ~= 'table') then + response = response .. string.format("|_ Couldn't get details for share: %s\n", share['details']) + else + local details = share['details'] - -- Try connecting to the tree - stdnse.print_debug(3, "EnumShares: Testing share %s", share) - status, err = smb.tree_connect(smbstate, share) - -- If it fails, checkwhy - if(status == false) then - -- If the result was ACCESS_DENIED, record it - if(err == 0xc0000022 or err == 'NT_STATUS_ACCESS_DENIED') then - stdnse.print_debug(3, "EnumShares: Access was denied") - denied_shares[#denied_shares + 1] = shares[i] + response = response .. string.format("|_ Type: %s\n", details['sharetype']) + response = response .. string.format("|_ Comment: %s\n", details['comment']) + response = response .. string.format("|_ Users: %s, Max: %s\n", details['current_users'], details['max_users']) + response = response .. string.format("|_ Path: %s\n", details['path']) + end + + + -- A share of 'NT_STATUS_OBJECT_NAME_NOT_FOUND' indicates this isn't a fileshare + if(share['user_can_write'] == "NT_STATUS_OBJECT_NAME_NOT_FOUND") then + -- Print details for a non-file share + if(share['anonymous_can_read']) then + response = response .. "|_ Anonymous access: READ \n" else - -- If we're here, an error that we weren't prepared for came up. --- smb.stop(smbstate) --- return false, string.format("Error while checking shares: %s", err) + response = response .. "|_ Anonymous access: \n" + end + + -- Don't bother printing this if we're already anonymous + if(username ~= '') then + if(share['user_can_read']) then + response = response .. "|_ Current user ('" .. username .. "') access: READ \n" + else + response = response .. "|_ Current user ('" .. username .. "') access: \n" + end end else - -- Add it to allowed shares - stdnse.print_debug(3, "EnumShares: Access was granted") - allowed_shares[#allowed_shares + 1] = shares[i] - smb.tree_disconnect(smbstate) + -- Print details for a file share + if(share['anonymous_can_read'] and share['anonymous_can_write']) then + response = response .. "|_ Anonymous access: READ/WRITE\n" + elseif(share['anonymous_can_read'] and not(share['anonymous_can_write'])) then + response = response .. "|_ Anonymous access: READ\n" + elseif(not(share['anonymous_can_read']) and share['anonymous_can_write']) then + response = response .. "|_ Anonymous access: WRITE\n" + else + response = response .. "|_ Anonymous access: \n" + end + + + + if(username ~= '') then + if(share['user_can_read'] and share['user_can_write']) then + response = response .. "|_ Current user ('" .. username .. "') access: READ/WRITE\n" + elseif(share['user_can_read'] and not(share['user_can_write'])) then + response = response .. "|_ Current user ('" .. username .. "') access: READ\n" + elseif(not(share['user_can_read']) and share['user_can_write']) then + response = response .. "|_ Current user ('" .. username .. "') access: WRITE\n" + else + response = response .. "|_ Current user ('" .. username .. "') access: \n" + end + end end end - -- Log off the user - smb.stop(smbstate) - - return true, allowed_shares, denied_shares + return true, response end + action = function(host) + local status, result - local enum_result - local result, shared - local response = " \n" - local shares = {} - local allowed, denied - - -- Try and do this the good way, make a MSRPC call to get the shares - enum_result, shares = msrpc.enum_shares(host) - - -- If that failed, try doing it with brute force. This almost certainly won't find everything, but it's the - -- best we can do. - if(enum_result == false) then - if(nmap.debugging() > 0) then - response = response .. string.format("ERROR: Couldn't enum all shares, checking for common ones (%s)\n", shares) - end - - -- Take some common share names I've seen - shares = {"IPC$", "ADMIN$", "TEST", "TEST$", "HOME", "HOME$", "PORN", "PR0N", "PUBLIC", "PRINT", "PRINT$", "GROUPS", "USERS", "MEDIA", "SOFTWARE", "XSERVE", "NETLOGON", "INFO", "PROGRAMS", "FILES", "WWW", "STMP", "TMP", "DATA", "BACKUP", "DOCS", "HD", "WEBSERVER", "WEB DOCUMENTS", "SHARED"} - - -- Try every alphabetic share, with and without a trailing '$' - for i = string.byte("A", 1), string.byte("Z", 1), 1 do - shares[#shares + 1] = string.char(i) - shares[#shares + 1] = string.char(i) .. "$" - end - end - - -- Break them into anonymous/authenticated shares - status, allowed, denied = check_shares(host, shares) + status, result = go(host) if(status == false) then - if(enum_result == false) then - -- At this point, we have nothing - if(nmap.debugging() > 0) then - return "ERROR: " .. allowed - else - return nil - end - else - -- If we're here, we have a valid list of shares, but couldn't check them - if(nmap.debugging() > 0) then - return "ERROR: " .. allowed .. "\nShares found: " .. stdnse.strjoin(", ", shares) - else - return stdnse.strjoin(", ", shares) - end + if(nmap.debugging() > 0) then + return "ERROR: " .. result end - end - - if(result == false or nmap.verbosity() == 0) then - return response .. string.format("Anonymous shares: %s\nRestricted shares: %s\n", stdnse.strjoin(", ", allowed), stdnse.strjoin(", ", denied)) else - response = response .. string.format("Anonymous shares:\n") - for i = 1, #allowed, 1 do - local status, info = msrpc.get_share_info(host, allowed[i]) - - response = response .. string.format(" %s\n", allowed[i]) - - if(status == false) then - stdnse.print_debug(2, "ERROR: Couldn't get information for share %s: %s", allowed[i], info) - else - info = info['info'] - - if(info['max_users'] == 0xFFFFFFFF) then - info['max_users'] = "" - end - - response = response .. string.format(" |_ Type: %s\n", msrpc.srvsvc_ShareType_tostr(info['sharetype'])) - response = response .. string.format(" |_ Comment: %s\n", info['comment']) - response = response .. string.format(" |_ Users: %s, Max: %s\n", info['current_users'], info['max_users']) - response = response .. string.format(" |_ Path: %s\n", info['path']) - end - end - - response = response .. string.format("Restricted shares:\n") - for i = 1, #denied, 1 do - local status, info = msrpc.get_share_info(host, denied[i]) - - response = response .. string.format(" %s\n", denied[i]) - - if(status == false) then - stdnse.print_debug(2, "ERROR: Couldn't get information for share %s: %s", denied[i], info) - else - info = info['info'] - if(info['max_users'] == 0xFFFFFFFF) then - info['max_users'] = "" - end - - response = response .. string.format(" |_ Type: %s\n", msrpc.srvsvc_ShareType_tostr(info['sharetype'])) - response = response .. string.format(" |_ Comment: %s\n", info['comment']) - response = response .. string.format(" |_ Users: %s, Max: %s\n", info['current_users'], info['max_users']) - response = response .. string.format(" |_ Path: %s\n", info['path']) - end - end - - return response + return result end end + diff --git a/scripts/smb-psexec.nse b/scripts/smb-psexec.nse new file mode 100644 index 000000000..decc234f5 --- /dev/null +++ b/scripts/smb-psexec.nse @@ -0,0 +1,1360 @@ +description = [[ +This script 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 nselib/data/psexec (if you aren't sure where that is, search your system +for 'default.lua'), then is passed to Nmap as a script argument (for example, +myconfig.lua would be passed as --script-args=config-myconfig. + +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 'net localgroup administrators', which returns a list of users in the 'administrators' +group (take a look at the 'examples.lua' configuration file for these examples): + + + mod = {} + mod.upload = false + mod.name = "Example 1: Membership of 'administrators'" + mod.program = "net.exe" + mod.args = "localgroup administrators" + table.insert(modules, mod) + + +mod.upload is false, 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). mod.name defines the name that the program will have in the +output. mod.program and mod.args obviously define which program +is going to be run. The output for this script is this: + + + | 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. + | | + | |_ + + +That works, but it's really ugly. In general, we can use mod.find, +mod.replace, mod.remove, and mod.noblank to clean up +the output. For this example, we're going to use mod.remove to remove a lot +of the useless lines, and mod.noblank to get rid of the blank lines that we +don't want: + + + 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) + + +We can see that the output is now much cleaner: + +| Example 2: Membership of 'administrators', cleaned +| | Administrator +| | ron +| |_test + + +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 mod.find and +mod.replace: + + + 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) + + +This module searches for lines that contain "IP Address", "Physical Address", or "Ethernet adapter". +In thiese 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: + + +| 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 + + +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 config table, most of which are listed below. The more interesting ones are: +* $lhost: The address of the scanner +* $rhost: The address being scanned +* $path: The path where the scripts are uploaded +* $share: The share where the script was uploaded + +User-supplied arguments are given on the commandline, and can be controlled by mod.req_args +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, mod.req_args would be set to {'host'}. + +Here is a module that pings the local ip address: + + 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) + + +And the output: + +| 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 + + +And this module pings an arbitrary address that the user is expected to give: + + 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) + + +And the output (note that we had to up the timeout so this would complete; we'll talk about override +values later): + +$ ./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. + + +For the final example, we'll use the 'upload' command to upload fgdump.exe, run it, +download its output file, and clean up its logfile. You'll have to put fgdump.exe +in the same folder as the script for this to work: + + 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) + +The -l argument for fgdump supplies the name of the logfile. That file is listed in the +mod.tempfiles field. What, exactly, does mod.tempfiles do? +It simply gives the service a list of files to delete while cleaning up. The cleanup +process will be discussed later. + +mod.url is displayed to the user if mod.program isn't found in +nselib/data/psexec/. And finally, mod.outfile 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 mod.outfile. + +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: + +* upload (boolean) true if it's a local file to upload, false if it's already on the host machine. If upload is true, program has to be in nselib/data/psexec. +* name (string) The name to display above the output. If this isn't given, program .. args are used. +* program (string) If upload is false, the name (fully qualified or relative) of the program on the remote system; if upload is true, the name of the local file that will be uploaded (stored in nselib/data/psexec). +* args (string) Arguments to pass to the process. +* env (string) Environmental variables to pass to the process, as name=value pairs, delimited, per Microosft's spec, by NULL characters (string.char(0)). +* maxtime (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 'maxtime' fields. +* extrafiles (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 (nselib/data/psexec), but the program doesn't necessarily need to be an uploaded one. +* tempfiles (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) +* find (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 {'IP Address'}. This allows Lua-style patterns, see: (don't forget to escape special characters with a '%'). 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. +* remove (string[]) Opposite of find; this removes lines containing the given string(s) instead of displaying them. Like find, this is client-side only and uses Lua-style patterns. If 'remove' and 'find' are in conflict, the 'remove' takes priority. +* noblank (boolean) Setting this to true removes all blank lines from the output. +* replace (table) A table of values to replace in the strings returned. Like find and replace, this is client-side only and uses Lua-style patterns. +* headless (boolean) If 'headless' 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: find, remove, noblank, replace, maxtime, outfile. +* enabled (boolean) Set to false, and optionally set disabled_message, if you don't want a module to run. Alternatively, you can comment out the process. +* disabled_message (string) Displayed if the module is disabled. +* url (string) A module where the user can download the uploadable file. Displayed if the uploadable file is missing. +* outfile (string) If set, the specified file will be returned instead of stdout. +* req_args (string[]) An array of arguments that the user must set in --script-args. + + +Any field in the configuration file can contain variables, as discussed. Here are some of the available built-in variables: +* $lhost: local ip address as a string. +* $lport: local port (meaningless; it'll change by the time the module is uploaded since multiple connections are made). +* $rhost: remote ip address as a string. +* $rport: remote port. +* $lmac: local mac address as a string in the xx:xx:xx:xx:xx:xx format (note: requires root). +* $path: the path where the file will be uploaded to. +* $service_name: the name of the service that will be running this program +* $service_file: the name of the executable file for the service +* $temp_output_file: The (ciphered) file where the programs' output will be written before being renamed to $output_file +* $output_file: The final name of the (ciphered) output file. When this file appears, the script downloads it and stops the service +* $timeout: The total amount of time the script is going to run before it gives up and stops the process +* $share: The share that everything was uploaded to +* (script-args): Any value passed as a script-arg will be replaced (for example, if Nmap is run with --script-args=var3=10, then '$var3' in any field will be replaced with '10'. See the req_args field above. script-args 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 smb-psexec.nse +for any reference to the config table; any value in the config +table can be overridden with the overrides table in the module. The most useful +value to override is probably timeout. + +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 nmap-dev@insecure.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 (PATH, SystemRoot, 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=,smbpass=[,config=] -p445 +-- sudo nmap -sU -sS --script smb-psexec.nse --script-args=smbuser=,smbpass=[,config=] -p U:137,T:139 +-- +-- @output +-- Host script results: +-- | smb-psexec: +-- | IP Address and MAC Address from 'ipconfig.exe' +-- | | Ethernet adapter Local Area Connection: +-- | | MAC Address: 00:0C:29:12:E6:DB +-- | |_ IP Address: 192.168.1.21 +-- | +-- | User list from 'net user' +-- | | Administrator Guest ron +-- | |_SUPPORT_388945a0 test +-- | +-- | Membership of 'administrators' from 'net localgroup administrators' +-- | | Administrator +-- | | ron +-- | |_test +-- | +-- | 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 +-- | +-- | Traceroute back to the scanner +-- | |_ 1 <1 ms <1 ms <1 ms 192.168.1.100 +-- | +-- | ARP Cache from arp.exe +-- | | Internet Address Physical Address Type +-- | |_ 192.168.1.100 00-21-9b-e5-78-ea dynamic +-- |_ +-- +--@args config The config file to use (eg, default). Config files require a .lua extension, and are located in nselib/data/psexec. +--@args nohide Don't set the uploaded files to hidden/system/etc. +--@args cleanup Set to '1' or 'true' to simply clean up any mess we made (leftover files, processes, etc. on the host os). +-- 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 +-- 'sharepath' to be set. +--@args sharepath The full path to the share (eg, "c:\windows"). 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: 15) +-- +--@args nocleanup If set to '1' or 'true', don't clean up at all; this leaves the files on the remote system and the wrapper +-- service instaleld. This is bad in practice, but significantly reduces the network traffic and makes analysis +-- easier. +--@args nocipher Set to '1' or 'true' 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 http://nmap.org/book/man-legal.html" +categories = {"intrusive"} + +require 'bit' +require 'msrpc' +require 'smb' +require 'stdnse' + + +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.print_debug("smb-psexec: 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.print_debug("smb-psexec: 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.print_debug("smb-psexec: 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.print_debug("smb-psexec: 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 + + -- If the user doesn't want to clean up, don't + if(nmap.registry.args.nocleanup == '1' or nmap.registry.args.nocleanup == "true") then + return + end + + stdnse.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: [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.print_debug(1, "smb-psexec: [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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: Attempting to find file: %s", filename) + + extension = extension or "" + + local filename_full = nmap.fetchfile(filename) + if(filename_full == nil) then + filename_full = nmap.fetchfile(filename .. "." .. extension) + + if(filename_full == nil) then + filename = "nselib/data/psexec/" .. filename + filename_full = nmap.fetchfile(filename) + + if(filename_full == nil) then + filename_full = nmap.fetchfile(filename .. "." .. extension) + end + end + end + + -- Die if we couldn't find the file + if(filename_full == nil) then + return nil + 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.print_debug(1, "smb-psexec: 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 + 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.print_debug(1, "smb-psexec: Found usable share %s (%s) (all writable shares: %s)", share, path, stdnse.strjoin(", ", 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. +--@return status true or false +--@return config The configuration table or an error message. +local function get_config(host) + local status + local filename = nmap.registry.args.config + local config = {} + local settings_file + 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 + stdnse.print_debug(1, "smb-psexec: Attempting to load config file: %s", filename) + settings_file = require(string.sub(filename, 1, #filename - 4)) + if(not(settings_file)) then + return false, "Couldn't load the configuration file" + end + + -- Generate a cipher key + if(nmap.registry.args.nocipher == "1" or nmap.registry.args.nocipher == "true") then + config.key = "" + elseif(nmap.registry.args.key) then + config.key = nmap.registry.args.key + else + math.randomseed( os.time() ) + config.key = "" + for i = 1, 127, 1 do + config.key = config.key .. string.char(math.random(0x20, 0x7F)) + end + 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, 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: " .. 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, service_name + end + + -- Make sure we got a proper modules array + if(type(settings_file.modules) ~= "table") then + return false, string.format("The chosen configuration file, %s.lua, doesn't have a proper 'modules' table. If possible, it should be modified to have a public array called 'modules' that contains a list of all modules that will be run.", filename) + end + + -- Loop through the modules for some pre-processing + stdnse.print_debug(1, "smb-psexec: Verifying uploadable executables exist") + for i, mod in ipairs(settings_file.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 uploadbility + 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. Please add --script-args=[arg]=[value] to your commandline to run this module", stdnse.strjoin("', '", missing_args))) + 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.print_debug(1, "smb-psexec: 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.print_debug(1, "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.print_debug(1, "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)) + end + else + -- We found it + stdnse.print_debug(1, "smb-psexec: 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.print_debug("smb-psexec: 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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: Timeout waiting for a response is %d seconds", config.timeout) + + -- Do config overrides + if(settings_file.overrides) then + config = do_overrides(config, settings_file.overrides) + end + + -- Replace variable values in the configuration (this has to go last) + stdnse.print_debug(1, "smb-psexec: 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(bit.bxor(c, string.byte(config.key, config.key_index + 1))) + + config.key_index = config.key_index + 1 + config.key_index = config.key_index % string.len(config.key) + + result = result .. c + end + + return 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 = bit.bor(0x00000004,0x00000002,0x00000800,0x00000100,0x00002000,0x00004000) + + -- Let the user override this behaviour + if(nmap.registry.args.nohide == '1' or nmap.registry.args.nohide == 'true') then + attr = 0 + end + + -- Create the overrides + return {file_create_attributes=attr} +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 overrides = get_overrides() + + -- Upload the service file + stdnse.print_debug(1, "smb-psexec: Uploading: nselib/data/psexec/nmap_service.exe => \\\\%s\\%s", config.share, config.service_file) + status, err = smb.file_upload(host, "nselib/data/psexec/nmap_service.exe", config.share, "\\" .. config.service_file, overrides) + if(status == false) then + cleanup(host, config) + return false, string.format("Couldn't upload the service file: %s\n", err) + end + stdnse.print_debug(1, "smb-psexec: Service file successfully uploaded!") + + -- Upload the modules and all their extras + stdnse.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: 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) + status, err = msrpc.service_create(host, config.service_name, config.path .. "\\" .. config.service_file) + if(status == false) then + stdnse.print_debug(1, "smb-psexec: Couldn't create the service: %s", err) + cleanup(host, config) + + if(string.find(err, "MARKED_FOR_DELETE")) then + return false, string.format("Service is stuck in 'being deleted' phase on remote machine; try setting script-args=randomseed=abc for now", err) + 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, "1") -- TODO: Turn off logging + 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 get_params function. +--@return status true or false +--@return err An error message if status is false. +local function start_service(host, config, params) + status, err = msrpc.service_start(host, config.service_name, params) + if(status == false) then + stdnse.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: 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.print_debug(1, "smb-psexec: Error in remote service: output file was never created!") + cleanup(host, config) + + return false, string.format("Error in remote service: output file was never created") + end + + stdnse.print_debug(1, "smb-psexec: 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 + + -- It looks like Lua doesn't like replacing the null character, so have a sidecase for it + if(v[1] == string.char(0)) then + local newline = "" + for i = 1, #line, 1 do + local char = string.sub(line, i, i) + if(string.byte(char) == 0) then + newline = newline .. v[2] + else + newline = newline .. char + end + end + line = newline + else + line = string.gsub(line, v[1], v[2]) + end + 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 = stdnse.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.print_debug(1, "Server's response wasn't formatted properly (mod %d); if you can reproduce, place report to nmap-dev@insecure.org", module_num) + stdnse.print_debug(1, "--\n" .. string.gsub("%%", "%%", data) .. "\n--") + return false, "Server's response wasn't formatted properly; if you can reproduce, place report to nmap-dev@insecure.org" + end + + -- Save this result + if(result ~= nil) then + table.insert(results, result) + end + result = {} + result['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['lines'], 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 + result['lines'] = mod.disabled_message + + table.insert(results, result) + end + + return true, results +end + +---Convert the array generated by parse_output into something a little friendlier. +local function results_to_string(results) + local response = " \n" + + for _, mod in ipairs(results) do + response = response .. string.format("%s\n", mod.name) + + for i = 1, #mod.lines, 1 do + if(i < #mod.lines) then + response = response .. string.format("| %s\n", mod.lines[i]) + else + response = response .. string.format("|_%s\n", mod.lines[i]) + response = response .. " \n" + end + end + end + + if(response == " \n") then + return "" + end + + return response +end + +function go(host) + local status, result, err + local key + + local i + + local params + + local config + local files + + -- Parse the configuration file + status, config = get_config(host) + if(not(status)) then + return 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(nmap.registry.args.cleanup == '1' or nmap.registry.args.cleanup == 'true') then + return 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 false, "Couldn't log in to check for remote files: " .. result + end + if(result > 0) then + return false, "One or more output files already exist on the host, and couldn't be removed. Try:\n" .. + " * Running the script with --script-args=cleanup=1 to force a cleanup (passing -d and looking for error messages might hlep),\n" .. + " * Running the script with --script-args=randomseed=ABCD (or something) to change the name of the uploaded files,\n" .. + " * Changing the share and path using, for example, --script-args=share=C$,sharepath=C:, or\n" .. + " * Deleting the affected file(s) off the server manually (\\\\" .. config.share .. "\\" .. stdnse.strjoin(", \\\\" .. config.share .. "\\", files) .. ")" + end + + -- Upload the modules + status, err = upload_everything(host, config) + if(not(status)) then + cleanup(host, config) + return false, err + end + + -- Create the service + status, err = create_service(host, config) + if(not(status)) then + cleanup(host, config) + return 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 false, params + end + + -- Start the service + status, params = start_service(host, config, params) + if(not(status)) then + cleanup(host, config) + return false, params + end + + -- Get the result + status, result = get_output_file(host, config, config.share) + if(not(status)) then + cleanup(host, config) + return 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 + status, results = parse_output(config, result) + if(status == false) then + return false, "Couldn't parse output: " .. results + end + + -- Parse the results into a pretty string + response = results_to_string(results) + + -- 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 + response = response .. "\n\nNo modules were enabled! Please fix any errors displayed above, or check your configuration file." + end + end + + -- Return the string + return true, response +end + +action = function(host) + + local status, result = go(host) + + if(status == false) then + if(nmap.debugging() > 0) then + return "ERROR: " .. result + else + return nil + end + end + + return result +end + + diff --git a/scripts/smb-pwdump.nse b/scripts/smb-pwdump.nse index 2aa4888c3..3f6c2ab73 100644 --- a/scripts/smb-pwdump.nse +++ b/scripts/smb-pwdump.nse @@ -89,8 +89,8 @@ require 'msrpc' require 'smb' require 'stdnse' -local SERVICE = "nmap-pwdump" -local PIPE = "nmap-pipe" +local SERVICE = "nmap-pwdump-" +local PIPE = "nmap-pipe-" local FILE1 = "nselib/data/lsremora.dll" local FILENAME1 = "lsremora.dll" @@ -105,31 +105,34 @@ end ---Stop/delete the service and delete the service file. This can be used alone to clean up the -- pwdump stuff, if this crashes. -function cleanup(host) +-- +--@param host The host object. +--@param share The share to clean up on. +--@param path The local path to the share. +--@param service_name The name to use for the service. +function cleanup(host, share, path, service_name) local status, err - stdnse.print_debug(1, "Entering cleanup() -- errors here can generally be ignored") + stdnse.print_debug(1, "Entering cleanup('%s', '%s', '%s') -- errors here can generally be ignored", share, path, service_name) -- Try stopping the service - status, err = msrpc.service_stop(host, SERVICE) + status, err = msrpc.service_stop(host, SERVICE .. service_name) if(status == false) then stdnse.print_debug(1, "Couldn't stop service: %s", err) end --- os.exit() - -- Try deleting the service - status, err = msrpc.service_delete(host, SERVICE) + status, err = msrpc.service_delete(host, SERVICE .. service_name) if(status == false) then stdnse.print_debug(1, "Couldn't delete service: %s", err) end -- Delete the files - status, err = smb.file_delete(host, "C$", "\\" .. FILENAME1) + status, err = smb.file_delete(host, share, "\\" .. FILENAME1) if(status == false) then stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME1, err) end - status, err = smb.file_delete(host, "C$", "\\" .. FILENAME2) + status, err = smb.file_delete(host, share, "\\" .. FILENAME2) if(status == false) then stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME2, err) end @@ -140,16 +143,16 @@ function cleanup(host) end -function upload_files(host) +function upload_files(host, share) local status, err - status, err = smb.file_upload(host, FILE1, "C$", "\\" .. FILENAME1) + status, err = smb.file_upload(host, FILE1, share, "\\" .. FILENAME1) if(status == false) then cleanup(host) return false, string.format("Couldn't upload %s: %s\n", FILE1, err) end - status, err = smb.file_upload(host, FILE2, "C$", "\\" .. FILENAME2) + status, err = smb.file_upload(host, FILE2, share, "\\" .. FILENAME2) if(status == false) then cleanup(host) return false, string.format("Couldn't upload %s: %s\n", FILE2, err) @@ -183,18 +186,19 @@ function read_and_decrypt(host, key, pipe) break end - stdnse.print_debug(1, "WaitForNamedPipe() failed: %s (this may be normal behaviour)", wait_result) j = j + 1 - -- TODO: Wait 50ms, if there's a time when we get an actual sleep()-style function. + -- Wait 50ms, if there's a time when we get an actual sleep()-style function. + stdnse.sleep(.05) until status == true - if(j == 100) then + if(j == 10) then smbstop(smbstate) return false, "WaitForNamedPipe() failed, service may not have been created properly." end -- Get a handle to the pipe - status, create_result = smb.create_file(smbstate, "\\" .. pipe) + local overrides = {} + status, create_result = smb.create_file(smbstate, "\\" .. pipe, overrides) if(status == false) then smb.stop(smbstate) return false, create_result @@ -267,8 +271,28 @@ function go(host) local key = "" local i + local result + local service_name + local share, path + + result, service_name = smb.get_uniqueish_name(host) + if(result == false) then + return false, string.format("Error generating service name: %s", service_name) + end + stdnse.print_debug("pwdump: Generated static service name: %s", service_name) + + -- Try and find a share to use. + result, share, path = smb.share_find_writable(host) + if(result == false) then + return false, string.format("Couldn't find a writable share: %s", share) + 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.print_debug(1, "pwdump: Found usable share %s (%s)", share, path) + -- Start by cleaning up, just in case. - cleanup(host) + cleanup(host, share, path, service_name) -- It seems that, in my tests, if a key contains either a null byte or a negative byte (>= 0x80), errors -- happen. So, at the cost of generating a weaker key (keeping in mind that it's already sent over the @@ -280,42 +304,42 @@ function go(host) end -- Upload the files - status, err = upload_files(host) + status, err = upload_files(host, share) if(status == false) then stdnse.print_debug(1, "Couldn't upload the files: %s", err) - cleanup(host) + cleanup(host, share, path, service_name) return false, string.format("Couldn't upload the files: %s", err) end -- Create the service - status, err = msrpc.service_create(host, SERVICE, "c:\\servpw.exe") + status, err = msrpc.service_create(host, SERVICE .. service_name, path .. "\\servpw.exe") if(status == false) then stdnse.print_debug(1, "Couldn't create the service: %s", err) - cleanup(host) + cleanup(host, share, path, service_name) return false, string.format("Couldn't create the service on the remote machine: %s", err) end -- Start the service - status, err = msrpc.service_start(host, SERVICE, {PIPE, key, tostring(string.char(16)), tostring(string.char(0)), "servpw.exe"}) + status, err = msrpc.service_start(host, SERVICE .. service_name, {PIPE .. service_name, key, tostring(string.char(16)), tostring(string.char(0)), "servpw.exe"}) if(status == false) then stdnse.print_debug(1, "Couldn't start the service: %s", err) - cleanup(host) + cleanup(host, share, path, service_name) return false, string.format("Couldn't start the service on the remote machine: %s", err) end -- Read the data - status, results = read_and_decrypt(host, key, PIPE) + status, results = read_and_decrypt(host, key, PIPE .. service_name) if(status == false) then stdnse.print_debug(1, "Error reading data from remote service") - cleanup(host) + cleanup(host, share, path, service_name) return false, string.format("Failed to read password data from the remote service: %s", err) end -- Clean up what we did - cleanup(host) + cleanup(host, share, path, service_name) return true, results end diff --git a/scripts/smb-security-mode.nse b/scripts/smb-security-mode.nse index 792d43376..87ffddbbe 100644 --- a/scripts/smb-security-mode.nse +++ b/scripts/smb-security-mode.nse @@ -27,7 +27,12 @@ author = "Ron Bowes" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"discovery", "safe"} +-- Set the runlevel to above 1 to ensure this runs after the bulk of the scripts. That lets us more effectively +-- find out which account we've been using. +runlevel = 1.01 + require 'smb' +require 'stdnse' -- Check whether or not this script should be run. hostrule = function(host) @@ -39,6 +44,7 @@ action = function(host) local state local status, err + local overrides = {} status, state = smb.start(host) if(status == false) then @@ -49,7 +55,7 @@ action = function(host) end end - status, err = smb.negotiate_protocol(state) + status, err = smb.negotiate_protocol(state, overrides) if(status == false) then smb.stop(state) @@ -63,6 +69,11 @@ action = function(host) local security_mode = state['security_mode'] local response = "" + + local result, username, domain = smb.get_account(host) + if(result ~= false) then + response = string.format("Account that was used for smb scripts: %s\%s\n", domain, stdnse.string_or_blank(username, '')) + end -- User-level authentication or share-level authentication if(bit.band(security_mode, 1) == 1) then diff --git a/scripts/smbv2-enabled.nse b/scripts/smbv2-enabled.nse new file mode 100644 index 000000000..c71c6b930 --- /dev/null +++ b/scripts/smbv2-enabled.nse @@ -0,0 +1,66 @@ +description = [[ +Check whether or not a server is running the SMBv2 protocol. +]] +--- +--@usage +-- nmap --script smbv2-enabled.nse -p445 +-- sudo nmap -sU -sS --script smbv2-enabled.nse -p U:137,T:139 +-- +--@output +-- Host script results: +-- |_ smb-v2-enabled: Server supports SMBv2 protocol +-- +-- Host script results: +-- |_ smb-v2-enabled: Server doesn't support SMBv2 protocol +----------------------------------------------------------------------- + +author = "Ron Bowes" +copyright = "Ron Bowes" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default", "safe"} + +require 'msrpc' +require 'smb' +require 'stdnse' + +hostrule = function(host) + return smb.get_port(host) ~= nil +end + +local function go(host) + local status, smbstate, result + local dialects = { "NT LM 0.12", "SMB 2.002", "SMB 2.???" } + local overrides = {dialects=dialects} + + status, smbstate = smb.start(host) + if(not(status)) then + return false, "Couldn't start SMB session: " .. smb + end + + status, result = smb.negotiate_protocol(smbstate, overrides) + if(not(status)) then + if(string.find(result, "SMBv2")) then + return true, "Server supports SMBv2 protocol" + end + return false, "Couldn't negotiate protocol: " .. result + end + + return true, "Server doesn't support SMBv2 protocol" +end + +action = function(host) + local status, result = go(host) + + if(not(status)) then + if(nmap.debugging() > 0) then + return "ERROR: " .. result + else + return nil + end + end + + return result +end + + +