diff --git a/CHANGELOG b/CHANGELOG index 0500fed75..f3e6220b9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the script socks-brute that performs brute force password + guessing against SOCKS 5 servers. [Patrik] + o [NSE] Added the script vmauthd-brute that performs brute force password guessing against the VMware authentication daemon. [Patrik] diff --git a/nselib/socks.lua b/nselib/socks.lua new file mode 100644 index 000000000..3bdc2d1c3 --- /dev/null +++ b/nselib/socks.lua @@ -0,0 +1,353 @@ +--- +-- A smallish SOCKS version 5 implementation +-- +-- @author "Patrik Karlsson " +-- + +module(... or "socks", package.seeall) + +-- SOCKS Authentication methods +AuthMethod = { + NONE = 0, + GSSAPI = 1, + USERPASS = 2, +} + +Request = { + + -- Class that handles the connection request to the server + Connect = { + + -- Creates a new instance of the class + -- @param auth_method table of requested authentication methods + -- @return o instance on success, nil on failure + new = function(self, auth_method) + local o = { + version = 5, + auth_method = ( "table" ~= type(auth_method) and { auth_method } or auth_method ) + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts the instance to string, so that it can be sent to the + -- server. + -- @return string containing the raw request + __tostring = function(self) + local methods = "" + for _, m in ipairs(self.auth_method) do + methods = methods .. string.char(m) + end + return bin.pack("Cp", self.version, methods) + end, + + }, + + -- Class that handles the authentication request to the server + Authenticate = { + + -- Creates a new instance of the class + -- @param auth_method number with the requested authentication method + -- @param creds method specific table of credentials + -- currently only user and pass authentication is supported + -- this method requires two fields to be present + -- username and password + -- @return o instance on success, nil on failure + new = function(self, auth_method, creds) + local o = { + auth_method = auth_method, + creds = creds + } + setmetatable(o, self) + self.__index = self + if ( auth_method == 2 ) then + return o + end + end, + + -- Converts the instance to string, so that it can be sent to the + -- server. + -- @return string containing the raw request + __tostring = function(self) + -- we really don't support anything but 2, but let's pretend that + -- we actually do + if ( 2 == self.auth_method ) then + local version = 1 + local username= self.creds.username or "" + local password= self.creds.password or "" + + username = (username == "") and "\0" or username + password = (password == "") and "\0" or password + + return bin.pack("Cpp", version, username, password) + end + end, + + } + +} + +Response = { + + -- Class that handles the connection response + Connect = { + + -- Creates a new instance of the class + -- @param data string containing the data as received over the socket + -- @return o instance on success, nil on failure + new = function(self, data) + local o = { data = data } + setmetatable(o, self) + self.__index = self + if ( o:parse() ) then + return o + end + end, + + -- Parses the received data and populates member variables + -- @return true on success, false on failure + parse = function(self) + if ( #self.data ~= 2 ) then + return + end + local pos + pos, self.version, self.method = bin.unpack("CC", self.data) + return true + end + + }, + + -- Class that handles the authentication response + Authenticate = { + + Status = { + SUCCESS = 0, + -- could be anything but zero + FAIL = 1, + }, + + -- Creates a new instance of the class + -- @param data string containing the data as received over the socket + -- @return o instance on success, nil on failure + new = function(self, data) + local o = { data = data } + setmetatable(o, self) + self.__index = self + if ( o:parse() ) then + return o + end + end, + + -- Parses the received data and populates member variables + -- @return true on success, false on failure + parse = function(self) + if ( #self.data ~= 2 ) then + return + end + local pos + pos, self.version, self.status = bin.unpack("CC", self.data) + return true + end, + + -- checks if the authentication was successful or not + -- @return true on success, false on failure + isSuccess = function(self) + return ( self.status == self.Status.SUCCESS ) + end, + + } + +} + +-- A buffered socket implementation +Socket = +{ + retries = 3, + + -- Creates a new socket instance + -- @param host table containing the host table + -- @param port table containing the port table + -- @param options table containing options, currenlty supports: + -- timeout - socket timeout in ms + -- @return o new instance of Socket + new = function(self, host, port, options) + local o = { + host = host, + port = port, + options = options or {} + } + setmetatable(o, self) + self.__index = self + o.Socket = nmap.new_socket() + o.Buffer = nil + return o + end, + + -- Connects the socket to the server + -- @return status true on success false on failure + -- @return err string containing error message on failure + connect = function( self ) + self.Socket:set_timeout(self.options.timeout or 10000) + return self.Socket:connect( self.host, self.port ) + end, + + -- Closes an open connection. + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + close = function( self ) + return self.Socket:close() + end, + + -- Opposed to the socket:receive_bytes function, that returns + -- at least x bytes, this function returns the amount of bytes requested. + -- + -- @param count of bytes to read + -- @return true on success, false on failure + -- @return data containing bytes read from the socket + -- err containing error message if status is false + recv = function( self, count ) + local status, data + + self.Buffer = self.Buffer or "" + + if ( #self.Buffer < count ) then + status, data = self.Socket:receive_bytes( count - #self.Buffer ) + if ( not(status) or #data < count - #self.Buffer ) then + return false, data + end + self.Buffer = self.Buffer .. data + end + + data = self.Buffer:sub( 1, count ) + self.Buffer = self.Buffer:sub( count + 1) + + return true, data + end, + + -- Sends data over the socket + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + send = function( self, data ) + return self.Socket:send( data ) + end, +} + + +-- The main script interface +Helper = { + + -- Create a new instance of the class + -- @param host table containing the host table + -- @param port table containing the port table + -- @param options table containing library options, currenlty: + -- timeout - socket timeout in ms + -- @return o instance of Helper + new = function(self, host, port, options) + local o = { host = host, port = port, options = options } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Get the authentication method name by number + -- @param method number containing the authentication method + -- @return string containing the method name or Unknown + authNameByNumber = function(self, method) + local methods = { + [0] = "No authentication", + [1] = "GSSAPI", + [2] = "Username and password", + } + return methods[method] or ("Unknown method (%d)"):format(method) + end, + + -- Connects to the SOCKS server + -- @param auth_method table containing the auth. methods to request + -- @return status true on success, false on failure + -- @return response table containing the respons or err string on failure + connect = function(self, auth_method) + self.socket = Socket:new(self.host, self.port, self.options) + local status, err = self.socket:connect() + if ( not(status) ) then + return status, err + end + + auth_method = auth_method or {AuthMethod.NONE, AuthMethod.GSSAPI, AuthMethod.USERPASS} + status = self.socket:send( tostring(Request.Connect:new(auth_method)) ) + if ( not(status) ) then + self.socket:close() + return false, "Failed to send connection request to server" + end + + local status, data = self.socket:recv(2) + if ( not(status) ) then + self.socket:close() + return false, "Failed to receive connection response from server" + end + + local response = Response.Connect:new(data) + if ( not(response) ) then + return false, "Failed to parse response from server" + end + + if ( response.version ~= 5 ) then + return false, ("Unsupported SOCKS version (%d)"):format(response.version) + end + if ( response.method == 0xFF ) then + return false, "No acceptable authentication methods" + end + + -- store the method so authenticate knows what to use + self.auth_method = response.method + return true, response + end, + + -- Authenticates to the SOCKS server + -- @param creds table containing authentication method specific fields + -- currently only authentication method 2 (username and pass) is + -- implemented. That method requires the following fields: + -- username - containing the username + -- password - containing the password + -- @return status true on success, false on failure + -- @return err string containing the error message + authenticate = function(self, creds) + if ( self.auth_method ~= 2 ) then + return false, "Authentication method not supported" + end + local req = Request.Authenticate:new(self.auth_method, creds) + if ( not(req) ) then + return false, "Failed to create authentication request" + end + + local status = self.socket:send(tostring(req)) + if ( not(status) ) then + return false, "Failed to send authentication request" + end + + if ( 2 == self.auth_method ) then + local status, data = self.socket:recv(2) + local auth = Response.Authenticate:new(data) + + if ( not(auth) ) then + return false, "Failed to parse authentication response" + end + + if ( auth:isSuccess() ) then + return true, "Authentication was successfull" + else + return false, "Authentication failed" + end + + end + return false, "Unsupported authentication method" + end, + + -- closes the connection to the server + close = function(self) + return self.socket:close() + end, + +} \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index 2f46f64e0..4a4ff29ab 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -264,6 +264,7 @@ Entry { filename = "snmp-win32-services.nse", categories = { "default", "discove Entry { filename = "snmp-win32-shares.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "snmp-win32-software.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "snmp-win32-users.nse", categories = { "auth", "default", "safe", } } +Entry { filename = "socks-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "socks-open-proxy.nse", categories = { "default", "discovery", "external", "safe", } } Entry { filename = "sql-injection.nse", categories = { "intrusive", "vuln", } } Entry { filename = "ssh-hostkey.nse", categories = { "default", "discovery", "safe", } } diff --git a/scripts/socks-brute.nse b/scripts/socks-brute.nse new file mode 100644 index 000000000..0495194f2 --- /dev/null +++ b/scripts/socks-brute.nse @@ -0,0 +1,98 @@ +description = [[ +Performs brute force password guessing against SOCKS 5 servers +]] + +--- +-- @usage +-- nmap --script socks-brute -p 1080 +-- +-- @output +-- PORT STATE SERVICE +-- 1080/tcp open socks +-- | socks-brute: +-- | Accounts +-- | patrik:12345 - Valid credentials +-- | Statistics +-- |_ Performed 1921 guesses in 6 seconds, average tps: 320 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"brute", "intrusive"} + +require 'brute' +require 'shortport' +require 'socks' + +portrule = shortport.port_or_service({1080, 9050}, {"socks", "socks5", "tor-socks"}) + +Driver = { + + new = function (self, host, port) + local o = { host = host, port = port } + setmetatable (o,self) + self.__index = self + return o + end, + + connect = function ( self ) + self.helper = socks.Helper:new(self.host, self.port, { timeout = 10000 }) + return self.helper:connect() + end, + + login = function( self, username, password ) + local status, err = self.helper:authenticate({username=username, password=password}) + + if (not(status)) then + -- the login failed + if ( "Authentication failed" == err ) then + return false, brute.Error:new( "Login failed" ) + end + + -- something else happend, let's retry + local err = brute.Error:new( err ) + err:setRetry( true ) + return false, err + end + + return true, brute.Account:new(username, password, creds.State.VALID) + end, + + disconnect = function( self ) + return self.helper:close() + end, +} + +local function checkAuth(host, port) + + local helper = socks.Helper:new(host, port) + local status, response = helper:connect() + if ( not(status) ) then + return false, response + end + + if ( response.method == socks.AuthMethod.NONE ) then + return false, "\n No authentication required" + end + + local status, err = helper:authenticate({username="nmap", password="nmapbruteprobe"}) + if ( err ~= "Authentication failed" ) then + return false, ("\n ERROR: %s"):format(err) + end + + helper:close() + return true +end + +action = function(host, port) + + local status, response = checkAuth(host, port) + if ( not(status) ) then + return response + end + + local engine = brute.Engine:new(Driver, host, port) + engine.options.script_name = SCRIPT_NAME + status, result = engine:start() + return result +end \ No newline at end of file