mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
FTP overhaul: better SSL connection, including ftp-anon over ftps or STARTTLS
This commit is contained in:
@@ -14,28 +14,59 @@ local ERROR_MESSAGES = {
|
||||
["ERROR"] = "failed to receive data"
|
||||
}
|
||||
|
||||
--- Connects to the FTP server based on the provided options.
|
||||
local crlf_pattern = "\r?\n"
|
||||
--- Connects to the FTP server based on the provided options and returns the parsed banner.
|
||||
--
|
||||
-- @param host The host table
|
||||
-- @param port The port table
|
||||
-- @param opts The connection option table, possible options:
|
||||
-- timeout: generic timeout value
|
||||
-- recv_before: receive data before returning
|
||||
-- @param opts The connection option table, from comm.lua.
|
||||
-- @return socket The socket descriptor, or nil on errors
|
||||
-- @return response The response received on success and when
|
||||
-- the recv_before is set, or the error message on failures.
|
||||
-- @return code The numeric response code, as returned by read_reply, or error message if socket is nil.
|
||||
-- @return message The response message
|
||||
-- @return buffer The socket read buffer function, to be passed to read_reply.
|
||||
-- @see comm.lua
|
||||
connect = function(host, port, opts)
|
||||
local socket, _, _, ret = comm.tryssl(host, port, '', opts)
|
||||
opts = opts or {}
|
||||
opts.recv_before = true
|
||||
local socket, err, proto, ret = comm.tryssl(host, port, '', opts)
|
||||
if not socket then
|
||||
return socket, (ERROR_MESSAGES[ret] or 'unspecified error')
|
||||
end
|
||||
return socket, ret
|
||||
local buffer = stdnse.make_buffer(socket, crlf_pattern)
|
||||
local pos = 1
|
||||
-- Should we just pass the output of buffer()?
|
||||
local usebuf = false
|
||||
-- Since we already read the first chunk of banner from the socket,
|
||||
-- we have to supply it line-by-line to read_reply.
|
||||
local code, message = read_reply(function()
|
||||
if usebuf then
|
||||
-- done reading the initial banner; pass along the socket buffer.
|
||||
return buffer()
|
||||
end
|
||||
-- Look for CRLF
|
||||
local i, j = ret:find(crlf_pattern, pos)
|
||||
if not i then
|
||||
-- Didn't find it! Grab another chunk (up to CRLF) and return it
|
||||
usebuf = true
|
||||
local chunk = buffer()
|
||||
return ret:sub(pos) .. chunk
|
||||
end
|
||||
local oldpos = pos
|
||||
-- start the next search just after CRLF
|
||||
pos = j + 1
|
||||
if pos >= #ret then
|
||||
-- We consumed the whole thing! Start calling buffer() next.
|
||||
usebuf = true
|
||||
end
|
||||
return ret:sub(oldpos, i - 1)
|
||||
end)
|
||||
return socket, code, message, buffer
|
||||
end
|
||||
|
||||
---
|
||||
-- Read an FTP reply and return the numeric code and the message. See RFC 959,
|
||||
-- section 4.2.
|
||||
-- @param buffer should have been created with
|
||||
-- @param buffer The buffer returned by ftp.connect or created with
|
||||
-- <code>stdnse.make_buffer(socket, "\r?\n")</code>.
|
||||
-- @return numeric code or <code>nil</code>.
|
||||
-- @return text reply or error message.
|
||||
|
||||
@@ -5,6 +5,7 @@ local shortport = require "shortport"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local table = require "table"
|
||||
local sslcert = require "sslcert"
|
||||
|
||||
description = [[
|
||||
Checks if an FTP server allows anonymous logins.
|
||||
@@ -38,7 +39,7 @@ license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||||
categories = {"default", "auth", "safe"}
|
||||
|
||||
|
||||
portrule = shortport.port_or_service(21, "ftp")
|
||||
portrule = shortport.port_or_service({21,990}, {"ftp","ftps"})
|
||||
|
||||
-- ---------------------
|
||||
-- Directory listing function.
|
||||
@@ -46,12 +47,11 @@ portrule = shortport.port_or_service(21, "ftp")
|
||||
-- LIST on the commands socket, connect to the data one and read the directory
|
||||
-- list sent.
|
||||
-- ---------------------
|
||||
local function list(socket, target, max_lines)
|
||||
local function list(socket, buffer, target, max_lines)
|
||||
local status, err
|
||||
|
||||
-- ask the server for a Passive Mode: it should give us a port to
|
||||
-- listen to, where it will dump the directory listing
|
||||
local buffer = stdnse.make_buffer(socket, "\r?\n")
|
||||
status, err = socket:send("PASV\r\n")
|
||||
if not status then
|
||||
return status, err
|
||||
@@ -98,14 +98,32 @@ local function list(socket, target, max_lines)
|
||||
return true, listing
|
||||
end
|
||||
|
||||
--- Connects to the FTP server and checks if the server allows anonymous logins.
|
||||
action = function(host, port)
|
||||
local socket = nmap.new_socket()
|
||||
local code, message
|
||||
local err_catch = function()
|
||||
socket:close()
|
||||
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")
|
||||
if not max_list then
|
||||
if nmap.verbosity() == 0 then
|
||||
@@ -120,13 +138,20 @@ action = function(host, port)
|
||||
end
|
||||
end
|
||||
|
||||
local try = nmap.new_try(err_catch)
|
||||
|
||||
try(socket:connect(host, port))
|
||||
local buffer = stdnse.make_buffer(socket, "\r?\n")
|
||||
local socket, code, message, buffer = ftp.connect(host, port, {request_timeout=8000})
|
||||
if not socket then
|
||||
stdnse.debug1("Couldn't connect: %s", code)
|
||||
return nil
|
||||
end
|
||||
|
||||
local try = nmap.new_try( function()
|
||||
socket:close()
|
||||
end)
|
||||
|
||||
-- Read banner.
|
||||
code, message = ftp.read_reply(buffer)
|
||||
::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)
|
||||
@@ -134,6 +159,13 @@ action = function(host, port)
|
||||
-- 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
|
||||
@@ -147,6 +179,13 @@ action = function(host, port)
|
||||
-- 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
|
||||
@@ -156,12 +195,20 @@ action = function(host, port)
|
||||
else
|
||||
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.
|
||||
-- 530: Not logged in.
|
||||
else
|
||||
stdnse.debug1("got code %d %q.", code, message)
|
||||
return ("got code %d %q."):format(code, message)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -170,7 +217,7 @@ action = function(host, port)
|
||||
result[#result + 1] = "Anonymous FTP login allowed (FTP code " .. code .. ")"
|
||||
|
||||
if not max_list or max_list > 0 then
|
||||
local status, listing = list(socket, host, max_list)
|
||||
local status, listing = list(socket, buffer, host, max_list)
|
||||
socket:close()
|
||||
|
||||
if not status then
|
||||
|
||||
@@ -158,17 +158,14 @@ vsFTPd version 2.3.4 backdoor, this was reported on 2011-07-04.]],
|
||||
end
|
||||
|
||||
-- Create socket.
|
||||
local sock, err = ftp.connect(host, port,
|
||||
{recv_before = false,
|
||||
timeout = 8000})
|
||||
local sock, code, message, buffer = ftp.connect(host, port,
|
||||
{request_timeout = 8000})
|
||||
if not sock then
|
||||
stdnse.debug1("can't connect: %s", err)
|
||||
stdnse.debug1("can't connect: %s", code)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Read banner.
|
||||
local buffer = stdnse.make_buffer(sock, "\r?\n")
|
||||
local code, message = ftp.read_reply(buffer)
|
||||
if not code then
|
||||
stdnse.debug1("can't read banner: %s", message)
|
||||
sock:close()
|
||||
|
||||
@@ -57,10 +57,9 @@ portrule = function (host, port)
|
||||
return shortport.port_or_service(21, "ftp")(host, port)
|
||||
end
|
||||
|
||||
local function get_proftpd_banner(response)
|
||||
local banner, version
|
||||
banner = response:match("^%d+%s(.*)")
|
||||
if banner and banner:match("ProFTPD") then
|
||||
local function get_proftpd_banner(banner)
|
||||
local version
|
||||
if banner then
|
||||
version = banner:match("ProFTPD%s([%w%.]+)%s")
|
||||
end
|
||||
return banner, version
|
||||
@@ -129,13 +128,12 @@ end
|
||||
|
||||
local function check_proftpd(ftp_opts)
|
||||
local ftp_server = {}
|
||||
local socket, ret = ftp.connect(ftp_opts.host, ftp_opts.port,
|
||||
{recv_before = true})
|
||||
local socket, code, message = ftp.connect(ftp_opts.host, ftp_opts.port)
|
||||
if not socket then
|
||||
return socket, ret
|
||||
return socket, code
|
||||
end
|
||||
|
||||
ftp_server.banner, ftp_server.version = get_proftpd_banner(ret)
|
||||
ftp_server.banner, ftp_server.version = get_proftpd_banner(message)
|
||||
if not ftp_server.banner then
|
||||
return ftp_finish(socket, false, "failed to get FTP banner.")
|
||||
elseif not ftp_server.banner:match("ProFTPD") then
|
||||
|
||||
Reference in New Issue
Block a user