Files
metasploit-gs/lib/msf/core/exploit/ftp.rb
T
bigendiansmalls 3289d89836 Added Extended passive mode to the core ftp module.
Extended passive mode (EPSV), as documented in RFC2428
is similar to the PASSive mode in that it requests that
the server open a port and wait for a data connection.
However, in unlike PASSive mode, Extended Passive mode
returns only the port on which the server listens for
the data connection, not the IP + Port.  The client is
expected to use the existing IP (e.g. the one it used to
create the initial control channel connection) to connect
to the new data port.

Where this becomes important is if the server is behind
some type of natting device, EPSV will work in this case,
PASS may not.
2019-04-30 12:41:11 -05:00

385 lines
9.5 KiB
Ruby

# -*- coding: binary -*-
module Msf
require 'msf/core/exploit/tcp'
###
#
# This module exposes methods that may be useful to exploits that deal with
# servers that speak the File Transfer Protocol (FTP).
#
###
module Exploit::Remote::Ftp
include Exploit::Remote::Tcp
#
# Creates an instance of an FTP exploit module.
#
def initialize(info = {})
super
# Register the options that all FTP exploits may make use of.
register_options(
[
Opt::RHOST,
Opt::RPORT(21),
OptString.new('FTPUSER', [ false, 'The username to authenticate as', 'anonymous']),
OptString.new('FTPPASS', [ false, 'The password for the specified username', 'mozilla@example.com'])
], Msf::Exploit::Remote::Ftp)
register_advanced_options(
[
OptInt.new('FTPTimeout', [ true, 'The number of seconds to wait for a reply from an FTP command', 16]),
OptBool.new('FTPDEBUG', [ false, 'Whether or not to print verbose debug statements', false ]),
OptBool.new('PassiveMode', [ false, 'Set true for extended passive (EPSV) ftp mode.', false])
], Msf::Exploit::Remote::Ftp)
register_autofilter_ports([ 21, 2121])
register_autofilter_services(%W{ ftp })
@ftpbuff = ""
end
#
# This method establishes an FTP connection to host and port specified by
# the 'rhost' and 'rport' methods. After connecting, the banner
# message is read in and stored in the 'banner' attribute.
#
def connect(global = true, verbose = nil)
verbose ||= datastore['FTPDEBUG']
verbose ||= datastore['VERBOSE']
print_status("Connecting to FTP server #{rhost}:#{rport}...") if verbose
fd = super(global)
# Wait for a banner to arrive...
self.banner = recv_ftp_resp(fd)
print_status("Connected to target FTP server.") if verbose
# Return the file descriptor to the caller
fd
end
#
# This method handles establishing datasocket for data channel
#
def data_connect(mode = nil, nsock = self.sock)
pass_mode = datastore['PassiveMode']
if mode
res = send_cmd([ 'TYPE' , mode ], true, nsock)
return nil if not res =~ /^200/
end
# force datasocket to renegotiate
self.datasocket.shutdown if self.datasocket != nil
# Need to be able to do both extended and normal
# passive modes. normal passive mode is default
# details of EPSV are in RFC2428
# pass_mode = true is EPSV; false is PASV
if pass_mode
res = send_cmd(['EPSV'], true, nsock)
return nil if not res =~ /^229/
# 229 Entering Passive Mode (|||port|)
if res =~ /\(\|\|\|(\d+)\|\)/
# convert port to FTP syntax
datahost = "#{rhost}"
dataport = $1.to_i
self.datasocket = Rex::Socket::Tcp.create(
'PeerHost' => datahost,
'PeerPort' => dataport,
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
)
end
else
res = send_cmd(['PASV'], true, nsock)
return nil if not res =~ /^227/
# 227 Entering Passive Mode (127,0,0,1,196,5)
if res =~ /\((\d+)\,(\d+),(\d+),(\d+),(\d+),(\d+)/
# convert port to FTP syntax
datahost = "#{$1}.#{$2}.#{$3}.#{$4}"
dataport = ($5.to_i * 256) + $6.to_i
self.datasocket = Rex::Socket::Tcp.create(
'PeerHost' => datahost,
'PeerPort' => dataport,
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
)
end
end
self.datasocket
end
#
# This method handles disconnecting our data channel
#
def data_disconnect
begin
if datasocket
datasocket.shutdown
datasocket.close
end
rescue IOError
end
datasocket = nil if datasocket
end
#
# Connect and login to the remote FTP server using the credentials
# that have been supplied in the exploit options.
#
def connect_login(global = true, verbose = nil)
verbose ||= datastore['FTPDEBUG']
verbose ||= datastore['VERBOSE']
ftpsock = connect(global, verbose)
if !(user and pass)
print_error("No username and password were supplied, unable to login")
return false
end
print_status("Authenticating as #{user} with password #{pass}...") if verbose
res = send_user(user, ftpsock)
if (res !~ /^(331|2)/)
print_error("The server rejected our username") if verbose
return false
end
if (pass)
print_status("Sending password...") if verbose
res = send_pass(pass, ftpsock)
if (res !~ /^2/)
print_error("The server rejected our password") if verbose
return false
end
end
return true
end
#
# This method logs in as the supplied user by transmitting the FTP
# 'USER <user>' command.
#
def send_user(user, nsock = self.sock)
raw_send("USER #{user}\r\n", nsock)
recv_ftp_resp(nsock)
end
#
# This method completes user authentication by sending the supplied
# password using the FTP 'PASS <pass>' command.
#
def send_pass(pass, nsock = self.sock)
raw_send("PASS #{pass}\r\n", nsock)
recv_ftp_resp(nsock)
end
#
# This method sends a QUIT command.
#
def send_quit(nsock = self.sock)
raw_send("QUIT\r\n", nsock)
recv_ftp_resp(nsock)
end
#
# This method sends one command with zero or more parameters
#
def send_cmd(args, recv = true, nsock = self.sock)
cmd = args.join(" ") + "\r\n"
ret = raw_send(cmd, nsock)
if (recv)
return recv_ftp_resp(nsock)
end
return ret
end
#
# This method transmits the command in args and receives / uploads DATA via data channel
# For commands not needing data, it will fall through to the original send_cmd
#
# For commands that send data only, the return will be the server response.
# For commands returning both data and a server response, an array will be returned.
#
# NOTE: This function always waits for a response from the server.
#
def send_cmd_data(args, data, mode = 'a', nsock = self.sock)
type = nil
# implement some aliases for various commands
if (args[0] =~ /^DIR$/i || args[0] =~ /^LS$/i)
# TODO || args[0] =~ /^MDIR$/i || args[0] =~ /^MLS$/i
args[0] = "LIST"
type = "get"
elsif (args[0] =~ /^GET$/i)
args[0] = "RETR"
type = "get"
elsif (args[0] =~ /^PUT$/i)
args[0] = "STOR"
type = "put"
end
# fall back if it's not a supported data command
if not type
return send_cmd(args, true, nsock)
end
# Set the transfer mode and connect to the remove server
return nil if not data_connect(mode)
# Our pending command should have got a connection now.
res = send_cmd(args, true, nsock)
# make sure could open port
return nil unless res =~ /^(150|125) /
# dispatch to the proper method
if (type == "get")
# failed listings just disconnect..
begin
data = datasocket.get(ftp_timeout, ftp_data_timeout)
rescue ::EOFError
data = nil
end
else
sent = self.datasocket.put(data)
end
# close data channel so command channel updates
data_disconnect
# get status of transfer
ret = nil
if (type == "get")
ret = recv_ftp_resp(nsock)
ret = [ ret, data ]
else
ret = recv_ftp_resp(nsock)
end
ret
end
#
# This method transmits a FTP command and waits for a response. If one is
# received, it is returned to the caller.
#
def raw_send_recv(cmd, nsock = self.sock)
nsock.put(cmd)
nsock.get_once(-1, ftp_timeout)
end
#
# This method reads an FTP response based on FTP continuation stuff
#
def recv_ftp_resp(nsock = self.sock)
found_end = false
resp = ""
left = ""
if !@ftpbuff.empty?
left << @ftpbuff
@ftpbuff = ""
end
while true
data = nsock.get_once(-1, ftp_timeout)
if not data
@ftpbuff << resp
@ftpbuff << left
return data
end
got = left + data
left = ""
# handle the end w/o newline case
enlidx = got.rindex(0x0a.chr)
if enlidx != (got.length-1)
if not enlidx
left << got
next
else
left << got.slice!((enlidx+1)..got.length)
end
end
# split into lines
rarr = got.split(/\r?\n/)
rarr.each do |ln|
if not found_end
resp << ln
resp << "\r\n"
if ln.length > 3 and ln[3,1] == ' '
found_end = true
end
else
left << ln
left << "\r\n"
end
end
if found_end
@ftpbuff << left
print_status("FTP recv: #{resp.inspect}") if datastore['FTPDEBUG']
return resp
end
end
end
#
# This method transmits a FTP command and does not wait for a response
#
def raw_send(cmd, nsock = self.sock)
print_status("FTP send: #{cmd.inspect}") if datastore['FTPDEBUG']
nsock.put(cmd)
end
##
#
# Wrappers for getters
#
##
#
# Returns the user string from the 'FTPUSER' option.
#
def user
datastore['FTPUSER']
end
#
# Returns the user string from the 'FTPPASS' option.
#
def pass
datastore['FTPPASS']
end
#
# Returns the number of seconds to wait for a FTP reply
#
def ftp_timeout
(datastore['FTPTimeout'] || 10).to_i
end
#
# Returns the number of seconds to wait to get more FTP data
#
def ftp_data_timeout
(datastore['FTPDataTimeout'] || 1).to_i
end
protected
#
# This attribute holds the banner that was read in after a successful call
# to connect or connect_login.
#
attr_accessor :banner, :datasocket
end
end