--- Call various MSRPC functions. -- \n\n -- By making heavy use of the smb library, this library will call various MSRPC -- functions. The functions used here can be access over TCP ports 445 and 139, -- with an established session. A NULL session (the default) will work for some -- functions and operating systems (or configurations), but not for others. \n --\n -- To make use of these function calls, a SMB session with the server has to be -- established. This can be done manually with the 'smb' library, or the function -- start_smb() can be called. \n --\n -- Next, the interface has to be bound. The bind() function will take care of that. \n --\n -- After that, you're free to call any function that's part of that interface. In -- other words, if you bind to the SAMR interface, you can only call the samr_ -- functions.\n --\n -- Although functions can be called in any order, many functions depend on the -- value returned by other functions. I indicate those in the function comments, -- so keep an eye out. \n --\n -- Something to note is that these functions, for the most part, return a whole ton -- of stuff in an array. I basically wrote them to return every possible value -- they get their hands on. I don't expect that most of them will be used, and I'm -- not going to document them all in the function header; rather, I will document -- the elements in the table that are more useful, you'll have to look at the actual -- code (or the table) to see what else is available. \n -- --@author Ron Bowes --@copyright See nmap's COPYING for licence ----------------------------------------------------------------------- module(... or "msrpc", package.seeall) require 'bit' require 'bin' require 'netbios' require 'smb' require 'stdnse' -- The path, UUID, and version for SAMR SAMR_PATH = "\\samr" SAMR_UUID = bin.pack("CCCCCCCCCCCCCCCC", 0x78, 0x57, 0x34, 0x12, 0x34, 0x12, 0xcd, 0xab, 0xef, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xac) SAMR_VERSION = 0x01 -- The path, UUID, and version for SRVSVC SRVSVC_PATH = "\\srvsvc" SRVSVC_UUID = bin.pack("CCCCCCCCCCCCCCCC", 0xc8, 0x4f, 0x32, 0x4b, 0x70, 0x16, 0xd3, 0x01, 0x12, 0x78, 0x5a, 0x47, 0xbf, 0x6e, 0xe1, 0x88) SRVSVC_VERSION = 0x03 -- The path, UUID, and version for LSA LSA_PATH = "\\lsarpc" LSA_UUID = bin.pack("CCCCCCCCCCCCCCCC", 0x78, 0x57, 0x34, 0x12, 0x34, 0x12, 0xcd, 0xab, 0xef, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab) LSA_VERSION = 0 -- This is the only transfer syntax I've seen in the wild, not that I've looked hard. It seems to work well. TRANSFER_SYNTAX = bin.pack("CCCCCCCCCCCCCCCC", 0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60) -- 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 -- A few error codes STATUS_SOME_NOT_MAPPED = 0x00000107 STATUS_INVALID_PARAMETER = 0xC000000D STATUS_ACCESS_DENIED = 0xC0000022 STATUS_BUFFER_TOO_SMALL = 0xC0000023 STATUS_NONE_MAPPED = 0xC0000073 STATUS_INSUFFICIENT_RESOURCES = 0xC000009A STATUS_MORE_ENTRIES = 0x00000105 STATUS_TOO_MANY_CONTEXT_IDS = 0xC000015A ---Convert the return status of a function into a string. This isn't nearly an -- exhaustive list, if I decide to go all out then I'll probably move this into -- its own library. --@param status the status to convert --@return The string equivalent to the status. local function status_to_string(status) if(status == STATUS_ACCESS_DENIED) then return "STATUS_ACCESS_DENIED" elseif(status == STATUS_MORE_ENTRIES) then return "STATUS_MORE_ENTRIES" elseif(status == STATUS_TOO_MANY_CONTEXT_IDS) then return "STATUS_TOO_MANY_CONTEXT_IDS" elseif(status == STATUS_INSUFFICIENT_RESOURCES) then return "STATUS_INSUFFICIENT_RESOURCES" elseif(status == STATUS_BUFFER_TOO_SMALL) then return "STATUS_BUFFER_TOO_SMALL" elseif(status == STATUS_INVALID_PARAMETER) then return "STATUS_INVALID_PARAMETER" elseif(status == STATUS_SOME_NOT_MAPPED) then return "STATUS_SOME_NOT_MAPPED" elseif(status == STATUS_NONE_MAPPED) then return "STATUS_NONE_MAPPED" else return string.format("STATUS_UNKNOWN_ERROR (0x%08x)", status) end end --- Convert a string to fake unicode (ascii with null characters between them), optionally add a null terminator, -- and optionally align it to 4-byte boundaries. This is frequently used in MSRPC calls, so I put it here, but -- it might be a good idea to move this function (and the converse one below) into a separate library. --@param string The string to convert. --@param do_null [optional] Add a null-terminator to the unicode string. Default false. --@param do_align [optional] Align the string to a multiple of 4 bytes. Default false. --@return The unicode version of the string. local function string_to_unicode(string, do_null, do_align) local i local result = "" if(do_null == nil) then do_null = false end if(do_align == nil) then do_align = false end -- Loop through the string, adding each character followed by a char(0) for i = 1, string.len(string), 1 do result = result .. string.sub(string, i, i) .. string.char(0) end -- Add a null, if the caller requestd it if(do_null == true) then result = result .. string.char(0) .. string.char(0) end -- Align it to a multiple of 4, if necessary if(do_align) then if(string.len(result) % 4 ~= 0) then result = result .. string.char(0) .. string.char(0) end end return result end --- Read a unicode string from a buffer, similar to how bin.unpack() would, optionally eat the null terminator, -- and optionally align it to 4-byte boundaries. --@param buffer The buffer to read from, typically the full 'arguments' value for MSRPC --@param pos The position in the buffer to start (just like bin.unpack()) --@param length The number of ascii characters that will be read (including the null, if do_null is set). --@param do_null [optional] Remove a null terminator from the string as the last character. Default false. --@param do_align [optional] Ensure that the number of bytes removed is a multiple of 4. --@return (pos, string) The new position and the string read, again imitating bin.unpack(). local function unicode_to_string(buffer, pos, length, do_null, do_align) local i, ch, dummy local string = "" stdnse.print_debug(5, "MSRPC: Entering unicode_to_string(pos = %d, length = %d)", pos, length) if(do_null == nil) then do_null = false end if(do_align == nil) then do_align = false end if(do_null == true) then length = length - 1 end for j = 1, length, 1 do pos, ch, dummy = bin.unpack("--). -- --@param sid A SID object. --@return A string representing the SID. function sid_to_string(sid) local i local str local authority = bit.bor(bit.lshift(sid['authority_high'], 32), sid['authority']) str = string.format("S-%u-%u", sid['revision'], sid['authority']) for i = 1, sid['count'], 1 do str = str .. string.format("-%u", sid['subauthorities'][i]) end return str end --- This is a wrapper around the SMB class, designed to get SMB going quickly in a script. 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 socket can be immediately -- used for MSRPC. -- --@param host The host object. --@param path The path to the named pipe; for example, msrpc.SAMR_PATH or msrpc.SRVSVC_PATH. --@return (status, socket, uid, tid, fid, negotiate_result, session_result, tree_result, create_result) -- if status is false, socket is an error message. Otherwise, the rest of the results are -- returned. function start_smb(host, path) local status, socket, negotiate_result, session_result, tree_result, create_result -- Begin the SMB session status, socket = smb.start(host) if(status == false) then return false, socket end -- Negotiate the protocol status, negotiate_result = smb.negotiate_protocol(socket) if(status == false) then smb.stop(socket) return false, negotiate_result end -- Start up a null session status, session_result = smb.start_session(socket, "", negotiate_result['session_key'], negotiate_result['capabilities']) if(status == false) then smb.stop(socket) return false, session_result end -- Connect to IPC$ share status, tree_result = smb.tree_connect(socket, "IPC$", session_result['uid']) if(status == false) then smb.stop(socket, session_result['uid']) return false, tree_result end -- Try to connect to requested pipe status, create_result = smb.create_file(socket, path, session_result['uid'], tree_result['tid']) if(status == false) then smb.stop(socket, session_result['uid'], tree_result['tid']) return false, create_result end -- Return everything return true, socket, session_result['uid'], tree_result['tid'], create_result['fid'], negotiate_result, session_result, tree_result, create_result end --- A wrapper around the smb.stop() function. I only created it to add symmetry, so the code uses -- the same class to start/stop the session. In the future, this may be expanded to close -- handles before exiting. -- --@param socket The socket to close. --@param uid The UserID, which will be logged off before closing the socket. --@param tid The TreeID, which will be disconnected before closing the socket. function stop_smb(socket, uid, tid) smb.stop(socket, uid, tid) end --- Bind to a MSRPC interface. Two common interfaces are SAML and SRVSVC, and can be found as -- constants at the top of this file. Once this function has successfully returned, any MSRPC -- call can be made (provided it doesn't depend on results from other MSRPC calls). -- --@param socket The socket in the appropriate state --@param interface_uuid The interface to bind to. There are constants defined for these (SAMR_UUID, -- etc.) --@param interface_version The interface version to use. There are constants at the top (SAMR_VERSION, -- etc.) --@param transfer_syntax The transfer syntax to use. I don't really know what this is, but the value -- was always the same on my tests. You can use the constant at the top (TRANSFER_SYNTAX), or -- just set this parameter to 'nil'. --@param uid The UserID we're sending the packets as --@param tid The TreeID we're sending the packets to --@param fid The FileID we're sending the packets to --@return (status, result) If status is false, result is an error message. Otherwise, result is a -- table of values, none of which are especially useful. function bind(socket, interface_uuid, interface_version, transfer_syntax, uid, tid, fid) local i local status, result local parameters, data local pos, align local response = {} stdnse.print_debug(2, "MSRPC: Sending Bind() request") -- Use the only transfer_syntax value I know of. if(transfer_syntax == nil) then transfer_syntax = TRANSFER_SYNTAX end data = bin.pack("IIIISI<", arguments) response['sid']['subauthorities'] = {} for i = 1, response['sid']['count'], 1 do pos, response['sid']['subauthorities'][i] = bin.unpack("SI<", sid['count'], sid['revision'], sid['count'], sid['authority_high'], sid['authority']) for i = 1, sid['count'], 1 do arguments = arguments .. bin.pack("SI<", arguments, pos) sid['subauthorities'] = {} for i = 1, sid['count'], 1 do pos, sid['subauthorities'][i] = bin.unpack("SI<", sid['count'] + 1, sid['revision'], sid['count'] + 1, sid['authority_high'], sid['authority']) for j = 1, sid['count'], 1 do arguments = arguments .. bin.pack("SI<", arguments, pos) sid['subauthorities'] = {} for i = 1, sid['count'], 1 do pos, sid['subauthorities'][i] = bin.unpack("