3289d89836
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.
385 lines
9.5 KiB
Ruby
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
|