diff --git a/CHANGELOG b/CHANGELOG index c9fc35df8..cceb37563 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added a rsync library and two new script: + + rsync-list-modules - list available rsync modules + + rsync-brute - attempts to brute force passwords against a rsync module + [Patrik] + o Added --with-apr and --with-subversion configuration options to support systems where those libraries aren't in the usual places. [David] diff --git a/nselib/rsync.lua b/nselib/rsync.lua new file mode 100644 index 000000000..4e20248a8 --- /dev/null +++ b/nselib/rsync.lua @@ -0,0 +1,168 @@ +--- +-- A minimalist RSYNC library +-- + +module(... or "rsync",package.seeall) + +require 'base64' +require 'match' +stdnse.silent_require 'openssl' + +-- The Helper class serves as the main interface for script writers +Helper = { + + -- Creates a new instance of the Helper class + -- @param host table as received by the action function + -- @param port table as received by the action function + -- @param options table containing any additional options + -- @return o instance of Helper + new = function(self, host, port, options) + local o = { host = host, port = port, options = options or {} } + assert(o.options.module, "No rsync module was specified, aborting ...") + setmetatable(o, self) + self.__index = self + return o + end, + + -- Handles send and receive of control messages + -- @param data string containing the command to send + -- @return status true on succes, false on failure + -- @return data containing the response from the server + -- err string, if status is false + ctrl_exch = function(self, data) + local status, err = self.socket:send(data.."\n") + if ( not(status) ) then + return false, err + end + local status, data = self.socket:receive_buf("\n") + if( not(status) ) then + return false, err + end + return true, data + end, + + -- Connects to the rsync server + -- @return status, true on success, false on failure + -- @return err string containing an error message if status is false + connect = function(self) + self.socket = nmap.new_socket() + self.socket:set_timeout(self.options.timeout or 5000) + local status, err = self.socket:connect(self.host, self.port) + if ( not(status) ) then + return false, err + end + + local data + status, data = self:ctrl_exch("@RSYNCD: 29") + if ( not(status) ) then + return false, data + end + if ( not(data:match("^@RSYNCD: [%.%d]+$")) ) then + return false, "Protocol error" + end + return true + end, + + -- Authenticates against the rsync module. If no username is given, assume + -- no authentication is required. + -- @param username [optional] string containing the username + -- @param password [optional] string containing the password + login = function(self, username, password) + password = password or "" + local status, data = self:ctrl_exch(self.options.module) + if (not(status)) then + return false, data + end + + local chall + if ( data:match("@RSYNCD: OK") ) then + return true, "No authentication was required" + else + chall = data:match("^@RSYNCD: AUTHREQD (.*)$") + if ( not(chall) and data:match("^@ERROR: Unknown module") ) then + return false, data:match("^@ERROR: (.*)$") + elseif ( not(chall) ) then + return false, "Failed to retrieve challenge" + end + end + + if ( chall and not(username) ) then + return false, "Authentication required" + end + + local md4 = openssl.md4("\0\0\0\0" .. password .. chall) + local resp = base64.enc(md4):sub(1,-3) + status, data = self:ctrl_exch(username .. " " .. resp) + if (not(status)) then + return false, data + end + + if ( data == "@RSYNCD: OK" ) then + return true, "Authentication successfull" + end + return false, "Authentication failed" + end, + + -- Lists accessible modules from the rsync server + -- @return status true on success, false on failure + -- @return modules table containing a list of modules + listModules = function(self) + local status, data = self.socket:send("\n") + if (not(status)) then + return false, data + end + + local modules = {} + while(true) do + status, data = self.socket:receive_buf("\n") + if (not(status)) then + return false, data + end + if ( data == "@RSYNCD: EXIT" ) then + break + else + table.insert(modules, data) + end + end + return true, modules + end, + + -- Lists the files available for the directory/module + -- TODO: Add support for parsing results, seemed straight forward at + -- first, but wasn't. + listFiles = function(self) + -- list recursively and enable MD4 checksums + local data = ("--server\n--sender\n-rc\n.\n%s\n\n"):format(self.options.module) + local status, data = self.socket:send(data) + if ( not(status) ) then + return false, data + end + status, data = self.socket:receive_bytes(4) + if ( not(status) ) then + return false, data + end + + status, data = self.socket:send("\0\0\0\0") + if ( not(status) ) then + return false, data + end + + status, data = self.socket:receive_buf(match.numbytes(4)) + if ( not(status) ) then + return false, data + end + + local pos, len = bin.unpack(" +-- +-- @output +-- PORT STATE SERVICE REASON +-- 873/tcp open rsync syn-ack +-- | rsync-brute: +-- | Accounts +-- | user1:laptop - Valid credentials +-- | user2:password - Valid credentials +-- | Statistics +-- |_ Performed 1954 guesses in 20 seconds, average tps: 97 +-- +-- @args rsync-brute.module - the module against which brute forcing should be performed + + +require 'shortport' +require 'brute' +require 'rsync' + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"brute", "intrusive"} + +portrule = shortport.port_or_service(873, "rsync", "tcp") + +Driver = { + + new = function(self, host, port, options) + local o = { host = host, port = port, options = options } + setmetatable(o, self) + self.__index = self + return o + end, + + connect = function(self) + self.helper = rsync.Helper:new(self.host, self.port, self.options) + return self.helper:connect() + end, + + login = function(self, username, password) + + local status, data = self.helper:login(username, password) + -- retry unless we have an authentication failed error + if( not(status) and data ~= "Authentication failed" ) then + local err = brute.Error:new( data ) + err:setRetry( true ) + return false, err + elseif ( not(status) ) then + return false, brute.Error:new( "Login failed" ) + else + return true, brute.Account:new(username, password, creds.State.VALID) + end + end, + + disconnect = function( self ) + return self.helper:disconnect() + end + +} + +local function isModuleValid(host, port, module) + local helper = rsync.Helper:new(host, port, { module = module }) + if ( not(helper) ) then + return false, "Failed to create helper" + end + local status, data = helper:connect() + if ( not(status) ) then + return false, "Failed to connect to server" + end + status, data = helper:login() + if ( status and data == "No authentication was required" ) then + return false, data + elseif ( not(status) and data == "Authentication required" ) then + return true + elseif ( not(status) and data == ("Unknown module '%s'"):format(module) ) then + return false, data + end + return false, ("Brute pre-check failed for unknown reason: (%s)"):format(data) +end + +action = function(host, port) + + local mod = stdnse.get_script_args(SCRIPT_NAME .. ".module") + if ( not(mod) ) then + return "\n ERROR: rsync-brute.module was not supplied" + end + + local status, err = isModuleValid(host, port, mod) + if ( not(status) ) then + return ("\n ERROR: %s"):format(err) + end + + local engine = brute.Engine:new(Driver, host, port, { module = mod }) + engine.options.script_name = SCRIPT_NAME + status, result = engine:start() + return result +end diff --git a/scripts/rsync-list-modules.nse b/scripts/rsync-list-modules.nse new file mode 100644 index 000000000..dc4fe3c83 --- /dev/null +++ b/scripts/rsync-list-modules.nse @@ -0,0 +1,44 @@ +description = [[ +List modules available for rsync synchronization +]] + +--- +-- @usage +-- nmap -p 873 --script rsync-list-modules +-- +-- @output +-- PORT STATE SERVICE +-- 873/tcp open rsync +-- | rsync-list-modules: +-- | www www directory +-- | log log directory +-- |_ etc etc directory +-- + +require 'rsync' +require 'shortport' + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +portrule = shortport.port_or_service(873, "rsync", "tcp") + +action = function(host, port) + local helper = rsync.Helper:new(host, port, { module = "" }) + if ( not(helper) ) then + return "\n ERROR: Failed to create rsync.Helper" + end + + local status, err = helper:connect() + if ( not(status) ) then + return "\n ERROR: Failed to connect to rsync server" + end + + local modules = {} + status, modules = helper:listModules() + if ( not(status) ) then + return "\n ERROR: Failed to retrieve a list of modules" + end + return stdnse.format_output(true, modules) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 0c2892051..0511a6730 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -244,6 +244,8 @@ Entry { filename = "riak-http-info.nse", categories = { "discovery", "safe", } } Entry { filename = "rlogin-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "rmi-dumpregistry.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "rpcinfo.nse", categories = { "default", "discovery", "safe", } } +Entry { filename = "rsync-brute.nse", categories = { "brute", "intrusive", } } +Entry { filename = "rsync-list-modules.nse", categories = { "discovery", "safe", } } Entry { filename = "rtsp-methods.nse", categories = { "default", "safe", } } Entry { filename = "rtsp-url-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "servicetags.nse", categories = { "default", "discovery", "safe", } }