From 743791ce631bec1cea76a55f17674e2c56e6d7e1 Mon Sep 17 00:00:00 2001 From: dmiller Date: Wed, 26 Jul 2017 19:34:23 +0000 Subject: [PATCH] Move starttls and auth functions into ftp.lua --- nselib/ftp.lua | 115 ++++++++++++++++++++++++++++++++++++++++++ nselib/sslcert.lua | 20 +++----- scripts/ftp-anon.nse | 89 +++----------------------------- scripts/ftp-brute.nse | 53 ++++++------------- 4 files changed, 145 insertions(+), 132 deletions(-) diff --git a/nselib/ftp.lua b/nselib/ftp.lua index 0735ca3ad..828715418 100644 --- a/nselib/ftp.lua +++ b/nselib/ftp.lua @@ -119,6 +119,121 @@ function close(socket) socket:close() end +--- Issue a STARTTLS command. +-- +-- @param socket The connected command socket +-- @param buffer The socket read buffer +-- @return Boolean true if AUTH TLS succeeded, false otherwise +-- @return error string on failure +function starttls(socket, buffer) + -- Send AUTH TLS command, ask the service to start encryption + local status, err = socket:send("AUTH TLS\r\n") + if not status then + return nil, err + end + local code, result = read_reply(buffer) + return code == 234, result +end + +local function is_ssl(socket) + return pcall(socket.get_ssl_certificate, socket) +end + +-- Should we try STARTTLS based on this error? +local function should_try_ssl(code, message) + return code and code >= 400 and ( + message:match('[Ss][Ss][Ll]') or + message:match('[Tt][Ll][Ss]') or + message:match('[Ss][Ee][Cc][Uu][Rr]') + ) +end + +-- Try to reconnect over STARTTLS. +local function reconnect_ssl(socket, buffer) + local status, err = starttls(socket, buffer) + if status then + status, err = socket:reconnect_ssl() + if status then + return true + end + end + return nil, err +end + +--- Authenticate with username and password +-- +-- May negotiate AUTH TLS if required +-- @param socket The connected command socket +-- @param buffer The socket read buffer +-- @param username The username to send +-- @param password The password to send +-- @param acct (optional) If the server requires it, send this account name. Default: username +-- @return Boolean true if auth succeeded, false otherwise +-- @return FTP response code +-- @return FTP response message +function auth(socket, buffer, username, password, acct) + local already_ssl = is_ssl(socket) + ::TRY_AGAIN:: + local status, err = socket:send(("USER %s\r\n"):format(username)) + if not status then + return nil, err + end + local code, message = read_reply(buffer) + if code == 331 then + -- 331: User name okay, need password. + status, err =socket:send(("PASS %s\r\n"):format(password)) + if not status then + return nil, err + end + code, message = read_reply(buffer) + elseif not already_ssl and should_try_ssl(code, message) then + if not reconnect_ssl(socket, buffer) then + return nil + end + already_ssl = true + goto TRY_AGAIN + end + + if code == 332 then + -- 332: Need account for login. + -- This is rarely seen but may come in response to a + -- USER or PASS command. + status, err = socket:send("ACCT %s\r\n"):format(acct or username) + if not status then + return nil, err + end + code, message = read_reply(buffer) + if code == 331 then + -- 331: User name okay, need password. + status, err = socket:send("PASS %s\r\n"):format(password) + if not status then + return nil, err + end + code, message = read_reply(buffer) + elseif not already_ssl and should_try_ssl(code, message) then + if not reconnect_ssl(socket, buffer) then + return nil + end + already_ssl = true + goto TRY_AGAIN + end + end + + if code and code >= 200 and code < 300 then + -- We are primarily looking for 230: User logged in, proceed. + return true, code, message + else + if code and not already_ssl and should_try_ssl(code, message) then + if not reconnect_ssl(socket, buffer) then + return nil + end + already_ssl = true + goto TRY_AGAIN + end + return nil, code, message + end +end + --- Start PASV mode -- -- For IPv6 connections, attempts to use EPSV (RFC 2428). If the server sends an address that is not the target address, then this is an error. diff --git a/nselib/sslcert.lua b/nselib/sslcert.lua index d5f5d084b..795ad3cae 100644 --- a/nselib/sslcert.lua +++ b/nselib/sslcert.lua @@ -204,28 +204,20 @@ StartTLS = { -- Works for FTP (21) -- Open a standard TCP socket - local s, err = comm.opencon(host, port) + local s, code, result, buf = ftp.connect(host, port) if not s then - return false, string.format("Failed to connect to FTP server: %s", err) + return false, string.format("Failed to connect to FTP server: %s", code) end - local buf = stdnse.make_buffer(s, "\r?\n") - - local code, result = ftp.read_reply(buf) if code ~= 220 then return false, string.format("FTP protocol error: %s", code or result) end -- Send AUTH TLS command, ask the service to start encryption - s:send("AUTH TLS\r\n") - code, result = ftp.read_reply(buf) - if code ~= 234 then + local status, err = ftp.starttls(s, buf) + if not status then starttls_supported(host, port, false) - stdnse.debug1("AUTH TLS failed or unavailable. Enable --script-trace to see what is happening.") - - -- Send QUIT to clean up server side connection - s:send("QUIT\r\n") - - return false, string.format("FTP AUTH TLS error: %s", code or result) + ftp.close(s) + return false, string.format("FTP AUTH TLS error: %s", err) end -- Should have a solid TLS over FTP session now... starttls_supported(host, port, true) diff --git a/scripts/ftp-anon.nse b/scripts/ftp-anon.nse index b5fe1788f..3679b5c6e 100644 --- a/scripts/ftp-anon.nse +++ b/scripts/ftp-anon.nse @@ -76,30 +76,6 @@ local function list(socket, buffer, target, max_lines) return true, listing end -local function is_ssl(socket) - return pcall(socket.get_ssl_certificate, socket) -end - --- Try to reconnect over STARTTLS. --- Returns a new connected socket and buffer -local function reconnect_ssl(socket, host, port) - socket:close() - local status, socket = sslcert.isPortSupported({service="ftp"})(host, port) - if status then - socket:set_timeout(8000) - return socket, stdnse.make_buffer(socket, "\r?\n") - end -end - --- Should we try STARTTLS based on this error? -local function should_try_ssl(code, message) - return code and code >= 400 and ( - message:match('[Ss][Ss][Ll]') or - message:match('[Tt][Ll][Ss]') or - message:match('[Ss][Ee][Cc][Uu][Rr]') - ) -end - --- Connects to the FTP server and checks if the server allows anonymous logins. action = function(host, port) local max_list = stdnse.get_script_args("ftp-anon.maxlist") @@ -119,67 +95,18 @@ action = function(host, port) local socket, code, message, buffer = ftp.connect(host, port, {request_timeout=8000}) if not socket then - stdnse.debug1("Couldn't connect: %s", code) + stdnse.debug1("Couldn't connect: %s", code or message) + return nil + end + if code and code ~= 220 then + stdnse.debug1("banner code %d %q.", code, message) return nil end - local try = nmap.new_try( function() - socket:close() - end) - - -- Read banner. - ::TRY_AGAIN:: - local already_ssl = is_ssl(socket) - if code and code == 220 then - try(socket:send("USER anonymous\r\n")) - code, message = ftp.read_reply(buffer) - if code == 331 then - -- 331: User name okay, need password. - try(socket:send("PASS IEUser@\r\n")) - code, message = ftp.read_reply(buffer) - elseif not already_ssl and should_try_ssl(code, message) then - socket, buffer = reconnect_ssl(socket, host, port) - if not socket then - return nil - end - code = 220 - goto TRY_AGAIN - end - - if code == 332 then - -- 332: Need account for login. - -- This is rarely seen but may come in response to a - -- USER or PASS command. As we're doing this - -- anonymously, send back a blank ACCT. - try(socket:send("ACCT\r\n")) - code, message = ftp.read_reply(buffer) - if code == 331 then - -- 331: User name okay, need password. - try(socket:send("PASS IEUser@\r\n")) - code, message = ftp.read_reply(buffer) - elseif not already_ssl and should_try_ssl(code, message) then - socket, buffer = reconnect_ssl(socket, host, port) - if not socket then - return nil - end - code = 220 - goto TRY_AGAIN - end - end - end - - if code and code >= 200 and code < 300 then - -- We are primarily looking for 230: User logged in, proceed. - else + local status, code, message = ftp.auth(socket, buffer, "anonymous", "IEUser@") + if not status then if not code then stdnse.debug1("got socket error %q.", message) - elseif not already_ssl and should_try_ssl(code, message) then - socket, buffer = reconnect_ssl(socket, host, port) - if not socket then - return nil - end - code = 220 - goto TRY_AGAIN elseif code == 421 or code == 530 then -- Don't log known error codes. -- 421: Service not available, closing control connection. @@ -196,7 +123,7 @@ action = function(host, port) if not max_list or max_list > 0 then local status, listing = list(socket, buffer, host, max_list) - socket:close() + ftp.close(socket) if not status then result[#result + 1] = "Can't get directory listing: " .. listing diff --git a/scripts/ftp-brute.nse b/scripts/ftp-brute.nse index b05de8f54..9a7a5653c 100644 --- a/scripts/ftp-brute.nse +++ b/scripts/ftp-brute.nse @@ -1,9 +1,8 @@ local brute = require "brute" local creds = require "creds" -local nmap = require "nmap" local shortport = require "shortport" local stdnse = require "stdnse" -local string = require "string" +local ftp = require "ftp" description = [[ Performs brute force password auditing against FTP servers. @@ -56,62 +55,42 @@ Driver = { connect = function( self ) self.socket = brute.new_socket() - local status, err = self.socket:connect(self.host, self.port) - self.socket:set_timeout(arg_timeout) - if(not(status)) then - return false, brute.Error:new( "Couldn't connect to host: " .. err ) + -- discard buffer, we'll create a new one over the BruteSocket later + local realsocket, code, message, buffer = ftp.connect(self.host, self.port, {request_timeout=arg_timeout}) + if not realsocket then + return false, brute.Error:new( "Couldn't connect to host: " .. (code or message) ) end + self.socket.socket = realsocket return true end, login = function (self, user, pass) - local status, err - local res = "" - - status, err = self.socket:send("USER " .. user .. "\r\n") - if(not(status)) then - return false, brute.Error:new("Couldn't send login: " .. err) - end - - status, err = self.socket:send("PASS " .. pass .. "\r\n") - if(not(status)) then - return false, brute.Error:new("Couldn't send login: " .. err) - end - - -- Create a buffer and receive the first line local buffer = stdnse.make_buffer(self.socket, "\r?\n") - local line = buffer() + local status, code, message = ftp.auth(self.socket, buffer, user, pass) - -- Loop over the lines - while(line)do - stdnse.debug1("Received: %s", line) - if(string.match(line, "^230")) then - stdnse.debug1("Successful login: %s/%s", user, pass) - return true, creds.Account:new( user, pass, creds.State.VALID) - elseif(string.match(line, "^530")) then + if not status then + if not code then + return false, brute.Error:new("socket error during login: " .. message) + elseif code == 530 then return false, brute.Error:new( "Incorrect password" ) - elseif(string.match(line, "^421")) then + elseif code == 421 then local err = brute.Error:new("Too many connections") err:setReduce(true) return false, err - elseif(string.match(line, "^220")) then - elseif(string.match(line, "^331")) then else - stdnse.debug1("WARNING: Unhandled response: %s", line) + stdnse.debug1("WARNING: Unhandled response: %d %s", code, message) local err = brute.Error:new("Unhandled response") err:setRetry(true) return false, err end - - line = buffer() end - - return false, brute.Error:new("Login didn't return a proper response") + stdnse.debug1("Successful login: %s/%s", user, pass) + return true, creds.Account:new( user, pass, creds.State.VALID) end, disconnect = function( self ) - self.socket:close() + ftp.close(self.socket) return true end }