From 773000b65abe802a76c570210c31108028cb2505 Mon Sep 17 00:00:00 2001 From: ron Date: Wed, 24 Dec 2008 00:53:01 +0000 Subject: [PATCH] Merging changes from my experimental branch; the new versions of this scripts, which have significant changes to their core functionality, managed to hold their own against Brandon's network. More testing would be very helpful, though, especially with credentials (most of Brandon's scans were anonymous). --- CHANGELOG | 4 + nse_main.cc | 7 +- nselib/data/passwords.lst | 1 + nselib/msrpc.lua | 514 +++++++++++++++++++++++++--- nselib/msrpcperformance.lua | 601 +++++++++++++++++++++++++++++++++ nselib/msrpctypes.lua | 183 +++++++--- nselib/netbios.lua | 3 + nselib/nmapdebug.lua | 62 ---- nselib/nsedebug.lua | 119 +++++++ nselib/smb.lua | 350 +++++++++++++++---- scripts/script.db | 2 + scripts/smb-check-vulns.nse | 4 + scripts/smb-enum-domains.nse | 1 + scripts/smb-enum-processes.nse | 282 ++++++++++++++++ scripts/smb-enum-sessions.nse | 1 + scripts/smb-enum-shares.nse | 5 +- scripts/smb-enum-users.nse | 306 +---------------- 17 files changed, 1930 insertions(+), 515 deletions(-) create mode 100644 nselib/msrpcperformance.lua delete mode 100644 nselib/nmapdebug.lua create mode 100644 nselib/nsedebug.lua create mode 100644 scripts/smb-enum-processes.nse diff --git a/CHANGELOG b/CHANGELOG index 721d29b74..906c6d8a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o Added smb-enum-processes.nse, a script that allows a user with administrator + credentials to view a tree of the processes running on the remote system + (uses HKEY_PERFORMANCE_DATA hive). [Ron Bowes] + o A problem that caused OS detection to fail for most hosts in a certain was fixed. It happened when sending raw Ethernet frames (by default on Windows or on other platforms with --send-eth) to diff --git a/nse_main.cc b/nse_main.cc index 0e9052bcb..8e4b39864 100644 --- a/nse_main.cc +++ b/nse_main.cc @@ -436,8 +436,11 @@ int process_mainloop(lua_State *L) { while (!running_scripts.empty()) { current = *(running_scripts.begin()); - if (current.rr.host->timedOut(&now)) - state = LUA_ERRRUN; + if (current.rr.host->timedOut(&now)) { + printf("thread (%p) timed out\n", (void *) current.thread); + SCRIPT_ENGINE_TRY(process_finalize(L, current.registry_idx)); + continue; + } else state = lua_resume(current.thread, current.resume_arguments); diff --git a/nselib/data/passwords.lst b/nselib/data/passwords.lst index 281b4fa34..9ebf7a66a 100644 --- a/nselib/data/passwords.lst +++ b/nselib/data/passwords.lst @@ -198,3 +198,4 @@ aaliyah1 zxcvbnm1 young1 +test diff --git a/nselib/msrpc.lua b/nselib/msrpc.lua index ce9988834..a55b4f921 100644 --- a/nselib/msrpc.lua +++ b/nselib/msrpc.lua @@ -81,6 +81,26 @@ TRANSFER_SYNTAX = string.char(0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x -- The 'referent_id' value is ignored, as far as I can tell, so this value is passed for it. No, it isn't random. :) REFERENT_ID = 0x50414d4e +-- The maximum length of a packet fragment +MAX_FRAGMENT = 0x800 + +---The number of SAMR records to pull at once. This was originally 1, but since I've written +-- proper fragmentation code, I've successfully done it with 110 users, although I'd be surprised +-- if you couldn't go a lot higher. I had some issues that I suspect was UNIX truncating packets, +-- so I scaled it back. +local SAMR_GROUPSIZE = 20 + +---The number of LSA RIDs to check at once. I've successfully tested with up to about 110. Note that +-- due to very long message sizes, Wireshark might truncate packets if you have more than 30 together, +-- so for debugging, setting this to 30 might be a plan. Like SAMR, I scaled this back due to UNIX +-- truncation. +local LSA_GROUPSIZE = 20 + +---The number of consecutive empty groups to stop after. Basically, this means that after +-- LSA_MINEMPTY groups of LSA_GROUPSIZE users come back empty, we give +-- up. Raising this could find more users, but at the expense of more packets. +local LSA_MINEMPTY = 10 + --- This is a wrapper around the SMB class, designed to get SMB going quickly for MSRPC calls. This will -- connect to the SMB server, negotiate the protocol, open a session, connect to the IPC$ share, and -- open the named pipe given by 'path'. When this successfully returns, the 'smbstate' table can be immediately @@ -180,8 +200,8 @@ function bind(smbstate, interface_uuid, interface_version, transfer_syntax) 0x0048, -- Frag length 0x0000, -- Auth length 0x41414141, -- Call ID (I use 'AAAA' because it's easy to recognize) - 0x10b8, -- Max transmit frag - 0x10b8, -- Max receive frag + MAX_FRAGMENT, -- Max transmit frag + MAX_FRAGMENT, -- Max receive frag 0x00000000, -- Assoc group 0x01, -- Number of items 0x00, -- Padding/alignment @@ -200,7 +220,12 @@ function bind(smbstate, interface_uuid, interface_version, transfer_syntax) 2 -- Syntax version ) - status, result = smb.send_transaction(smbstate, 0x0026, "", data) + status, result = smb.write_file(smbstate, data, 0) + if(status ~= true) then + return false, result + end + + status, result = smb.read_file(smbstate, 0, MAX_FRAGMENT) if(status ~= true) then return false, result end @@ -213,6 +238,9 @@ function bind(smbstate, interface_uuid, interface_version, transfer_syntax) -- Extract the first part from the resposne pos, result['version_major'], result['version_minor'], result['packet_type'], result['packet_flags'], result['data_representation'], result['frag_length'], result['auth_length'], result['call_id'] = bin.unpack("IIIImsrpctypes function that converts a ShareType to an english string. @@ -1054,16 +1130,21 @@ end --@param smbstate The SMB state table --@param domain_handle The domain handle, returned by samr_opendomain --@param index The index of the user to check; the first user is 0, next is 1, etc. +--@param count [optional] The number of users to return; you may want to be careful about going too high. Default: 1. --@return (status, result) If status is false, result is an error message. Otherwise, result is a table of values, the most -- useful ones being 'names', a list of all the usernames, and 'details', a further list of tables with the elements -- 'name', 'fullname', and 'description' (note that any of them can be nil if the server didn't return a value). Finally, -- 'flags' is the numeric flags for the user, while 'flags_list' is an array of strings, representing the flags. -function samr_querydisplayinfo(smbstate, domain_handle, index) +function samr_querydisplayinfo(smbstate, domain_handle, index, count) local i, j local status, result local arguments local pos, align + if(count == nil) then + count = 1 + end + -- This loop is because, in my testing, if I asked for all the results at once, it would blow up (ERR_BUFFER_OVERFLOW). So, instead, -- I put a little loop here and grab the names individually. stdnse.print_debug(2, "MSRPC: Calling QueryDisplayInfo(%d) [%s]", index, smbstate['ip']) @@ -1078,10 +1159,10 @@ function samr_querydisplayinfo(smbstate, domain_handle, index) arguments = arguments .. msrpctypes.marshall_int32(index) -- [in] uint32 max_entries, - arguments = arguments .. msrpctypes.marshall_int32(1) + arguments = arguments .. msrpctypes.marshall_int32(count) -- [in] uint32 buf_size, - arguments = arguments .. msrpctypes.marshall_int32(0) + arguments = arguments .. msrpctypes.marshall_int32(0x7FFFFFFF) -- [out] uint32 total_size, -- [out] uint32 returned_size, @@ -1110,7 +1191,6 @@ function samr_querydisplayinfo(smbstate, domain_handle, index) -- [out] uint32 returned_size, pos, result['returned_size'] = msrpctypes.unmarshall_int32(arguments, pos) - -- [out,switch_is(level)] samr_DispInfo info pos, result['info'] = msrpctypes.unmarshall_samr_DispInfo(arguments, pos) if(pos == nil) then @@ -1966,10 +2046,10 @@ function winreg_queryvalue(smbstate, handle, value) arguments = arguments .. msrpctypes.marshall_winreg_Type_ptr("REG_NONE") -- [in,out,size_is(*size),length_is(*length)] uint8 *data, - arguments = arguments .. msrpctypes.marshall_int8_array_ptr("", 520) + arguments = arguments .. msrpctypes.marshall_int8_array_ptr("", 1000000) -- [in,out] uint32 *size, - arguments = arguments .. msrpctypes.marshall_int32_ptr(520) + arguments = arguments .. msrpctypes.marshall_int32_ptr(1000000) -- [in,out] uint32 *length arguments = arguments .. msrpctypes.marshall_int32_ptr(0) @@ -1991,9 +2071,7 @@ function winreg_queryvalue(smbstate, handle, value) -- [in,ref] policy_handle *handle, -- [in] winreg_String value_name, -- [in,out] winreg_Type *type, - pos, - pos = pos + 4 - pos, result['type'] = msrpctypes.unmarshall_winreg_Type(arguments, pos) + pos, result['type'] = msrpctypes.unmarshall_winreg_Type_ptr(arguments, pos) -- [in,out,size_is(*size),length_is(*length)] uint8 *data, pos, result['data'] = msrpctypes.unmarshall_int8_array_ptr(arguments, pos) @@ -2004,6 +2082,8 @@ function winreg_queryvalue(smbstate, handle, value) _, result['value'] = bin.unpack("PERF_DATA_BLOCK structure, which is +-- stored in the registry under HKEY_PERFORMANCE_DATA. By querying this structure, you can +-- get a whole lot of information about what's going on. +-- +-- To use this from a script, see get_performance_data, it is the only +-- 'public' function in this module. +-- +-- My primary sources of information were: +-- * This 1996 journal by Matt Pietrek: +-- * The followup article: +-- * The WinPerf.h header file +-- +-- And my primary inspiration was PsTools, specifically, pstasklist.exe. +-- +--@author Ron Bowes +--@copyright See nmap's COPYING for licence +----------------------------------------------------------------------- +module(... or "msrpcperformance", package.seeall) + +require 'msrpctypes' + +---Parses the title database, which is a series of null-terminated string pairs. +-- +--@param data The data being processed. +--@param pos The position within data. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_title_database(data, pos) + local result = {} + + repeat + local number, name + pos, number, name = bin.unpack("= #data + + return true, pos, result +end + +---Parses a PERF_DATA_BLOCK, which has the following definition (from "WinPerf.h" on Visual Studio 8): +-- +-- +-- typedef struct _PERF_DATA_BLOCK { +-- WCHAR Signature[4]; // Signature: Unicode "PERF" +-- DWORD LittleEndian; // 0 = Big Endian, 1 = Little Endian +-- DWORD Version; // Version of these data structures +-- // starting at 1 +-- DWORD Revision; // Revision of these data structures +-- // starting at 0 for each Version +-- DWORD TotalByteLength; // Total length of data block +-- DWORD HeaderLength; // Length of this structure +-- DWORD NumObjectTypes; // Number of types of objects +-- // being reported +-- LONG DefaultObject; // Object Title Index of default +-- // object to display when data from +-- // this system is retrieved (-1 = +-- // none, but this is not expected to +-- // be used) +-- SYSTEMTIME SystemTime; // Time at the system under +-- // measurement +-- LARGE_INTEGER PerfTime; // Performance counter value +-- // at the system under measurement +-- LARGE_INTEGER PerfFreq; // Performance counter frequency +-- // at the system under measurement +-- LARGE_INTEGER PerfTime100nSec; // Performance counter time in 100 nsec +-- // units at the system under measurement +-- DWORD SystemNameLength; // Length of the system name +-- DWORD SystemNameOffset; // Offset, from beginning of this +-- // structure, to name of system +-- // being measured +-- } PERF_DATA_BLOCK, *PPERF_DATA_BLOCK; +-- +-- +--@param data The data being processed. +--@param pos The position within data. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_data_block(data, pos) + local result = {} + + pos, result['Signature'] = msrpctypes.unicode_to_string(data, pos, 4, false) + if(result['Signature'] ~= "PERF") then + return false, "MSRPC: PERF_DATA_BLOCK signature is missing or incorrect" + end + + pos, result['LittleEndian'] = msrpctypes.unmarshall_int32(data, pos) + if(result['LittleEndian'] ~= 1) then + return false, "MSRPC: PERF_DATA_BLOCK returned a non-understood endianness" + end + + -- Parse the header + pos, result['Version'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['Revision'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['TotalByteLength'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['HeaderLength'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['NumObjectTypes'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['DefaultObject'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['SystemTime'] = msrpctypes.unmarshall_SYSTEMTIME(data, pos) + pos, result['PerfTime'] = msrpctypes.unmarshall_int64(data, pos) + pos, result['PerfFreq'] = msrpctypes.unmarshall_int64(data, pos) + pos, result['PerfTime100nSec'] = msrpctypes.unmarshall_int64(data, pos) + pos = pos + 4 -- This value doesn't seem to line up, so add 4 + + pos, result['SystemNameLength'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['SystemNameOffset'] = msrpctypes.unmarshall_int32(data, pos) + + -- Ensure that the system name is directly after the header. This technically shouldn't matter, but Microsoft's documentation + -- (in WinPref.h) says that the actual object comes "after the PERF_DATA_BLOCK", so it doesn't make sense that the SystemName + -- could be anywhere else. + if(pos ~= result['SystemNameOffset'] + 1) then + return false, "MSRPC: PERF_DATA_BLOCK has SystemName in the wrong location" + end + + -- Read the system name from the next location (which happens to be identical to SystemNameOffset, on a proper system) + pos, result['SystemName'] = msrpctypes.unicode_to_string(data, pos, result['SystemNameLength'] / 2, true) + + pos = pos + 4 -- Again, we end up not lined up so here we fix it + + return true, pos, result +end + + +---Parse a PERF_OBJECT_TYPE structure. From Microsoft's documentation: +-- +-- +-- // +-- // The _PERF_DATA_BLOCK structure is followed by NumObjectTypes of +-- // data sections, one for each type of object measured. Each object +-- // type section begins with a _PERF_OBJECT_TYPE structure. +-- // +-- typedef struct _PERF_OBJECT_TYPE { +-- DWORD TotalByteLength; // Length of this object definition +-- // including this structure, the +-- // counter definitions, and the +-- // instance definitions and the +-- // counter blocks for each instance: +-- // This is the offset from this +-- // structure to the next object, if +-- // any +-- DWORD DefinitionLength; // Length of object definition, +-- // which includes this structure +-- // and the counter definition +-- // structures for this object: this +-- // is the offset of the first +-- // instance or of the counters +-- // for this object if there is +-- // no instance +-- DWORD HeaderLength; // Length of this structure: this +-- // is the offset to the first +-- // counter definition for this +-- // object +-- DWORD ObjectNameTitleIndex; +-- // Index to name in Title Database +-- #ifdef _WIN64 +-- DWORD ObjectNameTitle; // Should use this as an offset +-- #else +-- LPWSTR ObjectNameTitle; // Initially NULL, for use by +-- // analysis program to point to +-- // retrieved title string +-- #endif +-- DWORD ObjectHelpTitleIndex; +-- // Index to Help in Title Database +-- #ifdef _WIN64 +-- DWORD ObjectHelpTitle; // Should use this as an offset +-- #else +-- LPWSTR ObjectHelpTitle; // Initially NULL, for use by +-- // analysis program to point to +-- // retrieved title string +-- #endif +-- DWORD DetailLevel; // Object level of detail (for +-- // controlling display complexity); +-- // will be min of detail levels +-- // for all this object's counters +-- DWORD NumCounters; // Number of counters in each +-- // counter block (one counter +-- // block per instance) +-- LONG DefaultCounter; // Default counter to display when +-- // this object is selected, index +-- // starting at 0 (-1 = none, but +-- // this is not expected to be used) +-- LONG NumInstances; // Number of object instances +-- // for which counters are being +-- // returned from the system under +-- // measurement. If the object defined +-- // will never have any instance data +-- // structures (PERF_INSTANCE_DEFINITION) +-- // then this value should be -1, if the +-- // object can have 0 or more instances, +-- // but has none present, then this +-- // should be 0, otherwise this field +-- // contains the number of instances of +-- // this counter. +-- DWORD CodePage; // 0 if instance strings are in +-- // UNICODE, else the Code Page of +-- // the instance names +-- LARGE_INTEGER PerfTime; // Sample Time in "Object" units +-- // +-- LARGE_INTEGER PerfFreq; // Frequency of "Object" units in +-- // counts per second. +-- } PERF_OBJECT_TYPE, *PPERF_OBJECT_TYPE; +-- +-- +--@param data The data being processed. +--@param pos The position within data. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_object_type(data, pos) + local result = {} + + pos, result['TotalByteLength'] = msrpctypes.unmarshall_int32(data, pos) -- Offset to the next object + pos, result['DefinitionLength'] = msrpctypes.unmarshall_int32(data, pos) -- Offset to the first instance (or counter, if no instances) + pos, result['HeaderLength'] = msrpctypes.unmarshall_int32(data, pos) -- Offset to the first counter definition + pos, result['ObjectNameTitleIndex'] = msrpctypes.unmarshall_int32(data, pos) -- Index in the Title Database + pos, result['ObjectNameTitle'] = msrpctypes.unmarshall_int32(data, pos) -- TODO: will this work with 64-bit? + pos, result['ObjectHelpTitleIndex'] = msrpctypes.unmarshall_int32(data, pos) -- Index in the Help Database + pos, result['ObjectHelpTitle'] = msrpctypes.unmarshall_int32(data, pos) -- TODO: will this workw ith 64-bit? + pos, result['DetailLevel'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['NumCounters'] = msrpctypes.unmarshall_int32(data, pos) -- The number of counters in each counter block + pos, result['DefaultCounter'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['NumInstances'] = msrpctypes.unmarshall_int32(data, pos) -- Numer of object instances for which counters are being returned + pos, result['CodePage'] = msrpctypes.unmarshall_int32(data, pos) -- 0 if strings are in UNICODE, otherwise the Code Page +-- if(result['CodePage'] ~= 0) then +-- return false, string.format("Unknown Code Page for data: %d\n", result['CodePage']) +-- end + pos, result['PerfTime'] = msrpctypes.unmarshall_int64(data, pos) -- Sample time in "Object" units + pos, result['PerfFreq'] = msrpctypes.unmarshall_int64(data, pos) -- Frequency of "Object" units in counts/second + + return true, pos, result +end + + +---Parse a PERF_COUNTER_DEFINITION structure. From Microsoft's documentation: +-- +-- +-- // There is one of the following for each of the +-- // PERF_OBJECT_TYPE.NumCounters. The Unicode names in this structure MUST +-- // come from a message file. +-- typedef struct _PERF_COUNTER_DEFINITION { +-- DWORD ByteLength; // Length in bytes of this structure +-- DWORD CounterNameTitleIndex; +-- // Index of Counter name into +-- // Title Database +-- #ifdef _WIN64 +-- DWORD CounterNameTitle; +-- #else +-- LPWSTR CounterNameTitle; // Initially NULL, for use by +-- // analysis program to point to +-- // retrieved title string +-- #endif +-- DWORD CounterHelpTitleIndex; +-- // Index of Counter Help into +-- // Title Database +-- #ifdef _WIN64 +-- DWORD CounterHelpTitle; +-- #else +-- LPWSTR CounterHelpTitle; // Initially NULL, for use by +-- // analysis program to point to +-- // retrieved title string +-- #endif +-- LONG DefaultScale; // Power of 10 by which to scale +-- // chart line if vertical axis is 100 +-- // 0 ==> 1, 1 ==> 10, -1 ==>1/10, etc. +-- DWORD DetailLevel; // Counter level of detail (for +-- // controlling display complexity) +-- DWORD CounterType; // Type of counter +-- DWORD CounterSize; // Size of counter in bytes +-- DWORD CounterOffset; // Offset from the start of the +-- // PERF_COUNTER_BLOCK to the first +-- // byte of this counter +-- } PERF_COUNTER_DEFINITION, *PPERF_COUNTER_DEFINITION; +-- +-- +--@param data The data being processed. +--@param pos The position within data. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_counter_definition(data, pos) + local result = {} + local initial_pos = pos + + pos, result['ByteLength'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterNameTitleIndex'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterNameTitle'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterHelpTitleIndex'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterHelpTitle'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['DefaultScale'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['DetailLevel'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterType'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterSize'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['CounterOffset'] = msrpctypes.unmarshall_int32(data, pos) + + pos = initial_pos + result['ByteLength'] + + return true, pos, result +end + +---Parse the actual counter value. This is a fairly simple function, it takes a counter +-- definition and pulls out data based on it. +-- +-- Note: I don't think this is doing the 8-byte values right, I suspect that they're supposed +-- to be doubles. +-- +--@param data The data being processed. +--@param pos The position within data. +--@param counter_definition The matching counter_definition. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_counter(data, pos, counter_definition) + local result + + if(counter_definition['CounterSize'] == 4) then + pos, result = msrpctypes.unmarshall_int32(data, pos) + elseif(counter_definition['CounterSize'] == 8) then + pos, result = msrpctypes.unmarshall_int64(data, pos) +-- pos, result = bin.unpack(" +-- // If (PERF_DATA_BLOCK.NumInstances >= 0) then there will be +-- // PERF_DATA_BLOCK.NumInstances of a (PERF_INSTANCE_DEFINITION +-- // followed by a PERF_COUNTER_BLOCK followed by the counter data fields) +-- // for each instance. +-- // +-- // If (PERF_DATA_BLOCK.NumInstances < 0) then the counter definition +-- // strucutre above will be followed by only a PERF_COUNTER_BLOCK and the +-- // counter data for that COUNTER. +-- typedef struct _PERF_INSTANCE_DEFINITION { +-- DWORD ByteLength; // Length in bytes of this structure, +-- // including the subsequent name +-- DWORD ParentObjectTitleIndex; +-- // Title Index to name of "parent" +-- // object (e.g., if thread, then +-- // process is parent object type); +-- // if logical drive, the physical +-- // drive is parent object type +-- DWORD ParentObjectInstance; +-- // Index to instance of parent object +-- // type which is the parent of this +-- // instance. +-- LONG UniqueID; // A unique ID used instead of +-- // matching the name to identify +-- // this instance, -1 = none +-- DWORD NameOffset; // Offset from beginning of +-- // this struct to the Unicode name +-- // of this instance +-- DWORD NameLength; // Length in bytes of name; 0 = none +-- // this length includes the characters +-- // in the string plus the size of the +-- // terminating NULL char. It does not +-- // include any additional pad bytes to +-- // correct structure alignment +-- } PERF_INSTANCE_DEFINITION, *PPERF_INSTANCE_DEFINITION; +-- +-- +--@param data The data being processed. +--@param pos The position within data. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_instance_definition(data, pos) + local result = {} + + -- Remember where we started. I noticed that where the counter part starts can move around, so we have to + -- determine it by adding ByteLength to the initial position + local initial_pos = pos + + pos, result['ByteLength'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['ParentObjectTitleIndex'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['ParentObjectInstance'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['UniqueID'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['NameOffset'] = msrpctypes.unmarshall_int32(data, pos) + pos, result['NameLength'] = msrpctypes.unmarshall_int32(data, pos) + + pos, result['InstanceName'] = msrpctypes.unicode_to_string(data, pos, result['NameLength'] / 2, true) + + pos = initial_pos + result['ByteLength'] + + return true, pos, result +end + +---Parse a PERF_COUNTER_BLOCK structure. From Microsoft's documentation: +-- +-- +-- typedef struct _PERF_COUNTER_BLOCK { +-- DWORD ByteLength; // Length in bytes of this structure, +-- // including the following counters +-- } PERF_COUNTER_BLOCK, *PPERF_COUNTER_BLOCK; +-- +-- +-- +--@param data The data being processed. +--@param pos The position within data. +--@return (status, pos, result) The status (true if successful), the new position in data (or an error +-- message), and a table representing the datatype, if any. +local function parse_perf_counter_block(data, pos) + local result = {} + + pos, result['ByteLength'] = msrpctypes.unmarshall_int32(data, pos) + + return true, pos, result +end + +---Retrieve the parsed performance data from the given host for the requested object values. To get a list of possible +-- object values, leave 'objects' blank and look at result['title_database'] -- it'll contain a list of +-- indexes that can be looked up. These indexes are passed as a string or as a series of space-separated strings (eg, +-- "230" for "Process" and "238" for "Process" and "Processor"). +-- +--@param host The host object +--@param objects [optional] The space-separated list of object numbers to retrieve. Default: only retrieve the database. +function get_performance_data(host, objects) + + local status, smbstate + local bind_result, openhkpd_result, queryvalue_result, data_block + local pos + local result = {} + local i, j, k + local pos + + -- Create the SMB session + status, smbstate = msrpc.start_smb(host, msrpc.WINREG_PATH) + if(status == false) then + return false, smbstate + end + + -- Bind to WINREG service + status, bind_result = msrpc.bind(smbstate, msrpc.WINREG_UUID, msrpc.WINREG_VERSION, nil) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, bind_result + end + + -- Open HKEY_PERFORMANCE_DATA + status, openhkpd_result = msrpc.winreg_openhkpd(smbstate) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, openhkpd_result + end + + status, queryvalue_result = msrpc.winreg_queryvalue(smbstate, openhkpd_result['handle'], "Counter 009") + if(status == false) then + msrpc.stop_smb(smbstate) + return false, queryvalue_result + end + + -- Parse the title database + pos = 1 + status, pos, result['title_database'] = parse_perf_title_database(queryvalue_result['value'], pos) + result['title_database'][0] = "" + + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + + if(objects ~= nil and #objects > 0) then + -- Query for the objects + status, queryvalue_result = msrpc.winreg_queryvalue(smbstate, openhkpd_result['handle'], objects) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, queryvalue_result + end + + -- Parse the header + pos = 1 + status, pos, data_block = parse_perf_data_block(queryvalue_result['value'], pos) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + + -- Move past the header + pos = 1 + data_block['HeaderLength'] + + -- Parse the data sections + for i = 1, data_block['NumObjectTypes'], 1 do + local object_start = pos + local object_name + + local counter_definitions = {} + local object_instances = {} + local counter_definitions = {} + + -- Get the type of the object (this is basically the class definition -- info about the object instances) + status, pos, object_type = parse_perf_object_type(queryvalue_result['value'], pos) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + + -- Start setting up the result object +--io.write(string.format("Index = %d\n", object_type['ObjectNameTitleIndex'])) + object_name = result['title_database'][object_type['ObjectNameTitleIndex']] + result[object_name] = {} + +--io.write(string.format("\n\nOBJECT: %s\n", object_name)) +--io.write(string.format(" Counters: %d\n", object_type['NumCounters'])) +--io.write(string.format(" Instances: %d\n", object_type['NumInstances'])) +--io.write("-----------------\n") + + -- Bring the position to the beginning of the counter definitions + pos = object_start + object_type['HeaderLength'] + + -- Parse the counter definitions + for j = 1, object_type['NumCounters'], 1 do + status, pos, counter_definitions[j] = parse_perf_counter_definition(queryvalue_result['value'], pos) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end +--io.write(string.format(" Counter definition #%2d: [%d bytes] %s\n", j, counter_definitions[j]['CounterSize'], result['title_database'][counter_definitions[j]['CounterNameTitleIndex']])) + end + + -- Bring the position to the beginning of the instances (or counters) + pos = object_start + object_type['DefinitionLength'] + + -- Check if we have any instances (sometimes we don't -- if we don't, the value returned is a negative) + if(bit.band(object_type['NumInstances'], 0x80000000) == 0) then + -- Parse the object instances and counters + for j = 1, object_type['NumInstances'], 1 do + local instance_start = pos + local instance_name + local counter_block + -- Instance definition + status, pos, object_instances[j] = parse_perf_instance_definition(queryvalue_result['value'], pos) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + + -- Set up the instance array + instance_name = object_instances[j]['InstanceName'] + result[object_name][instance_name] = {} + + -- Bring the pos to the start of the counter block + pos = instance_start + object_instances[j]['ByteLength'] + +--io.write(string.format("\n INSTANCE: %s\n", instance_name)) +--io.write(string.format(" Length: %d\n", object_instances[j]['ByteLength'])) +--io.write(string.format(" NameOffset: %d\n", object_instances[j]['NameOffset'])) +--io.write(string.format(" NameLength: %d\n", object_instances[j]['NameLength'])) +--io.write(" --------------\n") + + -- The counter block + status, pos, counter_block = parse_perf_counter_block(queryvalue_result['value'], pos) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + + for k = 1, object_type['NumCounters'], 1 do + local counter_name + -- Each individual counter + status, pos, counter_result = parse_perf_counter(queryvalue_result['value'], pos, counter_definitions[k]) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + counter_name = result['title_database'][counter_definitions[k]['CounterNameTitleIndex']] +--io.write(string.format(" %s: %s\n", counter_name, counter_result)) + + -- Save it in the result + result[object_name][instance_name][counter_name] = counter_result + end + + -- Bring the pos to the end of the next section + pos = instance_start + object_instances[j]['ByteLength'] + counter_block['ByteLength'] + end + else + for k = 1, object_type['NumCounters'], 1 do + local counter_name + -- Each individual counter + status, pos, counter_result = parse_perf_counter(queryvalue_result['value'], pos, counter_definitions[k]) + if(status == false) then + msrpc.stop_smb(smbstate) + return false, pos + end + counter_name = result['title_database'][counter_definitions[k]['CounterNameTitleIndex']] +--io.write(string.format(" %s: %s\n", counter_name, counter_result)) + + -- Save it in the result + result[object_name][counter_name] = counter_result + end + end + end + + -- Blank out the database + result['title_database'] = nil + end + + msrpc.stop_smb(smbstate) + + return true, result +end + + diff --git a/nselib/msrpctypes.lua b/nselib/msrpctypes.lua index 006b5d969..73af3a18c 100644 --- a/nselib/msrpctypes.lua +++ b/nselib/msrpctypes.lua @@ -289,7 +289,7 @@ end -- (for the pointer data), or ALL (for both together). Generally, unless the -- referent_id is split from the data (for example, in an array), you will want -- ALL. ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data --@param func The function that's used to process the body data (only called if it isn't a null -- pointer). This function has to conform to a specific prototype, see above. @@ -311,6 +311,10 @@ local function unmarshall_ptr(location, data, pos, func, args, result) if(location == HEAD or location == ALL) then local referent_id pos, referent_id = bin.unpack("data. --@param count The number of elements in the array. --@param func The function to call to unmarshall each parameter. Has to match a specific prototype; @@ -454,6 +458,9 @@ local function unmarshall_array(data, pos, count, func, args) end pos, max_count = bin.unpack(") -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@param func The function to call to unmarshall each parameter. Has to match a specific prototype; -- see the function comment. @@ -594,6 +601,9 @@ function unmarshall_unicode(data, pos, do_null) end pos, max, offset, actual = bin.unpack("data. --@param do_null [optional] Assumes a null is at the end of the string. Default false. --@return (pos, result) The new position and the string. @@ -695,7 +705,7 @@ end --- Unmarshall an int64. See marshall_int64 for more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, int64) The new position, and the value. function unmarshall_int64(data, pos) @@ -703,6 +713,9 @@ function unmarshall_int64(data, pos) stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_int64()")) pos, value = bin.unpack("marshall_int32 for more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, int32) The new position, and the value. function unmarshall_int32(data, pos) @@ -718,6 +731,9 @@ function unmarshall_int32(data, pos) stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_int32()")) pos, value = bin.unpack(" +-- typedef struct _SYSTEMTIME { +-- WORD wYear; +-- WORD wMonth; +-- WORD wDayOfWeek; +-- WORD wDay; +-- WORD wHour; +-- WORD wMinute; +-- WORD wSecond; +-- WORD wMilliseconds; +-- } SYSTEMTIME +-- +-- +--@param data The data packet. +--@param pos The position within the data. +--@return (pos, time) The new position, and the time in seconds since 1970. +function unmarshall_SYSTEMTIME(data, pos) + local date = {} + + pos, date['year'], date['month'], _, date['day'], date['hour'], date['min'], date['sec'], _ = bin.unpack("hyper. I have no idea what a hyper is, just that it seems -- to be a 64-bit data type used for measuring time, and that the units happen to be negative -- microseconds. This function converts the value to seconds and returns it. @@ -1143,6 +1205,7 @@ end --@param pos The position within the data. --@param table The table to use for lookups. The keys should be the names, and the values should be -- the numbers. +--@return (pos, array) The new position, and a table representing the enumeration values. local function unmarshall_Enum32_array(data, pos, table) local array = {} local i, v @@ -1161,6 +1224,23 @@ local function unmarshall_Enum32_array(data, pos, table) return pos, array end +---Unmarshall raw data. +--@param data The data packet. +--@param pos The position within the data. +--@param length The number of bytes to unmarshall. +--@return (pos, data) The new position in the packet, and a string representing the raw data. +function unmarshall_raw(data, pos, length) + local val + stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_raw()")) + + pos, val = bin.unpack(string.format("A%d", length), data, pos) + if(val == nil) then + stdnse.print_debug(1, "MSRPC: ERROR: Ran off the end of a packet in unmarshall_raw(). Please report!") + end + + stdnse.print_debug(4, string.format("MSRPC: Leaving unmarshall_raw()")) + return pos, val +end ------------------------------------- @@ -1202,6 +1282,9 @@ local function unmarshall_guid(data, pos) stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_guid()")) pos, guid['time_low'], guid['time_high'], guid['time_hi_and_version'], guid['clock_seq'], guid['node'] = bin.unpack(" -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_dom_sid2(data, pos) @@ -1269,16 +1352,20 @@ function unmarshall_dom_sid2(data, pos) -- Read the SID from the packet local sid = {} - pos, sid['count'] = bin.unpack("SI", data, pos) + pos, sid['authority_high'], sid['authority_low'] = bin.unpack(">SI", data, pos) + if(sid['authority_low'] == nil) then + stdnse.print_debug(1, "MSRPC: ERROR: Ran off the end of a packet in unmarshall_dom_sid2(). Please report!") + end sid['authority'] = bit.bor(bit.lshift(sid['authority_high'], 32), sid['authority_low']) sid['sub_auths'] = {} for i = 1, sid['num_auths'], 1 do - pos, sid['sub_auths'][i] = bin.unpack("dom_sid2 struct. See the unmarshall_dom_sid2 function -- for more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_dom_sid2_ptr(data, pos) @@ -1452,7 +1539,9 @@ local function unmarshall_lsa_String_internal(location, data, pos, result) stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_lsa_String_internal()")) if(location == HEAD or location == ALL) then - pos, length, size = bin.unpack("data. --@param result This is required when unmarshalling the BODY section, which always comes after -- unmarshalling the HEAD. It is the result returned for this parameter during the @@ -1779,7 +1868,7 @@ end -- (for nothing, since this isn't a pointer), or ALL (for the data). Generally, unless the -- referent_id is split from the data (for example, in an array), you will want -- ALL. ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@param result This is required when unmarshalling the BODY section, which always comes after -- unmarshalling the HEAD. It is the result returned for this parameter during the @@ -1853,7 +1942,7 @@ end -- (for nothing, since this isn't a pointer), or ALL (for the data). Generally, unless the -- referent_id is split from the data (for example, in an array), you will want -- ALL. ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@param result This is required when unmarshalling the BODY section, which always comes after -- unmarshalling the HEAD. It is the result returned for this parameter during the @@ -1866,7 +1955,9 @@ local function unmarshall_lsa_StringLarge(location, data, pos, result) stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_lsa_StringLarge()")) if(location == HEAD or location == ALL) then - pos, length, size = bin.unpack("data. --@param result This is required when unmarshalling the BODY section, which always comes after -- unmarshalling the HEAD. It is the result returned for this parameter during the @@ -1928,7 +2019,7 @@ end -- } lsa_RefDomainList; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_lsa_RefDomainList(data, pos) @@ -1950,7 +2041,7 @@ end ---Unmarshall a pointer to a lsa_RefDomainList. See the unmarshall_lsa_RefDomainList function -- for more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_lsa_RefDomainList_ptr(data, pos) @@ -1972,7 +2063,7 @@ end -- } lsa_TransSidArray2; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_lsa_TransSidArray2(data, pos) @@ -2139,7 +2230,7 @@ end ---Unmarshall a lsa_TransNameArray2 structure. See the marshall_lsa_TransNameArray2 for more -- information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_lsa_TransNameArray2(data, pos) @@ -2403,7 +2494,9 @@ function unmarshall_winreg_StringBuf(data, pos) local str stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_winreg_StringBuf()")) - pos, length, size = bin.unpack("data. --@return (pos, result) The new position in data, and a table representing the datatype. This may be -- nil if there was an error. @@ -3155,7 +3248,7 @@ function unmarshall_srvsvc_NetSessCtr10(data, pos) local result = {} stdnse.print_debug(4, string.format("MSRPC: Entering unmarshall_srvsvc_NetSessCtr10()")) - pos, count = bin.unpack("data --@return (pos, result) The new position in data, and a table representing the datatype. Can be -- nil if there's an error. @@ -3249,7 +3342,7 @@ end -- -- Note that Wireshark (at least, the version I'm using, 1.0.3) gets this wrong, so be careful. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_srvsvc_Statistics(data, pos) @@ -3283,7 +3376,7 @@ end -- -- See unmarshall_srvsvc_Statistics for more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_srvsvc_Statistics_ptr(data, pos) @@ -3615,7 +3708,7 @@ end -- (for nothing, since this isn't a pointer), or ALL (for the data). Generally, unless the -- referent_id is split from the data (for example, in an array), you will want -- ALL. ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@param result This is required when unmarshalling the BODY section, which always comes after -- unmarshalling the HEAD. It is the result returned for this parameter during the @@ -3651,7 +3744,7 @@ end -- } samr_SamArray; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_samr_SamArray(data, pos) @@ -3668,7 +3761,7 @@ end ---Unmarshall a pointer to a samr_SamArray type. See unmarshall_samr_SamArray for -- more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_samr_SamArray_ptr(data, pos) @@ -3698,7 +3791,7 @@ end -- (for nothing, since this isn't a pointer), or ALL (for the data). Generally, unless the -- referent_id is split from the data (for example, in an array), you will want -- ALL. ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@param result This is required when unmarshalling the BODY section, which always comes after -- unmarshalling the HEAD. It is the result returned for this parameter during the @@ -3740,7 +3833,7 @@ end -- } samr_DispInfoGeneral; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_samr_DispInfoGeneral(data, pos) @@ -3767,7 +3860,7 @@ end -- } samr_DispInfo; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. It may also return -- nil, if there was an error. @@ -3802,7 +3895,7 @@ end -- } samr_DomInfo1; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_samr_DomInfo1(data, pos) @@ -3828,7 +3921,7 @@ end -- } samr_DomInfo8; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_samr_DomInfo8(data, pos) @@ -3852,7 +3945,7 @@ end -- } samr_DomInfo12; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. function unmarshall_samr_DomInfo12(data, pos) @@ -3886,7 +3979,7 @@ end -- } samr_DomainInfo; -- -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. May return -- nil if there was an error. @@ -3915,7 +4008,7 @@ end ---Unmarshall a pointer to a samr_DomainInfo. See unmarshall_samr_DomainInfo for -- more information. -- ---@param data The data packet being processed. +--@param data The data being processed. --@param pos The position within data. --@return (pos, result) The new position in data, and a table representing the datatype. May return -- nil if there was an error. @@ -3931,3 +4024,7 @@ end + + + + diff --git a/nselib/netbios.lua b/nselib/netbios.lua index 7d2617fab..43270af8b 100644 --- a/nselib/netbios.lua +++ b/nselib/netbios.lua @@ -353,6 +353,9 @@ function do_nbstat(host) rrlength = rrlength - 18 end + if(rrlength > 0) then + rrlength = rrlength - 1 + end pos, statistics = bin.unpack(string.format(">A%d", rrlength), result, pos) -- Put it in the registry, in case anybody else needs it diff --git a/nselib/nmapdebug.lua b/nselib/nmapdebug.lua deleted file mode 100644 index bb294078f..000000000 --- a/nselib/nmapdebug.lua +++ /dev/null @@ -1,62 +0,0 @@ ---- Debugging functions for Nmap scripts. --- --- This module contains various handy functions for debugging. These should --- never be used for actual results, only during testing. --- --- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html - -local require = require -local type = type -local pairs = pairs -local nmap = require "nmap"; -local stdnse = require "stdnse"; - -local EMPTY = {}; -- Empty constant table - -module(... or "nmapdebug"); - ----Converts an arbitrary data type into a string. Will recursively convert --- tables. This can be very useful for debugging. --- ---@param data The data to convert. ---@param indent (optional) The number of times to indent the line. Default --- is 0. ---@return A string representation of a data, will be one or more full lines. -function tostr(data, indent) - local str = "" - - if(indent == nil) then - indent = 0 - end - - -- Check the type - if(type(data) == "nil") then - str = str .. (" "):rep(indent) .. data .. "\n" - elseif(type(data) == "string") then - str = str .. (" "):rep(indent) .. data .. "\n" - elseif(type(data) == "number") then - str = str .. (" "):rep(indent) .. data .. "\n" - elseif(type(data) == "boolean") then - if(data == true) then - str = str .. "true" - else - str = str .. "false" - end - elseif(type(data) == "table") then - local i, v - for i, v in pairs(data) do - -- Check for a table in a table - if(type(v) == "table") then - str = str .. (" "):rep(indent) .. i .. ":\n" - str = str .. tostr(v, indent + 2) - else - str = str .. (" "):rep(indent) .. i .. ": " .. tostr(v, 0) - end - end - else - stdnse.print_debug(1, "Error: unknown data type: %s", type(data)) - end - - return str -end - diff --git a/nselib/nsedebug.lua b/nselib/nsedebug.lua new file mode 100644 index 000000000..885ea9cf6 --- /dev/null +++ b/nselib/nsedebug.lua @@ -0,0 +1,119 @@ +-- Debugging functions for Nmap scripts. +-- +-- This module contains various handy functions for debugging. These should +-- never be used for actual results, only during testing. +-- +-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html + +require "stdnse" + +local EMPTY = {}; -- Empty constant table + +module(... or "nsedebug", package.seeall); + +---Converts an arbitrary data type into a string. Will recursively convert +-- tables. This can be very useful for debugging. +-- +--@param data The data to convert. +--@param indent (optional) The number of times to indent the line. Default +-- is 0. +--@return A string representation of a data, will be one or more full lines. +function tostr(data, indent) + local str = "" + + if(indent == nil) then + indent = 0 + end + + -- Check the type + if(type(data) == "nil") then + str = str .. (" "):rep(indent) .. data .. "\n" + elseif(type(data) == "string") then + str = str .. (" "):rep(indent) .. data .. "\n" + elseif(type(data) == "number") then + str = str .. (" "):rep(indent) .. data .. "\n" + elseif(type(data) == "boolean") then + if(data == true) then + str = str .. "true" + else + str = str .. "false" + end + elseif(type(data) == "table") then + local i, v + for i, v in pairs(data) do + -- Check for a table in a table + if(type(v) == "table") then + str = str .. (" "):rep(indent) .. i .. ":\n" + str = str .. tostr(v, indent + 2) + else + str = str .. (" "):rep(indent) .. i .. ": " .. tostr(v, 0) + end + end + else + stdnse.print_debug(1, "Error: unknown data type: %s", type(data)) + end + + return str +end + +-- Print out a string in hex, for debugging. +function print_hex(str) + + -- Prints out the full lines + for line=1, string.len(str)/16, 1 do + io.write(string.format("%08x ", (line - 1) * 16)) + + -- Loop through the string, printing the hex + for char=1, 16, 1 do + ch = string.byte(str, ((line - 1) * 16) + char) + io.write(string.format("%02x ", ch)) + end + + io.write(" ") + + -- Loop through the string again, this time the ascii + for char=1, 16, 1 do + ch = string.byte(str, ((line - 1) * 16) + char) + if ch < 0x20 or ch > 0x7f then + ch = string.byte(".", 1) + end + io.write(string.format("%c", ch)) + end + + io.write("\n") + end + + -- Prints out the final, partial line + line = math.floor((string.len(str)/16)) + 1 + io.write(string.format("%08x ", (line - 1) * 16)) + + for char=1, string.len(str) % 16, 1 do + ch = string.byte(str, ((line - 1) * 16) + char) + io.write(string.format("%02x ", ch)) + end + io.write(string.rep(" ", 16 - (string.len(str) % 16))); + io.write(" ") + + for char=1, string.len(str) % 16, 1 do + ch = string.byte(str, ((line - 1) * 16) + char) + if ch < 0x20 or ch > 0x7f then + ch = string.byte(".", 1) + end + io.write(string.format("%c", ch)) + end + + -- Print out the length + io.write(string.format("\n Length: %d [0x%x]\n", string.len(str), string.len(str))) + +end + +---Print out a stacktrace. The stacktrace will naturally include this function call. +function print_stack() + local thread = coroutine.running() + local trace = debug.traceback(thread); + if trace ~= "stack traceback:" then + print(thread, "\n", trace, "\n"); + end +end + + diff --git a/nselib/smb.lua b/nselib/smb.lua index a2ebfd51a..a9c82fd5c 100644 --- a/nselib/smb.lua +++ b/nselib/smb.lua @@ -66,9 +66,9 @@ -- If that's successful, SMB_COM_SESSION_SETUP_ANDX is sent. It is essentially the logon -- packet, where the username, domain, and password are sent to the server for verification. -- The username and password are generally picked up from the program parameters, which are --- set when running a script, or from the registry [TODO: Where?], which are set by other --- scripts. However, they can also be passed as parameters to the function, which will --- override any other username/password set. +-- set when running a script, or from the registry (nmap.registry[]['smbaccounts']) +-- where it can be set by other scripts (for example, smb-brute.nse). However, they can also +-- be passed as parameters to the function, which will override any other username/password set. -- -- If a username is set without a password, then a NULL session is started. If a login fails, -- we attempt to log in as the 'GUEST' account with a blank password. If that fails, we try @@ -151,6 +151,8 @@ status_names = {} local mutexes = setmetatable({}, {__mode = "k"}); --local debug_mutex = nmap.mutex("SMB-DEBUG") +local TIMEOUT = 5000 + ---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. -- @@ -220,7 +222,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??) + -- 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) for i, v in pairs(status_names) do if(v == status) then return i @@ -380,6 +382,7 @@ function start_raw(host, port) local status, err local socket = nmap.new_socket() + socket:set_timeout(TIMEOUT) status, err = socket:connect(host.ip, port, "tcp") if(status == false) then @@ -486,6 +489,7 @@ function start_netbios(host, port, name) ); stdnse.print_debug(3, "SMB: Connecting to %s", host.ip) + socket:set_timeout(TIMEOUT) status, err = socket:connect(host.ip, port, "tcp") if(status == false) then socket:close() @@ -499,7 +503,7 @@ function start_netbios(host, port, name) socket:close() return false, "SMB: Failed to send: " .. err end - socket:set_timeout(5000) + socket:set_timeout(TIMEOUT) -- Receive the session response stdnse.print_debug(3, "SMB: Receiving NetBIOS session response") @@ -509,6 +513,9 @@ function start_netbios(host, port, name) return false, "SMB: Failed to close socket: " .. result end pos, result, flags, length = bin.unpack(">CCS", result) + if(length == nil) then + return false, "SMB: ERROR: Ran off the end of SMB packet; likely due to server truncation [1]" + end -- Check for a position session response (0x82) if result == 0x82 then @@ -747,6 +754,11 @@ local function smb_encode_header(smb, command) -- the server that we deal in ASCII. local flags2 = bit.bor(0x4000, 0x0040, 0x0001) -- SMB_FLAGS2_32BIT_STATUS | SMB_FLAGS2_IS_LONG_NAME | SMB_FLAGS2_KNOWS_LONG_NAMES + -- TreeID should never ever be 'nil', but it seems to happen once in awhile so print an error + if(smb['tid'] == nil) then + return false, string.format("SMB: ERROR: TreeID value was set to nil on host %s", smb['ip']) + end + local header = bin.pack("I", result) if(netbios_length == nil) then - return false, "SMB: SMB server didn't comply with standards (incorrect data was returned) [1]" + return false, "SMB: ERROR: Ran off the end of SMB packet; likely due to server truncation [2]" end -- Make the length 24 bits netbios_length = bit.band(netbios_length, 0x00FFFFFF) @@ -850,10 +862,9 @@ function smb_read(smb) -- If we haven't received enough bytes, try and get the rest (fragmentation!) if(#result < length) then local new_result - status, new_result = smb['socket']:receive_bytes(netbios_length) - stdnse.print_debug(1, "SMB: Received a fragmented packet, attempting to receive the rest of it (got %d bytes, need %d)", #result, length) + status, new_result = smb['socket']:receive_bytes(netbios_length - #result) -- Make sure the connection is still alive if(status ~= true) then return false, "SMB: Failed to receive bytes: " .. result @@ -872,31 +883,31 @@ function smb_read(smb) -- The header is 32 bytes. pos, header = bin.unpack(" #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 + +--- This sends a SMB request to write to a file (or a pipe). +-- +--@param smb The SMB object associated with the connection +--@param write_data The data to write +--@param offset The offset to write it to (ignored for pipes) +--@param path The path of the file or pipe to open +--@return (status, result) If status is false, result is an error message. Otherwise, result is a table +-- containing a lot of different elements, the most important one being 'fid', the handle to the opened file. +function write_file(smb, write_data, offset) + local header, parameters, data + local pos + local header1, header2, header3, header4, command, status, flags, flags2, pid_high, signature, unused, pid, mid + local andx_command, andx_reserved, andx_offset + local response = {} + + header = smb_encode_header(smb, command_codes['SMB_COM_WRITE_ANDX']) + parameters = bin.pack("2 so this runs last (so if it DOES crash something, it doesn't +-- till other scans have had a chance to run) +runlevel = 2 require 'msrpc' require 'smb' @@ -181,6 +184,7 @@ end action = function(host) + local status, result local response = " \n" local found = false diff --git a/scripts/smb-enum-domains.nse b/scripts/smb-enum-domains.nse index 92330d17a..cbc23da1e 100644 --- a/scripts/smb-enum-domains.nse +++ b/scripts/smb-enum-domains.nse @@ -57,6 +57,7 @@ hostrule = function(host) end action = function(host) + local response = " \n" local status, smbstate local i, j diff --git a/scripts/smb-enum-processes.nse b/scripts/smb-enum-processes.nse new file mode 100644 index 000000000..b812dafbb --- /dev/null +++ b/scripts/smb-enum-processes.nse @@ -0,0 +1,282 @@ + +description = [[ +Pulls a list of processes from the remote server over SMB (using the remote registry service and +HKEY_PERFORMANCE_DATA). + +Requires Administrator access. + +WARNING: I have experienced crashes in regsvc.exe while making registry calls against a fully patched Windows +2000 system; I've fixed the issue that caused it, but there's no guarantee that it (or a similar vuln in the +same code) won't show up again. +]] + +--- +-- @usage +-- nmap --script smb-enum-processes.nse -p445 +-- sudo nmap -sU -sS --script smb-enum-processes.nse -p U:137,T:139 +-- +--- +-- @output +-- Host script results: +-- |_ smb-enum-processes: Idle, _Total, System, wmiprvse, VMwareUser, VMwareTray, smss, csrss, winlogon, services, lsass, logon.scr, spoolsv, msdtc, VMwareService, svchost, alg, explorer +-- -- +-- Host script results: +-- | smb-enum-processes: +-- | Idle +-- | | PID: 0, Parent: 0 [Idle] +-- | | Priority: 0 +-- | |_Thread Count: 1, Handle Count: 0 +-- | System +-- | | PID: 4, Parent: 0 [Idle] +-- | | Priority: 8 +-- | |_Thread Count: 48, Handle Count: 392 +-- | VMwareUser +-- | | PID: 212, Parent: 1832 [explorer] +-- | | Priority: 8 +-- | |_Thread Count: 1, Handle Count: 45 +-- | VMwareTray +-- | | PID: 240, Parent: 1832 [explorer] +-- | | Priority: 8 +-- | |_Thread Count: 1, Handle Count: 41 +-- | smss +-- | | PID: 252, Parent: 4 [System] +-- | | Priority: 11 +-- | |_Thread Count: 3, Handle Count: 19 +-- | csrss +-- | | PID: 300, Parent: 252 [smss] +-- | | Priority: 13 +-- | |_Thread Count: 10, Handle Count: 347 +-- | winlogon +-- | | PID: 324, Parent: 252 [smss] +-- | | Priority: 13 +-- | |_Thread Count: 18, Handle Count: 513 +-- | services +-- | | PID: 372, Parent: 324 [winlogon] +-- | | Priority: 9 +-- | |_Thread Count: 17, Handle Count: 275 +-- | lsass +-- | | PID: 384, Parent: 324 [winlogon] +-- | | Priority: 9 +-- | |_Thread Count: 29, Handle Count: 415 +-- | logon.scr +-- | | PID: 868, Parent: 324 [winlogon] +-- | | Priority: 4 +-- | |_Thread Count: 1, Handle Count: 22 +-- ... +-- +-- @args smb* This script supports the smbusername, +-- smbpassword, smbhash, and smbtype +-- script arguments of the smb module. +----------------------------------------------------------------------- + +author = "Ron Bowes" +copyright = "Ron Bowes" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery","intrusive"} + +require "bin" +require 'msrpc' +require 'msrpcperformance' +require 'smb' +require 'stdnse' + +-- Strings used to separate processes from one another. +local separators = { + first = "-+-"; + last = " `-"; + middle = " +-"; + only = "---"; +} + +function psl_add (psl, ps) + -- Add process. + psl[ps.pid] = ps + + -- Add dummy parent if no real one exists. + if psl[ps.ppid] == nil then + psl[ps.ppid] = { + name = 'Unknown'; + pid = ps.ppid; + ppid = ps.ppid; + } + end +end + +function psl_mode (list, i) + local mode + + -- Decide connector for process. + if table.maxn(list) == 1 then + mode = "only" + elseif i == 1 then + mode = "first" + elseif i == table.maxn(list) then + mode = "last" + else + mode = "middle" + end + + return mode +end + +function psl_print (psl) + local result = "" + + -- Find how many root processes there are. + local roots = {} + for i,ps in pairs(psl) do + if psl[ps.ppid] == nil or ps.ppid == ps.pid then + table.insert(roots, i) + end + end + table.sort(roots) + + -- Create vertical sibling link. + local bars = {} + if table.maxn(roots) ~= 1 then + table.insert(bars, 2) + end + + -- Print out each root of the tree. + for i,root in ipairs(roots) do + local mode = psl_mode(roots, i) + result = result .. psl_tree(psl, root, 0, bars, mode) + end + + return result +end + +function psl_tree (psl, pid, column, bars, mode) + local ps = psl[pid] + + -- Delete vertical sibling link. + if mode == 'last' then + table.remove(bars) + end + + -- Print lead-in. + local prefix = '' + if mode == 'middle' or mode == 'last' then + prefix = '\n' + + local i = 1 + for j = 1, column do + if table.maxn(bars) >= i and + bars[i] == j then + prefix = prefix .. '|' + i = i + 1 + else + prefix = prefix .. ' ' + end + end + end + + -- Format process itself. + output = separators[mode] .. ps.name .. '(' .. ps.pid .. ')' + column = column + #output + local result = prefix .. output + + -- Find process' children. + local children = {} + for child_pid,child in pairs(psl) do + if child_pid ~= pid and child.ppid == pid then + table.insert(children, child_pid) + end + end + table.sort(children) + + -- Create vertical sibling link between children. + if table.maxn(children) > 1 then + table.insert(bars, column + 2) + end + + -- Format process' children. + for i,pid in ipairs(children) do + local mode = psl_mode(children, i) + result = result .. psl_tree(psl, pid, column, bars, mode) + end + + return result +end + +hostrule = function(host) + return smb.get_port(host) ~= nil +end + +action = function(host) + + local status, result + local process + local response = " \n" + + -- Get the process list + status, result = msrpcperformance.get_performance_data(host, "230") + if(status == false) then + if(nmap.debugging() > 0) then + return "ERROR: " .. result + else + return nil + end + end + + -- Get the process table + process = result['Process'] + +-- for i, v in pairs(result['Processor']['_Total']) do +-- io.write(string.format("i = %s\n", i)) +-- end + + -- Put the processes into an array, and sort them by process id + local names = {} + for i, v in pairs(process) do + if(i ~= '_Total') then + names[#names + 1] = i + end + end + table.sort(names, function (a, b) return process[a]['ID Process'] < process[b]['ID Process'] end) + + -- Put the processes into an array indexed by process id and with a value equal to the name (so we can look it up + -- easily when we need to) + local process_id = {} + for i, v in pairs(process) do + process_id[v['ID Process']] = i + end + + + if(nmap.verbosity() == 1) then + local psl = {} + for i,name in ipairs(names) do + if(name ~= '_Total') then + psl_add(psl, { + name = name; + pid = process[name]['ID Process']; + ppid = process[name]['Creating Process ID']; + }) + end + end + response = ' \n' .. psl_print(psl) + elseif(nmap.verbosity() > 0) then + for i = 1, #names, 1 do + local name = names[i] + if(name ~= '_Total') then + local parent = process_id[process[name]['Creating Process ID']] + if(parent == nil) then + parent = "n/a" + end + +-- response = response .. string.format("%6d %24s (Parent: %24s, Priority: %4d, Threads: %4d, Handles: %4d)\n", process[name]['ID Process'], name, parent, process[name]['Priority Base'], process[name]['Thread Count'], process[name]['Handle Count']) + + response = response .. string.format("%s [%d]\n", name, process[name]['ID Process']) + response = response .. string.format("| Parent: %s [%s]\n", process[name]['Creating Process ID'], parent) + response = response .. string.format("| Priority: %s, Thread Count: %s, Handle Count: %s\n", process[name]['Priority Base'], process[name]['Thread Count'], process[name]['Handle Count']) + end + + end + else + response = stdnse.strjoin(", ", names) + end + + return response +end + + diff --git a/scripts/smb-enum-sessions.nse b/scripts/smb-enum-sessions.nse index 7b3e9cbfe..13bb960c8 100644 --- a/scripts/smb-enum-sessions.nse +++ b/scripts/smb-enum-sessions.nse @@ -247,6 +247,7 @@ local function winreg_enum_rids(host) end action = function(host) + local response = " \n" local status1, status2 diff --git a/scripts/smb-enum-shares.nse b/scripts/smb-enum-shares.nse index 94e7947fc..e4c090b9c 100644 --- a/scripts/smb-enum-shares.nse +++ b/scripts/smb-enum-shares.nse @@ -191,7 +191,9 @@ function check_shares(host, shares) stdnse.print_debug(3, "EnumShares: Access was denied") denied_shares[#denied_shares + 1] = shares[i] else - stdnse.print_debug(3, "ERROR: EnumShares: Share didn't pan out: %s", err) + -- 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) end else -- Add it to allowed shares @@ -244,6 +246,7 @@ local function get_share_info(host, name) end action = function(host) + local enum_result local result, shared local response = " \n" diff --git a/scripts/smb-enum-users.nse b/scripts/smb-enum-users.nse index cd57de45d..3e9c37d1f 100644 --- a/scripts/smb-enum-users.nse +++ b/scripts/smb-enum-users.nse @@ -9,10 +9,10 @@ can be fine tuned using Nmap parameters. For the most possible information, leave the defaults; however, there are advantages to using them individually. Advantages of using SAMR enumeration: -* Stealthier (requires one packet/user account, whereas LSA uses at least 20 - packets; additionally, LSA makes a lot of noise in the Windows event log (LSA - enumeration is the only script I (Ron Bowes) have been called on by the - administrator of a box I was testing against). +* Stealthier (requires one packet/user account, whereas LSA uses at least 10 + packets while SAMR uses half that; additionally, LSA makes a lot of noise in + the Windows event log (LSA enumeration is the only script I (Ron Bowes) have + been called on by the administrator of a box I was testing against). * More information is returned (more than just the username). * Every account will be found, since they're being enumerated with a function that's designed to enumerate users. @@ -58,16 +58,11 @@ a user on a domain or system. An LSA function is exposed which lets us convert t (say, 1000) to the username (say, "Ron"). So, the technique will essentially try converting 1000 to a name, then 1001, 1002, etc., until we think we're done. -To do this, this script breaks users into groups of five RIDs, then checked individually -(checking too many at once causes problems). We continue checking until we reach -1100, and get an empty group of five. This probably isn't the most effective way, but it -seems to work. It might be a good idea to modify this, in the future, with some more -intelligence. I (Ron Bowes) performed a test on an old server with a lot of accounts, -and these were the active RIDs: 500, 501, 1000, 1030, 1031, 1053, 1054, 1055, -1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1070, -1075, 1081, 1088, 1090. The jump from 1000 to 1030 is quite large and can easily -result in missing accounts, in an automated check. An ideal solution might be to continue -doing groups of 5, but wait until we get 5-10 consecutive empty groups before giving up. +To do this, the script breaks users into groups of RIDs based on the LSA_GROUPSIZE +constant. All members of this group are checked simultaneously, and the responses recorded. +When a series of empty groups are found (LSA_MINEMPTY groups, specifically), +the scan ends. As long as you are getting a few groups with active accounts, the scan will +continue. Before attempting this conversion, the SID of the server has to be determined. The SID is determined by doing the reverse operation; that is, by converting a name into @@ -156,284 +151,8 @@ hostrule = function(host) return smb.get_port(host) ~= nil end ----Attempt to enumerate users through SAMR methods. See the file description for more information. --- ---@param host The host object. ---@return Status (true or false). ---@return Array of user tables (if status is true) or an an error string (if ---status is false). Each user table contains the fields name, ---domain, fullname, rid, and ---description. -local function enum_samr(host) - local i, j - - stdnse.print_debug(3, "Entering enum_samr()") - - local smbstate - local bind_result, connect4_result, enumdomains_result - local connect_handle - local status, smbstate - local response = {} - - -- Create the SMB session - status, smbstate = msrpc.start_smb(host, msrpc.SAMR_PATH) - - if(status == false) then - return false, smbstate - end - - -- Bind to SAMR service - status, bind_result = msrpc.bind(smbstate, msrpc.SAMR_UUID, msrpc.SAMR_VERSION, nil) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, bind_result - end - - -- Call connect4() - status, connect4_result = msrpc.samr_connect4(smbstate, host.ip) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, connect4_result - end - - -- Save the connect_handle - connect_handle = connect4_result['connect_handle'] - - -- Call EnumDomains() - status, enumdomains_result = msrpc.samr_enumdomains(smbstate, connect_handle) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, enumdomains_result - end - - -- If no domains were returned, go back with an error - if(#enumdomains_result['sam']['entries'] == 0) then - msrpc.stop_smb(smbstate) - return false, "Couldn't find any domains" - end - - -- Now, loop through the domains and find the users - for i = 1, #enumdomains_result['sam']['entries'], 1 do - - local domain = enumdomains_result['sam']['entries'][i]['name'] - -- We don't care about the 'builtin' domain, in all my tests it's empty - if(domain ~= 'Builtin') then - local sid - local domain_handle - local opendomain_result, querydisplayinfo_result - - -- Call LookupDomain() - status, lookupdomain_result = msrpc.samr_lookupdomain(smbstate, connect_handle, domain) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, lookupdomain_result - end - - -- Save the sid - sid = lookupdomain_result['sid'] - - -- Call OpenDomain() - status, opendomain_result = msrpc.samr_opendomain(smbstate, connect_handle, sid) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, opendomain_result - end - - -- Save the domain handle - domain_handle = opendomain_result['domain_handle'] - - -- Loop as long as we're getting valid results - j = 0 - repeat - -- Call QueryDisplayInfo() - status, querydisplayinfo_result = msrpc.samr_querydisplayinfo(smbstate, domain_handle, j) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, querydisplayinfo_result - end - - -- Save the response - if(querydisplayinfo_result['return'] ~= 0 and querydisplayinfo_result['info'] ~= nil and querydisplayinfo_result['info']['entries'] ~= nil and querydisplayinfo_result['info']['entries'][1] ~= nil) then - local array = {} - local k - - -- The reason these are all indexed from '1' is because we request names one at a time. - array['domain'] = domain - array['name'] = querydisplayinfo_result['info']['entries'][1]['account_name'] - array['fullname'] = querydisplayinfo_result['info']['entries'][1]['full_name'] - array['description'] = querydisplayinfo_result['info']['entries'][1]['description'] - array['rid'] = querydisplayinfo_result['info']['entries'][1]['rid'] - array['flags'] = querydisplayinfo_result['info']['entries'][1]['acct_flags'] - array['source'] = "SAMR Enumeration" - - -- Clean up the 'flags' array - for k = 1, #array['flags'], 1 do - array['flags'][k] = msrpc.samr_AcctFlags_tostr(array['flags'][k]) - end - - -- Add it to the array - response[#response + 1] = array - end - j = j + 1 - until querydisplayinfo_result['return'] == 0 - - -- Close the domain handle - msrpc.samr_close(smbstate, domain_handle) - - -- Finally, fill in the response! --- for i = 1, #querydisplayinfo_result['details'], 1 do --- querydisplayinfo_result['details'][i]['domain'] = domain --- -- All we get from this is users --- querydisplayinfo_result['details'][i]['typestr'] = "User" --- querydisplayinfo_result['details'][i]['source'] = "SAMR Enumeration" --- response[#response + 1] = querydisplayinfo_result['details'][i] --- end - end -- Checking for 'builtin' - end -- Domain loop - - -- Close the connect handle - msrpc.samr_close(smbstate, connect_handle) - - -- Stop the SAMR SMB - msrpc.stop_smb(smbstate) - - stdnse.print_debug(3, "Leaving enum_samr()") - - return true, response -end - ----Attempt to enumerate users through LSA methods. See the file description for more information. --- ---@param host The host object. ---@return Status (true or false). ---@return Array of user tables (if status is true) or an an error string (if ---status is false). Each user table contains the fields name, ---domain, and rid. -local function enum_lsa(host) - - local smbstate - local status - local response = {} - - stdnse.print_debug(3, "Entering enum_lsa()") - - -- Create the SMB session - status, smbstate = msrpc.start_smb(host, msrpc.LSA_PATH) - if(status == false) then - return false, smbstate - end - - -- Bind to LSA service - status, bind_result = msrpc.bind(smbstate, msrpc.LSA_UUID, msrpc.LSA_VERSION, nil) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, bind_result - end - - -- Open the LSA policy - status, openpolicy2_result = msrpc.lsa_openpolicy2(smbstate, host.ip) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, openpolicy2_result - end - - -- Start with some common names, as well as the name returned by the negotiate call - -- Vista doesn't like a 'null' after the server name, so fix that (TODO: the way I strip the null here feels hackish, is there a better way?) - names = {"administrator", "guest", "test", smbstate['domain'], string.sub(smbstate['server'], 1, #smbstate['server'] - 1) } - - -- Get the server's name from nbstat - local result, server_name = netbios.get_server_name(host.ip) - if(result == true) then - names[#names + 1] = server_name - end - - -- Get the logged in user from nbstat - local result, user_name = netbios.get_user_name(host.ip) - if(result == true) then - names[#names + 1] = user_name - end - - -- Look up the names, if any are valid than the server's SID will be returned - status, lookupnames2_result = msrpc.lsa_lookupnames2(smbstate, openpolicy2_result['policy_handle'], names) - if(status == false) then - msrpc.stop_smb(smbstate) - return false, lookupnames2_result - end - -- Loop through the domains returned and find the users in each - for i = 1, #lookupnames2_result['domains']['domains'], 1 do - local domain = lookupnames2_result['domains']['domains'][i]['name'] - local sid = lookupnames2_result['domains']['domains'][i]['sid'] - local sids = { } - local start = 1000 - - -- Start by looking up 500 - 505 (will likely be Administrator + guest) - for j = 500, 505, 1 do - sids[#sids + 1] = sid .. "-" .. j - end - status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], sids) - if(status == false) then - stdnse.print_debug(1, string.format("Error looking up RIDs: %s", lookupsids2_result)) - else - -- Put the details for each name into an array - -- NOTE: Be sure to mirror any changes here in the next bit! - for j = 1, #lookupsids2_result['names']['names'], 1 do - if(lookupsids2_result['names']['names'][j]['sid_type'] ~= "SID_NAME_UNKNOWN") then - local result = {} - result['name'] = lookupsids2_result['names']['names'][j]['name'] - result['rid'] = 500 + j - 1 - result['domain'] = domain - result['typestr'] = msrpc.lsa_SidType_tostr(lookupsids2_result['names']['names'][j]['sid_type']) - result['source'] = "LSA Bruteforce" - response[#response + 1] = result - end - end - end - - -- Now do groups of 5 users, until we get past 1100 and have an empty group - repeat - local used_names = 0 - local sids = {} - for j = start, start + 4, 1 do - sids[#sids + 1] = sid .. "-" .. j - end - - -- Try converting this group of RIDs into names - status, lookupsids2_result = msrpc.lsa_lookupsids2(smbstate, openpolicy2_result['policy_handle'], sids) - if(status == false) then - stdnse.print_debug(1, string.format("Error looking up RIDs: %s", lookupsids2_result)) - else - -- Put the details for each name into an array - for j = 1, #lookupsids2_result['names']['names'], 1 do - if(lookupsids2_result['names']['names'][j]['sid_type'] ~= "SID_NAME_UNKNOWN") then - local result = {} - result['name'] = lookupsids2_result['names']['names'][j]['name'] - result['rid'] = start + j - 1 - result['domain'] = domain - result['typestr'] = msrpc.lsa_SidType_tostr(lookupsids2_result['names']['names'][j]['sid_type']) - result['source'] = "LSA Bruteforce" - response[#response + 1] = result - end - end - end - - -- Go to the next set of RIDs - start = start + 5 - until status == false or (used_names == 0 and start > 1100) - end - - -- Close the handle - msrpc.lsa_close(smbstate, openpolicy2_result['policy_handle']) - - msrpc.stop_smb(smbstate) - - stdnse.print_debug(3, "Leaving enum_lsa()") - - return true, response -end - - - action = function(host) + local i, j local samr_status = false local lsa_status = false @@ -450,7 +169,7 @@ action = function(host) -- Try enumerating through LSA first. Since LSA provides less information, we want the -- SAMR result to overwrite it. if(do_lsa) then - lsa_status, lsa_result = enum_lsa(host) + lsa_status, lsa_result = msrpc.lsa_enum_users(host) if(lsa_status == false) then if(nmap.debugging() > 0) then response = response .. "ERROR: Couldn't enumerate through LSA: " .. lsa_result .. "\n" @@ -468,7 +187,7 @@ action = function(host) -- Try enumerating through SAMR if(do_samr) then - samr_status, samr_result = enum_samr(host) + samr_status, samr_result = msrpc.samr_enum_users(host) if(samr_status == false) then if(nmap.debugging() > 0) then response = response .. "ERROR: Couldn't enumerate through SAMR: " .. samr_result .. "\n" @@ -523,6 +242,7 @@ action = function(host) end if(names[name]['fullname'] ~= nil) then response = response .. string.format(" |_ Full name: %s\n", names[name]['fullname']) end if(names[name]['description'] ~= nil) then response = response .. string.format(" |_ Description: %s\n", names[name]['description']) end + if(names[name]['flags'] ~= nil) then response = response .. string.format(" |_ Flags: %s\n", stdnse.strjoin(", ", names[name]['flags'])) end if(nmap.verbosity() > 1) then