432 lines
12 KiB
Ruby
432 lines
12 KiB
Ruby
##
|
|
# This module requires Metasploit: http//metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
#
|
|
# Fuzzer written by corelanc0d3r - <peter.ve [at] corelan.be>
|
|
# http://www.corelan.be:8800/index.php/2010/10/12/death-of-an-ftp-client/
|
|
#
|
|
##
|
|
|
|
|
|
require 'msf/core'
|
|
|
|
class Metasploit3 < Msf::Auxiliary
|
|
|
|
include Exploit::Remote::TcpServer
|
|
|
|
def initialize()
|
|
super(
|
|
'Name' => 'Simple FTP Client Fuzzer',
|
|
'Description' => %q{
|
|
This module will serve an FTP server and perform FTP client interaction fuzzing
|
|
},
|
|
'Author' => [ 'corelanc0d3r <peter.ve[at]corelan.be>' ],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'URL', 'http://www.corelan.be:8800/index.php/2010/10/12/death-of-an-ftp-client/' ],
|
|
]
|
|
)
|
|
register_options(
|
|
[
|
|
OptPort.new('SRVPORT', [ true, "The local port to listen on.", 21 ]),
|
|
OptString.new('FUZZCMDS', [ true, "Comma separated list of commands to fuzz (Uppercase).", "LIST,NLST,LS,RETR", nil, /(?:[A-Z]+,?)+/ ]),
|
|
OptInt.new('STARTSIZE', [ true, "Fuzzing string startsize.",1000]),
|
|
OptInt.new('ENDSIZE', [ true, "Max Fuzzing string size.",200000]),
|
|
OptInt.new('STEPSIZE', [ true, "Increment fuzzing string each attempt.",1000]),
|
|
OptBool.new('RESET', [ true, "Reset fuzzing values after client disconnects with QUIT cmd.",true]),
|
|
OptString.new('WELCOME', [ true, "FTP Server welcome message.","Evil FTP Server Ready"]),
|
|
OptBool.new('CYCLIC', [ true, "Use Cyclic pattern instead of A's (fuzzing payload).",true]),
|
|
OptBool.new('ERROR', [ true, "Reply with error codes only",false]),
|
|
OptBool.new('EXTRALINE', [ true, "Add extra CRLF's in response to LIST",true])
|
|
], self.class)
|
|
end
|
|
|
|
|
|
# Not compatible today
|
|
def support_ipv6?
|
|
false
|
|
end
|
|
|
|
|
|
#---------------------------------------------------------------------------------
|
|
def setup
|
|
super
|
|
@state = {}
|
|
end
|
|
|
|
|
|
#---------------------------------------------------------------------------------
|
|
|
|
def run
|
|
@fuzzsize=datastore['STARTSIZE'].to_i
|
|
exploit()
|
|
end
|
|
|
|
#---------------------------------------------------------------------------------
|
|
# Handler for new FTP client connections
|
|
#---------------------------------------------------------------------------------
|
|
|
|
def on_client_connect(c)
|
|
@state[c] = {
|
|
:name => "#{c.peerhost}:#{c.peerport}",
|
|
:ip => c.peerhost,
|
|
:port => c.peerport,
|
|
:user => nil,
|
|
:pass => nil
|
|
}
|
|
#set up an active data port on port 20
|
|
print_status("Client connected : " + c.peerhost)
|
|
active_data_port_for_client(c, 20)
|
|
send_response(c,"","WELCOME",220," "+datastore['WELCOME'])
|
|
#from this point forward, on_client_data() will take over
|
|
end
|
|
|
|
def on_client_close(c)
|
|
@state.delete(c)
|
|
end
|
|
|
|
#---------------------------------------------------------------------------------
|
|
# Active and Passive data connections
|
|
#---------------------------------------------------------------------------------
|
|
def passive_data_port_for_client(c)
|
|
@state[c][:mode] = :passive
|
|
if(not @state[c][:passive_sock])
|
|
s = Rex::Socket::TcpServer.create(
|
|
'LocalHost' => '0.0.0.0',
|
|
'LocalPort' => 0,
|
|
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
|
|
)
|
|
dport = s.getsockname[2]
|
|
@state[c][:passive_sock] = s
|
|
@state[c][:passive_port] = dport
|
|
print_status(" - Set up passive data port #{dport}")
|
|
end
|
|
@state[c][:passive_port]
|
|
end
|
|
|
|
|
|
def active_data_port_for_client(c,port)
|
|
@state[c][:mode] = :active
|
|
connector = Proc.new {
|
|
host = c.peerhost.dup
|
|
sock = Rex::Socket::Tcp.create(
|
|
'PeerHost' => host,
|
|
'PeerPort' => port,
|
|
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
|
|
)
|
|
}
|
|
@state[c][:active_connector] = connector
|
|
@state[c][:active_port] = port
|
|
print_status(" - Set up active data port #{port}")
|
|
end
|
|
|
|
|
|
def establish_data_connection(c)
|
|
print_status(" - Establishing #{@state[c][:mode]} data connection")
|
|
begin
|
|
Timeout.timeout(20) do
|
|
if(@state[c][:mode] == :active)
|
|
return @state[c][:active_connector].call()
|
|
end
|
|
if(@state[c][:mode] == :passive)
|
|
return @state[c][:passive_sock].accept
|
|
end
|
|
end
|
|
print_status(" - Data connection active")
|
|
rescue ::Exception => e
|
|
print_error("Failed to establish data connection: #{e.class} #{e}")
|
|
end
|
|
nil
|
|
end
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------------
|
|
# FTP Client-to-Server Command handlers
|
|
#---------------------------------------------------------------------------------
|
|
|
|
def on_client_data(c)
|
|
#get the client data
|
|
data = c.get_once
|
|
return if not data
|
|
#split data into command and arguments
|
|
cmd,arg = data.strip.split(/\s+/, 2)
|
|
arg ||= ""
|
|
|
|
return if not cmd
|
|
#convert commands to uppercase and strip spaces
|
|
case cmd.upcase.strip
|
|
|
|
when 'USER'
|
|
@state[c][:user] = arg
|
|
send_response(c,arg,"USER",331," User name okay, need password")
|
|
return
|
|
|
|
when 'PASS'
|
|
@state[c][:pass] = arg
|
|
send_response(c,arg,"PASS",230,"-Password accepted.\r\n230 User logged in.")
|
|
return
|
|
|
|
when 'QUIT'
|
|
if (datastore['RESET'])
|
|
print_status("Resetting fuzz settings")
|
|
@fuzzsize = datastore['STARTSIZE']
|
|
@stepsize = datastore['STEPSIZE']
|
|
end
|
|
print_status("** Client disconnected **")
|
|
send_response(c,arg,"QUIT",221," User logged out")
|
|
return
|
|
|
|
when 'SYST'
|
|
send_response(c,arg,"SYST",215," UNIX Type: L8")
|
|
return
|
|
|
|
when 'TYPE'
|
|
send_response(c,arg,"TYPE",200," Type set to #{arg}")
|
|
return
|
|
|
|
when 'CWD'
|
|
send_response(c,arg,"CWD",250," CWD Command successful")
|
|
return
|
|
|
|
when 'PWD'
|
|
send_response(c,arg,"PWD",257," \"/\" is current directory.")
|
|
return
|
|
|
|
when 'REST'
|
|
send_response(c,arg,"REST",200," OK")
|
|
return
|
|
|
|
when 'XPWD'
|
|
send_response(c,arg,"PWD",257," \"/\" is current directory")
|
|
return
|
|
|
|
when 'SIZE'
|
|
send_response(c,arg,"SIZE",213," 1")
|
|
return
|
|
|
|
when 'MDTM'
|
|
send_response(c,arg,"MDTM",213," #{Time.now.strftime("%Y%m%d%H%M%S")}")
|
|
return
|
|
|
|
when 'CDUP'
|
|
send_response(c,arg,"CDUP",257," \"/\" is current directory")
|
|
return
|
|
|
|
when 'PORT'
|
|
port = arg.split(',')[4,2]
|
|
if(not port and port.length == 2)
|
|
c.put("500 Illegal PORT command.\r\n")
|
|
return
|
|
end
|
|
port = port.map{|x| x.to_i}.pack('C*').unpack('n')[0]
|
|
active_data_port_for_client(c, port)
|
|
send_response(c,arg,"PORT",200," PORT command successful")
|
|
return
|
|
|
|
when 'PASV'
|
|
print_status("Handling #{cmd.upcase} command")
|
|
daddr = Rex::Socket.source_address(c.peerhost)
|
|
dport = passive_data_port_for_client(c)
|
|
@state[c][:daddr] = daddr
|
|
@state[c][:dport] = dport
|
|
pasv = (daddr.split('.') + [dport].pack('n').unpack('CC')).join(',')
|
|
dofuzz = fuzz_this_cmd("PASV")
|
|
code = 227
|
|
if datastore['ERROR']
|
|
code = 557
|
|
end
|
|
if (dofuzz==1)
|
|
print_status(" * Fuzzing response for PASV, payload length #{@fuzzdata.length}")
|
|
send_response(c,arg,"PASV",code," Entering Passive Mode (#{@fuzzdata},1,1,1,1,1)\r\n")
|
|
incr_fuzzsize()
|
|
else
|
|
send_response(c,arg,"PASV",code," Entering Passive Mode (#{pasv})")
|
|
end
|
|
return
|
|
|
|
when /^(LIST|NLST|LS)$/
|
|
#special case - requires active/passive connection
|
|
print_status("Handling #{cmd.upcase} command")
|
|
conn = establish_data_connection(c)
|
|
if(not conn)
|
|
c.put("425 Can't build data connection\r\n")
|
|
return
|
|
end
|
|
print_status(" - Data connection set up")
|
|
code = 150
|
|
if datastore['ERROR']
|
|
code = 550
|
|
end
|
|
c.put("#{code} Here comes the directory listing.\r\n")
|
|
code = 226
|
|
if datastore['ERROR']
|
|
code = 550
|
|
end
|
|
c.put("#{code} Directory send ok.\r\n")
|
|
strfile = "passwords.txt"
|
|
strfolder = "Secret files"
|
|
dofuzz = fuzz_this_cmd("LIST")
|
|
if (dofuzz==1)
|
|
strfile = @fuzzdata + ".txt"
|
|
strfolder = @fuzzdata
|
|
paylen = @fuzzdata.length
|
|
print_status("* Fuzzing response for LIST, payload length #{paylen}")
|
|
incr_fuzzsize()
|
|
end
|
|
print_status(" - Sending directory list via data connection")
|
|
dirlist = ""
|
|
if datastore['EXTRALINE']
|
|
extra = "\r\n"
|
|
else
|
|
extra = ""
|
|
end
|
|
dirlist = "drwxrwxrwx 1 100 0 11111 Jun 11 21:10 #{strfolder}\r\n" + extra
|
|
dirlist << "-rw-rw-r-- 1 1176 1176 1060 Aug 16 22:22 #{strfile}\r\n" + extra
|
|
conn.put("total 2\r\n"+dirlist)
|
|
conn.close
|
|
return
|
|
|
|
when 'RETR'
|
|
#special case - requires active/passive connection
|
|
print_status("Handling #{cmd.upcase} command")
|
|
conn = establish_data_connection(c)
|
|
if(not conn)
|
|
c.put("425 Can't build data connection\r\n")
|
|
return
|
|
end
|
|
print_status(" - Data connection set up")
|
|
strcontent = "blahblahblah"
|
|
dofuzz = fuzz_this_cmd("LIST")
|
|
if (dofuzz==1)
|
|
strcontent = @fuzzdata
|
|
paylen = @fuzzdata.length
|
|
print_status("* Fuzzing response for RETR, payload length #{paylen}")
|
|
incr_fuzzsize()
|
|
end
|
|
c.put("150 Opening BINARY mode data connection #{strcontent}\r\n")
|
|
print_status(" - Sending data via data connection")
|
|
conn.put(strcontent)
|
|
c.put("226 Transfer complete\r\n")
|
|
conn.close
|
|
return
|
|
|
|
when /^(STOR|MKD|REM|DEL|RMD)$/
|
|
send_response(c,arg,cmd.upcase,500," Access denied")
|
|
return
|
|
|
|
when 'FEAT'
|
|
send_response(c,arg,"FEAT","","211-Features:\r\n211 End")
|
|
return
|
|
|
|
when 'HELP'
|
|
send_response(c,arg,"HELP",214," Syntax: #{arg} - (#{arg}-specific commands)")
|
|
|
|
when 'SITE'
|
|
send_response(c,arg,"SITE",200," OK")
|
|
return
|
|
|
|
when 'NOOP'
|
|
send_response(c,arg,"NOOP",200," OK")
|
|
return
|
|
|
|
when 'ABOR'
|
|
send_response(c,arg,"ABOR",225," Abor command successful")
|
|
return
|
|
|
|
when 'ACCT'
|
|
send_response(c,arg,"ACCT",200," OK")
|
|
return
|
|
|
|
when 'RNFR'
|
|
send_response(c,arg,"RNRF",350," File exists")
|
|
return
|
|
|
|
when 'RNTO'
|
|
send_response(c,arg,"RNTO",350," File exists")
|
|
return
|
|
else
|
|
send_response(c,arg,cmd.upcase,200," Command not understood")
|
|
return
|
|
end
|
|
return
|
|
end
|
|
|
|
|
|
|
|
#---------------------------------------------------------------------------------
|
|
# Fuzzer functions
|
|
#---------------------------------------------------------------------------------
|
|
|
|
# Do we need to fuzz this command ?
|
|
def fuzz_this_cmd(cmd)
|
|
@fuzzcommands = datastore['FUZZCMDS'].split(",")
|
|
fuzzme = 0
|
|
@fuzzcommands.each do |thiscmd|
|
|
if ((cmd.upcase == thiscmd.upcase) || (thiscmd=="*")) && (fuzzme==0)
|
|
fuzzme = 1
|
|
end
|
|
end
|
|
if fuzzme==1
|
|
# should we use a cyclic pattern, or just A's ?
|
|
if datastore['CYCLIC']
|
|
@fuzzdata = Rex::Text.pattern_create(@fuzzsize)
|
|
else
|
|
@fuzzdata = "A" * @fuzzsize
|
|
end
|
|
end
|
|
return fuzzme
|
|
end
|
|
|
|
def incr_fuzzsize
|
|
@stepsize = datastore['STEPSIZE'].to_i
|
|
@fuzzsize = @fuzzsize + @stepsize
|
|
print_status("(i) Setting next payload size to #{@fuzzsize}")
|
|
if (@fuzzsize > datastore['ENDSIZE'].to_i)
|
|
@fuzzsize = datastore['ENDSIZE'].to_i
|
|
end
|
|
end
|
|
|
|
|
|
# Send data back to the server
|
|
def send_response(c,arg,cmd,code,msg)
|
|
if arg.length > 40
|
|
showarg = arg[0,40] + "..."
|
|
else
|
|
showarg = arg
|
|
end
|
|
if cmd.length > 40
|
|
showcmd = cmd[0,40] + "..."
|
|
else
|
|
showcmd = cmd
|
|
end
|
|
print_status("Sending response for '#{showcmd}' command, arg #{showarg}")
|
|
dofuzz = fuzz_this_cmd(cmd)
|
|
## Fuzz this command ? (excluding PASV, which is handled in the command handler)
|
|
if (dofuzz==1) && (cmd.upcase != "PASV")
|
|
paylen = @fuzzdata.length
|
|
print_status("* Fuzzing response for #{cmd.upcase}, payload length #{paylen}")
|
|
if datastore['ERROR']
|
|
code = "550 "
|
|
end
|
|
if cmd=="FEAT"
|
|
@fuzzdata = "211-Features:\r\n "+@fuzzdata+"\r\n211 End"
|
|
end
|
|
if cmd=="PWD"
|
|
@fuzzdata = " \"/"+@fuzzdata+"\" is current directory"
|
|
end
|
|
cmsg = code.to_s + " " + @fuzzdata
|
|
c.put("#{cmsg}\r\n")
|
|
print_status("* Fuzz data sent")
|
|
incr_fuzzsize()
|
|
else
|
|
#Do not fuzz
|
|
cmsg = code.to_s + msg
|
|
cmsg = cmsg.strip
|
|
c.put("#{cmsg}\r\n")
|
|
end
|
|
return
|
|
end
|
|
end
|