diff --git a/nselib/ftp.lua b/nselib/ftp.lua index d104a3272..0735ca3ad 100644 --- a/nselib/ftp.lua +++ b/nselib/ftp.lua @@ -6,6 +6,8 @@ local comm = require "comm" local stdnse = require "stdnse" local string = require "string" +local ipOps = require "ipOps" +local nmap = require "nmap" _ENV = stdnse.module("ftp", stdnse.seeall) local ERROR_MESSAGES = { @@ -109,4 +111,82 @@ function read_reply(buffer) return nil, string.format("Unparseable response: %q", line) end +--- Close an FTP command connection +-- +-- @param socket The socket with the live connection +function close(socket) + socket:send("QUIT\r\n") + socket:close() +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. +-- @param socket The connected command socket +-- @param buffer The receive buffer +-- @return The connected data socket, or nil on error +-- @return Error message if data socket is nil +function pasv(socket, buffer) + local epsv = false + local status, lhost, lport, rhost, rport = socket:get_info() + if not status then + return nil, ("Can't determine remote host IP: %s"):format(lhost) + end + epsv = #ipOps.ip_to_str(rhost) > 4 + + ::TRY_AGAIN:: + local cmd = epsv and "EPSV" or "PASV" + -- ask the server for a Passive Mode: it should give us a port to + -- listen to, where it will dump the directory listing + local status, err = socket:send(cmd .. "\r\n") + if not status then + return status, err + end + local code, message = read_reply(buffer) + + local pasv_port + if epsv then + if not code then + return nil, ("EPSV failed: socket %s"):format(message) + elseif code ~= 229 then + stdnse.debug2("Falling back to PASV. EPSV: %d %s", code, message) + epsv = false + goto TRY_AGAIN + end + status, pasv_port = string.match(message, "%((.)%1%1(%d+)%1%)") + if not status then + stdnse.debug2("Can't parse EPSV response: %s", message) + epsv = false + goto TRY_AGAIN + end + else + if not code or code >= 300 then + return nil, ("PASV failed: %d %s"):format(code or "socket", message) + end + -- Compute the PASV port as given by the server + -- The server should answer with something like + -- 2xx Entering Passive Mode (a,b,c,d,hp,lp) + -- (-- IP--,PORT) + -- PORT is (hp x 256) + lp + local ip, high, low = string.match(message, "%((%d+,%d+,%d+,%d+),(%d+),(%d+)%)") + if not ip then + return nil, string.format("Can't parse PASV response: %q", message) + end + ip = ip:gsub(",", ".") + if not (ipOps.compare_ip(ip, "eq", rhost) or ipOps.compare_ip(ip, "eq", "0.0.0.0")) then + return nil, ("PASV IP %s is not the same as %s"):format(ip, rhost) + end + + pasv_port = high * 256 + low + end + + local list_socket = nmap.new_socket() + status, err = list_socket:connect(rhost, pasv_port, "tcp") + if not status then + return status, err + end + + return list_socket +end + return _ENV; diff --git a/scripts/ftp-anon.nse b/scripts/ftp-anon.nse index 3ebb16606..b5fe1788f 100644 --- a/scripts/ftp-anon.nse +++ b/scripts/ftp-anon.nse @@ -48,37 +48,15 @@ portrule = shortport.port_or_service({21,990}, {"ftp","ftps"}) -- list sent. -- --------------------- 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 - status, err = socket:send("PASV\r\n") - if not status then - return status, err + local list_socket, err = ftp.pasv(socket, buffer) + if not list_socket then + return nil, err end - local code, message = ftp.read_reply(buffer) - - -- Compute the PASV port as given by the server - -- The server should answer with something like - -- 2xx Entering Passive Mode (a,b,c,d,hp,lp) - -- (-- IP--,PORT) - -- PORT is (hp x 256) + lp - local high, low = string.match(message, "%(%d+,%d+,%d+,%d+,(%d+),(%d+)%)") - if not high then - return nil, string.format("Can't parse PASV response: %q", message) - end - - local pasv_port = high * 256 + low -- Send the LIST command on the commands socket. "Fire and forget"; we -- don't need to take care of the answer on this socket. - status, err = socket:send("LIST\r\n") - if not status then - return status, err - end - - local list_socket = nmap.new_socket() - status, err = list_socket:connect(target, pasv_port, "tcp") + local status, err = socket:send("LIST\r\n") if not status then return status, err end